Skip to content

Commit

Permalink
feat: acquire machine config inline from kernel cmdline
Browse files Browse the repository at this point in the history
Fixes #9175

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
  • Loading branch information
smira committed Sep 6, 2024
1 parent 3612834 commit 545f75f
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 2 deletions.
6 changes: 6 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ Talos Linux now supports [configuration](https://www.talos.dev/v1.8/talos-guides
title = "KubeSpan"
description = """\
Extra announced endpoints can be added using the [`KubespanEndpointsConfig` document](https://www.talos.dev/v1.8/talos-guides/network/kubespan/#configuration).
"""

[notes.cmdline-config]
title = "Machine Configuration via Kernel Command Line"
description = """\
Talos Linux supports supplying zstd-compressed, base64-encoded machine configuration small documents via the kernel command line parameter `talos.config.inline`.
"""

[make_deps]
Expand Down
105 changes: 103 additions & 2 deletions internal/app/machined/pkg/controllers/config/acquire.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/klauspost/compress/zstd"
"github.com/siderolabs/gen/optional"
"github.com/siderolabs/go-procfs/procfs"
"go.uber.org/zap"

talosruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
Expand Down Expand Up @@ -49,10 +53,17 @@ type Setter interface {
SetConfig(config.Provider) error
}

// ModeGetter gets the current runtime mode.
type ModeGetter interface {
InContainer() bool
}

// AcquireController loads the machine configuration from multiple sources.
type AcquireController struct {
PlatformConfiguration PlatformConfigurator
PlatformEvent PlatformEventer
Mode ModeGetter
CmdlineGetter func() *procfs.Cmdline
ConfigSetter Setter
EventPublisher talosruntime.Publisher
ValidationMode validation.RuntimeMode
Expand Down Expand Up @@ -261,7 +272,7 @@ func (ctrl *AcquireController) loadFromDisk(logger *zap.Logger) (config.Provider
//
// Transitions:
//
// --> maintenanceEnter: config loaded from platform, but it's incomplete, or no config from platform: proceed to maintenance
// --> cmdline: 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)
Expand All @@ -278,7 +289,7 @@ func (ctrl *AcquireController) statePlatform(ctx context.Context, r controller.R
fallthrough
case !cfg.CompleteForBoot():
// incomplete or missing config, proceed to maintenance
return ctrl.stateMaintenanceEnter, cfg, nil
return ctrl.stateCmdline, cfg, nil
default:
// complete config, we are done
return ctrl.stateDone, cfg, nil
Expand Down Expand Up @@ -341,6 +352,96 @@ func (ctrl *AcquireController) loadFromPlatform(ctx context.Context, logger *zap
return cfg, nil
}

// stateCmdline acquires machine configuration from the kernel cmdline source.
//
// Transitions:
//
// --> maintenanceEnter: config loaded from cmdline, but it's incomplete, or no config from platform: 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
}

cfg, err := ctrl.loadFromCmdline(logger)
if err != nil {
return nil, nil, err
}

if cfg != nil {
ctrl.configSourcesUsed = append(ctrl.configSourcesUsed, "cmdline")
}

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
}
}

// loadFromCmdline is a helper function for stateCmdline.
func (ctrl *AcquireController) loadFromCmdline(logger *zap.Logger) (config.Provider, error) {
cmdline := ctrl.CmdlineGetter()

param := cmdline.Get(constants.KernelParamConfigInline)

if param == nil {
return nil, nil
}

logger.Info("getting config from cmdline", zap.String("param", constants.KernelParamConfigInline))

var cfgEncoded strings.Builder

for i := 0; ; i++ {
v := param.Get(i)
if v == nil {
break
}

cfgEncoded.WriteString(*v)
}

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)
}

zr, err := zstd.NewReader(bytes.NewReader(cfgDecoded))
if err != nil {
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
}

defer zr.Close()

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)
}

cfg, err := configloader.NewFromBytes(cfgBytes)
if err != nil {
return nil, fmt.Errorf("failed to load config via cmdline %s: %w", constants.KernelParamConfigInline, 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)
}

for _, warning := range warnings {
logger.Warn("config validation warning", zap.String("cmdline", constants.KernelParamConfigInline), zap.String("warning", warning))
}

return cfg, nil
}

