Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion cmd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions cmd/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: <rundir>/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())
Expand Down
10 changes: 10 additions & 0 deletions cmd/install/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

@twz123 twz123 Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think this is a very unintuitive thing to do. By nature, an environment variable is ephemeral and belongs to the current process only. Capturing it in a file is not something I'd want to happen with it. Moreover, this creates a discrepancy between the arguments passed to the install controller/worker command and the arguments passed to controller/worker command, which we have managed to avoid so far. If folks need to persist the token, then they can just use --token-file without surprises.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you fine with keeping temporary bootstrap token inside init system file instead of keeping pointing it to a file?

And no, there are use cases that do not allow just using "--token-file without surprises"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@makhov pointed out that there's a straight forward solution which wouldn't require any special-handling at all here: We could simply use k0s install -e K0S_JOIN_TOKEN controller [...] and be done with it. That would store the token somewhere in the init system config, and the behavior would be much more obvious. /cc #6763

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I am working on this RN, ETA PR today

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternative PR #6766

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 {
Expand Down
1 change: 1 addition & 0 deletions cmd/install/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: <rundir>/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:
Expand Down
1 change: 1 addition & 0 deletions cmd/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
28 changes: 27 additions & 1 deletion cmd/install/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package install
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

Expand All @@ -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 {
Expand All @@ -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
}
15 changes: 15 additions & 0 deletions cmd/install/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 34 additions & 7 deletions cmd/worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still reading a token file, so we could keep the old name.

var problem string
tokenBytes, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type WorkerOptions struct {
Labels map[string]string
Taints []string
TokenFile string
TokenEnv string
TokenArg string
WorkerProfile string
IPTablesMode string
Expand Down Expand Up @@ -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.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not add an additional flag for this. Instead, I'd simply check for K0S_JOIN_TOKEN if the token isn't provided as an argument or a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hiding flag means, extra document section about secret environ values

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need to document that.

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")
Expand Down
Loading