package token import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "text/tabwriter" "time" "github.com/erikdubbelboer/gspt" "github.com/k3s-io/k3s/pkg/cli/cmds" "github.com/k3s-io/k3s/pkg/clientaccess" "github.com/k3s-io/k3s/pkg/kubeadm" "github.com/k3s-io/k3s/pkg/server" "github.com/k3s-io/k3s/pkg/util" "github.com/k3s-io/k3s/pkg/version" "github.com/pkg/errors" "github.com/urfave/cli" "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/client-go/tools/clientcmd" bootstrapapi "k8s.io/cluster-bootstrap/token/api" bootstraputil "k8s.io/cluster-bootstrap/token/util" "k8s.io/utils/ptr" ) func Create(app *cli.Context) error { if err := cmds.InitLogging(); err != nil { return err } return create(app, &cmds.TokenConfig) } func create(app *cli.Context, cfg *cmds.Token) error { if err := kubeadm.SetDefaults(app, cfg); err != nil { return err } cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) client, err := util.GetClientSet(cfg.Kubeconfig) if err != nil { return err } restConfig, err := clientcmd.BuildConfigFromFlags("", cfg.Kubeconfig) if err != nil { return err } if len(restConfig.TLSClientConfig.CAData) == 0 && restConfig.TLSClientConfig.CAFile != "" { restConfig.TLSClientConfig.CAData, err = os.ReadFile(restConfig.TLSClientConfig.CAFile) if err != nil { return err } } bts, err := kubeadm.NewBootstrapTokenString(cfg.Token) if err != nil { return err } bt := kubeadm.BootstrapToken{ Token: bts, Description: cfg.Description, TTL: &metav1.Duration{Duration: cfg.TTL}, Usages: cfg.Usages, Groups: cfg.Groups, } secretName := bootstraputil.BootstrapTokenSecretName(bt.Token.ID) if secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}); secret != nil && err == nil { return fmt.Errorf("a token with id %q already exists", bt.Token.ID) } secret := kubeadm.BootstrapTokenToSecret(&bt) if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil { return err } token, err := clientaccess.FormatTokenBytes(bt.Token.String(), restConfig.TLSClientConfig.CAData) if err != nil { return err } fmt.Println(token) return nil } func Delete(app *cli.Context) error { if err := cmds.InitLogging(); err != nil { return err } return delete(app, &cmds.TokenConfig) } func delete(app *cli.Context, cfg *cmds.Token) error { args := app.Args() if len(args) < 1 { return errors.New("missing argument; 'token delete' is missing token") } cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) client, err := util.GetClientSet(cfg.Kubeconfig) if err != nil { return err } for _, token := range args { if !bootstraputil.IsValidBootstrapTokenID(token) { bts, err := kubeadm.NewBootstrapTokenString(cfg.Token) if err != nil { return fmt.Errorf("given token didn't match pattern %q or %q", bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern) } token = bts.ID } secretName := bootstraputil.BootstrapTokenSecretName(token) if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), secretName, metav1.DeleteOptions{}); err != nil { return errors.Wrapf(err, "failed to delete bootstrap token %q", err) } fmt.Printf("bootstrap token %q deleted\n", token) } return nil } func Generate(app *cli.Context) error { if err := cmds.InitLogging(); err != nil { return err } return generate(app, &cmds.TokenConfig) } func generate(app *cli.Context, cfg *cmds.Token) error { token, err := bootstraputil.GenerateBootstrapToken() if err != nil { return err } fmt.Println(token) return nil } func Rotate(app *cli.Context) error { if err := cmds.InitLogging(); err != nil { return err } fmt.Println("\033[33mWARNING\033[0m: Recommended to keep a record of the old token. If restoring from a snapshot, you must use the token associated with that snapshot.") info, err := serverAccess(&cmds.TokenConfig) if err != nil { return err } b, err := json.Marshal(server.TokenRotateRequest{ NewToken: ptr.To(cmds.TokenConfig.NewToken), }) if err != nil { return err } if err = info.Put("/v1-"+version.Program+"/token", b); err != nil { return err } // wait for etcd db propagation delay time.Sleep(1 * time.Second) fmt.Println("Token rotated, restart", version.Program, "nodes with new token") return nil } func serverAccess(cfg *cmds.Token) (*clientaccess.Info, error) { // hide process arguments from ps output, since they likely contain tokens. gspt.SetProcTitle(os.Args[0] + " token") dataDir, err := server.ResolveDataDir("") if err != nil { return nil, err } if cfg.Token == "" { fp := filepath.Join(dataDir, "token") tokenByte, err := os.ReadFile(fp) if err != nil { return nil, err } cfg.Token = string(bytes.TrimRight(tokenByte, "\n")) } return clientaccess.ParseAndValidateToken(cfg.ServerURL, cfg.Token, clientaccess.WithUser("server")) } func List(app *cli.Context) error { if err := cmds.InitLogging(); err != nil { return err } return list(app, &cmds.TokenConfig) } func list(app *cli.Context, cfg *cmds.Token) error { if err := kubeadm.SetDefaults(app, cfg); err != nil { return err } cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig) client, err := util.GetClientSet(cfg.Kubeconfig) if err != nil { return err } tokenSelector := fields.SelectorFromSet( map[string]string{ "type": string(bootstrapapi.SecretTypeBootstrapToken), }, ) listOptions := metav1.ListOptions{ FieldSelector: tokenSelector.String(), } secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions) if err != nil { return errors.Wrapf(err, "failed to list bootstrap tokens") } tokens := make([]*kubeadm.BootstrapToken, len(secrets.Items)) for i, secret := range secrets.Items { token, err := kubeadm.BootstrapTokenFromSecret(&secret) if err != nil { fmt.Printf("%v", err) continue } tokens[i] = token } switch cfg.Output { case "json": if err := json.NewEncoder(os.Stdout).Encode(tokens); err != nil { return err } return nil case "yaml": if err := yaml.NewEncoder(os.Stdout).Encode(tokens); err != nil { return err } return nil default: format := "%s\t%s\t%s\t%s\t%s\t%s\n" w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0) defer w.Flush() fmt.Fprintf(w, format, "TOKEN", "TTL", "EXPIRES", "USAGES", "DESCRIPTION", "EXTRA GROUPS") for _, token := range tokens { ttl := "" expires := "" if token.Expires != nil { ttl = duration.ShortHumanDuration(token.Expires.Sub(time.Now())) expires = token.Expires.Format(time.RFC3339) } fmt.Fprintf(w, format, token.Token.ID, ttl, expires, joinOrNone(token.Usages...), joinOrNone(token.Description), joinOrNone(token.Groups...)) } } return nil } // joinOrNone joins strings with a comma. If the resulting output is an empty string, // it instead returns the replacement string "" func joinOrNone(s ...string) string { j := strings.Join(s, ",") if j == "" { return "" } return j }