feat: rewrite install disk selector to use CEL expressions

Rewrite matcher to take out old go-blockdevice library out of the way,
implementing translation from go-blockdevice format to CEL.

Implement facilities to build CEL expressions programmatically.

Now we can add a machine config disk match expression (CEL) easily.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2024-11-07 17:07:49 +04:00
parent eba35f4413
commit 9a02ecc49f
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
31 changed files with 609 additions and 200 deletions

View File

@ -18,11 +18,11 @@ preface = """
[notes.updates]
title = "Component Updates"
description = """\
Linux: 6.6.59
containerd: 2.0.0
Flannel: 0.26.0
Kubernetes: 1.32.0-beta.0
runc: 1.2.1
* Linux: 6.6.59
* containerd: 2.0.0
* Flannel: 0.26.0
* Kubernetes: 1.32.0-beta.0
* runc: 1.2.1
Talos is built with Go 1.23.2.
"""
@ -68,7 +68,7 @@ This command allows you to view the cgroup resource consumption and limits for a
[notes.udevd]
title = "udevd"
description = """\
Talos previously used `udevd` to provide `udevd`, now it uses `systemd-udevd` instead.
Talos previously used `eudev` to provide `udevd`, now it uses `systemd-udevd` instead.
"""
[make_deps]

View File

@ -179,16 +179,23 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
return nil, status.Error(codes.InvalidArgument, err.Error())
}
warnings, err := cfgProvider.Validate(
modeWrapper{
Mode: s.Controller.Runtime().State().Platform().Mode(),
installed: s.Controller.Runtime().State().Machine().Installed(),
},
)
validationMode := modeWrapper{
Mode: s.Controller.Runtime().State().Platform().Mode(),
installed: s.Controller.Runtime().State().Machine().Installed(),
}
warnings, err := cfgProvider.Validate(validationMode)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
warningsRuntime, err := cfgProvider.RuntimeValidate(ctx, s.Controller.Runtime().State().V1Alpha2().Resources(), validationMode)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
warnings = slices.Concat(warnings, warningsRuntime)
//nolint:exhaustive
switch in.Mode {
// --mode=try

View File

@ -22,6 +22,7 @@ import (
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/volumes"
blockpb "github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/block"
"github.com/siderolabs/talos/pkg/machinery/proto"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
"github.com/siderolabs/talos/pkg/machinery/resources/hardware"
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
@ -164,7 +165,7 @@ func (ctrl *VolumeManagerController) Run(ctx context.Context, r controller.Runti
discoveredVolumesSpecs, err := safe.Map(discoveredVolumes, func(dv *block.DiscoveredVolume) (*blockpb.DiscoveredVolumeSpec, error) {
spec := &blockpb.DiscoveredVolumeSpec{}
return spec, volumes.ResourceSpecToProto(dv, spec)
return spec, proto.ResourceSpecToProto(dv, spec)
})
if err != nil {
return fmt.Errorf("error mapping discovered volumes: %w", err)
@ -204,7 +205,7 @@ func (ctrl *VolumeManagerController) Run(ctx context.Context, r controller.Runti
diskSpecs, err := safe.Map(disks, func(d *block.Disk) (volumes.DiskContext, error) {
spec := &blockpb.DiskSpec{}
if err := volumes.ResourceSpecToProto(d, spec); err != nil {
if err := proto.ResourceSpecToProto(d, spec); err != nil {
return volumes.DiskContext{}, err
}

View File

@ -14,6 +14,7 @@ import (
"io"
"net/http"
"os"
"slices"
"strings"
"github.com/cosi-project/runtime/pkg/controller"
@ -68,6 +69,7 @@ type AcquireController struct {
EventPublisher talosruntime.Publisher
ValidationMode validation.RuntimeMode
ConfigPath string
ResourceState state.State
configSourcesUsed []string
}
@ -345,7 +347,12 @@ func (ctrl *AcquireController) loadFromPlatform(ctx context.Context, logger *zap
return nil, fmt.Errorf("failed to validate config acquired via platform %s: %w", platformName, err)
}
for _, warning := range warnings {
warningsRuntime, err := cfg.RuntimeValidate(ctx, ctrl.ResourceState, ctrl.ValidationMode)
if err != nil {
return nil, fmt.Errorf("failed to runtime validate config acquired via platform %s: %w", platformName, err)
}
for _, warning := range slices.Concat(warnings, warningsRuntime) {
logger.Warn("config validation warning", zap.String("platform", platformName), zap.String("warning", warning))
}
@ -364,7 +371,7 @@ func (ctrl *AcquireController) stateCmdline(ctx context.Context, r controller.Ru
return ctrl.stateMaintenanceEnter, nil, nil
}
cfg, err := ctrl.loadFromCmdline(logger)
cfg, err := ctrl.loadFromCmdline(ctx, logger)
if err != nil {
return nil, nil, err
}
@ -386,7 +393,9 @@ func (ctrl *AcquireController) stateCmdline(ctx context.Context, r controller.Ru
}
// loadFromCmdline is a helper function for stateCmdline.
func (ctrl *AcquireController) loadFromCmdline(logger *zap.Logger) (config.Provider, error) {
//
//nolint:gocyclo
func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.Logger) (config.Provider, error) {
cmdline := ctrl.CmdlineGetter()
param := cmdline.Get(constants.KernelParamConfigInline)
@ -435,7 +444,12 @@ func (ctrl *AcquireController) loadFromCmdline(logger *zap.Logger) (config.Provi
return nil, fmt.Errorf("failed to validate config acquired via cmdline %s: %w", constants.KernelParamConfigInline, err)
}
for _, warning := range warnings {
warningsRuntime, err := cfg.RuntimeValidate(ctx, ctrl.ResourceState, ctrl.ValidationMode)
if err != nil {
return nil, fmt.Errorf("failed to validate config acquired via cmdline %s: %w", constants.KernelParamConfigInline, err)
}
for _, warning := range slices.Concat(warnings, warningsRuntime) {
logger.Warn("config validation warning", zap.String("cmdline", constants.KernelParamConfigInline), zap.String("warning", warning))
}

View File

@ -199,6 +199,7 @@ func TestAcquireSuite(t *testing.T) {
EventPublisher: s.eventPublisher,
ValidationMode: validationModeMock{},
ConfigPath: s.configPath,
ResourceState: s.State(),
}))
}

View File

@ -71,6 +71,7 @@ import (
"github.com/siderolabs/talos/pkg/kubernetes"
machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
"github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/config/types/block/blockhelpers"
"github.com/siderolabs/talos/pkg/machinery/constants"
metamachinery "github.com/siderolabs/talos/pkg/machinery/meta"
blockres "github.com/siderolabs/talos/pkg/machinery/resources/block"
@ -2015,9 +2016,27 @@ func Install(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
var disk string
disk, err = r.Config().Machine().Install().Disk()
matchExpr, err := r.Config().Machine().Install().DiskMatchExpression()
if err != nil {
return err
return fmt.Errorf("failed to get disk match expression: %w", err)
}
switch {
case matchExpr != nil:
logger.Printf("using disk match expression: %s", matchExpr)
matchedDisks, err := blockhelpers.MatchDisks(ctx, r.State().V1Alpha2().Resources(), matchExpr)
if err != nil {
return err
}
if len(matchedDisks) == 0 {
return fmt.Errorf("no disks matched the expression: %s", matchExpr)
}
disk = matchedDisks[0].TypedSpec().DevPath
case r.Config().Machine().Install().Disk() != "":
disk = r.Config().Machine().Install().Disk()
}
disk, err = filepath.EvalSymlinks(disk)
@ -2025,6 +2044,8 @@ func Install(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
return err
}
logger.Printf("installing Talos to disk %s", disk)
err = install.RunInstallerContainer(
disk,
r.State().Platform().Name(),

View File

@ -125,6 +125,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
ConfigSetter: ctrl.v1alpha1Runtime,
EventPublisher: ctrl.v1alpha1Runtime.Events(),
ValidationMode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
ResourceState: ctrl.v1alpha1Runtime.State().V1Alpha2().Resources(),
},
&config.MachineTypeController{},
&cri.SeccompProfileController{},

View File

@ -10,6 +10,7 @@ import (
"fmt"
"io/fs"
"log"
"slices"
"strings"
cosiv1alpha1 "github.com/cosi-project/runtime/api/v1alpha1"
@ -75,7 +76,7 @@ func (s *Server) Register(obj *grpc.Server) {
}
// ApplyConfiguration implements [machine.MachineServiceServer].
func (s *Server) ApplyConfiguration(_ context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
if s.mode.IsAgent() {
return nil, status.Error(codes.Unimplemented, "API is not implemented in agent mode")
}
@ -102,10 +103,15 @@ func (s *Server) ApplyConfiguration(_ context.Context, in *machine.ApplyConfigur
return nil, status.Errorf(codes.InvalidArgument, "configuration validation failed: %s", err)
}
warningsRuntime, err := cfgProvider.RuntimeValidate(ctx, s.controller.Runtime().State().V1Alpha2().Resources(), s.controller.Runtime().State().Platform().Mode())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "runtime configuration validation failed: %s", err)
}
reply := &machine.ApplyConfigurationResponse{
Messages: []*machine.ApplyConfiguration{
{
Warnings: warnings,
Warnings: slices.Concat(warnings, warningsRuntime),
},
},
}

View File

@ -95,9 +95,6 @@ func (suite *GenerateConfigSuite) TestGenerate() {
suite.Require().NoError(err)
disk, err := config.Machine().Install().Disk()
suite.Require().NoError(err)
suite.Require().EqualValues(request.MachineConfig.Type, config.Machine().Type())
suite.Require().EqualValues(request.ClusterConfig.Name, config.Cluster().Name())
suite.Require().EqualValues(request.ClusterConfig.ControlPlane.Endpoint, config.Cluster().Endpoint().String())
@ -114,7 +111,7 @@ func (suite *GenerateConfigSuite) TestGenerate() {
fmt.Sprintf("%s:v%s", constants.KubeletImage, request.MachineConfig.KubernetesVersion),
config.Machine().Kubelet().Image(),
)
suite.Require().EqualValues(request.MachineConfig.InstallConfig.InstallDisk, disk)
suite.Require().EqualValues(request.MachineConfig.InstallConfig.InstallDisk, config.Machine().Install().Disk())
suite.Require().EqualValues(request.MachineConfig.InstallConfig.InstallImage, config.Machine().Install().Image())
suite.Require().EqualValues(request.MachineConfig.NetworkConfig.Hostname, config.Machine().Network().Hostname())
suite.Require().EqualValues(request.MachineConfig.NetworkConfig.Hostname, config.Machine().Network().Hostname())
@ -149,9 +146,6 @@ func (suite *GenerateConfigSuite) TestGenerate() {
suite.Require().NoError(err)
disk, err = config.Machine().Install().Disk()
suite.Require().NoError(err)
suite.Require().EqualValues(request.MachineConfig.Type, joinedConfig.Machine().Type())
suite.Require().EqualValues(request.ClusterConfig.Name, joinedConfig.Cluster().Name())
suite.Require().EqualValues(request.ClusterConfig.ControlPlane.Endpoint, joinedConfig.Cluster().Endpoint().String())
@ -163,7 +157,7 @@ func (suite *GenerateConfigSuite) TestGenerate() {
fmt.Sprintf("%s:v%s", constants.KubeletImage, request.MachineConfig.KubernetesVersion),
joinedConfig.Machine().Kubelet().Image(),
)
suite.Require().EqualValues(request.MachineConfig.InstallConfig.InstallDisk, disk)
suite.Require().EqualValues(request.MachineConfig.InstallConfig.InstallDisk, config.Machine().Install().Disk())
suite.Require().EqualValues(
request.MachineConfig.InstallConfig.InstallImage,
joinedConfig.Machine().Install().Image(),

View File

@ -0,0 +1,66 @@
// 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 cel
import (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/types"
)
// Builder allows building CEL expressions programmatically.
type Builder struct {
ast.ExprFactory
env *cel.Env
nextID int64
}
// NewBuilder creates a new builder.
func NewBuilder(env *cel.Env) *Builder {
return &Builder{
ExprFactory: ast.NewExprFactory(),
env: env,
}
}
// NextID returns the next unique ID.
func (b *Builder) NextID() int64 {
b.nextID++
return b.nextID
}
// ToBooleanExpression converts the AST to a boolean expression.
func (b *Builder) ToBooleanExpression(expr ast.Expr) (*Expression, error) {
rawAst := ast.NewAST(expr, nil)
pbAst, err := ast.ToProto(rawAst)
if err != nil {
return nil, err
}
celAst, err := cel.CheckedExprToAstWithSource(pbAst, common.NewTextSource(""))
if err != nil {
return nil, err
}
var issues *cel.Issues
celAst, issues = b.env.Check(celAst)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
if outputType := celAst.OutputType(); !outputType.IsExactType(types.BoolType) {
return nil, fmt.Errorf("expression output type is %s, expected bool", outputType)
}
return &Expression{
ast: celAst,
}, nil
}

View File

@ -0,0 +1,32 @@
// 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 cel_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/pkg/machinery/cel"
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
)
func TestBuildDiskExpression(t *testing.T) {
t.Parallel()
builder := cel.NewBuilder(celenv.DiskLocator())
expr := builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"rotational",
)
out, err := builder.ToBooleanExpression(expr)
require.NoError(t, err)
assert.Equal(t, "disk.rotational", out.String())
}

View File

@ -11,6 +11,8 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/ryanuber/go-glob"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/block"
@ -36,6 +38,13 @@ var DiskLocator = sync.OnceValue(func() *cel.Env {
cel.Types(&diskSpec),
cel.Variable("disk", cel.ObjectType(string(diskSpec.ProtoReflect().Descriptor().FullName()))),
cel.Variable("system_disk", types.BoolType),
cel.Function("glob", // glob(pattern, string)
cel.Overload("glob_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType,
cel.BinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
return types.Bool(glob.Glob(string(arg1.(types.String)), string(arg2.(types.String))))
}),
),
),
},
celUnitMultipliersConstants(),
)...,

View File

@ -30,6 +30,10 @@ func TestDiskLocator(t *testing.T) {
name: "disk size",
expression: "disk.size > 1000u * GiB && !disk.rotational",
},
{
name: "glob",
expression: "glob('sd[a-z]', disk.dev_path)",
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

View File

@ -13,6 +13,7 @@ import (
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/siderolabs/crypto/x509"
"github.com/siderolabs/talos/pkg/machinery/cel"
"github.com/siderolabs/talos/pkg/machinery/config/machine"
)
@ -91,7 +92,8 @@ type File interface {
type Install interface {
Image() string
Extensions() []Extension
Disk() (string, error)
Disk() string
DiskMatchExpression() (*cel.Expression, error)
ExtraKernelArgs() []string
Zero() bool
LegacyBIOSSupport() bool

View File

@ -4,7 +4,13 @@
package config
import "github.com/siderolabs/talos/pkg/machinery/config/validation"
import (
"context"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/talos/pkg/machinery/config/validation"
)
// Validator is the interface to validate configuration.
//
@ -13,3 +19,15 @@ type Validator interface {
// Validate checks configuration and returns warnings and fatal errors (as multierror).
Validate(validation.RuntimeMode, ...validation.Option) ([]string, error)
}
// RuntimeValidator is the interface to validate configuration in the runtime context.
//
// This interface is used by Talos itself to validate configuration on the machine (vs. the Validator interface).
//
// The errors reported by Validator & RuntimeValidator are different.
type RuntimeValidator interface {
// RuntimeValidate validates the config in the runtime context.
//
// The method returns warnings and fatal errors (as multierror).
RuntimeValidate(context.Context, state.State, validation.RuntimeMode, ...validation.Option) ([]string, error)
}

View File

@ -21,7 +21,7 @@ import (
func callMethods(t testing.TB, obj reflect.Value, chain ...string) {
t.Helper()
if obj.Kind() == reflect.Interface && obj.IsNil() {
if (obj.Kind() == reflect.Interface || obj.Kind() == reflect.Pointer) && obj.IsNil() {
return
}

View File

@ -7,10 +7,12 @@ package container
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"github.com/cosi-project/runtime/pkg/state"
"github.com/hashicorp/go-multierror"
"github.com/siderolabs/gen/xslices"
@ -305,6 +307,35 @@ func (container *Container) Validate(mode validation.RuntimeMode, opt ...validat
return warnings, multiErr.ErrorOrNil()
}
// RuntimeValidate validates the config in the runtime context.
func (container *Container) RuntimeValidate(ctx context.Context, st state.State, mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) {
var (
warnings []string
err error
)
if container.v1alpha1Config != nil {
warnings, err = container.v1alpha1Config.RuntimeValidate(ctx, st, mode, opt...)
}
var multiErr *multierror.Error
if err != nil {
multiErr = multierror.Append(multiErr, err)
}
for _, doc := range container.documents {
if validatableDoc, ok := doc.(config.RuntimeValidator); ok {
docWarnings, docErr := validatableDoc.RuntimeValidate(ctx, st, mode, opt...)
warnings = append(warnings, docWarnings...)
multiErr = multierror.Append(multiErr, docErr)
}
}
return warnings, multiErr.ErrorOrNil()
}
// RedactSecrets returns a copy of the Provider with all secrets replaced with the given string.
func (container *Container) RedactSecrets(replacement string) coreconfig.Provider {
clone := container.clone()

View File

@ -15,6 +15,9 @@ type Encoder = config.Encoder
// Validator provides the interface to validate configuration.
type Validator = config.Validator
// RuntimeValidator provides the interface to validate configuration in the runtime context.
type RuntimeValidator = config.RuntimeValidator
// Container provides the interface to access configuration documents.
//
// Container might contain multiple config documents, supporting encoding/decoding,
@ -22,6 +25,7 @@ type Validator = config.Validator
type Container interface {
Encoder
Validator
RuntimeValidator
Readonly() bool

View File

@ -2245,9 +2245,9 @@
"busPath": {
"type": "string",
"title": "busPath",
"description": "Disk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.\n",
"markdownDescription": "Disk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.",
"x-intellij-html-description": "\u003cp\u003eDisk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see \u003ca href=\"https://github.com/siderolabs/go-blockdevice/issues/114\" target=\"_blank\"\u003ehttps://github.com/siderolabs/go-blockdevice/issues/114\u003c/a\u003e.\u003c/p\u003e\n"
"description": "Disk bus path.\n",
"markdownDescription": "Disk bus path.",
"x-intellij-html-description": "\u003cp\u003eDisk bus path.\u003c/p\u003e\n"
}
},
"additionalProperties": false,

View File

@ -0,0 +1,51 @@
// 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 blockhelpers provides helper functions for working with block resources.
package blockhelpers
import (
"context"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
blockpb "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"
"github.com/siderolabs/talos/pkg/machinery/proto"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
// MatchDisks returns a list of disks that match the given expression.
func MatchDisks(ctx context.Context, st state.State, expression *cel.Expression) ([]*block.Disk, error) {
disks, err := safe.StateListAll[*block.Disk](ctx, st)
if err != nil {
return nil, err
}
var matchedDisks []*block.Disk
for disk := range disks.All() {
spec := &blockpb.DiskSpec{}
if err = proto.ResourceSpecToProto(disk, spec); err != nil {
return nil, err
}
matches, err := expression.EvalBool(celenv.DiskLocator(), map[string]any{
"disk": spec,
"system_disk": false,
})
if err != nil {
return nil, err
}
if matches {
matchedDisks = append(matchedDisks, disk)
}
}
return matchedDisks, nil
}

View File

@ -8,8 +8,6 @@ import (
"fmt"
"testing"
humanize "github.com/dustin/go-humanize"
"github.com/siderolabs/go-blockdevice/blockdevice/util/disk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
yaml "gopkg.in/yaml.v3"
@ -125,25 +123,11 @@ func TestDiskSizeMatcherUnmarshal(t *testing.T) {
match: false,
},
} {
var (
size uint64
err error
)
if test.size != "" {
size, err = humanize.ParseBytes(test.size)
require.NoError(t, err)
}
err = yaml.Unmarshal([]byte(fmt.Sprintf("m: '%s'\n", test.condition)), &obj)
err := yaml.Unmarshal([]byte(fmt.Sprintf("m: '%s'\n", test.condition)), &obj)
if test.err {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if test.size != "" {
require.Equal(t, obj.M.Matcher(&disk.Disk{Size: size}), test.match, test.size)
}
}
}

View File

@ -7,20 +7,23 @@ package v1alpha1
import (
"crypto/tls"
stdx509 "crypto/x509"
"errors"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/siderolabs/crypto/x509"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/go-blockdevice/blockdevice/util/disk"
"github.com/siderolabs/go-blockdevice/v2/encryption"
"github.com/siderolabs/go-pointer"
"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/machine"
"github.com/siderolabs/talos/pkg/machinery/constants"
@ -1217,72 +1220,174 @@ func (i *InstallConfig) Extensions() []config.Extension {
}
// Disk implements the config.Provider interface.
func (i *InstallConfig) Disk() (string, error) {
matchers := i.DiskMatchers()
if len(matchers) > 0 {
d, err := disk.Find(matchers...)
if err != nil {
return "", err
}
if d != nil {
return d.DeviceName, nil
}
return "", errors.New("no disk found matching provided parameters")
}
return i.InstallDisk, nil
func (i *InstallConfig) Disk() string {
return i.InstallDisk
}
// DiskMatchers implements the config.Provider interface.
// DiskMatchExpression returns the disk matcher expression by inspecting the InstallDiskSelector.
//
//nolint:gocyclo
func (i *InstallConfig) DiskMatchers() []disk.Matcher {
if i.InstallDiskSelector != nil {
selector := i.InstallDiskSelector
var matchers []disk.Matcher
if selector.Size != nil {
matchers = append(matchers, selector.Size.Matcher)
}
if selector.UUID != "" {
matchers = append(matchers, disk.WithUUID(selector.UUID))
}
if selector.WWID != "" {
matchers = append(matchers, disk.WithWWID(selector.WWID))
}
if selector.Model != "" {
matchers = append(matchers, disk.WithModel(selector.Model))
}
if selector.Name != "" {
matchers = append(matchers, disk.WithName(selector.Name))
}
if selector.Serial != "" {
matchers = append(matchers, disk.WithSerial(selector.Serial))
}
if selector.Modalias != "" {
matchers = append(matchers, disk.WithModalias(selector.Modalias))
}
if disk.Type(selector.Type) != disk.TypeUnknown {
matchers = append(matchers, disk.WithType(disk.Type(selector.Type)))
}
if selector.BusPath != "" {
matchers = append(matchers, disk.WithBusPath(selector.BusPath))
}
return matchers
func (i *InstallConfig) DiskMatchExpression() (*cel.Expression, error) {
if i.InstallDiskSelector == nil {
return nil, nil
}
return nil
var exprs []ast.Expr
builder := cel.NewBuilder(celenv.DiskLocator())
selector := i.InstallDiskSelector
if selector.Size != nil {
op := selector.Size.MatchData.Op
if op == "" {
op = "=="
}
exprs = append(exprs, // disk.size op value
builder.NewCall(
builder.NextID(),
"_"+op+"_",
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"size",
),
builder.NewLiteral(
builder.NextID(),
types.Uint(selector.Size.MatchData.Size),
),
),
)
}
patternMatcherExpr := func(pattern, field string) ast.Expr { // glob(pattern, disk.$field)
return builder.NewCall(
builder.NextID(),
"glob",
builder.NewLiteral(builder.NextID(), types.String(pattern)),
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
field,
),
)
}
directMatchExpr := func(value, field string) ast.Expr { // disk.$field == value
return builder.NewCall(
builder.NextID(),
operators.Equals,
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
field,
),
builder.NewLiteral(builder.NextID(), types.String(value)),
)
}
if selector.UUID != "" {
// not supported
return nil, fmt.Errorf("selector on uuid is not supported")
}
if selector.WWID != "" {
exprs = append(exprs, patternMatcherExpr(selector.WWID, "wwid"))
}
if selector.Model != "" {
exprs = append(exprs, patternMatcherExpr(selector.Model, "model"))
}
if selector.Name != "" {
// not supported
return nil, fmt.Errorf("selector on name is not supported")
}
if selector.Serial != "" {
exprs = append(exprs, patternMatcherExpr(selector.Serial, "serial"))
}
if selector.Modalias != "" {
exprs = append(exprs, patternMatcherExpr(selector.Modalias, "modalias"))
}
if selector.Type != "" {
switch selector.Type {
case "nvme": // disk.transport == "nvme"
exprs = append(exprs, directMatchExpr("nvme", "transport"))
case "sd": // disk.transport == "mmc"
exprs = append(exprs, directMatchExpr("mmc", "transport"))
case "hdd": // disk.rotational
exprs = append(exprs, builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"rotational",
))
case "ssd": // disk.transport != "" && !disk.rotational
exprs = append(exprs,
builder.NewCall(
builder.NextID(),
operators.NotEquals,
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"transport",
),
builder.NewLiteral(builder.NextID(), types.String("")),
),
builder.NewCall(
builder.NextID(),
operators.LogicalNot,
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"rotational",
),
),
)
default:
return nil, fmt.Errorf("unsupported disk type %q", selector.Type)
}
}
if selector.BusPath != "" {
exprs = append(exprs, patternMatcherExpr(selector.BusPath, "buspath"))
}
// exclude readonly disks: !disk.readonly
exprs = append(exprs, builder.NewCall(
builder.NextID(),
operators.LogicalNot,
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"readonly",
),
))
// exclude CD-ROMs: !disk.cdrom
exprs = append(exprs, builder.NewCall(
builder.NextID(),
operators.LogicalNot,
builder.NewSelect(
builder.NextID(),
builder.NewIdent(builder.NextID(), "disk"),
"cdrom",
),
))
// reduce all expressions to a single one with &&
for len(exprs) > 1 {
exprs = append(exprs[:len(exprs)-2], builder.NewCall(
builder.NextID(),
operators.LogicalAnd,
exprs[len(exprs)-2],
exprs[len(exprs)-1],
))
}
return builder.ToBooleanExpression(exprs[0])
}
// ExtraKernelArgs implements the config.Provider interface.

View File

@ -0,0 +1,93 @@
// 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 v1alpha1_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
)
func TestInstallDiskSelector(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
selector v1alpha1.InstallDiskSelector
expected string
}{
{
name: "size",
selector: v1alpha1.InstallDiskSelector{
Size: &v1alpha1.InstallDiskSizeMatcher{
MatchData: v1alpha1.InstallDiskSizeMatchData{
Op: "<=",
Size: 256 * 1024,
},
},
},
expected: `disk.size <= 262144u && !disk.readonly && !disk.cdrom`,
},
{
name: "size and type",
selector: v1alpha1.InstallDiskSelector{
Size: &v1alpha1.InstallDiskSizeMatcher{
MatchData: v1alpha1.InstallDiskSizeMatchData{
Size: 1024 * 1024,
},
},
Type: v1alpha1.InstallDiskType("nvme"),
},
expected: `disk.size == 1048576u && disk.transport == "nvme" && !disk.readonly && !disk.cdrom`,
},
{
name: "size and type and modalias",
selector: v1alpha1.InstallDiskSelector{
Size: &v1alpha1.InstallDiskSizeMatcher{
MatchData: v1alpha1.InstallDiskSizeMatchData{
Size: 1024 * 1024,
},
},
Type: v1alpha1.InstallDiskType("hdd"),
Modalias: "pci:1234:5678*",
},
expected: `disk.size == 1048576u && glob("pci:1234:5678*", disk.modalias) && disk.rotational &&
!disk.readonly && !disk.cdrom`,
},
{
name: "ssd",
selector: v1alpha1.InstallDiskSelector{
Type: v1alpha1.InstallDiskType("ssd"),
},
expected: `disk.transport != "" && !disk.rotational && !disk.readonly && !disk.cdrom`,
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
installCfg := &v1alpha1.InstallConfig{
InstallDiskSelector: &test.selector,
}
expr, err := installCfg.DiskMatchExpression()
require.NoError(t, err)
assert.Equal(t, test.expected, expr.String())
})
}
}

View File

@ -30,7 +30,6 @@ import (
"github.com/dustin/go-humanize"
"github.com/siderolabs/crypto/x509"
"github.com/siderolabs/go-blockdevice/blockdevice/util/disk"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/pkg/machinery/config/config"
@ -879,11 +878,6 @@ func (m *InstallDiskSizeMatcher) UnmarshalYAML(unmarshal func(any) error) error
return nil
}
// Matcher is a method that can handle some custom disk matching logic.
func (m *InstallDiskSizeMatcher) Matcher(d *disk.Disk) bool {
return m.MatchData.Compare(d)
}
// InstallDiskSizeMatchData contains data for comparison - Op and Size.
//
//docgen:nodoc
@ -892,53 +886,8 @@ type InstallDiskSizeMatchData struct {
Size uint64
}
// Compare is the method to compare disk size.
func (in *InstallDiskSizeMatchData) Compare(d *disk.Disk) bool {
switch in.Op {
case ">=":
return d.Size >= in.Size
case "<=":
return d.Size <= in.Size
case ">":
return d.Size > in.Size
case "<":
return d.Size < in.Size
case "":
fallthrough
case "==":
return d.Size == in.Size
default:
return false
}
}
// InstallDiskType custom type for disk type selector.
type InstallDiskType disk.Type
// MarshalYAML is a custom marshaller for `InstallDiskSizeMatcher`.
func (it InstallDiskType) MarshalYAML() (any, error) {
return disk.Type(it).String(), nil
}
// UnmarshalYAML is a custom unmarshaler for `InstallDiskType`.
func (it *InstallDiskType) UnmarshalYAML(unmarshal func(any) error) error {
var (
t string
err error
)
if err = unmarshal(&t); err != nil {
return err
}
if dt, err := disk.ParseType(t); err == nil {
*it = InstallDiskType(dt)
} else {
return err
}
return nil
}
type InstallDiskType string
// InstallDiskSelector represents a disk query parameters for the install disk lookup.
type InstallDiskSelector struct {
@ -974,7 +923,6 @@ type InstallDiskSelector struct {
Type InstallDiskType `yaml:"type,omitempty"`
// description: |
// Disk bus path.
// Warning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.
// examples:
// - value: '"/pci0000:00/0000:00:17.0/ata1/host0/target0:0:0/0:0:0:0"'
// - value: '"/pci0000:00/*"'

View File

@ -1150,7 +1150,7 @@ func (InstallDiskSelector) Doc() *encoder.Doc {
Name: "busPath",
Type: "string",
Note: "",
Description: "Disk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.",
Description: "Disk bus path.",
Comments: [3]string{"" /* encoder.HeadComment */, "Disk bus path." /* encoder.LineComment */, "" /* encoder.FootComment */},
},
},

View File

@ -5,23 +5,25 @@
package v1alpha1
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"github.com/cosi-project/runtime/pkg/state"
"github.com/hashicorp/go-multierror"
sideronet "github.com/siderolabs/net"
"github.com/siderolabs/talos/pkg/machinery/config/config"
"github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/config/types/block/blockhelpers"
"github.com/siderolabs/talos/pkg/machinery/config/validation"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/kubelet"
@ -94,24 +96,13 @@ func (c *Config) Validate(mode validation.RuntimeMode, options ...validation.Opt
if c.MachineConfig.MachineInstall == nil {
result = multierror.Append(result, fmt.Errorf("install instructions are required in %q mode", mode))
} else {
if opts.Local {
if c.MachineConfig.MachineInstall.InstallDisk == "" && len(c.MachineConfig.MachineInstall.DiskMatchers()) == 0 {
result = multierror.Append(result, errors.New("either install disk or diskSelector should be defined"))
}
} else {
disk, err := c.MachineConfig.MachineInstall.Disk()
matcher, err := c.MachineConfig.MachineInstall.DiskMatchExpression()
if err != nil {
result = multierror.Append(result, fmt.Errorf("install disk selector is invalid: %w", err))
}
if err != nil {
result = multierror.Append(result, err)
} else {
if disk == "" {
result = multierror.Append(result, fmt.Errorf("an install disk is required in %q mode", mode))
}
if _, err := os.Stat(disk); os.IsNotExist(err) {
result = multierror.Append(result, fmt.Errorf("specified install disk does not exist: %q", disk))
}
}
if c.MachineConfig.MachineInstall.InstallDisk == "" && matcher == nil {
result = multierror.Append(result, errors.New("either install disk or diskSelector should be defined"))
}
}
}
@ -916,3 +907,33 @@ func (e *EtcdConfig) Validate() error {
return result.ErrorOrNil()
}
// RuntimeValidate validates the config in runtime context.
//
// In runtime context, resource state is available.
func (c *Config) RuntimeValidate(ctx context.Context, st state.State, mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) {
var (
warnings []string
result *multierror.Error
)
if c.MachineConfig != nil {
if mode.RequiresInstall() && c.MachineConfig.MachineInstall != nil {
diskExpr, err := c.MachineConfig.MachineInstall.DiskMatchExpression()
if err != nil {
result = multierror.Append(result, fmt.Errorf("install disk selector is invalid: %w", err))
} else if diskExpr != nil {
matchedDisks, err := blockhelpers.MatchDisks(ctx, st, diskExpr)
if err != nil {
result = multierror.Append(result, err)
}
if len(matchedDisks) == 0 {
result = multierror.Append(result, fmt.Errorf("no disks matched the expression: %s", diskExpr))
}
}
}
}
return warnings, result.ErrorOrNil()
}

View File

@ -22,11 +22,11 @@ require (
github.com/mdlayher/ethtool v0.2.0
github.com/opencontainers/runtime-spec v1.2.0
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10
github.com/ryanuber/go-glob v1.0.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/siderolabs/crypto v0.5.0
github.com/siderolabs/gen v0.7.0
github.com/siderolabs/go-api-signature v0.3.6
github.com/siderolabs/go-blockdevice v0.4.8
github.com/siderolabs/go-blockdevice/v2 v2.0.2
github.com/siderolabs/go-pointer v1.0.0
github.com/siderolabs/net v0.4.0
@ -63,7 +63,6 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.28.0 // indirect

View File

@ -109,8 +109,6 @@ github.com/siderolabs/gen v0.7.0 h1:uHAt3WD0dof28NHFuguWBbDokaXQraR/HyVxCLw2QCU=
github.com/siderolabs/gen v0.7.0/go.mod h1:an3a2Y53O7kUjnnK8Bfu3gewtvnIOu5RTU6HalFtXQQ=
github.com/siderolabs/go-api-signature v0.3.6 h1:wDIsXbpl7Oa/FXvxB6uz4VL9INA9fmr3EbmjEZYFJrU=
github.com/siderolabs/go-api-signature v0.3.6/go.mod h1:hoH13AfunHflxbXfh+NoploqV13ZTDfQ1mQJWNVSW9U=
github.com/siderolabs/go-blockdevice v0.4.8 h1:KfdWvIx0Jft5YVuCsFIJFwjWEF1oqtzkgX9PeU9cX4c=
github.com/siderolabs/go-blockdevice v0.4.8/go.mod h1:4PeOuk71pReJj1JQEXDE7kIIQJPVe8a+HZQa+qjxSEA=
github.com/siderolabs/go-blockdevice/v2 v2.0.2 h1:GIdOBrCLQ7X9jbr0P/+7paw5SIfp/LL+dx9mTOzmw8w=
github.com/siderolabs/go-blockdevice/v2 v2.0.2/go.mod h1:74htzCV913UzaLZ4H+NBXkwWlYnBJIq5m/379ZEcu8w=
github.com/siderolabs/go-pointer v1.0.0 h1:6TshPKep2doDQJAAtHUuHWXbca8ZfyRySjSBT/4GsMU=

View File

@ -2,21 +2,20 @@
// 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 volumes
// Package proto defines a functions to work with proto messages.
package proto
import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/protoenc"
"github.com/siderolabs/talos/pkg/machinery/proto"
)
// ResourceSpecToProto converts a resource spec to a proto message.
func ResourceSpecToProto(i resource.Resource, o proto.Message) error {
func ResourceSpecToProto(i resource.Resource, o Message) error {
marshaled, err := protoenc.Marshal(i.Spec())
if err != nil {
return err
}
return proto.Unmarshal(marshaled, o)
return Unmarshal(marshaled, o)
}

View File

@ -1976,7 +1976,7 @@ size: <= 2TB
|`uuid` |string |Disk UUID `/sys/block/<dev>/uuid`. | |
|`wwid` |string |Disk WWID `/sys/block/<dev>/wwid`. | |
|`type` |InstallDiskType |Disk Type. |`ssd`<br />`hdd`<br />`nvme`<br />`sd`<br /> |
|`busPath` |string |<details><summary>Disk bus path.</summary>Warning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.</details> <details><summary>Show example(s)</summary>{{< highlight yaml >}}
|`busPath` |string |Disk bus path. <details><summary>Show example(s)</summary>{{< highlight yaml >}}
busPath: /pci0000:00/0000:00:17.0/ata1/host0/target0:0:0/0:0:0:0
{{< /highlight >}}{{< highlight yaml >}}
busPath: /pci0000:00/*

View File

@ -2245,9 +2245,9 @@
"busPath": {
"type": "string",
"title": "busPath",
"description": "Disk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.\n",
"markdownDescription": "Disk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see https://github.com/siderolabs/go-blockdevice/issues/114.",
"x-intellij-html-description": "\u003cp\u003eDisk bus path.\nWarning: This requires special configuration for NVMe drives. For details, see \u003ca href=\"https://github.com/siderolabs/go-blockdevice/issues/114\" target=\"_blank\"\u003ehttps://github.com/siderolabs/go-blockdevice/issues/114\u003c/a\u003e.\u003c/p\u003e\n"
"description": "Disk bus path.\n",
"markdownDescription": "Disk bus path.",
"x-intellij-html-description": "\u003cp\u003eDisk bus path.\u003c/p\u003e\n"
}
},
"additionalProperties": false,