feat: implement talos.config.early command line arg

Fixes #11449

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2025-08-01 19:41:07 +04:00
parent a5f3000f2e
commit 326a005382
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
6 changed files with 140 additions and 44 deletions

View File

@ -1439,7 +1439,7 @@ func (slb *siderolinkBuilder) SetKernelArgs(extraKernelArgs *procfs.Cmdline, tun
return fmt.Errorf("failed to close zstd encoder: %w", err)
}
extraKernelArgs.Append(constants.KernelParamConfigInline, base64.StdEncoding.EncodeToString(buf.Bytes()))
extraKernelArgs.Append(constants.KernelParamConfigEarly, base64.StdEncoding.EncodeToString(buf.Bytes()))
return nil
}

View File

@ -135,6 +135,14 @@ Legacy configuration in `valpha1` machine configuration is still supported.
New per-key option `lockToSTATE` is added to the `VolumeConfig` document, which allows to lock the volume encryption key to the secret salt in the `STATE` volume.
So, if the `STATE` volume is wiped or replaced, the volume encryption key will not be usable anymore.
"""
[notes.early-config]
title = "Early Inline Configuration"
description = """\
Talos now supports passing early inline configuration via the `talos.config.early` kernel parameter.
This allows to pass the configuration before the platform config source is probed, which is useful for early boot configuration.
The value of this parameter has same format as the `talos.config.inline` parameter, i.e. it should be base64 encoded and zstd-compressed.
"""
[make_deps]

View File

@ -222,7 +222,7 @@ func (ctrl *AcquireController) Run(ctx context.Context, r controller.Runtime, lo
//
// Transitions:
//
// --> platform: no config found on disk, proceed to platform
// --> cmdlineEarly: no config found on disk, proceed to cmdlineEarly
// --> maintenanceEnter: config found on disk, but it's incomplete, proceed to maintenance
// --> done: config found on disk, and it's complete
//
@ -239,8 +239,8 @@ func (ctrl *AcquireController) stateDisk(ctx context.Context, r controller.Runti
// wait for the status to be available
return nil, nil, nil
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseMissing:
// STATE is missing, proceed to platform
return ctrl.statePlatform, nil, nil
// STATE is missing, proceed to cmdlineEarly
return ctrl.stateCmdlineEarly, nil, nil
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseReady:
// STATE is ready, proceed to to the action
default:
@ -264,8 +264,8 @@ func (ctrl *AcquireController) stateDisk(ctx context.Context, r controller.Runti
switch {
case cfg == nil:
// no config loaded, proceed to platform
return ctrl.statePlatform, nil, nil
// no config loaded, proceed to cmdlineEarly
return ctrl.stateCmdlineEarly, nil, nil
case cfg.CompleteForBoot():
// complete config, we are done
return ctrl.stateDone, cfg, nil
@ -356,11 +356,23 @@ func (ctrl *AcquireController) loadConfigFromDisk(ctx context.Context, r control
return nil
}
// stateCmdlineEarly acquires machine configuration from the kernel cmdline source (talos.config.early).
//
// It is called before the platform source.
//
// Transitions:
//
// --> platform: config loaded from cmdline, but it's incomplete, or no config: proceed to platform
// --> done: config loaded from cmdline, and it's complete
func (ctrl *AcquireController) stateCmdlineEarly(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
return ctrl.stateCmdlineGeneric(constants.KernelParamConfigEarly, "cmdline-early", ctrl.statePlatform)(ctx, r, logger)
}
// statePlatform acquires machine configuration from the platform source.
//
// Transitions:
//
// --> cmdline: config loaded from platform, but it's incomplete, or no config from platform: proceed to cmdline
// --> cmdlineLate: config loaded from platform, but it's incomplete, or no config from platform: proceed to cmdline
// --> done: config loaded from platform, and it's complete
func (ctrl *AcquireController) statePlatform(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
cfg, err := ctrl.loadFromPlatform(ctx, logger)
@ -377,7 +389,7 @@ func (ctrl *AcquireController) statePlatform(ctx context.Context, r controller.R
fallthrough
case !cfg.CompleteForBoot():
// incomplete or missing config, proceed to maintenance
return ctrl.stateCmdline, cfg, nil
return ctrl.stateCmdlineLate, cfg, nil
default:
// complete config, we are done
return ctrl.stateDone, cfg, nil
@ -445,25 +457,35 @@ func (ctrl *AcquireController) loadFromPlatform(ctx context.Context, logger *zap
return cfg, nil
}
// stateCmdline acquires machine configuration from the kernel cmdline source.
// stateCmdlineLate acquires machine configuration from the kernel cmdline source (talos.config.inline).
//
// It is called after the platform source.
//
// Transitions:
//
// --> maintenanceEnter: config loaded from cmdline, but it's incomplete, or no config from platform: proceed to maintenance
// --> maintenanceEnter: config loaded from cmdline, but it's incomplete, or no config from cmdline: proceed to maintenance
// --> done: config loaded from cmdline, and it's complete
func (ctrl *AcquireController) stateCmdline(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
if ctrl.Mode.InContainer() {
// no cmdline in containers
return ctrl.stateMaintenanceEnter, nil, nil
func (ctrl *AcquireController) stateCmdlineLate(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
return ctrl.stateCmdlineGeneric(constants.KernelParamConfigInline, "cmdline-late", ctrl.stateMaintenanceEnter)(ctx, r, logger)
}
cfg, err := ctrl.loadFromCmdline(ctx, logger)
// stateCmdlineGeneric is a generic function to load config from cmdline given a parameter name and source name, and the next state in the state machine.
func (ctrl *AcquireController) stateCmdlineGeneric(
paramName, sourceName string, next stateMachineFunc,
) func(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
return func(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
if ctrl.Mode.InContainer() {
// no cmdline in containers
return next, nil, nil
}
cfg, err := ctrl.loadFromCmdline(ctx, logger, paramName)
if err != nil {
return nil, nil, err
}
if cfg != nil {
ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, "cmdline")
ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, sourceName)
}
switch {
@ -471,26 +493,27 @@ func (ctrl *AcquireController) stateCmdline(ctx context.Context, r controller.Ru
fallthrough
case !cfg.CompleteForBoot():
// incomplete or missing config, proceed to maintenance
return ctrl.stateMaintenanceEnter, cfg, nil
return next, cfg, nil
default:
// complete config, we are done
return ctrl.stateDone, cfg, nil
}
}
}
// loadFromCmdline is a helper function for stateCmdline.
//
//nolint:gocyclo
func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.Logger) (config.Provider, error) {
func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.Logger, paramName string) (config.Provider, error) {
cmdline := ctrl.CmdlineGetter()
param := cmdline.Get(constants.KernelParamConfigInline)
param := cmdline.Get(paramName)
if param == nil {
return nil, nil
}
logger.Info("getting config from cmdline", zap.String("param", constants.KernelParamConfigInline))
logger.Info("getting config from cmdline", zap.String("param", paramName))
var cfgEncoded strings.Builder
@ -505,7 +528,7 @@ func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.
cfgDecoded, err := base64.StdEncoding.DecodeString(cfgEncoded.String())
if err != nil {
return nil, fmt.Errorf("failed to decode base64 config from cmdline %s: %w", constants.KernelParamConfigInline, err)
return nil, fmt.Errorf("failed to decode base64 config from cmdline %s: %w", paramName, err)
}
zr, err := zstd.NewReader(bytes.NewReader(cfgDecoded))
@ -517,26 +540,26 @@ func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.
cfgBytes, err := io.ReadAll(zr)
if err != nil {
return nil, fmt.Errorf("failed to read zstd compressed config from cmdline %s: %w", constants.KernelParamConfigInline, err)
return nil, fmt.Errorf("failed to read zstd compressed config from cmdline %s: %w", paramName, err)
}
cfg, err := configloader.NewFromBytes(cfgBytes)
if err != nil {
return nil, fmt.Errorf("failed to load config via cmdline %s: %w", constants.KernelParamConfigInline, err)
return nil, fmt.Errorf("failed to load config via cmdline %s: %w", paramName, err)
}
warnings, err := cfg.Validate(ctrl.ValidationMode)
if err != nil {
return nil, fmt.Errorf("failed to validate config acquired via cmdline %s: %w", constants.KernelParamConfigInline, err)
return nil, fmt.Errorf("failed to validate config acquired via cmdline %s: %w", paramName, err)
}
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)
return nil, fmt.Errorf("failed to validate config acquired via cmdline %s: %w", paramName, err)
}
for _, warning := range slices.Concat(warnings, warningsRuntime) {
logger.Warn("config validation warning", zap.String("cmdline", constants.KernelParamConfigInline), zap.String("warning", warning))
logger.Warn("config validation warning", zap.String("cmdline", paramName), zap.String("warning", warning))
}
return cfg, nil

View File

@ -526,7 +526,7 @@ func (suite *AcquireSuite) TestFromPlatformToMaintenance() {
)
}
func (suite *AcquireSuite) TestFromCmdlineToMaintenance() {
func (suite *AcquireSuite) TestFromCmdlineLateToMaintenance() {
var cfgCompressed bytes.Buffer
zw, err := zstd.NewWriter(&cfgCompressed)
@ -593,6 +593,57 @@ func (suite *AcquireSuite) TestFromCmdlineToMaintenance() {
)
}
func (suite *AcquireSuite) TestFromCmdlineEarlyToPlatform() {
var cfgCompressed bytes.Buffer
zw, err := zstd.NewWriter(&cfgCompressed)
suite.Require().NoError(err)
_, err = zw.Write(suite.partialMachineConfig)
suite.Require().NoError(err)
suite.Require().NoError(zw.Close())
cfgEncoded := base64.StdEncoding.EncodeToString(cfgCompressed.Bytes())
suite.cmdline.cmdline = procfs.NewCmdline(fmt.Sprintf("%s=%s", constants.KernelParamConfigEarly, cfgEncoded))
suite.noStateVolume()
suite.platformConfig.configuration = suite.completeMachineConfig
suite.platformConfig.err = nil
suite.triggerAcquire()
var cfg config.Provider
select {
case cfg = <-suite.configSetter.cfgCh:
case <-suite.Ctx().Done():
suite.Require().Fail("timed out waiting for config")
}
select {
case <-suite.configSetter.persistedCfgCh:
case <-suite.Ctx().Done():
suite.Require().Fail("timed out waiting for persisted config")
}
suite.Require().Equal(cfg.SideroLink().APIUrl().Host, "siderolink.api")
cfg = suite.waitForConfig(true)
suite.Require().Equal(cfg.Cluster().Name(), suite.clusterName)
suite.Assert().Empty(suite.eventPublisher.getEvents())
suite.Assert().Equal(
[]platform.Event{
{
Type: platform.EventTypeConfigLoaded,
Message: "Talos machine config loaded successfully.",
},
},
suite.platformEvent.getEvents(),
)
}
func (suite *AcquireSuite) TestFromMaintenance() {
suite.noStateVolume()
suite.triggerAcquire()

View File

@ -25,6 +25,11 @@ const (
// The inline config should be base64 encoded and zstd-compressed.
KernelParamConfigInline = "talos.config.inline"
// KernelParamConfigEarly is the kernel parameter name for specifying the inline config (as the first source).
//
// The inline config should be base64 encoded and zstd-compressed.
KernelParamConfigEarly = "talos.config.early"
// KernelParamConfigOAuthClientID is the kernel parameter name for specifying the OAuth2 client ID.
KernelParamConfigOAuthClientID = "talos.config.oauth.client_id"

View File

@ -145,10 +145,19 @@ mkisofs -joliet -rock -volid 'metal-iso' -output config.iso iso/
Kernel parameters prefixed with `talos.config.auth.` are used to configure [OAuth2 authentication for the machine configuration]({{< relref "../advanced/machine-config-oauth" >}}).
#### `talos.config.inline`
#### `talos.config.early` and `talos.config.inline`
The kernel parameter `talos.config.inline` can be used to provide initial minimal machine configuration directly on the kernel command line, when other means of providing the configuration are not available.
The machine configuration should be `zstd` compressed and base64-encoded to be passed as a kernel parameter.
The kernel parameters `talos.config.early` and `talos.config.inline` are used to provide the initial machine configuration directly on the kernel command line.
The difference between the two is the order of configuration loading:
* first, the persisted configuration is loaded from the `STATE` partition, if it exists;
* `talos.config.early` is tried next;
* any platform-specific configuration source is tried next (e.g. `talos.config` for the `metal` platform, our VM user data source, etc.);
* finally, `talos.config.inline` is tried;
* if no complete configuration is found, the system will enter maintenance mode.
For both arguments, the machine configuration should be `zstd` compressed and base64-encoded to be passed as a kernel parameter.
> Note: The kernel command line has a limited size (4096 bytes), so this method is only suitable for small configuration documents.