mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-06 06:37:07 +02:00
feat: implement talos.config.early
command line arg
Fixes #11449 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
parent
a5f3000f2e
commit
326a005382
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user