mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-06 14:47:05 +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)
|
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
|
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.
|
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.
|
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]
|
[make_deps]
|
||||||
|
@ -222,7 +222,7 @@ func (ctrl *AcquireController) Run(ctx context.Context, r controller.Runtime, lo
|
|||||||
//
|
//
|
||||||
// Transitions:
|
// 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
|
// --> maintenanceEnter: config found on disk, but it's incomplete, proceed to maintenance
|
||||||
// --> done: config found on disk, and it's complete
|
// --> 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
|
// wait for the status to be available
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseMissing:
|
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseMissing:
|
||||||
// STATE is missing, proceed to platform
|
// STATE is missing, proceed to cmdlineEarly
|
||||||
return ctrl.statePlatform, nil, nil
|
return ctrl.stateCmdlineEarly, nil, nil
|
||||||
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseReady:
|
case stateVolumeStatus.TypedSpec().Phase == block.VolumePhaseReady:
|
||||||
// STATE is ready, proceed to to the action
|
// STATE is ready, proceed to to the action
|
||||||
default:
|
default:
|
||||||
@ -264,8 +264,8 @@ func (ctrl *AcquireController) stateDisk(ctx context.Context, r controller.Runti
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case cfg == nil:
|
case cfg == nil:
|
||||||
// no config loaded, proceed to platform
|
// no config loaded, proceed to cmdlineEarly
|
||||||
return ctrl.statePlatform, nil, nil
|
return ctrl.stateCmdlineEarly, nil, nil
|
||||||
case cfg.CompleteForBoot():
|
case cfg.CompleteForBoot():
|
||||||
// complete config, we are done
|
// complete config, we are done
|
||||||
return ctrl.stateDone, cfg, nil
|
return ctrl.stateDone, cfg, nil
|
||||||
@ -356,11 +356,23 @@ func (ctrl *AcquireController) loadConfigFromDisk(ctx context.Context, r control
|
|||||||
return nil
|
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.
|
// statePlatform acquires machine configuration from the platform source.
|
||||||
//
|
//
|
||||||
// Transitions:
|
// 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
|
// --> 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) {
|
func (ctrl *AcquireController) statePlatform(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
|
||||||
cfg, err := ctrl.loadFromPlatform(ctx, logger)
|
cfg, err := ctrl.loadFromPlatform(ctx, logger)
|
||||||
@ -377,7 +389,7 @@ func (ctrl *AcquireController) statePlatform(ctx context.Context, r controller.R
|
|||||||
fallthrough
|
fallthrough
|
||||||
case !cfg.CompleteForBoot():
|
case !cfg.CompleteForBoot():
|
||||||
// incomplete or missing config, proceed to maintenance
|
// incomplete or missing config, proceed to maintenance
|
||||||
return ctrl.stateCmdline, cfg, nil
|
return ctrl.stateCmdlineLate, cfg, nil
|
||||||
default:
|
default:
|
||||||
// complete config, we are done
|
// complete config, we are done
|
||||||
return ctrl.stateDone, cfg, nil
|
return ctrl.stateDone, cfg, nil
|
||||||
@ -445,52 +457,63 @@ func (ctrl *AcquireController) loadFromPlatform(ctx context.Context, logger *zap
|
|||||||
return cfg, nil
|
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:
|
// 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
|
// --> 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) {
|
func (ctrl *AcquireController) stateCmdlineLate(ctx context.Context, r controller.Runtime, logger *zap.Logger) (stateMachineFunc, config.Provider, error) {
|
||||||
if ctrl.Mode.InContainer() {
|
return ctrl.stateCmdlineGeneric(constants.KernelParamConfigInline, "cmdline-late", ctrl.stateMaintenanceEnter)(ctx, r, logger)
|
||||||
// no cmdline in containers
|
}
|
||||||
return ctrl.stateMaintenanceEnter, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
||||||
if err != nil {
|
func (ctrl *AcquireController) stateCmdlineGeneric(
|
||||||
return nil, nil, err
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if cfg != nil {
|
cfg, err := ctrl.loadFromCmdline(ctx, logger, paramName)
|
||||||
ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, "cmdline")
|
if err != nil {
|
||||||
}
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
if cfg != nil {
|
||||||
case cfg == nil:
|
ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, sourceName)
|
||||||
fallthrough
|
}
|
||||||
case !cfg.CompleteForBoot():
|
|
||||||
// incomplete or missing config, proceed to maintenance
|
switch {
|
||||||
return ctrl.stateMaintenanceEnter, cfg, nil
|
case cfg == nil:
|
||||||
default:
|
fallthrough
|
||||||
// complete config, we are done
|
case !cfg.CompleteForBoot():
|
||||||
return ctrl.stateDone, cfg, nil
|
// incomplete or missing config, proceed to maintenance
|
||||||
|
return next, cfg, nil
|
||||||
|
default:
|
||||||
|
// complete config, we are done
|
||||||
|
return ctrl.stateDone, cfg, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadFromCmdline is a helper function for stateCmdline.
|
// loadFromCmdline is a helper function for stateCmdline.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//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()
|
cmdline := ctrl.CmdlineGetter()
|
||||||
|
|
||||||
param := cmdline.Get(constants.KernelParamConfigInline)
|
param := cmdline.Get(paramName)
|
||||||
|
|
||||||
if param == nil {
|
if param == nil {
|
||||||
return nil, 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
|
var cfgEncoded strings.Builder
|
||||||
|
|
||||||
@ -505,7 +528,7 @@ func (ctrl *AcquireController) loadFromCmdline(ctx context.Context, logger *zap.
|
|||||||
|
|
||||||
cfgDecoded, err := base64.StdEncoding.DecodeString(cfgEncoded.String())
|
cfgDecoded, err := base64.StdEncoding.DecodeString(cfgEncoded.String())
|
||||||
if err != nil {
|
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))
|
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)
|
cfgBytes, err := io.ReadAll(zr)
|
||||||
if err != nil {
|
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)
|
cfg, err := configloader.NewFromBytes(cfgBytes)
|
||||||
if err != nil {
|
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)
|
warnings, err := cfg.Validate(ctrl.ValidationMode)
|
||||||
if err != nil {
|
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)
|
warningsRuntime, err := cfg.RuntimeValidate(ctx, ctrl.ResourceState, ctrl.ValidationMode)
|
||||||
if err != nil {
|
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) {
|
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
|
return cfg, nil
|
||||||
|
@ -526,7 +526,7 @@ func (suite *AcquireSuite) TestFromPlatformToMaintenance() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AcquireSuite) TestFromCmdlineToMaintenance() {
|
func (suite *AcquireSuite) TestFromCmdlineLateToMaintenance() {
|
||||||
var cfgCompressed bytes.Buffer
|
var cfgCompressed bytes.Buffer
|
||||||
|
|
||||||
zw, err := zstd.NewWriter(&cfgCompressed)
|
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() {
|
func (suite *AcquireSuite) TestFromMaintenance() {
|
||||||
suite.noStateVolume()
|
suite.noStateVolume()
|
||||||
suite.triggerAcquire()
|
suite.triggerAcquire()
|
||||||
|
@ -25,6 +25,11 @@ const (
|
|||||||
// The inline config should be base64 encoded and zstd-compressed.
|
// The inline config should be base64 encoded and zstd-compressed.
|
||||||
KernelParamConfigInline = "talos.config.inline"
|
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 is the kernel parameter name for specifying the OAuth2 client ID.
|
||||||
KernelParamConfigOAuthClientID = "talos.config.oauth.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" >}}).
|
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 kernel parameters `talos.config.early` and `talos.config.inline` are used to provide the initial machine configuration directly on the kernel command line.
|
||||||
The machine configuration should be `zstd` compressed and base64-encoded to be passed as a kernel parameter.
|
|
||||||
|
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.
|
> 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