From 48790938bd98ccfc3d2ca4df76c2b5da4914f9b6 Mon Sep 17 00:00:00 2001 From: s3rj1k Date: Mon, 17 Nov 2025 13:44:23 +0100 Subject: [PATCH] Add `--token-env` flag to install command Signed-off-by: s3rj1k --- cmd/controller/controller.go | 23 ++++++++++++++++- cmd/controller/controller_test.go | 1 + cmd/install/controller.go | 10 ++++++++ cmd/install/controller_test.go | 1 + cmd/install/install.go | 1 + cmd/install/util.go | 28 ++++++++++++++++++++- cmd/install/worker.go | 15 +++++++++++ cmd/worker/worker.go | 41 +++++++++++++++++++++++++------ pkg/config/cli.go | 2 ++ 9 files changed, 113 insertions(+), 9 deletions(-) diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go index 696f1fcff652..3b2b1519499e 100644 --- a/cmd/controller/controller.go +++ b/cmd/controller/controller.go @@ -182,10 +182,31 @@ func (c *command) start(ctx context.Context, flags *config.ControllerOptions, de var joinClient *token.JoinClient - if (c.TokenArg != "" || c.TokenFile != "") && c.needToJoin(nodeConfig) { + if (c.TokenArg != "" || c.TokenFile != "" || c.TokenEnv != "") && c.needToJoin(nodeConfig) { + tokenSources := 0 + if c.TokenArg != "" { + tokenSources++ + } + if c.TokenFile != "" { + tokenSources++ + } + if c.TokenEnv != "" { + tokenSources++ + } + + if tokenSources > 1 { + return errors.New("you can only pass one token source: either as a CLI argument 'k0s controller [token]', via '--token-file [path]', or via '--token-env [var]'") + } + var tokenData string if c.TokenArg != "" { tokenData = c.TokenArg + } else if c.TokenEnv != "" { + tokenValue := os.Getenv(c.TokenEnv) + if tokenValue == "" { + return fmt.Errorf("environment variable %q is not set or is empty", c.TokenEnv) + } + tokenData = tokenValue } else { data, err := os.ReadFile(c.TokenFile) if err != nil { diff --git a/cmd/controller/controller_test.go b/cmd/controller/controller_test.go index fca8e2ae013d..30c2faca8750 100644 --- a/cmd/controller/controller_test.go +++ b/cmd/controller/controller_test.go @@ -75,6 +75,7 @@ Flags: --single enable single node (implies --enable-worker, default false) --status-socket string Full file path to the socket file. (default: /status.sock) --taints strings Node taints, list of key=value:effect strings + --token-env string Environment variable name containing the join-token. --token-file string Path to the file containing join-token. -v, --verbose Verbose logging (default true) `, out.String()) diff --git a/cmd/install/controller.go b/cmd/install/controller.go index 5210f5882fd0..4ff7b433a311 100644 --- a/cmd/install/controller.go +++ b/cmd/install/controller.go @@ -49,11 +49,21 @@ With the controller subcommand you can setup a single node cluster by running: return fmt.Errorf("invalid node config: %w", errors.Join(errs...)) } + // Convert --token-env to --token-file + tokenFilePath, err := handleTokenEnv(cmd, k0sVars.DataDir) + if err != nil { + return err + } + flagsAndVals, err := cmdFlagsToArgs(cmd) if err != nil { return err } + if tokenFilePath != "" { + flagsAndVals = append(flagsAndVals, "--token-file="+tokenFilePath) + } + systemUsers := nodeConfig.Spec.Install.SystemUsers homeDir := k0sVars.DataDir if err := install.EnsureControllerUsers(systemUsers, homeDir); err != nil { diff --git a/cmd/install/controller_test.go b/cmd/install/controller_test.go index e0044f8bb4b3..a38e52ad67af 100644 --- a/cmd/install/controller_test.go +++ b/cmd/install/controller_test.go @@ -68,6 +68,7 @@ Flags: --single enable single node (implies --enable-worker, default false) --status-socket string Full file path to the socket file. (default: /status.sock) --taints strings Node taints, list of key=value:effect strings + --token-env string Environment variable name containing the join-token. --token-file string Path to the file containing join-token. Global Flags: diff --git a/cmd/install/install.go b/cmd/install/install.go index 95520922afe8..7619def83e67 100644 --- a/cmd/install/install.go +++ b/cmd/install/install.go @@ -6,6 +6,7 @@ package install import ( "github.com/k0sproject/k0s/cmd/internal" "github.com/k0sproject/k0s/pkg/config" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/cmd/install/util.go b/cmd/install/util.go index 347f08ad9ee0..606ac9c6deba 100644 --- a/cmd/install/util.go +++ b/cmd/install/util.go @@ -6,6 +6,7 @@ package install import ( "errors" "fmt" + "os" "path/filepath" "strings" @@ -24,7 +25,7 @@ func cmdFlagsToArgs(cmd *cobra.Command) ([]string, error) { flagsAndVals = append(flagsAndVals, fmt.Sprintf(`--%s=%s`, f.Name, strings.Trim(val, "[]"))) default: switch f.Name { - case "env", "force": + case "env", "force", "token-env": return case "data-dir", "kubelet-root-dir", "token-file", "config": if absVal, err := filepath.Abs(val); err != nil { @@ -44,3 +45,28 @@ func cmdFlagsToArgs(cmd *cobra.Command) ([]string, error) { return flagsAndVals, nil } + +// handleTokenEnv converts --token-env to a token file and returns its path. +func handleTokenEnv(cmd *cobra.Command, dataDir string) (string, error) { + tokenEnvFlag := cmd.Flags().Lookup("token-env") + if tokenEnvFlag == nil || !tokenEnvFlag.Changed { + return "", nil + } + + envVarName := tokenEnvFlag.Value.String() + tokenValue := os.Getenv(envVarName) + if tokenValue == "" { + return "", fmt.Errorf("environment variable %q is not set or is empty", envVarName) + } + + tokenFilePath := filepath.Join(dataDir, ".token") + if err := os.MkdirAll(dataDir, 0755); err != nil { + return "", fmt.Errorf("failed to create data directory: %w", err) + } + + if err := os.WriteFile(tokenFilePath, []byte(tokenValue), 0600); err != nil { + return "", fmt.Errorf("failed to write token file: %w", err) + } + + return tokenFilePath, nil +} diff --git a/cmd/install/worker.go b/cmd/install/worker.go index dd261dd0e235..6496a30dcda8 100644 --- a/cmd/install/worker.go +++ b/cmd/install/worker.go @@ -27,11 +27,26 @@ All default values of worker command will be passed to the service stub unless o return errors.New("this command must be run as root") } + k0sVars, err := config.NewCfgVars(cmd) + if err != nil { + return fmt.Errorf("failed to initialize configuration variables: %w", err) + } + + // Convert --token-env to --token-file + tokenFilePath, err := handleTokenEnv(cmd, k0sVars.DataDir) + if err != nil { + return err + } + flagsAndVals, err := cmdFlagsToArgs(cmd) if err != nil { return err } + if tokenFilePath != "" { + flagsAndVals = append(flagsAndVals, "--token-file="+tokenFilePath) + } + args := append([]string{"worker"}, flagsAndVals...) if err := install.InstallService(args, installFlags.envVars, installFlags.force); err != nil { return fmt.Errorf("failed to install worker service: %w", err) diff --git a/cmd/worker/worker.go b/cmd/worker/worker.go index 78add1ad0a16..f166ed8f8789 100644 --- a/cmd/worker/worker.go +++ b/cmd/worker/worker.go @@ -80,7 +80,7 @@ func NewWorkerCmd() *cobra.Command { c.TokenArg = args[0] } - getBootstrapKubeconfig, err := kubeconfigGetterFromJoinToken(c.TokenFile, c.TokenArg) + getBootstrapKubeconfig, err := kubeconfigGetterFromJoinToken(c.TokenFile, c.TokenEnv, c.TokenArg) if err != nil { return err } @@ -150,12 +150,23 @@ func GetNodeName(opts *config.WorkerOptions) (apitypes.NodeName, stringmap.Strin return nodeName, kubeletExtraArgs, nil } -func kubeconfigGetterFromJoinToken(tokenFile, tokenArg string) (clientcmd.KubeconfigGetter, error) { +func kubeconfigGetterFromJoinToken(tokenFile, tokenEnv, tokenArg string) (clientcmd.KubeconfigGetter, error) { + tokenSources := 0 if tokenArg != "" { - if tokenFile != "" { - return nil, errors.New("you can only pass one token argument either as a CLI argument 'k0s worker [token]' or as a flag 'k0s worker --token-file [path]'") - } + tokenSources++ + } + if tokenFile != "" { + tokenSources++ + } + if tokenEnv != "" { + tokenSources++ + } + if tokenSources > 1 { + return nil, errors.New("you can only pass one token source: either as a CLI argument 'k0s worker [token]', via '--token-file [path]', or via '--token-env [var]'") + } + + if tokenArg != "" { kubeconfig, err := loadKubeconfigFromJoinToken(tokenArg) if err != nil { return nil, err @@ -166,12 +177,28 @@ func kubeconfigGetterFromJoinToken(tokenFile, tokenArg string) (clientcmd.Kubeco }, nil } + if tokenEnv != "" { + tokenValue := os.Getenv(tokenEnv) + if tokenValue == "" { + return nil, fmt.Errorf("environment variable %q is not set or is empty", tokenEnv) + } + + kubeconfig, err := loadKubeconfigFromJoinToken(tokenValue) + if err != nil { + return nil, err + } + + return func() (*clientcmdapi.Config, error) { + return kubeconfig, nil + }, nil + } + if tokenFile == "" { return nil, nil } return func() (*clientcmdapi.Config, error) { - return loadKubeconfigFromTokenFile(tokenFile) + return loadKubeconfigFromToken(tokenFile) }, nil } @@ -193,7 +220,7 @@ func loadKubeconfigFromJoinToken(tokenData string) (*clientcmdapi.Config, error) return kubeconfig, nil } -func loadKubeconfigFromTokenFile(path string) (*clientcmdapi.Config, error) { +func loadKubeconfigFromToken(path string) (*clientcmdapi.Config, error) { var problem string tokenBytes, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { diff --git a/pkg/config/cli.go b/pkg/config/cli.go index 1d5dd6ef24ba..ae91f074cd70 100644 --- a/pkg/config/cli.go +++ b/pkg/config/cli.go @@ -70,6 +70,7 @@ type WorkerOptions struct { Labels map[string]string Taints []string TokenFile string + TokenEnv string TokenArg string WorkerProfile string IPTablesMode string @@ -251,6 +252,7 @@ func GetWorkerFlags() *pflag.FlagSet { flagset.StringVar(&workerOpts.WorkerProfile, "profile", defaultWorkerProfile, "worker profile to use on the node") flagset.BoolVar(&workerOpts.CloudProvider, "enable-cloud-provider", false, "Whether or not to enable cloud provider support in kubelet") flagset.StringVar(&workerOpts.TokenFile, "token-file", "", "Path to the file containing join-token.") + flagset.StringVar(&workerOpts.TokenEnv, "token-env", "", "Environment variable name containing the join-token.") flagset.VarP((*logLevelsFlag)(&workerOpts.LogLevels), "logging", "l", "Logging Levels for the different components") flagset.Var((*cliflag.ConfigurationMap)(&workerOpts.Labels), "labels", "Node labels, list of key=value pairs") flagset.StringSliceVarP(&workerOpts.Taints, "taints", "", []string{}, "Node taints, list of key=value:effect strings")