Skip to content

Commit 308c6bc

Browse files
committed
feat: add full disk volumes
When set to `disk`, a full block device is used for the volume. When `volumeType = "disk"`: - Size specific settings are not allowed in the provisioning block (`minSize`, `maxSize`, `grow`). Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
1 parent 82ac111 commit 308c6bc

File tree

15 files changed

+519
-125
lines changed

15 files changed

+519
-125
lines changed

hack/release.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,17 @@ It should not be used for workloads requiring predictable storage quotas.
168168
title = "CRI Registry Configuration"
169169
description = """\
170170
The CRI registry configuration in v1apha1 legacy machine configuration under `.machine.registries` is now deprecated, but still supported for backwards compatibility.
171-
New configuration documents `RegistryMirrorConfig`, `RegistryAuthConfig` and `RegistryTLSConfig` should be used instead.
171+
New configuration documents `RegistryMirrorConfig`, `RegistryAuthConfig` and `RegistryTLSConfig` should be used instead.
172+
"""
173+
174+
[notes.disk-user-volumes]
175+
title = "New User Volume type - disk"
176+
description = """\
177+
`volumeType` in UserVolumeConfig can be set to `disk`.
178+
When set to `disk`, a full block device is used for the volume.
179+
180+
When `volumeType = "disk"`:
181+
- Size specific settings are not allowed in the provisioning block (`minSize`, `maxSize`, `grow`).
172182
"""
173183

174184
[make_deps]

internal/app/machined/pkg/controllers/block/internal/volumes/format.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func Format(ctx context.Context, logger *zap.Logger, volumeContext ManagerContex
115115
makefsOptions = append(makefsOptions, makefs.WithConfigFile(quirks.New("").XFSMkfsConfig()))
116116

117117
if err = makefs.XFS(volumeContext.Status.MountLocation, makefsOptions...); err != nil {
118-
return fmt.Errorf("error formatting XFS: %w", err)
118+
return xerrors.NewTaggedf[Retryable]("error formatting XFS: %w", err)
119119
}
120120
case block.FilesystemTypeEXT4:
121121
var makefsOptions []makefs.Option
@@ -125,14 +125,14 @@ func Format(ctx context.Context, logger *zap.Logger, volumeContext ManagerContex
125125
}
126126

127127
if err = makefs.Ext4(volumeContext.Status.MountLocation, makefsOptions...); err != nil {
128-
return fmt.Errorf("error formatting ext4: %w", err)
128+
return xerrors.NewTaggedf[Retryable]("error formatting ext4: %w", err)
129129
}
130130
case block.FilesystemTypeSwap:
131131
if err = swap.Format(volumeContext.Status.MountLocation, swap.FormatOptions{
132132
Label: volumeContext.Cfg.TypedSpec().Provisioning.FilesystemSpec.Label,
133133
UUID: uuid.New(),
134134
}); err != nil {
135-
return fmt.Errorf("error formatting swap: %w", err)
135+
return xerrors.NewTaggedf[Retryable]("error formatting swap: %w", err)
136136
}
137137
default:
138138
return fmt.Errorf("unsupported filesystem type: %s", volumeContext.Cfg.TypedSpec().Provisioning.FilesystemSpec.Type)

internal/app/machined/pkg/controllers/block/internal/volumes/locate.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010

11+
"github.com/google/cel-go/cel"
1112
"github.com/siderolabs/gen/value"
1213
"github.com/siderolabs/gen/xerrors"
1314
"github.com/siderolabs/go-blockdevice/v2/partitioning"
@@ -44,8 +45,18 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
4445

4546
// attempt to discover the volume
4647
for _, dv := range volumeContext.DiscoveredVolumes {
47-
matchContext := map[string]any{
48-
"volume": dv,
48+
var locator *cel.Env
49+
50+
matchContext := map[string]any{}
51+
52+
switch volumeType { //nolint:exhaustive // we do not need to repeat exhaustive check here
53+
case block.VolumeTypeDisk:
54+
locator = celenv.DiskLocator()
55+
56+
case block.VolumeTypePartition:
57+
locator = celenv.VolumeLocator()
58+
59+
matchContext["volume"] = dv
4960
}
5061

5162
// add disk to the context, so we can use it in CEL expressions
@@ -63,7 +74,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
6374
}
6475
}
6576

66-
matches, err := volumeContext.Cfg.TypedSpec().Locator.Match.EvalBool(celenv.VolumeLocator(), matchContext)
77+
matches, err := volumeContext.Cfg.TypedSpec().Locator.Match.EvalBool(locator, matchContext)
6778
if err != nil {
6879
return fmt.Errorf("error evaluating volume locator: %w", err)
6980
}
@@ -127,6 +138,10 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
127138
return fmt.Errorf("no disks matched selector for volume")
128139
}
129140

