Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/resource/definitions/block/block.proto
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ message MountSpec {
int64 uid = 6;
int64 gid = 7;
bool recursive_relabel = 8;
string bind_target = 9;
}

// MountStatusSpec is the spec for MountStatus.
Expand Down
18 changes: 17 additions & 1 deletion hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,27 @@ When using Factory or Imager supply as `-module.sig_enfore module.sig_enforce=0`
[notes.grub]
title = "GRUB"
description = """\
Talos Linux introduces new machine configuration option `.machine.install.grubUseUKICmdline` to control whether GRUB should use the kernel command line
Talos Linux introduces new machine configuration option `.machine.install.grubUseUKICmdline` to control whether GRUB should use the kernel command line
provided by the boot assets (UKI) or to use the command line constructed by Talos itself (legacy behavior).

This option defaults to `true` for new installations, which means that GRUB will use the command line from the UKI, making it easier to customize kernel parameters via boot asset generation.
For existing installations upgrading to v1.12, this option will default to `false` to preserve the legacy behavior.
"""

[notes.directory-user-volumes]
title = "New User Volume type - bind"
description = """\
New field in UserVolumeConfig - `volumeType` that defaults to `partition`, but can be set to `directory`.
When set to `directory`, provisioning and filesystem operations are skipped and a directory is created under `/var/mnt/<name>`.

The `directory` type enables lightweight storage volumes backed by a host directory, instead of requiring a full block device partition.

When `volumeType = "directory"`:
- A directory is created at `/var/mnt/<metadata.name>`;
- `provisioning`, `filesystem` and `encryption` are prohibited.

Note: this mode does not provide filesystem-level isolation and inherits the EPHEMERAL partition capacity limits.
It should not be used for workloads requiring predictable storage quotas.
"""

