diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/create.go b/cmd/talosctl/cmd/mgmt/cluster/create/create.go index 2df2b527d..62d1c74f0 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/create.go @@ -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 } diff --git a/hack/release.toml b/hack/release.toml index a4984fbe6..38a29fe90 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -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] diff --git a/internal/app/machined/pkg/controllers/config/acquire.go b/internal/app/machined/pkg/controllers/config/acquire.go index 56fc1b5b0..98a3fc2c6 100644 --- a/internal/app/machined/pkg/controllers/config/acquire.go +++ b/internal/app/machined/pkg/controllers/config/acquire.go @@ -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,52 +457,63 @@ 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) - if err != nil { - return nil, nil, err - } +// 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 + } - if cfg != nil { - ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, "cmdline") - } + cfg, err := ctrl.loadFromCmdline(ctx, logger, paramName) + if err != nil { + return nil, nil, err + } - switch { - case cfg == nil: - fallthrough - case !cfg.CompleteForBoot(): - // incomplete or missing config, proceed to maintenance - return ctrl.stateMaintenanceEnter, cfg, nil - default: - // complete config, we are done - return ctrl.stateDone, cfg, nil + if cfg != nil { + ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, sourceName) + } + + switch { + case cfg == nil: + fallthrough + case !cfg.CompleteForBoot(): + // 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. // //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 diff --git a/internal/app/machined/pkg/controllers/config/acquire_test.go b/internal/app/machined/pkg/controllers/config/acquire_test.go index 3db7074d7..88459c3fc 100644 --- a/internal/app/machined/pkg/controllers/config/acquire_test.go +++ b/internal/app/machined/pkg/controllers/config/acquire_test.go @@ -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() diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index f5c91cddc..141db0085 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -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" diff --git a/website/content/v1.11/reference/kernel.md b/website/content/v1.11/reference/kernel.md index 5e88ae50a..484a711e8 100644 --- a/website/content/v1.11/reference/kernel.md +++ b/website/content/v1.11/reference/kernel.md @@ -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.