141+
if volumeType == block.VolumeTypeDisk && len(matchedDisks) > 1 {
142+
return fmt.Errorf("multiple disks matched selector for disk volume; matched disks: %v", matchedDisks)
143+
}
144+
130145
logger.Debug("matched disks", zap.Strings("disks", matchedDisks))
131146

132147
// analyze each disk, until we find the one which is the best fit

internal/app/machined/pkg/controllers/block/user_volumes.go

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,48 @@ var (
4646
}
4747

4848
switch userVolumeConfig.Type().ValueOr(block.VolumeTypePartition) {
49+
case block.VolumeTypeDirectory:
50+
userVolumeResource.TransformFunc = newVolumeConfigBuilder().
51+
WithType(block.VolumeTypeDirectory).
52+
WithMount(block.MountSpec{
53+
TargetPath: userVolumeConfig.Name(),
54+
ParentID: constants.UserVolumeMountPoint,
55+
SelinuxLabel: constants.EphemeralSelinuxLabel,
56+
FileMode: 0o755,
57+
UID: 0,
58+
GID: 0,
59+
BindTarget: pointer.To(userVolumeConfig.Name()),
60+
}).
61+
WriterFunc()
62+
63+
case block.VolumeTypeDisk:
64+
userVolumeResource.TransformFunc = newVolumeConfigBuilder().
65+
WithType(block.VolumeTypeDisk).
66+
WithLocator(userVolumeConfig.Provisioning().DiskSelector().ValueOr(noMatch)).
67+
WithProvisioning(block.ProvisioningSpec{
68+
Wave: block.WaveUserVolumes,
69+
DiskSelector: block.DiskSelector{
70+
Match: userVolumeConfig.Provisioning().DiskSelector().ValueOr(noMatch),
71+
},
72+
PartitionSpec: block.PartitionSpec{
73+
TypeUUID: partition.LinuxFilesystemData,
74+
},
75+
FilesystemSpec: block.FilesystemSpec{
76+
Type: userVolumeConfig.Filesystem().Type(),
77+
},
78+
}).
79+
WithMount(block.MountSpec{
80+
TargetPath: userVolumeConfig.Name(),
81+
ParentID: constants.UserVolumeMountPoint,
82+
SelinuxLabel: constants.EphemeralSelinuxLabel,
83+
FileMode: 0o755,
84+
UID: 0,
85+
GID: 0,
86+
ProjectQuotaSupport: userVolumeConfig.Filesystem().ProjectQuotaSupport(),
87+
}).
88+
WithConvertEncryptionConfiguration(userVolumeConfig.Encryption()).
89+
WriterFunc()
90+
4991
case block.VolumeTypePartition:
5092
userVolumeResource.TransformFunc = newVolumeConfigBuilder().
5193
WithType(block.VolumeTypePartition).
@@ -77,20 +119,8 @@ var (
77119
}).
78120
WithConvertEncryptionConfiguration(userVolumeConfig.Encryption()).
79121
WriterFunc()
80-
case block.VolumeTypeDirectory:
81-
userVolumeResource.TransformFunc = newVolumeConfigBuilder().
82-
WithType(block.VolumeTypeDirectory).
83-
WithMount(block.MountSpec{
84-
TargetPath: userVolumeConfig.Name(),
85-
ParentID: constants.UserVolumeMountPoint,
86-
SelinuxLabel: constants.EphemeralSelinuxLabel,
87-
FileMode: 0o755,
88-
UID: 0,
89-
GID: 0,
90-
BindTarget: pointer.To(userVolumeConfig.Name()),
91-
}).
92-
WriterFunc()
93-
case block.VolumeTypeDisk, block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
122+
123+
case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
94124
fallthrough
95125