[make_deps]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
func Close(ctx context.Context, logger *zap.Logger, volumeContext ManagerContext) error {
switch volumeContext.Cfg.TypedSpec().Type {
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
// tmpfs, directory, symlink and overlay volumes can be always closed
// volume types can be always closed
volumeContext.Status.Phase = block.VolumePhaseClosed

return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M

switch volumeType {
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
// tmpfs, directory, symlink and overlays volumes are always ready
// volume types above are always ready
volumeContext.Status.Phase = block.VolumePhaseReady

return nil
Expand Down
115 changes: 113 additions & 2 deletions internal/app/machined/pkg/controllers/block/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func (ctrl *MountController) handleMountOperation(
) error {
switch volumeStatus.TypedSpec().Type {
case block.VolumeTypeDirectory:
return ctrl.handleDirectoryMountOperation(rootPath, mountTarget, volumeStatus)
return ctrl.handleDirectoryMountOperation(logger, rootPath, mountTarget, mountRequest, volumeStatus)
case block.VolumeTypeOverlay:
return ctrl.handleOverlayMountOperation(logger, filepath.Join(rootPath, mountTarget), mountRequest, volumeStatus)
case block.VolumeTypeSymlink:
Expand All @@ -312,8 +312,10 @@ func (ctrl *MountController) handleMountOperation(
}

func (ctrl *MountController) handleDirectoryMountOperation(
logger *zap.Logger,
rootPath string,
target string,
mountRequest *block.MountRequest,
volumeStatus *block.VolumeStatus,
) error {
targetPath := filepath.Join(rootPath, target)
Expand All @@ -333,9 +335,93 @@ func (ctrl *MountController) handleDirectoryMountOperation(
}
}

if volumeStatus.TypedSpec().MountSpec.BindTarget != nil {
if err := ctrl.handleBindMountOperation(
logger,
rootPath, target, *volumeStatus.TypedSpec().MountSpec.BindTarget,
mountRequest, volumeStatus,
); err != nil {
return fmt.Errorf("target path %q is not a directory", targetPath)
}
}

return ctrl.updateTargetSettings(targetPath, volumeStatus.TypedSpec().MountSpec)
}

func (ctrl *MountController) handleBindMountOperation(
logger *zap.Logger,
rootPath string,
source string,
bindTarget string,
mountRequest *block.MountRequest,
volumeStatus *block.VolumeStatus,
) error {
_, ok := ctrl.activeMounts[mountRequest.Metadata().ID()]

// mount hasn't been done yet
if !ok {
mountSource := filepath.Join(rootPath, source)
mountTarget := filepath.Join(rootPath, bindTarget)

if err := os.Mkdir(mountTarget, volumeStatus.TypedSpec().MountSpec.FileMode); err != nil {
if !os.IsExist(err) {
return fmt.Errorf("failed to create target path: %w", err)
}

st, err := os.Stat(mountTarget)
if err != nil {
return fmt.Errorf("failed to stat target path: %w", err)
}

if !st.IsDir() {
return fmt.Errorf("target path %q is not a directory", mountTarget)
}
}

var opts []mount.ManagerOption

opts = append(opts,
mount.WithSelinuxLabel(volumeStatus.TypedSpec().MountSpec.SelinuxLabel),
)

manager := mount.NewManager(slices.Concat(
[]mount.ManagerOption{
mount.WithTarget(mountTarget),
mount.WithOpentreeFromPath(mountSource),
mount.WithPrinter(logger.Sugar().Infof),
},
opts,
)...)

mountpoint, err := manager.Mount()
if err != nil {
return fmt.Errorf("failed to mount %q: %w", mountRequest.Metadata().ID(), err)
}

if !mountRequest.TypedSpec().ReadOnly && !mountRequest.TypedSpec().Detached {
if err = ctrl.updateTargetSettings(mountTarget, volumeStatus.TypedSpec().MountSpec); err != nil {
manager.Unmount() //nolint:errcheck

return fmt.Errorf("failed to update target settings %q: %w", mountRequest.Metadata().ID(), err)
}
}

logger.Info("bind mount",
zap.String("volume", volumeStatus.Metadata().ID()),
zap.String("source", mountSource),
zap.String("target", mountTarget),
)

ctrl.activeMounts[mountRequest.Metadata().ID()] = &mountContext{
point: mountpoint,
readOnly: mountRequest.TypedSpec().ReadOnly,
unmounter: manager.Unmount,
}
}

return nil
}

//nolint:gocyclo
func (ctrl *MountController) handleSymlinkMountOperation(
logger *zap.Logger,
Expand Down Expand Up @@ -645,7 +731,7 @@ func (ctrl *MountController) handleUnmountOperation(
) error {
switch volumeStatus.TypedSpec().Type {
case block.VolumeTypeDirectory:
return nil
return ctrl.handleDirectoryUnmountOperation(logger, mountRequest, volumeStatus)
case block.VolumeTypeTmpfs:
return fmt.Errorf("not implemented yet")
case block.VolumeTypeDisk, block.VolumeTypePartition, block.VolumeTypeOverlay:
Expand Down Expand Up @@ -687,6 +773,31 @@ func (ctrl *MountController) handleDiskUnmountOperation(
return nil
}

func (ctrl *MountController) handleDirectoryUnmountOperation(
logger *zap.Logger,
mountRequest *block.MountRequest,
_ *block.VolumeStatus,
) error {
mountCtx, ok := ctrl.activeMounts[mountRequest.Metadata().ID()]
if !ok {
return nil
}

if err := mountCtx.unmounter(); err != nil {
return fmt.Errorf("failed to unmount %q: %w", mountRequest.Metadata().ID(), err)
}

delete(ctrl.activeMounts, mountRequest.Metadata().ID())

logger.Info("volume unmount",
zap.String("volume", mountRequest.Metadata().ID()),
zap.String("source", mountCtx.point.Source()),
zap.String("target", mountCtx.point.Target()),
)

return nil
}

func (ctrl *MountController) handleSymlinkUmountOperation(
mountRequest *block.MountRequest,
) error {
Expand Down
39 changes: 39 additions & 0 deletions internal/app/machined/pkg/controllers/block/user_volume_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/siderolabs/gen/optional"
"github.com/siderolabs/gen/xerrors"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/go-pointer"
"go.uber.org/zap"

"github.com/siderolabs/talos/internal/pkg/partition"
Expand Down Expand Up @@ -301,6 +302,26 @@ func (ctrl *UserVolumeConfigController) handleUserVolumeConfig(
userVolumeConfig configconfig.UserVolumeConfig,
v *block.VolumeConfig,
volumeID string,
) error {
switch userVolumeConfig.Type().ValueOr(block.VolumeTypePartition) {
case block.VolumeTypePartition:
return ctrl.handlePartitionUserVolumeConfig(userVolumeConfig, v, volumeID)

case block.VolumeTypeDirectory:
return ctrl.handleDirectoryUserVolumeConfig(userVolumeConfig, v)

case block.VolumeTypeDisk, block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
fallthrough

default:
return fmt.Errorf("unsupported volume type %q", userVolumeConfig.Type().ValueOr(block.VolumeTypePartition).String())
}
}

func (ctrl *UserVolumeConfigController) handlePartitionUserVolumeConfig(
userVolumeConfig configconfig.UserVolumeConfig,
v *block.VolumeConfig,
volumeID string,
) error {
diskSelector, ok := userVolumeConfig.Provisioning().DiskSelector().Get()
if !ok {
Expand Down Expand Up @@ -343,6 +364,24 @@ func (ctrl *UserVolumeConfigController) handleUserVolumeConfig(
return nil
}

func (ctrl *UserVolumeConfigController) handleDirectoryUserVolumeConfig(
userVolumeConfig configconfig.UserVolumeConfig,
v *block.VolumeConfig,
) error {
v.TypedSpec().Type = block.VolumeTypeDirectory
v.TypedSpec().Mount = block.MountSpec{
TargetPath: userVolumeConfig.Name(),
ParentID: constants.UserVolumeMountPoint,
SelinuxLabel: constants.EphemeralSelinuxLabel,
FileMode: 0o755,
UID: 0,
GID: 0,
BindTarget: pointer.To(userVolumeConfig.Name()),
}

return nil
}

//nolint:dupl
func (ctrl *UserVolumeConfigController) handleRawVolumeConfig(
rawVolumeConfig configconfig.RawVolumeConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/go-pointer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"

Expand Down Expand Up @@ -64,12 +65,16 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
},
}

uv3 := blockcfg.NewUserVolumeConfigV1Alpha1()
uv3.MetaName = "data3"
uv3.VolumeType = pointer.To(block.VolumeTypeDirectory)

sv1 := blockcfg.NewSwapVolumeConfigV1Alpha1()
sv1.MetaName = "swap"
suite.Require().NoError(sv1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`disk.transport == "nvme"`)))
sv1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("2GiB")

ctr, err := container.New(uv1, uv2, sv1)
ctr, err := container.New(uv1, uv2, uv3, sv1)
suite.Require().NoError(err)

cfg := config.NewMachineConfig(ctr)
Expand All @@ -78,20 +83,28 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
userVolumes := []string{
constants.UserVolumePrefix + "data1",
constants.UserVolumePrefix + "data2",
constants.UserVolumePrefix + "data3",
}

ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
asrt.Contains(vc.Metadata().Labels().Raw(), block.UserVolumeLabel)

asrt.Equal(block.VolumeTypePartition, vc.TypedSpec().Type)
asrt.Contains(userVolumes, vc.TypedSpec().Provisioning.PartitionSpec.Label)
switch vc.Metadata().ID() {
case userVolumes[0], userVolumes[1]:
asrt.Equal(block.VolumeTypePartition, vc.TypedSpec().Type)

locator, err := vc.TypedSpec().Locator.Match.MarshalText()
asrt.NoError(err)
asrt.Contains(userVolumes, vc.TypedSpec().Provisioning.PartitionSpec.Label)

asrt.Contains(string(locator), vc.TypedSpec().Provisioning.PartitionSpec.Label)
locator, err := vc.TypedSpec().Locator.Match.MarshalText()
asrt.NoError(err)

asrt.Contains([]string{"data1", "data2"}, vc.TypedSpec().Mount.TargetPath)
asrt.Contains(string(locator), vc.TypedSpec().Provisioning.PartitionSpec.Label)

case userVolumes[2]:
asrt.Equal(block.VolumeTypeDirectory, vc.TypedSpec().Type)
}

asrt.Contains([]string{"data1", "data2", "data3"}, vc.TypedSpec().Mount.TargetPath)
asrt.Equal(constants.UserVolumeMountPoint, vc.TypedSpec().Mount.ParentID)

switch vc.Metadata().ID() {
Expand Down Expand Up @@ -138,39 +151,34 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
newCfg.Metadata().SetVersion(cfg.Metadata().Version())
suite.Update(newCfg)

// controller should tear down removed volumes
// controller should tear down removed resources
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
if vc.Metadata().ID() == userVolumes[0] {
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
} else {
if vc.Metadata().ID() == userVolumes[1] {
asrt.Equal(resource.PhaseRunning, vc.Metadata().Phase())
}
})

// controller should tear down removed volume resources
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
if vc.Metadata().ID() == userVolumes[0] {
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
} else {
asrt.Equal(resource.PhaseRunning, vc.Metadata().Phase())
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
}
})

ctest.AssertResources(suite, userVolumes, func(vmr *block.VolumeMountRequest, asrt *assert.Assertions) {
if vmr.Metadata().ID() == userVolumes[0] {
asrt.Equal(resource.PhaseTearingDown, vmr.Metadata().Phase())
} else {
if vmr.Metadata().ID() == userVolumes[1] {
asrt.Equal(resource.PhaseRunning, vmr.Metadata().Phase())
} else {
asrt.Equal(resource.PhaseTearingDown, vmr.Metadata().Phase())
}
})

// remove finalizers
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[0]).Metadata(), "test")
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[0]).Metadata(), "test")
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[2]).Metadata(), "test")
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[2]).Metadata(), "test")

// now the resources should be removed
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[0])
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[0])
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[2])
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[2])
}

func (suite *UserVolumeConfigSuite) TestReconcileRawVolumes() {
Expand Down
Loading