k3s/pkg/cli/token/token.go

275 lines
7.1 KiB
Go

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/pointer"
)
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: pointer.String(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 k3s 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 := "<forever>"
expires := "<never>"
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 "<none>"
func joinOrNone(s ...string) string {
j := strings.Join(s, ",")
if j == "" {
return "<none>"
}
return j
}