Skip to content

Commit 0d8c022

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 7bf3aac commit 0d8c022

File tree

15 files changed

+510
-111
lines changed

15 files changed

+510
-111
lines changed

hack/release.toml

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

158168
[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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
127127
return fmt.Errorf("no disks matched selector for volume")
128128
}
129129

130+
if volumeType == block.VolumeTypeDisk && len(matchedDisks) > 1 {
131+
return fmt.Errorf("multiple disks matched selector for disk volume; matched disks: %v", matchedDisks)
132+
}
133+
130134
logger.Debug("matched disks", zap.Strings("disks", matchedDisks))
131135

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

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 = diskSelector
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: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/cosi-project/runtime/pkg/resource"
12+
"github.com/siderolabs/gen/xslices"
1213
"github.com/siderolabs/go-pointer"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/suite"
@@ -40,18 +41,26 @@ func TestUserVolumeConfigSuite(t *testing.T) {
4041
}
4142

4243
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{
44+
userVolumeNames := []string{
45+
"data-part1",
46+
"data-part2",
47+
"data-dir1",
48+
"data-disk1",
49+
}
50+
51+
uvPart1 := blockcfg.NewUserVolumeConfigV1Alpha1()
52+
uvPart1.MetaName = userVolumeNames[0]
53+
suite.Require().NoError(uvPart1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`system_disk`)))
54+
uvPart1.ProvisioningSpec.ProvisioningMinSize = blockcfg.MustByteSize("10GiB")
55+
uvPart1.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("100GiB")
56+
uvPart1.FilesystemSpec.FilesystemType = block.FilesystemTypeXFS
57+
58+
uvPart2 := blockcfg.NewUserVolumeConfigV1Alpha1()
59+
uvPart2.MetaName = userVolumeNames[1]
60+
uvPart2.VolumeType = pointer.To(block.VolumeTypePartition)
61+
suite.Require().NoError(uvPart2.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
62+
uvPart2.ProvisioningSpec.ProvisioningMaxSize = blockcfg.MustByteSize("1TiB")
63+
uvPart2.EncryptionSpec = blockcfg.EncryptionSpec{
5564
EncryptionProvider: block.EncryptionProviderLUKS2,
5665
EncryptionKeys: []blockcfg.EncryptionKey{
5766
{
@@ -65,32 +74,45 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
6574
},
6675
}
6776

68-
uv3 := blockcfg.NewUserVolumeConfigV1Alpha1()
69-
uv3.MetaName = "data3"
70-
uv3.VolumeType = pointer.To(block.VolumeTypeDirectory)
77+
uvDir1 := blockcfg.NewUserVolumeConfigV1Alpha1()
78+
uvDir1.MetaName = userVolumeNames[2]
79+
uvDir1.VolumeType = pointer.To(block.VolumeTypeDirectory)
80+
81+
uvDisk1 := blockcfg.NewUserVolumeConfigV1Alpha1()
82+
uvDisk1.MetaName = userVolumeNames[3]
83+
suite.Require().NoError(uvDisk1.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`!system_disk`)))
84+
uvDisk1.EncryptionSpec = blockcfg.EncryptionSpec{
85+
EncryptionProvider: block.EncryptionProviderLUKS2,
86+
EncryptionKeys: []blockcfg.EncryptionKey{
87+
{
88+
KeySlot: 0,
89+
KeyTPM: &blockcfg.EncryptionKeyTPM{},
90+
},
91+
{
92+
KeySlot: 1,
93+
KeyStatic: &blockcfg.EncryptionKeyStatic{KeyData: "secret"},
94+
},
95+
},
96+
}
7197

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

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

80106
cfg := config.NewMachineConfig(ctr)
81107
suite.Create(cfg)
82108

83-
userVolumes := []string{
84-
constants.UserVolumePrefix + "data1",
85-
constants.UserVolumePrefix + "data2",
86-
constants.UserVolumePrefix + "data3",
87-
}
109+
userVolumes := xslices.Map(userVolumeNames, func(in string) string { return constants.UserVolumePrefix + in })
88110

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

92114
switch vc.Metadata().ID() {
93-
case userVolumes[0], userVolumes[1]:
115+
case userVolumes[0], userVolumes[1], userVolumes[3]:
94116
asrt.Equal(block.VolumeTypePartition, vc.TypedSpec().Type)
95117

96118
asrt.Contains(userVolumes, vc.TypedSpec().Provisioning.PartitionSpec.Label)
@@ -104,7 +126,7 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
104126
asrt.Equal(block.VolumeTypeDirectory, vc.TypedSpec().Type)
105127
}
106128

107-
asrt.Contains([]string{"data1", "data2", "data3"}, vc.TypedSpec().Mount.TargetPath)
129+
asrt.Contains(userVolumeNames, vc.TypedSpec().Mount.TargetPath)
108130
asrt.Equal(constants.UserVolumeMountPoint, vc.TypedSpec().Mount.ParentID)
109131

110132
switch vc.Metadata().ID() {
@@ -143,8 +165,8 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
143165
suite.AddFinalizer(block.NewVolumeMountRequest(block.NamespaceName, volumeID).Metadata(), "test")
144166
}
145167

146-
// drop the first volume
147-
ctr, err = container.New(uv2)
168+
// keep only the first volume
169+
ctr, err = container.New(uvPart1)
148170
suite.Require().NoError(err)
149171

150172
newCfg := config.NewMachineConfig(ctr)
@@ -153,32 +175,32 @@ func (suite *UserVolumeConfigSuite) TestReconcileUserVolumesSwapVolumes() {
153175

154176
// controller should tear down removed resources
155177
ctest.AssertResources(suite, userVolumes, func(vc *block.VolumeConfig, asrt *assert.Assertions) {
156-
if vc.Metadata().ID() == userVolumes[1] {
178+
if vc.Metadata().ID() == userVolumes[0] {
157179
asrt.Equal(resource.PhaseRunning, vc.Metadata().Phase())
158180
} else {
159181
asrt.Equal(resource.PhaseTearingDown, vc.Metadata().Phase())
160182
}
161183
})
162184

163185
ctest.AssertResources(suite, userVolumes, func(vmr *block.VolumeMountRequest, asrt *assert.Assertions) {
164-
if vmr.Metadata().ID() == userVolumes[1] {
186+
if vmr.Metadata().ID() == userVolumes[0] {
165187
asrt.Equal(resource.PhaseRunning, vmr.Metadata().Phase())
166188
} else {
167189
asrt.Equal(resource.PhaseTearingDown, vmr.Metadata().Phase())
168190
}
169191
})
170192

171193
// 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")
194+
for _, userVolume := range userVolumes[1:] {
195+
suite.RemoveFinalizer(block.NewVolumeConfig(block.NamespaceName, userVolume).Metadata(), "test")
196+
suite.RemoveFinalizer(block.NewVolumeMountRequest(block.NamespaceName, userVolume).Metadata(), "test")
197+
}
176198

177199
// 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])
200+
for _, userVolume := range userVolumes[1:] {
201+
ctest.AssertNoResource[*block.VolumeConfig](suite, userVolume)
202+
ctest.AssertNoResource[*block.VolumeMountRequest](suite, userVolume)
203+
}
182204
}
183205

184206
func (suite *UserVolumeConfigSuite) TestReconcileRawVolumes() {

0 commit comments

Comments
 (0)