96126
default:

internal/app/machined/pkg/controllers/block/volume_config_test.go

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -404,18 +404,26 @@ func (suite *VolumeConfigSuite) TestReconcileUserRawVolumes() {
404404
}
405405

406406
func (suite *VolumeConfigSuite) TestReconcileUserSwapVolumes() {
407-
uv1 := blockcfg.NewUserVolumeConfigV1Alpha1()
408-
uv1.MetaName = "data1"
409-
suite.Require().NoError(uv1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`system_disk`)))
410-
uv1.ProvisioningSpec.ProvisioningMinSize = blockcfg.MustByteSize("10GiB")
411-
uv1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("100GiB")
412-
uv1.FilesystemSpec.FilesystemType = block.FilesystemTypeXFS
413-
414-
uv2 := blockcfg.NewUserVolumeConfigV1Alpha1()
415-
uv2.MetaName = "data2"
416-
suite.Require().NoError(uv2.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
417-
uv2.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("1TiB")
418-
uv2.EncryptionSpec = blockcfg.EncryptionSpec{
407+
userVolumeNames := []string{
408+
"data-part1",
409+
"data-part2",
410+
"data-dir1",
411+
"data-disk1",
412+
}
413+
414+
uvPart1 := blockcfg.NewUserVolumeConfigV1Alpha1()
415+
uvPart1.MetaName = userVolumeNames[0]
416+
suite.Require().NoError(uvPart1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`system_disk`)))
417+
uvPart1.ProvisioningSpec.ProvisioningMinSize = blockcfg.MustByteSize("10GiB")
418+
uvPart1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("100GiB")
419+
uvPart1.FilesystemSpec.FilesystemType = block.FilesystemTypeXFS
420+
421+
uvPart2 := blockcfg.NewUserVolumeConfigV1Alpha1()
422+
uvPart2.MetaName = userVolumeNames[1]
423+
uvPart2.VolumeType = pointer.To(block.VolumeTypePartition)
424+
suite.Require().NoError(uvPart2.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
425+
uvPart2.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("1TiB")
426+
uvPart2.EncryptionSpec = blockcfg.EncryptionSpec{
419427
EncryptionProvider: block.EncryptionProviderLUKS2,
420428
EncryptionKeys: []blockcfg.EncryptionKey{
421429
{
@@ -429,32 +437,45 @@ func (suite *VolumeConfigSuite) TestReconcileUserSwapVolumes() {
429437
},
430438
}
431439

432-
uv3 := blockcfg.NewUserVolumeConfigV1Alpha1()
433-
uv3.MetaName = "data3"
434-
uv3.VolumeType = pointer.To(block.VolumeTypeDirectory)
440+
uvDir1 := blockcfg.NewUserVolumeConfigV1Alpha1()
441+
uvDir1.MetaName = userVolumeNames[2]
442+
uvDir1.VolumeType = pointer.To(block.VolumeTypeDirectory)
443+
444+
uvDisk1 := blockcfg.NewUserVolumeConfigV1Alpha1()
445+
uvDisk1.MetaName = userVolumeNames[3]
446+
suite.Require().NoError(uvDisk1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
447+
uvDisk1.EncryptionSpec = blockcfg.EncryptionSpec{
448+
EncryptionProvider: block.EncryptionProviderLUKS2,
449+
EncryptionKeys: []blockcfg.EncryptionKey{
450+
{
451+
KeySlot: 0,
452+
KeyTPM: &blockcfg.EncryptionKeyTPM{},
453+
},
454+
{
455+
KeySlot: 1,
456+
KeyStatic: &blockcfg.EncryptionKeyStatic{KeyData: "secret"},
457+
},
458+
},
459+
}
435460

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

441-
ctr, err := container.New(uv1, uv2, uv3, sv1)
466+
ctr, err := container.New(uvPart1, uvPart2, uvDir1, uvDisk1, sv1)
442467
suite.Require().NoError(err)
443468

444469
cfg := config.NewMachineConfig(ctr)
445470
suite.Create(cfg)
446471

447-
userVolumes := []string{
448-
constants.UserVolumePrefix + "data1",
449-
constants.UserVolumePrefix + "data2",
450-
constants.UserVolumePrefix + "data3",
451-
}
472+
userVolumes := xslices.Map(userVolumeNames, func(in string) string { return constants.UserVolumePrefix + in })
452473

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

456477
switch vc.Metadata().ID() {
457-
case userVolumes[0], userVolumes[1]:
478+
case userVolumes[0], userVolumes[1], userVolumes[3]:
458479
asrt.Equal(block.VolumeTypePartition, vc.TypedSpec().Type)
459480

460481
asrt.Contains(userVolumes, vc.TypedSpec().Provisioning.PartitionSpec.Label)
@@ -463,11 +484,12 @@ func (suite *VolumeConfigSuite) TestReconcileUserSwapVolumes() {
463484
asrt.NoError(err)
464485

465486
asrt.Contains(string(locator), vc.TypedSpec().Provisioning.PartitionSpec.Label)
487+
466488
case userVolumes[2]:
467489
asrt.Equal(block.VolumeTypeDirectory, vc.TypedSpec().Type)
468490
}
469491

470-
asrt.Contains([]string{"data1", "data2", "data3"}, vc.TypedSpec().Mount.TargetPath)
492+
asrt.Contains(userVolumeNames, vc.TypedSpec().Mount.TargetPath)
471493
asrt.Equal(constants.UserVolumeMountPoint, vc.TypedSpec().Mount.ParentID)
472494

473495
switch vc.Metadata().ID() {
@@ -506,8 +528,8 @@ func (suite *VolumeConfigSuite) TestReconcileUserSwapVolumes() {
506528
suite.AddFinalizer(block.NewVolumeMountRequest(block.NamespaceName, volumeID).Metadata(), "test")
507529
}
508530

509-
// drop the first volume
510-
ctr, err = container.New(uv2)
531+
// keep only the first volume
532+
ctr, err = container.New(uvPart1)
511533
suite.Require().NoError(err)
512534

513535
newCfg := config.NewMachineConfig(ctr)
@@ -516,30 +538,30 @@ func (suite *VolumeConfigSuite) TestReconcileUserSwapVolumes() {
516538

517539
// controller should tear down removed resources
518540
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
519-
if vc.Metadata().ID() == userVolumes[1] {
541+
if vc.Metadata().ID() == userVolumes[0] {
520542
asrt.Equal(resource.PhaseRunning, vc.Metadata().Phase())
521543
} else {
522544
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
523545
}
524546
})
525547

526548
ctest.AssertResources(suite, userVolumes, func(vmr *block.VolumeMountRequest, asrt *assert.Assertions) {
527-
if vmr.Metadata().ID() == userVolumes[1] {
549+
if vmr.Metadata().ID() == userVolumes[0] {
528550
asrt.Equal(resource.PhaseRunning, vmr.Metadata().Phase())
529551
} else {
530552
asrt.Equal(resource.PhaseTearingDown, vmr.Metadata().Phase())
531553
}
532554
})
533555

534556
// remove finalizers
535-
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[0]).Metadata(), "test")
536-
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[0]).Metadata(), "test")
537-
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[2]).Metadata(), "test")
538-
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[2]).Metadata(), "test")
557+
for _, userVolume := range userVolumes[1:] {
558+
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolume).Metadata(), "test")
559+
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolume).Metadata(), "test")
560+
}
539561

540562
// now the resources should be removed
541-
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[0])
542-
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[0])
543-
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[2])
544-
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[2])
563+
for _, userVolume := range userVolumes[1:] {
564+
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolume)
565+
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolume)
566+
}
545567
}

0 commit comments

Comments
 (0)