feat: add configuration for EPHEMERAL volume
Fixes #9261 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
parent
faffa4c3f1
commit
3038ccfa88
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@ -1,6 +1,6 @@
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
|
||||
#
|
||||
# Generated on 2024-09-03T14:18:03Z by kres b5ca957.
|
||||
# Generated on 2024-09-05T10:32:02Z by kres b5ca957.
|
||||
|
||||
name: default
|
||||
concurrency:
|
||||
@ -2556,6 +2556,10 @@ jobs:
|
||||
- name: e2e-qemu
|
||||
env:
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
|
||||
run: |
|
||||
sudo -E make e2e-qemu
|
||||
- name: save artifacts
|
||||
@ -2855,6 +2859,10 @@ jobs:
|
||||
- name: e2e-qemu
|
||||
env:
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
|
||||
WITH_DISK_ENCRYPTION: "true"
|
||||
WITH_KUBESPAN: "true"
|
||||
WITH_VIRTUAL_IP: "true"
|
||||
|
6
.github/workflows/integration-qemu-cron.yaml
vendored
6
.github/workflows/integration-qemu-cron.yaml
vendored
@ -1,6 +1,6 @@
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
|
||||
#
|
||||
# Generated on 2024-05-27T16:20:10Z by kres bcb280a.
|
||||
# Generated on 2024-09-04T16:05:13Z by kres b5ca957.
|
||||
|
||||
name: integration-qemu-cron
|
||||
concurrency:
|
||||
@ -77,6 +77,10 @@ jobs:
|
||||
- name: e2e-qemu
|
||||
env:
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
|
||||
run: |
|
||||
sudo -E make e2e-qemu
|
||||
- name: save artifacts
|
||||
|
@ -1,6 +1,6 @@
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
|
||||
#
|
||||
# Generated on 2024-05-27T16:20:10Z by kres bcb280a.
|
||||
# Generated on 2024-09-05T10:32:02Z by kres b5ca957.
|
||||
|
||||
name: integration-qemu-encrypted-vip-cron
|
||||
concurrency:
|
||||
@ -77,6 +77,10 @@ jobs:
|
||||
- name: e2e-qemu
|
||||
env:
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
|
||||
WITH_DISK_ENCRYPTION: "true"
|
||||
WITH_KUBESPAN: "true"
|
||||
WITH_VIRTUAL_IP: "true"
|
||||
|
@ -325,6 +325,10 @@ spec:
|
||||
withSudo: true
|
||||
environment:
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: "ide,nvme"
|
||||
WITH_CONFIG_PATCH_WORKER: "@hack/test/patches/ephemeral-nvme.yaml"
|
||||
- name: save-talos-logs
|
||||
conditions:
|
||||
- always
|
||||
@ -1098,6 +1102,10 @@ spec:
|
||||
WITH_VIRTUAL_IP: true
|
||||
WITH_KUBESPAN: true
|
||||
IMAGE_REGISTRY: registry.dev.siderolabs.io
|
||||
QEMU_EXTRA_DISKS: "2"
|
||||
QEMU_EXTRA_DISKS_SIZE: "10240"
|
||||
QEMU_EXTRA_DISKS_DRIVERS: "ide,nvme"
|
||||
WITH_CONFIG_PATCH_WORKER: "@hack/test/patches/ephemeral-nvme.yaml"
|
||||
- name: save-talos-logs
|
||||
conditions:
|
||||
- always
|
||||
|
@ -164,5 +164,6 @@ message VolumeStatusSpec {
|
||||
talos.resource.definitions.enums.BlockFilesystemType filesystem = 10;
|
||||
string mount_location = 11;
|
||||
talos.resource.definitions.enums.BlockEncryptionProviderType encryption_provider = 12;
|
||||
string pretty_size = 13;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/block"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/network"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/runtime"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/runtime/extensions"
|
||||
@ -130,6 +131,10 @@ var docsCmd = &cobra.Command{
|
||||
name: "security",
|
||||
fileDoc: security.GetFileDoc(),
|
||||
},
|
||||
{
|
||||
name: "block",
|
||||
fileDoc: block.GetFileDoc(),
|
||||
},
|
||||
} {
|
||||
path := filepath.Join(dir, pkg.name)
|
||||
|
||||
|
@ -223,6 +223,12 @@ This can be also explicitly enabled by setting `talos.halt_if_installed=1` in ke
|
||||
description = """\
|
||||
Talos Linux installer now never wipes the system disk on upgrades, which means that the flag
|
||||
`--preserve` is always set for `talosctl upgrade`.
|
||||
"""
|
||||
|
||||
[notes.disk-management]
|
||||
title = "Disk Management"
|
||||
description = """\
|
||||
Talos Linux now supports [configuration](https://www.talos.dev/v1.8/talos-guides/configuration/disk-management/#machine-configuration) for the `EPHEMERAL` volume.
|
||||
"""
|
||||
|
||||
[make_deps]
|
||||
|
@ -196,6 +196,7 @@ function create_cluster {
|
||||
--disk=15360 \
|
||||
--extra-disks="${QEMU_EXTRA_DISKS:-0}" \
|
||||
--extra-disks-size="${QEMU_EXTRA_DISKS_SIZE:-5120}" \
|
||||
--extra-disks-drivers="${QEMU_EXTRA_DISKS_DRIVERS:-}" \
|
||||
--mtu=1430 \
|
||||
--memory=2048 \
|
||||
--memory-workers="${QEMU_MEMORY_WORKERS:-2048}" \
|
||||
|
8
hack/test/patches/ephemeral-nvme.yaml
Normal file
8
hack/test/patches/ephemeral-nvme.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL
|
||||
provisioning:
|
||||
diskSelector:
|
||||
match: disk.transport == 'nvme'
|
||||
minSize: 4GB
|
||||
maxSize: 8GB
|
@ -14,7 +14,6 @@ import (
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/safe"
|
||||
"github.com/cosi-project/runtime/pkg/state"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/siderolabs/gen/maps"
|
||||
"github.com/siderolabs/go-blockdevice/v2/blkid"
|
||||
"github.com/siderolabs/go-blockdevice/v2/partitioning"
|
||||
@ -208,8 +207,7 @@ func (ctrl *DiscoveryController) rescan(ctx context.Context, r controller.Runtim
|
||||
dv.TypedSpec().Parent = device.TypedSpec().Parent
|
||||
dv.TypedSpec().ParentDevPath = filepath.Join("/dev", device.TypedSpec().Parent)
|
||||
|
||||
dv.TypedSpec().Size = info.Size
|
||||
dv.TypedSpec().PrettySize = humanize.Bytes(info.Size)
|
||||
dv.TypedSpec().SetSize(info.Size)
|
||||
dv.TypedSpec().SectorSize = info.SectorSize
|
||||
dv.TypedSpec().IOSize = info.IOSize
|
||||
|
||||
@ -232,14 +230,12 @@ func (ctrl *DiscoveryController) rescan(ctx context.Context, r controller.Runtim
|
||||
dv.TypedSpec().Parent = id
|
||||
dv.TypedSpec().ParentDevPath = filepath.Join("/dev", id)
|
||||
|
||||
dv.TypedSpec().Size = nested.ProbedSize
|
||||
|
||||
if dv.TypedSpec().Size == 0 {
|
||||
dv.TypedSpec().Size = nested.PartitionSize
|
||||
if nested.ProbedSize != 0 {
|
||||
dv.TypedSpec().SetSize(nested.ProbedSize)
|
||||
} else {
|
||||
dv.TypedSpec().SetSize(nested.PartitionSize)
|
||||
}
|
||||
|
||||
dv.TypedSpec().PrettySize = humanize.Bytes(dv.TypedSpec().Size)
|
||||
|
||||
dv.TypedSpec().SectorSize = info.SectorSize
|
||||
dv.TypedSpec().IOSize = info.IOSize
|
||||
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/cosi-project/runtime/pkg/controller"
|
||||
"github.com/cosi-project/runtime/pkg/safe"
|
||||
"github.com/dustin/go-humanize"
|
||||
blkdev "github.com/siderolabs/go-blockdevice/v2/block"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -157,9 +156,9 @@ func (ctrl *DisksController) analyzeBlockDevice(ctx context.Context, r controlle
|
||||
touchedDisks[device.Metadata().ID()] = struct{}{}
|
||||
|
||||
return safe.WriterModify(ctx, r, block.NewDisk(block.NamespaceName, device.Metadata().ID()), func(d *block.Disk) error {
|
||||
d.TypedSpec().SetSize(size)
|
||||
|
||||
d.TypedSpec().DevPath = filepath.Join("/dev", device.Metadata().ID())
|
||||
d.TypedSpec().Size = size
|
||||
d.TypedSpec().PrettySize = humanize.Bytes(size)
|
||||
d.TypedSpec().IOSize = ioSize
|
||||
d.TypedSpec().SectorSize = sectorSize
|
||||
d.TypedSpec().Readonly = readOnly
|
||||
|
@ -84,7 +84,7 @@ func Grow(ctx context.Context, logger *zap.Logger, volumeContext ManagerContext)
|
||||
}
|
||||
|
||||
volumeContext.Status.Phase = block.VolumePhaseProvisioned
|
||||
volumeContext.Status.Size += availableGrowth
|
||||
volumeContext.Status.SetSize(volumeContext.Status.Size + availableGrowth)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
|
||||
|
||||
volumeContext.Status.UUID = dv.Uuid
|
||||
volumeContext.Status.PartitionUUID = dv.PartitionUuid
|
||||
volumeContext.Status.Size = dv.Size
|
||||
volumeContext.Status.SetSize(dv.Size)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -138,7 +138,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
|
||||
volumeContext.Status.Phase = block.VolumePhaseProvisioned
|
||||
volumeContext.Status.Location = pickedDisk
|
||||
volumeContext.Status.ParentLocation = ""
|
||||
volumeContext.Status.Size = diskCheckResult.DiskSize
|
||||
volumeContext.Status.SetSize(diskCheckResult.DiskSize)
|
||||
case block.VolumeTypePartition:
|
||||
// we need to create a partition on the disk
|
||||
partitionCreateResult, err := CreatePartition(ctx, logger, pickedDisk, volumeContext.Cfg, diskCheckResult.HasGPT)
|
||||
@ -151,7 +151,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
|
||||
volumeContext.Status.PartitionIndex = partitionCreateResult.PartitionIdx
|
||||
volumeContext.Status.ParentLocation = pickedDisk
|
||||
volumeContext.Status.PartitionUUID = partitionCreateResult.Partition.PartGUID.String()
|
||||
volumeContext.Status.Size = partitionCreateResult.Size
|
||||
volumeContext.Status.SetSize(partitionCreateResult.Size)
|
||||
default:
|
||||
panic("unexpected volume type")
|
||||
}
|
||||
|
@ -204,16 +204,19 @@ func (ctrl *VolumeConfigController) Run(ctx context.Context, r controller.Runtim
|
||||
|
||||
func (ctrl *VolumeConfigController) manageEphemeral(config cfg.Config) func(vc *block.VolumeConfig) error {
|
||||
return func(vc *block.VolumeConfig) error {
|
||||
extraVolumeConfig := config.Volumes().ByName(constants.EphemeralPartitionLabel)
|
||||
|
||||
vc.TypedSpec().Type = block.VolumeTypePartition
|
||||
|
||||
vc.TypedSpec().Provisioning = block.ProvisioningSpec{
|
||||
Wave: block.WaveSystemDisk,
|
||||
DiskSelector: block.DiskSelector{
|
||||
Match: systemDiskMatch(),
|
||||
Match: extraVolumeConfig.Provisioning().DiskSelector().ValueOr(systemDiskMatch()),
|
||||
},
|
||||
PartitionSpec: block.PartitionSpec{
|
||||
MinSize: partition.EphemeralMinSize,
|
||||
Grow: true,
|
||||
MinSize: extraVolumeConfig.Provisioning().MinSize().ValueOr(partition.EphemeralMinSize),
|
||||
MaxSize: extraVolumeConfig.Provisioning().MaxSize().ValueOr(0),
|
||||
Grow: extraVolumeConfig.Provisioning().Grow().ValueOr(true),
|
||||
Label: constants.EphemeralPartitionLabel,
|
||||
TypeUUID: partition.LinuxFilesystemData,
|
||||
},
|
||||
|
@ -10,12 +10,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/siderolabs/go-pointer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
blockctrls "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block"
|
||||
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
|
||||
"github.com/siderolabs/talos/internal/pkg/partition"
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel"
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/container"
|
||||
blockcfg "github.com/siderolabs/talos/pkg/machinery/config/types/block"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
"github.com/siderolabs/talos/pkg/machinery/meta"
|
||||
@ -105,6 +110,14 @@ func (suite *VolumeConfigSuite) TestReconcileDefaults() {
|
||||
asrt.NoError(err)
|
||||
asrt.Equal(`volume.partition_label == "EPHEMERAL"`, string(locator))
|
||||
|
||||
locator, err = r.TypedSpec().Provisioning.DiskSelector.Match.MarshalText()
|
||||
asrt.NoError(err)
|
||||
asrt.Equal(`system_disk`, string(locator))
|
||||
|
||||
asrt.True(r.TypedSpec().Provisioning.PartitionSpec.Grow)
|
||||
asrt.EqualValues(0, r.TypedSpec().Provisioning.PartitionSpec.MaxSize)
|
||||
asrt.EqualValues(partition.EphemeralMinSize, r.TypedSpec().Provisioning.PartitionSpec.MinSize)
|
||||
|
||||
asrt.Equal(constants.EphemeralMountPoint, r.TypedSpec().Mount.TargetPath)
|
||||
})
|
||||
}
|
||||
@ -198,3 +211,52 @@ func (suite *VolumeConfigSuite) TestReconcileEncryptedSTATE() {
|
||||
asrt.Empty(r.TypedSpec().Encryption)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *VolumeConfigSuite) TestReconcileExtraEPHEMERALConfig() {
|
||||
ctest.AssertNoResource[*block.VolumeConfig](suite, constants.EphemeralPartitionLabel)
|
||||
|
||||
u, err := url.Parse("https://foo:6443")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
ctr, err := container.New(
|
||||
&v1alpha1.Config{
|
||||
ConfigVersion: "v1alpha1",
|
||||
MachineConfig: &v1alpha1.MachineConfig{},
|
||||
ClusterConfig: &v1alpha1.ClusterConfig{
|
||||
ControlPlane: &v1alpha1.ControlPlaneConfig{
|
||||
Endpoint: &v1alpha1.Endpoint{
|
||||
URL: u,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&blockcfg.VolumeConfigV1Alpha1{
|
||||
MetaName: constants.EphemeralPartitionLabel,
|
||||
ProvisioningSpec: blockcfg.ProvisioningSpec{
|
||||
DiskSelectorSpec: blockcfg.DiskSelector{
|
||||
Match: cel.MustExpression(cel.ParseBooleanExpression(`disk.transport == "nvme"`, celenv.DiskLocator())),
|
||||
},
|
||||
ProvisioningGrow: pointer.To(false),
|
||||
ProvisioningMaxSize: blockcfg.MustByteSize("2.5TiB"),
|
||||
},
|
||||
},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cfg := config.NewMachineConfig(ctr)
|
||||
suite.Require().NoError(suite.State().Create(suite.Ctx(), cfg))
|
||||
|
||||
// now the volume config should be created
|
||||
ctest.AssertResource(suite, constants.EphemeralPartitionLabel, func(r *block.VolumeConfig, asrt *assert.Assertions) {
|
||||
asrt.NotEmpty(r.TypedSpec().Provisioning)
|
||||
asrt.Empty(r.TypedSpec().Encryption)
|
||||
|
||||
locator, err := r.TypedSpec().Provisioning.DiskSelector.Match.MarshalText()
|
||||
asrt.NoError(err)
|
||||
asrt.Equal(`disk.transport == "nvme"`, string(locator))
|
||||
|
||||
asrt.False(r.TypedSpec().Provisioning.PartitionSpec.Grow)
|
||||
asrt.EqualValues(2.5*1024*1024*1024*1024, r.TypedSpec().Provisioning.PartitionSpec.MaxSize)
|
||||
asrt.EqualValues(partition.EphemeralMinSize, r.TypedSpec().Provisioning.PartitionSpec.MinSize)
|
||||
})
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
pprocfs "github.com/prometheus/procfs"
|
||||
"github.com/siderolabs/gen/maps"
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
"github.com/siderolabs/go-blockdevice/v2/blkid"
|
||||
"github.com/siderolabs/go-blockdevice/v2/block"
|
||||
@ -1501,29 +1502,68 @@ func ResetSystemDiskPartitions(seq runtime.Sequence, _ any) (runtime.TaskExecuti
|
||||
}
|
||||
|
||||
// ResetSystemDisk represents the task to reset the system disk.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func ResetSystemDisk(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
|
||||
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) error {
|
||||
systemDisks := map[string]struct{}{}
|
||||
|
||||
// fetch system disk (where Talos is installed)
|
||||
systemDisk, err := blockres.GetSystemDisk(ctx, r.State().V1Alpha2().Resources())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if systemDisk == nil {
|
||||
if systemDisk != nil {
|
||||
systemDisks[systemDisk.DevPath] = struct{}{}
|
||||
}
|
||||
|
||||
// fetch additional system volumes (which might be on the same or other disks)
|
||||
for _, volumeID := range []string{constants.StatePartitionLabel, constants.EphemeralPartitionLabel} {
|
||||
volumeStatus, err := safe.ReaderGetByID[*blockres.VolumeStatus](ctx, r.State().V1Alpha2().Resources(), volumeID)
|
||||
if err != nil {
|
||||
if state.IsNotFoundError(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if volumeStatus.TypedSpec().ParentLocation != "" {
|
||||
systemDisks[volumeStatus.TypedSpec().ParentLocation] = struct{}{}
|
||||
} else if volumeStatus.TypedSpec().Location != "" {
|
||||
systemDisks[volumeStatus.TypedSpec().Location] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(systemDisks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dev, err := block.NewFromPath(systemDisk.DevPath, block.OpenForWrite())
|
||||
if err != nil {
|
||||
return err
|
||||
systemDiskPaths := maps.Keys(systemDisks)
|
||||
|
||||
for _, systemDiskPath := range systemDiskPaths {
|
||||
if err := func(devPath string) error {
|
||||
logger.Printf("wiping system disk %s", devPath)
|
||||
|
||||
dev, err := block.NewFromPath(devPath, block.OpenForWrite())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = dev.RetryLockWithTimeout(ctx, true, time.Minute); err != nil {
|
||||
return fmt.Errorf("failed to lock device %s: %w", systemDisk.DevPath, err)
|
||||
}
|
||||
|
||||
defer dev.Close() //nolint:errcheck
|
||||
|
||||
return dev.FastWipe()
|
||||
}(systemDiskPath); err != nil {
|
||||
return fmt.Errorf("failed to wipe system disk %s: %w", systemDiskPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = dev.RetryLockWithTimeout(ctx, true, time.Minute); err != nil {
|
||||
return fmt.Errorf("failed to lock device %s: %w", systemDisk.DevPath, err)
|
||||
}
|
||||
|
||||
defer dev.Close() //nolint:errcheck
|
||||
|
||||
return dev.FastWipe()
|
||||
return nil
|
||||
}, "resetSystemDisk"
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ func (suite *ResetSuite) TestResetWithSpecStateAndUserDisks() {
|
||||
switch {
|
||||
case disk.SystemDisk:
|
||||
return false
|
||||
case disk.Type == storage.Disk_UNKNOWN, disk.Type == storage.Disk_CD, disk.Type == storage.Disk_SD:
|
||||
case disk.Type == storage.Disk_UNKNOWN, disk.Type == storage.Disk_CD, disk.Type == storage.Disk_SD, disk.Type == storage.Disk_NVME:
|
||||
return false
|
||||
case disk.Readonly:
|
||||
return false
|
||||
|
@ -71,7 +71,7 @@ func (suite *VolumesSuite) testDiscoveredVolumes(node string) {
|
||||
Names: []string{"xfs"},
|
||||
},
|
||||
"EPHEMERAL": {
|
||||
Names: []string{"xfs"},
|
||||
Names: []string{"xfs", ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1306,6 +1306,7 @@ type VolumeStatusSpec struct {
|
||||
Filesystem enums.BlockFilesystemType `protobuf:"varint,10,opt,name=filesystem,proto3,enum=talos.resource.definitions.enums.BlockFilesystemType" json:"filesystem,omitempty"`
|
||||
MountLocation string `protobuf:"bytes,11,opt,name=mount_location,json=mountLocation,proto3" json:"mount_location,omitempty"`
|
||||
EncryptionProvider enums.BlockEncryptionProviderType `protobuf:"varint,12,opt,name=encryption_provider,json=encryptionProvider,proto3,enum=talos.resource.definitions.enums.BlockEncryptionProviderType" json:"encryption_provider,omitempty"`
|
||||
PrettySize string `protobuf:"bytes,13,opt,name=pretty_size,json=prettySize,proto3" json:"pretty_size,omitempty"`
|
||||
}
|
||||
|
||||
func (x *VolumeStatusSpec) Reset() {
|
||||
@ -1424,6 +1425,13 @@ func (x *VolumeStatusSpec) GetEncryptionProvider() enums.BlockEncryptionProvider
|
||||
return enums.BlockEncryptionProviderType(0)
|
||||
}
|
||||
|
||||
func (x *VolumeStatusSpec) GetPrettySize() string {
|
||||
if x != nil {
|
||||
return x.PrettySize
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_resource_definitions_block_block_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_resource_definitions_block_block_proto_rawDesc = []byte{
|
||||
@ -1641,7 +1649,7 @@ var file_resource_definitions_block_block_proto_rawDesc = []byte{
|
||||
0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x62, 0x6c, 0x6f, 0x63,
|
||||
0x6b, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63,
|
||||
0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x05, 0x0a,
|
||||
0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa7, 0x05, 0x0a,
|
||||
0x10, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65,
|
||||
0x63, 0x12, 0x48, 0x0a, 0x05, 0x70, 0x68, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x32, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
|
||||
@ -1682,15 +1690,17 @@ var file_resource_definitions_block_block_proto_rawDesc = []byte{
|
||||
0x65, 0x6e, 0x75, 0x6d, 0x73, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x63, 0x72, 0x79,
|
||||
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70,
|
||||
0x65, 0x52, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f,
|
||||
0x76, 0x69, 0x64, 0x65, 0x72, 0x42, 0x74, 0x0a, 0x28, 0x64, 0x65, 0x76, 0x2e, 0x74, 0x61, 0x6c,
|
||||
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x62, 0x6c, 0x6f, 0x63,
|
||||
0x6b, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69,
|
||||
0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70,
|
||||
0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69,
|
||||
0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f,
|
||||
0x73, 0x69, 0x7a, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x74,
|
||||
0x74, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x74, 0x0a, 0x28, 0x64, 0x65, 0x76, 0x2e, 0x74, 0x61,
|
||||
0x6c, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x62, 0x6c, 0x6f,
|
||||
0x63, 0x6b, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73,
|
||||
0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f,
|
||||
0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70,
|
||||
0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e,
|
||||
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -1159,6 +1159,13 @@ func (m *VolumeStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||
i -= len(m.unknownFields)
|
||||
copy(dAtA[i:], m.unknownFields)
|
||||
}
|
||||
if len(m.PrettySize) > 0 {
|
||||
i -= len(m.PrettySize)
|
||||
copy(dAtA[i:], m.PrettySize)
|
||||
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.PrettySize)))
|
||||
i--
|
||||
dAtA[i] = 0x6a
|
||||
}
|
||||
if m.EncryptionProvider != 0 {
|
||||
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.EncryptionProvider))
|
||||
i--
|
||||
@ -1738,6 +1745,10 @@ func (m *VolumeStatusSpec) SizeVT() (n int) {
|
||||
if m.EncryptionProvider != 0 {
|
||||
n += 1 + protohelpers.SizeOfVarint(uint64(m.EncryptionProvider))
|
||||
}
|
||||
l = len(m.PrettySize)
|
||||
if l > 0 {
|
||||
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||
}
|
||||
n += len(m.unknownFields)
|
||||
return n
|
||||
}
|
||||
@ -5035,6 +5046,38 @@ func (m *VolumeStatusSpec) UnmarshalVT(dAtA []byte) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 13:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field PrettySize", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return protohelpers.ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return protohelpers.ErrInvalidLength
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return protohelpers.ErrInvalidLength
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.PrettySize = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
|
||||
|
@ -43,25 +43,58 @@ func MustExpression(expr Expression, err error) Expression {
|
||||
|
||||
// ParseBooleanExpression parses the expression and asserts the result to boolean.
|
||||
func ParseBooleanExpression(expression string, env *cel.Env) (Expression, error) {
|
||||
ast, issues := env.Parse(expression)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
return Expression{}, issues.Err()
|
||||
}
|
||||
|
||||
ast, issues = env.Check(ast)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
return Expression{}, issues.Err()
|
||||
}
|
||||
|
||||
if outputType := ast.OutputType(); !outputType.IsExactType(types.BoolType) {
|
||||
return Expression{}, fmt.Errorf("expression output type is %s, expected bool", outputType)
|
||||
ast, err := parseBooleanExpression(expression, env)
|
||||
if err != nil {
|
||||
return Expression{}, err
|
||||
}
|
||||
|
||||
return Expression{ast: ast}, nil
|
||||
}
|
||||
|
||||
// parseBooleanExpression parses the expression and asserts the result to boolean.
|
||||
func parseBooleanExpression(expression string, env *cel.Env) (*cel.Ast, error) {
|
||||
ast, issues := env.Parse(expression)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
return nil, issues.Err()
|
||||
}
|
||||
|
||||
ast, issues = env.Check(ast)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
return nil, issues.Err()
|
||||
}
|
||||
|
||||
if outputType := ast.OutputType(); !outputType.IsExactType(types.BoolType) {
|
||||
return nil, fmt.Errorf("expression output type is %s, expected bool", outputType)
|
||||
}
|
||||
|
||||
return ast, nil
|
||||
}
|
||||
|
||||
// ParseBool parses the expression and asserts the result to boolean.
|
||||
//
|
||||
// ParseBoolean can be used after unmarshaling the expression from text.
|
||||
func (expr *Expression) ParseBool(env *cel.Env) error {
|
||||
if expr.ast != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if expr.expression == nil {
|
||||
panic("expression is not set")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
expr.ast, err = parseBooleanExpression(*expr.expression, env)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// EvalBool evaluates the expression in the given environment.
|
||||
func (expr Expression) EvalBool(env *cel.Env, values map[string]any) (bool, error) {
|
||||
if err := expr.ParseBool(env); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
prog, err := env.Program(expr.ast)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -7,9 +7,11 @@ package cel_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/block"
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel"
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
|
||||
)
|
||||
@ -64,3 +66,66 @@ func TestCELMarshal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCELEvalFromYAML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := celenv.DiskLocator()
|
||||
|
||||
type yamlRaw struct {
|
||||
Expr string `yaml:"expr,omitempty"`
|
||||
}
|
||||
|
||||
type yamlTest struct {
|
||||
Expr cel.Expression `yaml:"expr,omitempty"`
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
expression string
|
||||
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "consts",
|
||||
|
||||
expression: "1u * GiB < 2u * GiB",
|
||||
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "vars",
|
||||
|
||||
expression: "!system_disk",
|
||||
|
||||
expected: false,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
yamlRaw := yamlRaw{
|
||||
Expr: test.expression,
|
||||
}
|
||||
|
||||
marshaled, err := yaml.Marshal(yamlRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
var yamlTest yamlTest
|
||||
|
||||
err = yaml.Unmarshal(marshaled, &yamlTest)
|
||||
require.NoError(t, err)
|
||||
|
||||
val, err := yamlTest.Expr.EvalBool(env, map[string]any{
|
||||
"system_disk": true,
|
||||
"disk": block.DiskSpec{
|
||||
Size: 1024,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, val)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,12 @@
|
||||
package celenv
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/block"
|
||||
)
|
||||
@ -19,9 +21,14 @@ var DiskLocator = sync.OnceValue(func() *cel.Env {
|
||||
var diskSpec block.DiskSpec
|
||||
|
||||
env, err := cel.NewEnv(
|
||||
cel.Types(&diskSpec),
|
||||
cel.Variable("disk", cel.ObjectType(string(diskSpec.ProtoReflect().Descriptor().FullName()))),
|
||||
cel.Variable("system_disk", types.BoolType),
|
||||
slices.Concat(
|
||||
[]cel.EnvOption{
|
||||
cel.Types(&diskSpec),
|
||||
cel.Variable("disk", cel.ObjectType(string(diskSpec.ProtoReflect().Descriptor().FullName()))),
|
||||
cel.Variable("system_disk", types.BoolType),
|
||||
},
|
||||
celUnitMultipliersConstants(),
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -35,8 +42,13 @@ var VolumeLocator = sync.OnceValue(func() *cel.Env {
|
||||
var volumeSpec block.DiscoveredVolumeSpec
|
||||
|
||||
env, err := cel.NewEnv(
|
||||
cel.Types(&volumeSpec),
|
||||
cel.Variable("volume", cel.ObjectType(string(volumeSpec.ProtoReflect().Descriptor().FullName()))),
|
||||
slices.Concat(
|
||||
[]cel.EnvOption{
|
||||
cel.Types(&volumeSpec),
|
||||
cel.Variable("volume", cel.ObjectType(string(volumeSpec.ProtoReflect().Descriptor().FullName()))),
|
||||
},
|
||||
celUnitMultipliersConstants(),
|
||||
)...,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -44,3 +56,31 @@ var VolumeLocator = sync.OnceValue(func() *cel.Env {
|
||||
|
||||
return env
|
||||
})
|
||||
|
||||
type unitMultiplier struct {
|
||||
unit string
|
||||
multiplier uint64
|
||||
}
|
||||
|
||||
var unitMultipliers = []unitMultiplier{
|
||||
// IEC.
|
||||
{"KiB", 1024},
|
||||
{"MiB", 1024 * 1024},
|
||||
{"GiB", 1024 * 1024 * 1024},
|
||||
{"TiB", 1024 * 1024 * 1024 * 1024},
|
||||
{"PiB", 1024 * 1024 * 1024 * 1024 * 1024},
|
||||
{"EiB", 1024 * 1024 * 1024 * 1024 * 1024 * 1024},
|
||||
// Metric (used for disk sizes).
|
||||
{"kB", 1000},
|
||||
{"MB", 1000 * 1000},
|
||||
{"GB", 1000 * 1000 * 1000},
|
||||
{"TB", 1000 * 1000 * 1000 * 1000},
|
||||
{"PB", 1000 * 1000 * 1000 * 1000 * 1000},
|
||||
{"EB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000},
|
||||
}
|
||||
|
||||
func celUnitMultipliersConstants() []cel.EnvOption {
|
||||
return xslices.Map(unitMultipliers, func(um unitMultiplier) cel.EnvOption {
|
||||
return cel.Constant(um.unit, types.UintType, types.Uint(um.multiplier))
|
||||
})
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ func TestDiskLocator(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "disk size",
|
||||
expression: "disk.size > 1000u && !disk.rotational",
|
||||
expression: "disk.size > 1000u * GiB && !disk.rotational",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@ -53,6 +53,10 @@ func TestVolumeLocator(t *testing.T) {
|
||||
name: "by label",
|
||||
expression: "volume.label == 'EPHEMERAL'",
|
||||
},
|
||||
{
|
||||
name: "by filesystem and size",
|
||||
expression: "volume.name == 'ext4' && volume.size > 1000u * TB",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -15,4 +15,5 @@ type Config interface {
|
||||
Runtime() RuntimeConfig
|
||||
NetworkRules() NetworkRuleConfig
|
||||
TrustedRoots() TrustedRootsConfig
|
||||
Volumes() VolumesConfig
|
||||
}
|
||||
|
76
pkg/machinery/config/config/volume.go
Normal file
76
pkg/machinery/config/config/volume.go
Normal file
@ -0,0 +1,76 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/siderolabs/gen/optional"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel"
|
||||
)
|
||||
|
||||
// VolumesConfig defines the interface to access volume configuration.
|
||||
type VolumesConfig interface {
|
||||
// ByName returns a volume config configuration by name.
|
||||
//
|
||||
// If the configuration is missing, the method a stub which returns implements 'nothing is set' stub.
|
||||
ByName(name string) VolumeConfig
|
||||
}
|
||||
|
||||
// VolumeConfig defines the interface to access volume configuration.
|
||||
type VolumeConfig interface {
|
||||
NamedDocument
|
||||
Provisioning() VolumeProvisioningConfig
|
||||
}
|
||||
|
||||
// VolumeProvisioningConfig defines the interface to access volume provisioning configuration.
|
||||
type VolumeProvisioningConfig interface {
|
||||
DiskSelector() optional.Optional[cel.Expression]
|
||||
Grow() optional.Optional[bool]
|
||||
MinSize() optional.Optional[uint64]
|
||||
MaxSize() optional.Optional[uint64]
|
||||
}
|
||||
|
||||
// WrapVolumesConfigList wraps a list of VolumeConfig providing access by name.
|
||||
func WrapVolumesConfigList(configs ...VolumeConfig) VolumesConfig {
|
||||
return volumesConfigWrapper(configs)
|
||||
}
|
||||
|
||||
type volumesConfigWrapper []VolumeConfig
|
||||
|
||||
func (w volumesConfigWrapper) ByName(name string) VolumeConfig {
|
||||
for _, doc := range w {
|
||||
if doc.Name() == name {
|
||||
return doc
|
||||
}
|
||||
}
|
||||
|
||||
return emptyVolumeConfig{}
|
||||
}
|
||||
|
||||
type emptyVolumeConfig struct{}
|
||||
|
||||
func (emptyVolumeConfig) Name() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (emptyVolumeConfig) Provisioning() VolumeProvisioningConfig {
|
||||
return emptyVolumeConfig{}
|
||||
}
|
||||
|
||||
func (emptyVolumeConfig) DiskSelector() optional.Optional[cel.Expression] {
|
||||
return optional.None[cel.Expression]()
|
||||
}
|
||||
|
||||
func (emptyVolumeConfig) Grow() optional.Optional[bool] {
|
||||
return optional.None[bool]()
|
||||
}
|
||||
|
||||
func (emptyVolumeConfig) MinSize() optional.Optional[uint64] {
|
||||
return optional.None[uint64]()
|
||||
}
|
||||
|
||||
func (emptyVolumeConfig) MaxSize() optional.Optional[uint64] {
|
||||
return optional.None[uint64]()
|
||||
}
|
@ -194,6 +194,11 @@ func (container *Container) TrustedRoots() config.TrustedRootsConfig {
|
||||
return config.WrapTrustedRootsConfig(findMatchingDocs[config.TrustedRootsConfig](container.documents)...)
|
||||
}
|
||||
|
||||
// Volumes implements config.Config interface.
|
||||
func (container *Container) Volumes() config.VolumesConfig {
|
||||
return config.WrapVolumesConfigList(findMatchingDocs[config.VolumeConfig](container.documents)...)
|
||||
}
|
||||
|
||||
// Bytes returns source YAML representation (if available) or does default encoding.
|
||||
func (container *Container) Bytes() ([]byte, error) {
|
||||
if !container.readonly {
|
||||
|
@ -2,6 +2,95 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://talos.dev/v1.8/schemas/config.schema.json",
|
||||
"$defs": {
|
||||
"block.DiskSelector": {
|
||||
"properties": {
|
||||
"match": {
|
||||
"type": "string",
|
||||
"title": "match",
|
||||
"description": "The Common Expression Language (CEL) expression to match the disk.\n",
|
||||
"markdownDescription": "The Common Expression Language (CEL) expression to match the disk.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe Common Expression Language (CEL) expression to match the disk.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"block.ProvisioningSpec": {
|
||||
"properties": {
|
||||
"diskSelector": {
|
||||
"$ref": "#/$defs/block.DiskSelector",
|
||||
"title": "diskSelector",
|
||||
"description": "The disk selector expression.\n",
|
||||
"markdownDescription": "The disk selector expression.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe disk selector expression.\u003c/p\u003e\n"
|
||||
},
|
||||
"grow": {
|
||||
"type": "boolean",
|
||||
"title": "grow",
|
||||
"description": "Should the volume grow to the size of the disk (if possible).\n",
|
||||
"markdownDescription": "Should the volume grow to the size of the disk (if possible).",
|
||||
"x-intellij-html-description": "\u003cp\u003eShould the volume grow to the size of the disk (if possible).\u003c/p\u003e\n"
|
||||
},
|
||||
"minSize": {
|
||||
"type": "string",
|
||||
"title": "minSize",
|
||||
"description": "The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n",
|
||||
"markdownDescription": "The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe minimum size of the volume.\u003c/p\u003e\n\n\u003cp\u003eSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\u003c/p\u003e\n"
|
||||
},
|
||||
"maxSize": {
|
||||
"type": "string",
|
||||
"title": "maxSize",
|
||||
"description": "The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n",
|
||||
"markdownDescription": "The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\u003c/p\u003e\n\n\u003cp\u003eSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"block.VolumeConfigV1Alpha1": {
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": [
|
||||
"v1alpha1"
|
||||
],
|
||||
"title": "apiVersion",
|
||||
"description": "apiVersion is the API version of the resource.\n",
|
||||
"markdownDescription": "apiVersion is the API version of the resource.",
|
||||
"x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n"
|
||||
},
|
||||
"kind": {
|
||||
"enum": [
|
||||
"VolumeConfig"
|
||||
],
|
||||
"title": "kind",
|
||||
"description": "kind is the kind of the resource.\n",
|
||||
"markdownDescription": "kind is the kind of the resource.",
|
||||
"x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "name",
|
||||
"description": "Name of the volume.\n",
|
||||
"markdownDescription": "Name of the volume.",
|
||||
"x-intellij-html-description": "\u003cp\u003eName of the volume.\u003c/p\u003e\n"
|
||||
},
|
||||
"provisioning": {
|
||||
"$ref": "#/$defs/block.ProvisioningSpec",
|
||||
"title": "provisioning",
|
||||
"description": "The provisioning describes how the volume is provisioned.\n",
|
||||
"markdownDescription": "The provisioning describes how the volume is provisioned.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe provisioning describes how the volume is provisioned.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiVersion",
|
||||
"kind"
|
||||
]
|
||||
},
|
||||
"extensions.ConfigFile": {
|
||||
"properties": {
|
||||
"content": {
|
||||
@ -3471,6 +3560,9 @@
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/block.VolumeConfigV1Alpha1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/extensions.ServiceConfigV1Alpha1"
|
||||
},
|
||||
|
10
pkg/machinery/config/types/block/block.go
Normal file
10
pkg/machinery/config/types/block/block.go
Normal file
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package block provides block device and volume configuration documents.
|
||||
package block
|
||||
|
||||
//go:generate docgen -output block_doc.go block.go volume_config.go
|
||||
|
||||
//go:generate deep-copy -type VolumeConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go .
|
130
pkg/machinery/config/types/block/block_doc.go
Normal file
130
pkg/machinery/config/types/block/block_doc.go
Normal file
@ -0,0 +1,130 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Code generated by hack/docgen tool. DO NOT EDIT.
|
||||
|
||||
package block
|
||||
|
||||
import (
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
)
|
||||
|
||||
func (VolumeConfigV1Alpha1) Doc() *encoder.Doc {
|
||||
doc := &encoder.Doc{
|
||||
Type: "VolumeConfig",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "VolumeConfig is a volume configuration document." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
Description: "VolumeConfig is a volume configuration document.",
|
||||
Fields: []encoder.Doc{
|
||||
{},
|
||||
{
|
||||
Name: "name",
|
||||
Type: "string",
|
||||
Note: "",
|
||||
Description: "Name of the volume.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "Name of the volume." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
{
|
||||
Name: "provisioning",
|
||||
Type: "ProvisioningSpec",
|
||||
Note: "",
|
||||
Description: "The provisioning describes how the volume is provisioned.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "The provisioning describes how the volume is provisioned." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
doc.AddExample("", exampleVolumeConfigEphemeralV1Alpha1())
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func (ProvisioningSpec) Doc() *encoder.Doc {
|
||||
doc := &encoder.Doc{
|
||||
Type: "ProvisioningSpec",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "ProvisioningSpec describes how the volume is provisioned." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
Description: "ProvisioningSpec describes how the volume is provisioned.",
|
||||
AppearsIn: []encoder.Appearance{
|
||||
{
|
||||
TypeName: "VolumeConfigV1Alpha1",
|
||||
FieldName: "provisioning",
|
||||
},
|
||||
},
|
||||
Fields: []encoder.Doc{
|
||||
{
|
||||
Name: "diskSelector",
|
||||
Type: "DiskSelector",
|
||||
Note: "",
|
||||
Description: "The disk selector expression.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "The disk selector expression." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
{
|
||||
Name: "grow",
|
||||
Type: "bool",
|
||||
Note: "",
|
||||
Description: "Should the volume grow to the size of the disk (if possible).",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "Should the volume grow to the size of the disk (if possible)." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
{
|
||||
Name: "minSize",
|
||||
Type: "ByteSize",
|
||||
Note: "",
|
||||
Description: "The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "The minimum size of the volume." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
{
|
||||
Name: "maxSize",
|
||||
Type: "ByteSize",
|
||||
Note: "",
|
||||
Description: "The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "The maximum size of the volume, if not specified the volume can grow to the size of the" /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
doc.Fields[2].AddExample("", "2.5GiB")
|
||||
doc.Fields[3].AddExample("", "50GiB")
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func (DiskSelector) Doc() *encoder.Doc {
|
||||
doc := &encoder.Doc{
|
||||
Type: "DiskSelector",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "DiskSelector selects a disk for the volume." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
Description: "DiskSelector selects a disk for the volume.",
|
||||
AppearsIn: []encoder.Appearance{
|
||||
{
|
||||
TypeName: "ProvisioningSpec",
|
||||
FieldName: "diskSelector",
|
||||
},
|
||||
},
|
||||
Fields: []encoder.Doc{
|
||||
{
|
||||
Name: "match",
|
||||
Type: "Expression",
|
||||
Note: "",
|
||||
Description: "The Common Expression Language (CEL) expression to match the disk.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "The Common Expression Language (CEL) expression to match the disk." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
doc.Fields[0].AddExample("match disks with size between 120GB and 1TB", exampleDiskSelector1())
|
||||
doc.Fields[0].AddExample("match SATA disks that are not rotational and not system disks", exampleDiskSelector2())
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
// GetFileDoc returns documentation for the file block_doc.go.
|
||||
func GetFileDoc() *encoder.FileDoc {
|
||||
return &encoder.FileDoc{
|
||||
Name: "block",
|
||||
Description: "Package block provides block device and volume configuration documents.\n",
|
||||
Structs: []*encoder.Doc{
|
||||
VolumeConfigV1Alpha1{}.Doc(),
|
||||
ProvisioningSpec{}.Doc(),
|
||||
DiskSelector{}.Doc(),
|
||||
},
|
||||
}
|
||||
}
|
82
pkg/machinery/config/types/block/byte_size.go
Normal file
82
pkg/machinery/config/types/block/byte_size.go
Normal file
@ -0,0 +1,82 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package block
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/siderolabs/go-pointer"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Check interfaces.
|
||||
var (
|
||||
_ encoding.TextMarshaler = ByteSize{}
|
||||
_ encoding.TextUnmarshaler = (*ByteSize)(nil)
|
||||
_ yaml.IsZeroer = ByteSize{}
|
||||
)
|
||||
|
||||
// ByteSize is a byte size which can be convienintly represented as a human readable string
|
||||
// with IEC sizes, e.g. 100MB.
|
||||
type ByteSize struct {
|
||||
value *uint64
|
||||
raw []byte
|
||||
}
|
||||
|
||||
// MustByteSize returns a new ByteSize with the given value.
|
||||
//
|
||||
// It panics if the value is invalid.
|
||||
func MustByteSize(value string) ByteSize {
|
||||
var bs ByteSize
|
||||
|
||||
if err := bs.UnmarshalText([]byte(value)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return bs
|
||||
}
|
||||
|
||||
// Value returns the value.
|
||||
func (bs ByteSize) Value() uint64 {
|
||||
return pointer.SafeDeref(bs.value)
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (bs ByteSize) MarshalText() ([]byte, error) {
|
||||
if bs.raw != nil {
|
||||
return bs.raw, nil
|
||||
}
|
||||
|
||||
if bs.value != nil {
|
||||
return []byte(strconv.FormatUint(*bs.value, 10)), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (bs *ByteSize) UnmarshalText(text []byte) error {
|
||||
if len(text) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, err := humanize.ParseBytes(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bs.value = pointer.To(value)
|
||||
bs.raw = slices.Clone(text)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZero implements yaml.IsZeroer.
|
||||
func (bs ByteSize) IsZero() bool {
|
||||
return bs.value == nil && bs.raw == nil
|
||||
}
|
46
pkg/machinery/config/types/block/bytes_size_test.go
Normal file
46
pkg/machinery/config/types/block/bytes_size_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package block_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/block"
|
||||
)
|
||||
|
||||
func TestByteSizeUnmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
|
||||
want uint64
|
||||
}{
|
||||
{in: "", want: 0},
|
||||
{in: "1048576", want: 1048576},
|
||||
{in: "2.5GiB", want: 2684354560},
|
||||
{in: "2.5GB", want: 2500000000},
|
||||
{in: "2.5G", want: 2500000000},
|
||||
{in: "1MiB", want: 1048576},
|
||||
} {
|
||||
t.Run(test.in, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var bs block.ByteSize
|
||||
|
||||
require.NoError(t, bs.UnmarshalText([]byte(test.in)))
|
||||
|
||||
assert.Equal(t, test.want, bs.Value())
|
||||
|
||||
out, err := bs.MarshalText()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.in, string(out))
|
||||
})
|
||||
}
|
||||
}
|
33
pkg/machinery/config/types/block/deep_copy.generated.go
Normal file
33
pkg/machinery/config/types/block/deep_copy.generated.go
Normal file
@ -0,0 +1,33 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Code generated by "deep-copy -type VolumeConfigV1Alpha1 -pointer-receiver -header-file ../../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
|
||||
|
||||
package block
|
||||
|
||||
// DeepCopy generates a deep copy of *VolumeConfigV1Alpha1.
|
||||
func (o *VolumeConfigV1Alpha1) DeepCopy() *VolumeConfigV1Alpha1 {
|
||||
var cp VolumeConfigV1Alpha1 = *o
|
||||
if o.ProvisioningSpec.ProvisioningGrow != nil {
|
||||
cp.ProvisioningSpec.ProvisioningGrow = new(bool)
|
||||
*cp.ProvisioningSpec.ProvisioningGrow = *o.ProvisioningSpec.ProvisioningGrow
|
||||
}
|
||||
if o.ProvisioningSpec.ProvisioningMinSize.value != nil {
|
||||
cp.ProvisioningSpec.ProvisioningMinSize.value = new(uint64)
|
||||
*cp.ProvisioningSpec.ProvisioningMinSize.value = *o.ProvisioningSpec.ProvisioningMinSize.value
|
||||
}
|
||||
if o.ProvisioningSpec.ProvisioningMinSize.raw != nil {
|
||||
cp.ProvisioningSpec.ProvisioningMinSize.raw = make([]byte, len(o.ProvisioningSpec.ProvisioningMinSize.raw))
|
||||
copy(cp.ProvisioningSpec.ProvisioningMinSize.raw, o.ProvisioningSpec.ProvisioningMinSize.raw)
|
||||
}
|
||||
if o.ProvisioningSpec.ProvisioningMaxSize.value != nil {
|
||||
cp.ProvisioningSpec.ProvisioningMaxSize.value = new(uint64)
|
||||
*cp.ProvisioningSpec.ProvisioningMaxSize.value = *o.ProvisioningSpec.ProvisioningMaxSize.value
|
||||
}
|
||||
if o.ProvisioningSpec.ProvisioningMaxSize.raw != nil {
|
||||
cp.ProvisioningSpec.ProvisioningMaxSize.raw = make([]byte, len(o.ProvisioningSpec.ProvisioningMaxSize.raw))
|
||||
copy(cp.ProvisioningSpec.ProvisioningMaxSize.raw, o.ProvisioningSpec.ProvisioningMaxSize.raw)
|
||||
}
|
||||
return &cp
|
||||
}
|
6
pkg/machinery/config/types/block/testdata/volumeconfig_diskselector.yaml
vendored
Normal file
6
pkg/machinery/config/types/block/testdata/volumeconfig_diskselector.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL
|
||||
provisioning:
|
||||
diskSelector:
|
||||
match: disk.transport == "nvme" && !system_disk
|
3
pkg/machinery/config/types/block/testdata/volumeconfig_empty.yaml
vendored
Normal file
3
pkg/machinery/config/types/block/testdata/volumeconfig_empty.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL
|
6
pkg/machinery/config/types/block/testdata/volumeconfig_maxsize.yaml
vendored
Normal file
6
pkg/machinery/config/types/block/testdata/volumeconfig_maxsize.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL
|
||||
provisioning:
|
||||
minSize: 10GiB
|
||||
maxSize: 2.5TiB
|
214
pkg/machinery/config/types/block/volume_config.go
Normal file
214
pkg/machinery/config/types/block/volume_config.go
Normal file
@ -0,0 +1,214 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package block
|
||||
|
||||
//docgen:jsonschema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/siderolabs/gen/optional"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel"
|
||||
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/internal/registry"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/meta"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/validation"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
// VolumeConfigKind is a config document kind.
|
||||
const VolumeConfigKind = "VolumeConfig"
|
||||
|
||||
func init() {
|
||||
registry.Register(VolumeConfigKind, func(version string) config.Document {
|
||||
switch version {
|
||||
case "v1alpha1":
|
||||
return &VolumeConfigV1Alpha1{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check interfaces.
|
||||
var (
|
||||
_ config.VolumeConfig = &VolumeConfigV1Alpha1{}
|
||||
_ config.NamedDocument = &VolumeConfigV1Alpha1{}
|
||||
_ config.Validator = &VolumeConfigV1Alpha1{}
|
||||
)
|
||||
|
||||
// VolumeConfigV1Alpha1 is a volume configuration document.
|
||||
//
|
||||
// Note: at the moment, only EPHEMERAL volumes are supported.
|
||||
//
|
||||
// examples:
|
||||
// - value: exampleVolumeConfigEphemeralV1Alpha1()
|
||||
// alias: VolumeConfig
|
||||
// schemaRoot: true
|
||||
// schemaMeta: v1alpha1/VolumeConfig
|
||||
type VolumeConfigV1Alpha1 struct {
|
||||
meta.Meta `yaml:",inline"`
|
||||
// description: |
|
||||
// Name of the volume.
|
||||
MetaName string `yaml:"name"`
|
||||
// description: |
|
||||
// The provisioning describes how the volume is provisioned.
|
||||
ProvisioningSpec ProvisioningSpec `yaml:"provisioning,omitempty"`
|
||||
}
|
||||
|
||||
// ProvisioningSpec describes how the volume is provisioned.
|
||||
type ProvisioningSpec struct {
|
||||
// description: |
|
||||
// The disk selector expression.
|
||||
DiskSelectorSpec DiskSelector `yaml:"diskSelector,omitempty"`
|
||||
// description: |
|
||||
// Should the volume grow to the size of the disk (if possible).
|
||||
ProvisioningGrow *bool `yaml:"grow,omitempty"`
|
||||
// description: |
|
||||
// The minimum size of the volume.
|
||||
//
|
||||
// Size is specified in bytes, but can be expressed in human readable format, e.g. 100MB.
|
||||
// examples:
|
||||
// - value: >
|
||||
// "2.5GiB"
|
||||
// schema:
|
||||
// type: string
|
||||
ProvisioningMinSize ByteSize `yaml:"minSize,omitempty"`
|
||||
// description: |
|
||||
// The maximum size of the volume, if not specified the volume can grow to the size of the
|
||||
// disk.
|
||||
//
|
||||
// Size is specified in bytes, but can be expressed in human readable format, e.g. 100MB.
|
||||
// examples:
|
||||
// - value: >
|
||||
// "50GiB"
|
||||
// schema:
|
||||
// type: string
|
||||
ProvisioningMaxSize ByteSize `yaml:"maxSize,omitempty"`
|
||||
}
|
||||
|
||||
// DiskSelector selects a disk for the volume.
|
||||
type DiskSelector struct {
|
||||
// description: |
|
||||
// The Common Expression Language (CEL) expression to match the disk.
|
||||
// schema:
|
||||
// type: string
|
||||
// examples:
|
||||
// - value: >
|
||||
// exampleDiskSelector1()
|
||||
// name: match disks with size between 120GB and 1TB
|
||||
// - value: >
|
||||
// exampleDiskSelector2()
|
||||
// name: match SATA disks that are not rotational and not system disks
|
||||
Match cel.Expression `yaml:"match,omitempty"`
|
||||
}
|
||||
|
||||
// NewVolumeConfigV1Alpha1 creates a new volume config document.
|
||||
func NewVolumeConfigV1Alpha1() *VolumeConfigV1Alpha1 {
|
||||
return &VolumeConfigV1Alpha1{
|
||||
Meta: meta.Meta{
|
||||
MetaKind: VolumeConfigKind,
|
||||
MetaAPIVersion: "v1alpha1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func exampleVolumeConfigEphemeralV1Alpha1() *VolumeConfigV1Alpha1 {
|
||||
cfg := NewVolumeConfigV1Alpha1()
|
||||
cfg.MetaName = constants.EphemeralPartitionLabel
|
||||
cfg.ProvisioningSpec = ProvisioningSpec{
|
||||
DiskSelectorSpec: DiskSelector{
|
||||
Match: cel.MustExpression(cel.ParseBooleanExpression(`disk.transport == "nvme"`, celenv.DiskLocator())),
|
||||
},
|
||||
ProvisioningMaxSize: MustByteSize("50GiB"),
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func exampleDiskSelector1() cel.Expression {
|
||||
return cel.MustExpression(cel.ParseBooleanExpression(`disk.size > 120u * GB && disk.size < 1u * TB`, celenv.DiskLocator()))
|
||||
}
|
||||
|
||||
func exampleDiskSelector2() cel.Expression {
|
||||
return cel.MustExpression(cel.ParseBooleanExpression(`disk.transport == "sata" && !disk.rotational && !system_disk`, celenv.DiskLocator()))
|
||||
}
|
||||
|
||||
// Name implements config.NamedDocument interface.
|
||||
func (s *VolumeConfigV1Alpha1) Name() string {
|
||||
return s.MetaName
|
||||
}
|
||||
|
||||
// Clone implements config.Document interface.
|
||||
func (s *VolumeConfigV1Alpha1) Clone() config.Document {
|
||||
return s.DeepCopy()
|
||||
}
|
||||
|
||||
// Validate implements config.Validator interface.
|
||||
func (s *VolumeConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation.Option) ([]string, error) {
|
||||
if s.MetaName != constants.EphemeralPartitionLabel {
|
||||
return nil, errors.New("only EPHEMERAL volumes are supported")
|
||||
}
|
||||
|
||||
var validationErrors error
|
||||
|
||||
if !s.ProvisioningSpec.DiskSelectorSpec.Match.IsZero() {
|
||||
if err := s.ProvisioningSpec.DiskSelectorSpec.Match.ParseBool(celenv.DiskLocator()); err != nil {
|
||||
validationErrors = errors.Join(validationErrors, fmt.Errorf("disk selector is invalid: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if !s.ProvisioningSpec.ProvisioningMinSize.IsZero() && !s.ProvisioningSpec.ProvisioningMaxSize.IsZero() {
|
||||
if s.ProvisioningSpec.ProvisioningMinSize.Value() > s.ProvisioningSpec.ProvisioningMaxSize.Value() {
|
||||
validationErrors = errors.Join(validationErrors, errors.New("min size is greater than max size"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil, validationErrors
|
||||
}
|
||||
|
||||
// Provisioning implements config.VolumeConfig interface.
|
||||
func (s *VolumeConfigV1Alpha1) Provisioning() config.VolumeProvisioningConfig {
|
||||
return s.ProvisioningSpec
|
||||
}
|
||||
|
||||
// DiskSelector implements config.VolumeProvisioningConfig interface.
|
||||
func (s ProvisioningSpec) DiskSelector() optional.Optional[cel.Expression] {
|
||||
if s.DiskSelectorSpec.Match.IsZero() {
|
||||
return optional.None[cel.Expression]()
|
||||
}
|
||||
|
||||
return optional.Some(s.DiskSelectorSpec.Match)
|
||||
}
|
||||
|
||||
// Grow implements config.VolumeProvisioningConfig interface.
|
||||
func (s ProvisioningSpec) Grow() optional.Optional[bool] {
|
||||
if s.ProvisioningGrow == nil {
|
||||
return optional.None[bool]()
|
||||
}
|
||||
|
||||
return optional.Some(*s.ProvisioningGrow)
|
||||
}
|
||||
|
||||
// MinSize implements config.VolumeProvisioningConfig interface.
|
||||
func (s ProvisioningSpec) MinSize() optional.Optional[uint64] {
|
||||
if s.ProvisioningMinSize.IsZero() {
|
||||
return optional.None[uint64]()
|
||||
}
|
||||
|
||||
return optional.Some(s.ProvisioningMinSize.Value())
|
||||
}
|
||||
|
||||
// MaxSize implements config.VolumeProvisioningConfig interface.
|
||||
func (s ProvisioningSpec) MaxSize() optional.Optional[uint64] {
|
||||
if s.ProvisioningMaxSize.IsZero() {
|
||||
return optional.None[uint64]()
|
||||
}
|
||||
|
||||
return optional.Some(s.ProvisioningMaxSize.Value())
|
||||
}
|
188
pkg/machinery/config/types/block/volume_config_test.go
Normal file
188
pkg/machinery/config/types/block/volume_config_test.go
Normal file
@ -0,0 +1,188 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package block_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/configloader"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/block"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
func TestVolumeConfigMarshalUnmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
filename string
|
||||
cfg func(t *testing.T) *block.VolumeConfigV1Alpha1
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
filename: "volumeconfig_empty.yaml",
|
||||
cfg: func(*testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
return c
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "disk selector",
|
||||
filename: "volumeconfig_diskselector.yaml",
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
require.NoError(t, c.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`disk.transport == "nvme" && !system_disk`)))
|
||||
|
||||
return c
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max size",
|
||||
filename: "volumeconfig_maxsize.yaml",
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
c.ProvisioningSpec.ProvisioningMaxSize = block.MustByteSize("2.5TiB")
|
||||
c.ProvisioningSpec.ProvisioningMinSize = block.MustByteSize("10GiB")
|
||||
|
||||
return c
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := test.cfg(t)
|
||||
|
||||
marshaled, err := encoder.NewEncoder(cfg, encoder.WithComments(encoder.CommentsDisabled)).Encode()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(string(marshaled))
|
||||
|
||||
expectedMarshaled, err := os.ReadFile(filepath.Join("testdata", test.filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(expectedMarshaled), string(marshaled))
|
||||
|
||||
provider, err := configloader.NewFromBytes(expectedMarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
docs := provider.Documents()
|
||||
require.Len(t, docs, 1)
|
||||
|
||||
assert.Equal(t, cfg, docs[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
cfg func(t *testing.T) *block.VolumeConfigV1Alpha1
|
||||
|
||||
expectedErrors string
|
||||
}{
|
||||
{
|
||||
name: "wrong name",
|
||||
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = "wrong"
|
||||
|
||||
return c
|
||||
},
|
||||
|
||||
expectedErrors: "only EPHEMERAL volumes are supported",
|
||||
},
|
||||
{
|
||||
name: "invalid disk selector",
|
||||
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
require.NoError(t, c.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`disk.size > 120`)))
|
||||
|
||||
return c
|
||||
},
|
||||
|
||||
expectedErrors: "disk selector is invalid: ERROR: <input>:1:11: found no matching overload for '_>_' applied to '(uint, int)'\n | disk.size > 120\n | ..........^",
|
||||
},
|
||||
{
|
||||
name: "min size greater than max size",
|
||||
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
c.ProvisioningSpec.ProvisioningMinSize = block.MustByteSize("2.5TiB")
|
||||
c.ProvisioningSpec.ProvisioningMaxSize = block.MustByteSize("10GiB")
|
||||
|
||||
return c
|
||||
},
|
||||
|
||||
expectedErrors: "min size is greater than max size",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
|
||||
cfg: func(t *testing.T) *block.VolumeConfigV1Alpha1 {
|
||||
c := block.NewVolumeConfigV1Alpha1()
|
||||
c.MetaName = constants.EphemeralPartitionLabel
|
||||
|
||||
require.NoError(t, c.ProvisioningSpec.DiskSelectorSpec.Match.UnmarshalText([]byte(`disk.size > 120u * GiB`)))
|
||||
c.ProvisioningSpec.ProvisioningMaxSize = block.MustByteSize("2.5TiB")
|
||||
c.ProvisioningSpec.ProvisioningMinSize = block.MustByteSize("10GiB")
|
||||
|
||||
return c
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := test.cfg(t)
|
||||
|
||||
_, err := cfg.Validate(validationMode{})
|
||||
|
||||
if test.expectedErrors == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.EqualError(t, err, test.expectedErrors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type validationMode struct{}
|
||||
|
||||
func (validationMode) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (validationMode) RequiresInstall() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (validationMode) InContainer() bool {
|
||||
return false
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
_ "github.com/siderolabs/talos/pkg/machinery/config/types/block" // import config types to register them
|
||||
_ "github.com/siderolabs/talos/pkg/machinery/config/types/network" // import config types to register them
|
||||
_ "github.com/siderolabs/talos/pkg/machinery/config/types/runtime" // import config types to register them
|
||||
_ "github.com/siderolabs/talos/pkg/machinery/config/types/runtime/extensions" // import config types to register them
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
"github.com/cosi-project/runtime/pkg/resource/protobuf"
|
||||
"github.com/cosi-project/runtime/pkg/resource/typed"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/proto"
|
||||
)
|
||||
@ -53,6 +54,12 @@ type DiscoveredVolumeSpec struct {
|
||||
PartitionIndex uint `yaml:"partition_index,omitempty" protobuf:"13"`
|
||||
}
|
||||
|
||||
// SetSize sets the size of the DiscoveredVolume, including the pretty size.
|
||||
func (s *DiscoveredVolumeSpec) SetSize(size uint64) {
|
||||
s.Size = size
|
||||
s.PrettySize = humanize.Bytes(size)
|
||||
}
|
||||
|
||||
// NewDiscoveredVolume initializes a BlockDiscoveredVolume resource.
|
||||
func NewDiscoveredVolume(namespace resource.Namespace, id resource.ID) *DiscoveredVolume {
|
||||
return typed.NewResource[DiscoveredVolumeSpec, DiscoveredVolumeExtension](
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
"github.com/cosi-project/runtime/pkg/resource/protobuf"
|
||||
"github.com/cosi-project/runtime/pkg/resource/typed"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/proto"
|
||||
)
|
||||
@ -43,6 +44,12 @@ type DiskSpec struct {
|
||||
Rotational bool `yaml:"rotational,omitempty" protobuf:"12"`
|
||||
}
|
||||
|
||||
// SetSize sets the size of the disk, including the pretty size.
|
||||
func (s *DiskSpec) SetSize(size uint64) {
|
||||
s.Size = size
|
||||
s.PrettySize = humanize.Bytes(size)
|
||||
}
|
||||
|
||||
// NewDisk initializes a BlockDisk resource.
|
||||
func NewDisk(namespace resource.Namespace, id resource.ID) *Disk {
|
||||
return typed.NewResource[DiskSpec, DiskExtension](
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/cosi-project/runtime/pkg/resource/meta"
|
||||
"github.com/cosi-project/runtime/pkg/resource/protobuf"
|
||||
"github.com/cosi-project/runtime/pkg/resource/typed"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/proto"
|
||||
)
|
||||
@ -38,6 +39,7 @@ type VolumeStatusSpec struct {
|
||||
UUID string `yaml:"uuid,omitempty" protobuf:"4"`
|
||||
PartitionUUID string `yaml:"partitionUUID,omitempty" protobuf:"5"`
|
||||
Size uint64 `yaml:"size,omitempty" protobuf:"9"`
|
||||
PrettySize string `yaml:"prettySize,omitempty" protobuf:"13"`
|
||||
|
||||
// Filesystem is the filesystem type.
|
||||
Filesystem FilesystemType `yaml:"filesystem,omitempty" protobuf:"10"`
|
||||
@ -48,6 +50,12 @@ type VolumeStatusSpec struct {
|
||||
ErrorMessage string `yaml:"errorMessage,omitempty" protobuf:"3"`
|
||||
}
|
||||
|
||||
// SetSize sets the size of the volume status, including the pretty size.
|
||||
func (s *VolumeStatusSpec) SetSize(size uint64) {
|
||||
s.Size = size
|
||||
s.PrettySize = humanize.Bytes(size)
|
||||
}
|
||||
|
||||
// NewVolumeStatus initializes a BlockVolumeStatus resource.
|
||||
func NewVolumeStatus(namespace resource.Namespace, id resource.ID) *VolumeStatus {
|
||||
return typed.NewResource[VolumeStatusSpec, VolumeStatusExtension](
|
||||
@ -74,6 +82,10 @@ func (VolumeStatusExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
|
||||
Name: "Location",
|
||||
JSONPath: `{.location}`,
|
||||
},
|
||||
{
|
||||
Name: "Size",
|
||||
JSONPath: `{.prettySize}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1108,6 +1108,7 @@ VolumeStatusSpec is the spec for VolumeStatus resource.
|
||||
| filesystem | [talos.resource.definitions.enums.BlockFilesystemType](#talos.resource.definitions.enums.BlockFilesystemType) | | |
|
||||
| mount_location | [string](#string) | | |
|
||||
| encryption_provider | [talos.resource.definitions.enums.BlockEncryptionProviderType](#talos.resource.definitions.enums.BlockEncryptionProviderType) | | |
|
||||
| pretty_size | [string](#string) | | |
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
description: |
|
||||
Package block provides block device and volume configuration documents.
|
||||
title: block
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable -->
|
||||
|
@ -0,0 +1,84 @@
|
||||
---
|
||||
description: VolumeConfig is a volume configuration document.
|
||||
title: VolumeConfig
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{< highlight yaml >}}
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL # Name of the volume.
|
||||
# The provisioning describes how the volume is provisioned.
|
||||
provisioning:
|
||||
# The disk selector expression.
|
||||
diskSelector:
|
||||
match: disk.transport == "nvme" # The Common Expression Language (CEL) expression to match the disk.
|
||||
maxSize: 50GiB # The maximum size of the volume, if not specified the volume can grow to the size of the
|
||||
|
||||
# # The minimum size of the volume.
|
||||
# minSize: 2.5GiB
|
||||
{{< /highlight >}}
|
||||
|
||||
|
||||
| Field | Type | Description | Value(s) |
|
||||
|-------|------|-------------|----------|
|
||||
|`name` |string |Name of the volume. | |
|
||||
|`provisioning` |<a href="#VolumeConfig.provisioning">ProvisioningSpec</a> |The provisioning describes how the volume is provisioned. | |
|
||||
|
||||
|
||||
|
||||
|
||||
## provisioning {#VolumeConfig.provisioning}
|
||||
|
||||
ProvisioningSpec describes how the volume is provisioned.
|
||||
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Value(s) |
|
||||
|-------|------|-------------|----------|
|
||||
|`diskSelector` |<a href="#VolumeConfig.provisioning.diskSelector">DiskSelector</a> |The disk selector expression. | |
|
||||
|`grow` |bool |Should the volume grow to the size of the disk (if possible). | |
|
||||
|`minSize` |ByteSize |<details><summary>The minimum size of the volume.</summary><br />Size is specified in bytes, but can be expressed in human readable format, e.g. 100MB.</details> <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
minSize: 2.5GiB
|
||||
{{< /highlight >}}</details> | |
|
||||
|`maxSize` |ByteSize |<details><summary>The maximum size of the volume, if not specified the volume can grow to the size of the</summary>disk.<br /><br />Size is specified in bytes, but can be expressed in human readable format, e.g. 100MB.</details> <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
maxSize: 50GiB
|
||||
{{< /highlight >}}</details> | |
|
||||
|
||||
|
||||
|
||||
|
||||
### diskSelector {#VolumeConfig.provisioning.diskSelector}
|
||||
|
||||
DiskSelector selects a disk for the volume.
|
||||
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Value(s) |
|
||||
|-------|------|-------------|----------|
|
||||
|`match` |Expression |The Common Expression Language (CEL) expression to match the disk. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|
||||
match: disk.size > 120u * GB && disk.size < 1u * TB
|
||||
{{< /highlight >}}{{< highlight yaml >}}
|
||||
match: disk.transport == "sata" && !disk.rotational && !system_disk
|
||||
{{< /highlight >}}</details> | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2,6 +2,95 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://talos.dev/v1.8/schemas/config.schema.json",
|
||||
"$defs": {
|
||||
"block.DiskSelector": {
|
||||
"properties": {
|
||||
"match": {
|
||||
"type": "string",
|
||||
"title": "match",
|
||||
"description": "The Common Expression Language (CEL) expression to match the disk.\n",
|
||||
"markdownDescription": "The Common Expression Language (CEL) expression to match the disk.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe Common Expression Language (CEL) expression to match the disk.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"block.ProvisioningSpec": {
|
||||
"properties": {
|
||||
"diskSelector": {
|
||||
"$ref": "#/$defs/block.DiskSelector",
|
||||
"title": "diskSelector",
|
||||
"description": "The disk selector expression.\n",
|
||||
"markdownDescription": "The disk selector expression.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe disk selector expression.\u003c/p\u003e\n"
|
||||
},
|
||||
"grow": {
|
||||
"type": "boolean",
|
||||
"title": "grow",
|
||||
"description": "Should the volume grow to the size of the disk (if possible).\n",
|
||||
"markdownDescription": "Should the volume grow to the size of the disk (if possible).",
|
||||
"x-intellij-html-description": "\u003cp\u003eShould the volume grow to the size of the disk (if possible).\u003c/p\u003e\n"
|
||||
},
|
||||
"minSize": {
|
||||
"type": "string",
|
||||
"title": "minSize",
|
||||
"description": "The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n",
|
||||
"markdownDescription": "The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe minimum size of the volume.\u003c/p\u003e\n\n\u003cp\u003eSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\u003c/p\u003e\n"
|
||||
},
|
||||
"maxSize": {
|
||||
"type": "string",
|
||||
"title": "maxSize",
|
||||
"description": "The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n",
|
||||
"markdownDescription": "The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\u003c/p\u003e\n\n\u003cp\u003eSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"block.VolumeConfigV1Alpha1": {
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": [
|
||||
"v1alpha1"
|
||||
],
|
||||
"title": "apiVersion",
|
||||
"description": "apiVersion is the API version of the resource.\n",
|
||||
"markdownDescription": "apiVersion is the API version of the resource.",
|
||||
"x-intellij-html-description": "\u003cp\u003eapiVersion is the API version of the resource.\u003c/p\u003e\n"
|
||||
},
|
||||
"kind": {
|
||||
"enum": [
|
||||
"VolumeConfig"
|
||||
],
|
||||
"title": "kind",
|
||||
"description": "kind is the kind of the resource.\n",
|
||||
"markdownDescription": "kind is the kind of the resource.",
|
||||
"x-intellij-html-description": "\u003cp\u003ekind is the kind of the resource.\u003c/p\u003e\n"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "name",
|
||||
"description": "Name of the volume.\n",
|
||||
"markdownDescription": "Name of the volume.",
|
||||
"x-intellij-html-description": "\u003cp\u003eName of the volume.\u003c/p\u003e\n"
|
||||
},
|
||||
"provisioning": {
|
||||
"$ref": "#/$defs/block.ProvisioningSpec",
|
||||
"title": "provisioning",
|
||||
"description": "The provisioning describes how the volume is provisioned.\n",
|
||||
"markdownDescription": "The provisioning describes how the volume is provisioned.",
|
||||
"x-intellij-html-description": "\u003cp\u003eThe provisioning describes how the volume is provisioned.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiVersion",
|
||||
"kind"
|
||||
]
|
||||
},
|
||||
"extensions.ConfigFile": {
|
||||
"properties": {
|
||||
"content": {
|
||||
@ -3471,6 +3560,9 @@
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/block.VolumeConfigV1Alpha1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/extensions.ServiceConfigV1Alpha1"
|
||||
},
|
||||
|
@ -3,12 +3,12 @@ title: "Disk Management"
|
||||
description: "Guide on managing disks"
|
||||
---
|
||||
|
||||
Talos Linux in version 1.8.0 provides a new backend to manage system and user disks.
|
||||
The machine configuration changes are minimal, and the new backend is fully compatible with the existing machine configuration.
|
||||
Talos Linux version 1.8.0 introduces a new backend for managing system and user disks.
|
||||
The machine configuration changes required are minimal, and the new backend is fully compatible with the existing machine configuration.
|
||||
|
||||
## Listing Disks
|
||||
|
||||
To list all disks (block devices) available on the machine, use the following command:
|
||||
To obtain a list of all available block devices (disks) on the machine, you can use the following command:
|
||||
|
||||
```bash
|
||||
$ talosctl get disks
|
||||
@ -21,7 +21,7 @@ NODE NAMESPACE TYPE ID VERSION SIZE READ ONLY TRANSPOR
|
||||
172.20.0.5 runtime Disk vda 1 13 GB false virtio true
|
||||
```
|
||||
|
||||
To get details about a specific disk, use the following command:
|
||||
To obtain detailed information about a specific disk, execute the following command:
|
||||
|
||||
```yaml
|
||||
# talosctl get disk sda -o yaml
|
||||
@ -53,8 +53,8 @@ spec:
|
||||
|
||||
## Discovering Volumes
|
||||
|
||||
Talos Linux watches all block devices and partitions on the machine.
|
||||
The information about all block devices, partitions, their type can be found in the `DiscoveredVolume` resource:
|
||||
Talos Linux monitors all block devices and partitions on the machine.
|
||||
Details about these devices, including their type, can be found in the `DiscoveredVolume` resource.
|
||||
|
||||
```bash
|
||||
$ talosctl get discoveredvolumes
|
||||
@ -79,8 +79,8 @@ NODE NAMESPACE TYPE ID VERSION TYPE SIZE
|
||||
172.20.0.5 runtime DiscoveredVolume vda6 1 partition 12 GB xfs EPHEMERAL EPHEMERAL
|
||||
```
|
||||
|
||||
Talos Linux automatically detects the filesystem type, GPT partition tables.
|
||||
The following filesystem types are detected at the moment:
|
||||
Talos Linux has built-in automatic detection for various filesystem types and GPT partition tables.
|
||||
Currently, the following filesystem types are supported:
|
||||
|
||||
- `bluestore` (Ceph)
|
||||
- `ext2`, `ext3`, `ext4`
|
||||
@ -94,15 +94,14 @@ The following filesystem types are detected at the moment:
|
||||
- `xfs`
|
||||
- `zfs`
|
||||
|
||||
The discovered volumes might include Talos-managed volumes, and also any other volumes that are present on the machine (e.g. Ceph volumes).
|
||||
The discovered volumes can include both Talos-managed volumes and any other volumes present on the machine, such as Ceph volumes.
|
||||
|
||||
## Volume Management
|
||||
|
||||
Talos Linux disk management is based on the volume concept: a volume is something that can be provisioned,
|
||||
located, mounted and unmounted.
|
||||
A volume can be a disk, a partition, a `tmpfs` filesystem, etc.
|
||||
Talos Linux implements disk management through the concept of volumes.
|
||||
A volume represents a provisioned, located, mounted, or unmounted entity, such as a disk, partition, or `tmpfs` filesystem.
|
||||
|
||||
Volume configuration is described in the `VolumeConfig` resource, while the actual volume state is stored in the `VolumeStatus` resource.
|
||||
The configuration of volumes is defined using the `VolumeConfig` resource, while the current state of volumes is stored in the `VolumeStatus` resource.
|
||||
|
||||
### Configuration
|
||||
|
||||
@ -120,7 +119,7 @@ NODE NAMESPACE TYPE ID
|
||||
172.20.0.5 runtime VolumeConfig STATE 4
|
||||
```
|
||||
|
||||
In the output above, `EPHEMERAL`, `META`, and `STATE` are Talos-managed system volumes, while the other volumes are based on the machine configuration for `machine.disks`.
|
||||
In the provided output, the volumes `EPHEMERAL`, `META`, and `STATE` are system volumes managed by Talos, while the remaining volumes are based on the machine configuration for `machine.disks`.
|
||||
|
||||
To get details about a specific volume configuration, use the following command:
|
||||
|
||||
@ -166,25 +165,116 @@ spec:
|
||||
|
||||
### Status
|
||||
|
||||
Current volume status can be checked using the following command:
|
||||
Current volume status can be obtained using the following command:
|
||||
|
||||
```bash
|
||||
$ talosctl get volumestatus
|
||||
NODE NAMESPACE TYPE ID VERSION PHASE LOCATION
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001-1 1 ready /dev/sdc1
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001-2 1 ready /dev/sdc2
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00003-1 1 ready /dev/sdd1
|
||||
172.20.0.5 runtime VolumeStatus EPHEMERAL 1 ready /dev/vda6
|
||||
172.20.0.5 runtime VolumeStatus META 2 ready /dev/vda4
|
||||
172.20.0.5 runtime VolumeStatus STATE 3 ready /dev/vda5
|
||||
NODE NAMESPACE TYPE ID VERSION PHASE LOCATION SIZE
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001-1 1 ready /dev/sdc1 957 MB
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001-2 1 ready /dev/sdc2 957 MB
|
||||
172.20.0.5 runtime VolumeStatus /dev/disk/by-id/ata-QEMU_HARDDISK_QM00003-1 1 ready /dev/sdd1 957 MB
|
||||
172.20.0.5 runtime VolumeStatus EPHEMERAL 1 ready /dev/nvme0n1p1 10 GB
|
||||
172.20.0.5 runtime VolumeStatus META 2 ready /dev/vda4 524 kB
|
||||
172.20.0.5 runtime VolumeStatus STATE 2 ready /dev/vda5 92 MB
|
||||
```
|
||||
|
||||
Each volume transitions through different phases during its lifecycle:
|
||||
Each volume goes through different phases during its lifecycle:
|
||||
|
||||
- `waiting`: the volume is waiting for provisioning
|
||||
- `missing`: all disks were discovered, but the volume is not found
|
||||
- `waiting`: the volume is waiting to be provisioned
|
||||
- `missing`: all disks have been discovered, but the volume cannot be found
|
||||
- `located`: the volume is found without prior provisioning
|
||||
- `provisioned`: the volume is provisioned (e.g. partitioned, grown as needed)
|
||||
- `provisioned`: the volume has been provisioned (e.g., partitioned, resized if necessary)
|
||||
- `prepared`: the encrypted volume is open
|
||||
- `ready`: the volume is formatted, ready to be mounted
|
||||
- `ready`: the volume is formatted and ready to be mounted
|
||||
- `closed`: the encrypted volume is closed
|
||||
|
||||
## Machine Configuration
|
||||
|
||||
> Note: In Talos Linux 1.8, only `EPHEMERAL` system volume configuration can be managed through the machine configuration.
|
||||
>
|
||||
> Note: The volume configuration in the machine configuration is only applied when the volume has not been provisioned yet.
|
||||
> So applying changes after the initial provisioning will not have any effect.
|
||||
|
||||
To configure the `EPHEMERAL` (`/var`) volume, add the following [document]({{< relref "../../reference/configuration/block/volumeconfig" >}}) to the machine configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: VolumeConfig
|
||||
name: EPHEMERAL
|
||||
provisioning:
|
||||
diskSelector:
|
||||
match: disk.transport == 'nvme'
|
||||
minSize: 2GB
|
||||
maxSize: 40GB
|
||||
grow: false
|
||||
```
|
||||
|
||||
Every field in the `VolumeConfig` resource is optional, and if a field is not specified, the default value is used.
|
||||
The default built-in values are:
|
||||
|
||||
```yaml
|
||||
provisioning:
|
||||
diskSelector:
|
||||
match: system_disk
|
||||
minSize: 2GiB
|
||||
grow: true
|
||||
```
|
||||
|
||||
By default, the `EPHEMERAL` volume is provisioned on the system disk, which is the disk where Talos Linux is installed.
|
||||
It has a minimum size of 2 GiB and automatically grows to utilize the maximum available space on the disk.
|
||||
|
||||
### Disk Selector
|
||||
|
||||
The `diskSelector` field is utilized to choose the disk where the volume will be provisioned.
|
||||
It is a [Common Expression Language (CEL)](https://cel.dev/) expression that evaluates against the available disks.
|
||||
The volume will be provisioned on the first disk that matches the expression and has sufficient free space for the volume.
|
||||
|
||||
The expression is evaluated in the following context:
|
||||
|
||||
- `system_disk` (`bool`) - indicates if the disk is the system disk
|
||||
- `disk` (`Disks.block.talos.dev`) - the disk resource being evaluated
|
||||
|
||||
For the disk resource, any field available in the resource specification can be used (use `talosctl get disks -o yaml` to see the output for your machine):
|
||||
|
||||
```yaml
|
||||
dev_path: /dev/nvme0n1
|
||||
size: 10485760000
|
||||
pretty_size: 10 GB
|
||||
io_size: 512
|
||||
sector_size: 512
|
||||
readonly: false
|
||||
cdrom: false
|
||||
model: QEMU NVMe Ctrl
|
||||
serial: deadbeef
|
||||
wwid: nvme.1b36-6465616462656566-51454d55204e564d65204374726c-00000001
|
||||
bus_path: /pci0000:00/0000:00:09.0/nvme
|
||||
sub_system: /sys/class/block
|
||||
transport: nvme
|
||||
```
|
||||
|
||||
Additionally, constants for disk size multipliers are available:
|
||||
|
||||
- `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB` - binary size multipliers (1024)
|
||||
- `kB`, `MB`, `GB`, `TB`, `PB`, `EB` - decimal size multipliers (1000)
|
||||
|
||||
The disk expression is evaluated against each available disk, and the expression should either return `true` or `false`.
|
||||
If the expression returns `true`, the disk is selected for provisioning.
|
||||
|
||||
> Note: In CEL, signed and unsigned integers are not interchangeable.
|
||||
> Disk sizes are represented as unsigned integers, so suffix `u` should be used in constants to avoid type mismatch, e.g. `disk.size > 10u * GiB`.
|
||||
|
||||
Examples of disk selector expressions:
|
||||
|
||||
- `disk.transport == 'nvme'`: select the NVMe disks only
|
||||
- `disk.transport == 'scsi' && disk.size < 2u * TiB`: select SCSI disks smaller than 2 TiB
|
||||
- `disk.serial.startsWith('deadbeef') && !cdrom`: select disks with serial number starting with `deadbeef` and not of CD-ROM type
|
||||
|
||||
### Minimum and Maximum Size
|
||||
|
||||
The `minSize` and `maxSize` fields define the minimum and maximum size of the volume, respectively.
|
||||
Talos Linux will always ensure that the volume is at least `minSize` in size and will not exceed `maxSize`.
|
||||
If `maxSize` is not set, the volume will grow to utilize the maximum available space on the disk.
|
||||
|
||||
If `grow` is set to `true`, the volume will automatically grow to utilize the maximum available space on the disk on each boot.
|
||||
|
||||
Setting `minSize` might influence disk selection - if the disk does not have enough free space to satisfy the minimum size requirement, it will not be selected for provisioning.
|
||||
|
Loading…
x
Reference in New Issue
Block a user