// stateMaintenanceEnter initializes maintenance service.
//
// Transitions:
Expand Down
80 changes: 80 additions & 0 deletions internal/app/machined/pkg/controllers/config/acquire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
stderrors "errors"
"fmt"
"math/rand/v2"
Expand All @@ -22,6 +23,8 @@ import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/state"
"github.com/klauspost/compress/zstd"
"github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
Expand All @@ -37,6 +40,7 @@ import (
"github.com/siderolabs/talos/pkg/machinery/config/generate"
"github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/config/types/siderolink"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/proto"
configresource "github.com/siderolabs/talos/pkg/machinery/resources/config"
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
Expand All @@ -51,6 +55,7 @@ type AcquireSuite struct {
platformEvent *platformEventMock
configSetter *configSetterMock
eventPublisher *eventPublisherMock
cmdline *cmdlineGetterMock

clusterName string
completeMachineConfig []byte
Expand Down Expand Up @@ -118,6 +123,16 @@ func (e *eventPublisherMock) getEvents() []proto.Message {
return slices.Clone(e.events)
}

type cmdlineGetterMock struct {
cmdline *procfs.Cmdline
}

func (c *cmdlineGetterMock) Getter() func() *procfs.Cmdline {
return func() *procfs.Cmdline {
return c.cmdline
}
}

type validationModeMock struct{}

func (v validationModeMock) String() string {
Expand Down Expand Up @@ -152,6 +167,9 @@ func TestAcquireSuite(t *testing.T) {
cfgCh: make(chan config.Provider, 1),
}
s.eventPublisher = &eventPublisherMock{}
s.cmdline = &cmdlineGetterMock{
procfs.NewCmdline(""),
}

s.clusterName = fmt.Sprintf("cluster-%d", rand.Int32())
input, err := generate.NewInput(s.clusterName, "https://localhost:6443", "")
Expand All @@ -176,6 +194,8 @@ func TestAcquireSuite(t *testing.T) {
PlatformConfiguration: s.platformConfig,
PlatformEvent: s.platformEvent,
ConfigSetter: s.configSetter,
Mode: validationModeMock{},
CmdlineGetter: s.cmdline.Getter(),
EventPublisher: s.eventPublisher,
ValidationMode: validationModeMock{},
ConfigPath: s.configPath,
Expand Down Expand Up @@ -429,6 +449,66 @@ func (suite *AcquireSuite) TestFromPlatformToMaintenance() {
)
}

func (suite *AcquireSuite) TestFromCmdlineToMaintenance() {
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.KernelParamConfigInline, cfgEncoded))

suite.triggerAcquire()

var cfg config.Provider

select {
case cfg = <-suite.configSetter.cfgCh:
case <-suite.Ctx().Done():
suite.Require().Fail("timed out waiting for config")
}

suite.Require().Equal(cfg.SideroLink().APIUrl().Host, "siderolink.api")

suite.injectViaMaintenance(suite.completeMachineConfig)

cfg = suite.waitForConfig()
suite.Require().Equal(cfg.Cluster().Name(), suite.clusterName)

suite.Assert().Equal(
[]proto.Message{
&machineapi.TaskEvent{
Action: machineapi.TaskEvent_START,
Task: "runningMaintenance",
},
&machineapi.TaskEvent{
Action: machineapi.TaskEvent_STOP,
Task: "runningMaintenance",
},
},
suite.eventPublisher.getEvents(),
)
suite.Assert().Equal(
[]platform.Event{
{
Type: platform.EventTypeActivate,
Message: "Talos booted into maintenance mode. Ready for user interaction.",
},
{
Type: platform.EventTypeConfigLoaded,
Message: "Talos machine config loaded successfully.",
},
},
suite.platformEvent.getEvents(),
)
}

func (suite *AcquireSuite) TestFromMaintenance() {
suite.triggerAcquire()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
PlatformEvent: &platformEventer{
platform: ctrl.v1alpha1Runtime.State().Platform(),
},
Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
CmdlineGetter: procfs.ProcCmdline,
ConfigSetter: ctrl.v1alpha1Runtime,
EventPublisher: ctrl.v1alpha1Runtime.Events(),
ValidationMode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
Expand Down
5 changes: 5 additions & 0 deletions pkg/machinery/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const (
// to the config.
KernelParamConfig = "talos.config"

// KernelParamConfigInline is the kernel parameter name for specifying the inline config.
//
// The inline config should be base64 encoded and zstd-compressed.
KernelParamConfigInline = "talos.config.inline"

// KernelParamConfigOAuthClientID is the kernel parameter name for specifying the OAuth2 client ID.
KernelParamConfigOAuthClientID = "talos.config.oauth.client_id"

Expand Down
13 changes: 13 additions & 0 deletions website/content/v1.8/reference/kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +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`

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.

> Note: The kernel command line has a limited size (4096 bytes), so this method is only suitable for small configuration documents.
One such example is to provide [a custom CA certificate]({{< relref "../talos-guides/configuration/certificate-authorities" >}}) via `TrustedRootsConfig` in the machine configuration:

```shell
cat config.yaml | zstd --compress --ultra -22 | base64 -w 0
```

#### `talos.platform`

The platform name on which Talos will run.
Expand Down

0 comments on commit 545f75f

Please sign in to comment.