Skip to content

Commit 6cf202a

Browse files
committed
feat: add full disk volumes
Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
1 parent d69305a commit 6cf202a

File tree

13 files changed

+457
-63
lines changed

13 files changed

+457
-63
lines changed

hack/release.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ When `volumeType = "directory"`:
151151
152152
Note: this mode does not provide filesystem-level isolation and inherits the EPHEMERAL partition capacity limits.
153153
It should not be used for workloads requiring predictable storage quotas.
154+
"""
155+
156+
[notes.disk-user-volumes]
157+
title = "New User Volume type - disk"
158+
description = """\
159+
`volumeType` in UserVolumeConfig can be set to `disk`.
160+
When set to `disk`, a full block device is used for the volume.
161+
162+
When `volumeType = "disk"`:
163+
- Size specific settings are not allowed in the provisioning block (`minSize`, `maxSize`, `grow`).
154164
"""
155165

156166
[make_deps]

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

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,13 +304,16 @@ func (ctrl *UserVolumeConfigController) handleUserVolumeConfig(
304304
volumeID string,
305305
) error {
306306
switch userVolumeConfig.Type().ValueOr(block.VolumeTypePartition) {
307-
case block.VolumeTypePartition:
308-
return ctrl.handlePartitionUserVolumeConfig(userVolumeConfig, v, volumeID)
309-
310307
case block.VolumeTypeDirectory:
311308
return ctrl.handleDirectoryUserVolumeConfig(userVolumeConfig, v)
312309

313-
case block.VolumeTypeDisk, block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
310+
case block.VolumeTypeDisk:
311+
return ctrl.handleDiskUserVolumeConfig(userVolumeConfig, v, volumeID)
312+
313+
case block.VolumeTypePartition:
314+
return ctrl.handlePartitionUserVolumeConfig(userVolumeConfig, v, volumeID)
315+
316+
case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
314317
fallthrough
315318

316319
default:
@@ -364,6 +367,49 @@ func (ctrl *UserVolumeConfigController) handlePartitionUserVolumeConfig(
364367
return nil
365368
}
366369

370+
func (ctrl *UserVolumeConfigController) handleDiskUserVolumeConfig(
371+
userVolumeConfig configconfig.UserVolumeConfig,
372+
v *block.VolumeConfig,
373+
volumeID string,
374+
) error {
375+
diskSelector, ok := userVolumeConfig.Provisioning().DiskSelector().Get()
376+
if !ok {
377+
// this shouldn't happen due to validation
378+
return fmt.Errorf("disk selector not found for volume %q", volumeID)
379+
}
380+
381+
v.TypedSpec().Type = block.VolumeTypeDisk
382+
v.TypedSpec().Locator.Match = labelVolumeMatch(volumeID)
383+
v.TypedSpec().Provisioning = block.ProvisioningSpec{
384+
Wave: block.WaveUserVolumes,
385+
DiskSelector: block.DiskSelector{
386+
Match: diskSelector,
387+
},
388+
PartitionSpec: block.PartitionSpec{
389+
Label: volumeID,
390+
TypeUUID: partition.LinuxFilesystemData,
391+
},
392+
FilesystemSpec: block.FilesystemSpec{
393+
Type: userVolumeConfig.Filesystem().Type(),
394+
},
395+
}
396+
v.TypedSpec().Mount = block.MountSpec{
397+
TargetPath: userVolumeConfig.Name(),
398+
ParentID: constants.UserVolumeMountPoint,
399+
SelinuxLabel: constants.EphemeralSelinuxLabel,
400+
FileMode: 0o755,
401+
UID: 0,
402+
GID: 0,
403+
ProjectQuotaSupport: userVolumeConfig.Filesystem().ProjectQuotaSupport(),
404+
}
405+
406+
if err := convertEncryptionConfiguration(userVolumeConfig.Encryption(), v.TypedSpec()); err != nil {
407+
return fmt.Errorf("error apply encryption configuration: %w", err)
408+
}
409+
410+
return nil
411+
}
412+
367413
func (ctrl *UserVolumeConfigController) handleDirectoryUserVolumeConfig(
368414
userVolumeConfig configconfig.UserVolumeConfig,
369415
v *block.VolumeConfig,

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

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@ func TestUserVolumeConfigSuite(t *testing.T) {
4040
}
4141

4242
func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
43-
uv1 := blockcfg.NewUserVolumeConfigV1Alpha1()
44-
uv1.MetaName = "data1"
45-
suite.Require().NoError(uv1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`system_disk`)))
46-
uv1.ProvisioningSpec.ProvisioningMinSize = blockcfg.MustByteSize("10GiB")
47-
uv1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("100GiB")
48-
uv1.FilesystemSpec.FilesystemType = block.FilesystemTypeXFS
49-
50-
uv2 := blockcfg.NewUserVolumeConfigV1Alpha1()
51-
uv2.MetaName = "data2"
52-
suite.Require().NoError(uv2.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
53-
uv2.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("1TiB")
54-
uv2.EncryptionSpec = blockcfg.EncryptionSpec{
43+
uvPart1 := blockcfg.NewUserVolumeConfigV1Alpha1()
44+
uvPart1.MetaName = "data-part1"
45+
suite.Require().NoError(uvPart1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`system_disk`)))
46+
uvPart1.ProvisioningSpec.ProvisioningMinSize = blockcfg.MustByteSize("10GiB")
47+
uvPart1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("100GiB")
48+
uvPart1.FilesystemSpec.FilesystemType = block.FilesystemTypeXFS
49+
50+
uvPart2 := blockcfg.NewUserVolumeConfigV1Alpha1()
51+
uvPart2.MetaName = "data-part2"
52+
suite.Require().NoError(uvPart2.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
53+
uvPart2.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("1TiB")
54+
uvPart2.EncryptionSpec = blockcfg.EncryptionSpec{
5555
EncryptionProvider: block.EncryptionProviderLUKS2,
5656
EncryptionKeys: []blockcfg.EncryptionKey{
5757
{
@@ -65,25 +65,43 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
6565
},
6666
}
6767

68-
uv3 := blockcfg.NewUserVolumeConfigV1Alpha1()
69-
uv3.MetaName = "data3"
70-
uv3.VolumeType = pointer.To(block.VolumeTypeDirectory)
68+
uvDir1 := blockcfg.NewUserVolumeConfigV1Alpha1()
69+
uvDir1.MetaName = "data-dir1"
70+
uvDir1.VolumeType = pointer.To(block.VolumeTypeDirectory)
71+
72+
uvDisk1 := blockcfg.NewUserVolumeConfigV1Alpha1()
73+
uvDisk1.MetaName = "data-disk1"
74+
suite.Require().NoError(uvDisk1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
75+
uvDisk1.EncryptionSpec = blockcfg.EncryptionSpec{
76+
EncryptionProvider: block.EncryptionProviderLUKS2,
77+
EncryptionKeys: []blockcfg.EncryptionKey{
78+
{
79+
KeySlot: 0,
80+
KeyTPM: &blockcfg.EncryptionKeyTPM{},
81+
},
82+
{
83+
KeySlot: 1,
84+
KeyStatic: &blockcfg.EncryptionKeyStatic{KeyData: "secret"},
85+
},
86+
},
87+
}
7188

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

77-
ctr, err := container.New(uv1, uv2, uv3, sv1)
94+
ctr, err := container.New(uvPart1, uvPart2, uvDir1, uvDisk1, sv1)
7895
suite.Require().NoError(err)
7996

8097
cfg := config.NewMachineConfig(ctr)
8198
suite.Create(cfg)
8299

83100
userVolumes := []string{
84-
constants.UserVolumePrefix + "data1",
85-
constants.UserVolumePrefix + "data2",
86-
constants.UserVolumePrefix + "data3",
101+
constants.UserVolumePrefix + "data-part1",
102+
constants.UserVolumePrefix + "data-part2",
103+
constants.UserVolumePrefix + "data-dir1",
104+
constants.UserVolumePrefix + "data-disk1",
87105
}
88106

89107
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
@@ -104,7 +122,7 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
104122
asrt.Equal(block.VolumeTypeDirectory, vc.TypedSpec().Type)
105123
}
106124

107-
asrt.Contains([]string{"data1", "data2", "data3"}, vc.TypedSpec().Mount.TargetPath)
125+
asrt.Contains(userVolumes, vc.TypedSpec().Mount.TargetPath)
108126
asrt.Equal(constants.UserVolumeMountPoint, vc.TypedSpec().Mount.ParentID)
109127

110128
switch vc.Metadata().ID() {
@@ -143,8 +161,8 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
143161
suite.AddFinalizer(block.NewVolumeMountRequest(block.NamespaceName, volumeID).Metadata(), "test")
144162
}
145163

146-
// drop the first volume
147-
ctr, err = container.New(uv2)
164+
// keep only the first volume
165+
ctr, err = container.New(uvPart1)
148166
suite.Require().NoError(err)
149167

150168
newCfg := config.NewMachineConfig(ctr)
@@ -153,32 +171,32 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
153171

154172
// controller should tear down removed resources
155173
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
156-
if vc.Metadata().ID() == userVolumes[1] {
174+
if vc.Metadata().ID() == userVolumes[0] {
157175
asrt.Equal(resource.PhaseRunning, vc.Metadata().Phase())
158176
} else {
159177
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
160178
}
161179
})
162180

163181
ctest.AssertResources(suite, userVolumes, func(vmr *block.VolumeMountRequest, asrt *assert.Assertions) {
164-
if vmr.Metadata().ID() == userVolumes[1] {
182+
if vmr.Metadata().ID() == userVolumes[0] {
165183
asrt.Equal(resource.PhaseRunning, vmr.Metadata().Phase())
166184
} else {
167185
asrt.Equal(resource.PhaseTearingDown, vmr.Metadata().Phase())
168186
}
169187
})
170188

171189
// remove finalizers
172-
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[0]).Metadata(), "test")
173-
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[0]).Metadata(), "test")
174-
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolumes[2]).Metadata(), "test")
175-
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolumes[2]).Metadata(), "test")
190+
for _, userVolume := range userVolumes[1:] {
191+
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolume).Metadata(), "test")
192+
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolume).Metadata(), "test")
193+
}
176194

177195
// now the resources should be removed
178-
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[0])
179-
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[0])
180-
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolumes[2])
181-
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolumes[2])
196+
for _, userVolume := range userVolumes[1:] {
197+
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolume)
198+
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolume)
199+
}
182200
}
183201

184202
func (suite *UserVolumeConfigSuite) TestReconcileRawVolumes() {

0 commit comments

Comments
 (0)