mirror of https://github.com/k3s-io/k3s
275 lines
7.2 KiB
Go
275 lines
7.2 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", 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 := "<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
|
|
}
|