341 lines
12 KiB
Go
341 lines
12 KiB
Go
package token
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/fields"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/duration"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
kubeclient "k8s.io/client-go/kubernetes"
|
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/kubectl/pkg/cmd/get"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
|
|
"github.com/karmada-io/karmada/pkg/karmadactl/options"
|
|
"github.com/karmada-io/karmada/pkg/karmadactl/util"
|
|
tokenutil "github.com/karmada-io/karmada/pkg/karmadactl/util/bootstraptoken"
|
|
)
|
|
|
|
var (
|
|
tokenLong = templates.LongDesc(`
|
|
This command manages bootstrap tokens. It is optional and needed only for advanced use cases.
|
|
|
|
In short, bootstrap tokens are used for establishing bidirectional trust between a client and a server.
|
|
A bootstrap token can be used when a client (for example a member cluster that is about to join control plane) needs
|
|
to trust the server it is talking to. Then a bootstrap token with the "signing" usage can be used.
|
|
bootstrap tokens can also function as a way to allow short-lived authentication to the API Server
|
|
(the token serves as a way for the API Server to trust the client), for example for doing the TLS Bootstrap.
|
|
|
|
What is a bootstrap token more exactly?
|
|
- It is a Secret in the kube-system namespace of type "bootstrap.kubernetes.io/token".
|
|
- A bootstrap token must be of the form "[a-z0-9]{6}.[a-z0-9]{16}". The former part is the public token ID,
|
|
while the latter is the Token Secret and it must be kept private at all circumstances!
|
|
- The name of the Secret must be named "bootstrap-token-(token-id)".
|
|
|
|
This command is same as 'kubeadm token', but it will create tokens that are used by member clusters.`)
|
|
|
|
tokenExamples = templates.Examples(`
|
|
# Create a token and print the full '%[1]s register' flag needed to join the cluster using the token.
|
|
%[1]s token create --print-register-command
|
|
`)
|
|
)
|
|
|
|
// NewCmdToken returns cobra.Command for token management
|
|
func NewCmdToken(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command {
|
|
opts := &CommandTokenOptions{
|
|
parentCommand: parentCommand,
|
|
TTL: &metav1.Duration{
|
|
Duration: 0,
|
|
},
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "token",
|
|
Short: "Manage bootstrap tokens",
|
|
Long: tokenLong,
|
|
Example: fmt.Sprintf(tokenExamples, parentCommand),
|
|
Annotations: map[string]string{
|
|
util.TagCommandGroup: util.GroupClusterRegistration,
|
|
},
|
|
}
|
|
|
|
cmd.AddCommand(NewCmdTokenCreate(f, streams.Out, opts))
|
|
cmd.AddCommand(NewCmdTokenList(f, streams.Out, streams.ErrOut, opts))
|
|
cmd.AddCommand(NewCmdTokenDelete(f, streams.Out, opts))
|
|
|
|
return cmd
|
|
}
|
|
|
|
// CommandTokenOptions holds all command options for token
|
|
type CommandTokenOptions struct {
|
|
TTL *metav1.Duration
|
|
Description string
|
|
Groups []string
|
|
Usages []string
|
|
|
|
PrintRegisterCommand bool
|
|
|
|
parentCommand string
|
|
}
|
|
|
|
// NewCmdTokenCreate returns cobra.Command to create token
|
|
func NewCmdTokenCreate(f util.Factory, out io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "create",
|
|
Short: "Create bootstrap tokens on the server",
|
|
Long: templates.LongDesc(`
|
|
This command will create a bootstrap token for you.
|
|
You can specify the usages for this token, the "time to live" and an optional human friendly description.
|
|
|
|
This should be a securely generated random token of the form "[a-z0-9]{6}.[a-z0-9]{16}".
|
|
`),
|
|
SilenceUsage: true,
|
|
DisableFlagsInUseLine: true,
|
|
RunE: func(Cmd *cobra.Command, args []string) error {
|
|
// Get control plane kube-apiserver client
|
|
client, err := f.KubernetesClientSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tokenOpts.runCreateToken(out, client)
|
|
},
|
|
Args: cobra.NoArgs,
|
|
}
|
|
|
|
options.AddKubeConfigFlags(cmd.Flags())
|
|
cmd.Flags().BoolVar(&tokenOpts.PrintRegisterCommand, "print-register-command", false, fmt.Sprintf("Instead of printing only the token, print the full '%s register' flag needed to register the member cluster using the token.", tokenOpts.parentCommand))
|
|
cmd.Flags().DurationVar(&tokenOpts.TTL.Duration, "ttl", tokenutil.DefaultTokenDuration, "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). If set to '0', the token will never expire")
|
|
cmd.Flags().StringSliceVar(&tokenOpts.Usages, "usages", tokenutil.DefaultUsages, fmt.Sprintf("Describes the ways in which this token can be used. You can pass --usages multiple times or provide a comma separated list of options. Valid options: [%s]", strings.Join(bootstrapapi.KnownTokenUsages, ",")))
|
|
cmd.Flags().StringSliceVar(&tokenOpts.Groups, "groups", tokenutil.DefaultGroups, fmt.Sprintf("Extra groups that this token will authenticate as when used for authentication. Must match %q", bootstrapapi.BootstrapGroupPattern))
|
|
cmd.Flags().StringVar(&tokenOpts.Description, "description", tokenOpts.Description, "A human friendly description of how this token is used.")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// NewCmdTokenList returns cobra.Command to list tokens
|
|
func NewCmdTokenList(f util.Factory, out io.Writer, errW io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List bootstrap tokens on the server",
|
|
Long: "This command will list all bootstrap tokens for you.",
|
|
RunE: func(tokenCmd *cobra.Command, args []string) error {
|
|
// Get control plane kube-apiserver client
|
|
client, err := f.KubernetesClientSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tokenOpts.runListTokens(client, out, errW)
|
|
},
|
|
Args: cobra.NoArgs,
|
|
}
|
|
|
|
options.AddKubeConfigFlags(cmd.Flags())
|
|
|
|
return cmd
|
|
}
|
|
|
|
// NewCmdTokenDelete returns cobra.Command to delete tokens
|
|
func NewCmdTokenDelete(f util.Factory, out io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "delete [token-value] ...",
|
|
DisableFlagsInUseLine: true,
|
|
Short: "Delete bootstrap tokens on the server",
|
|
Long: templates.LongDesc(`
|
|
This command will delete a list of bootstrap tokens for you.
|
|
|
|
The [token-value] is the full Token of the form "[a-z0-9]{6}.[a-z0-9]{16}" or the
|
|
Token ID of the form "[a-z0-9]{6}" to delete.
|
|
`),
|
|
RunE: func(tokenCmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("missing subcommand; 'token delete' is missing token of form %q", bootstrapapi.BootstrapTokenIDPattern)
|
|
}
|
|
|
|
// Get control plane kube-apiserver client
|
|
client, err := f.KubernetesClientSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tokenOpts.runDeleteTokens(out, client, args)
|
|
},
|
|
}
|
|
|
|
options.AddKubeConfigFlags(cmd.Flags())
|
|
|
|
return cmd
|
|
}
|
|
|
|
// runCreateToken generates a new bootstrap token and stores it as a secret on the server.
|
|
func (o *CommandTokenOptions) runCreateToken(out io.Writer, client kubeclient.Interface) error {
|
|
klog.V(1).Infoln("[token] creating token")
|
|
bootstrapToken, err := tokenutil.GenerateRandomBootstrapToken(o.TTL, o.Description, o.Groups, o.Usages)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tokenutil.CreateNewToken(client, bootstrapToken); err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenStr := bootstrapToken.Token.ID + "." + bootstrapToken.Token.Secret
|
|
|
|
// if --print-register-command was specified, print a machine-readable full `karmadactl register` command
|
|
// otherwise, just print the token
|
|
if o.PrintRegisterCommand {
|
|
joinCommand, err := tokenutil.GenerateRegisterCommand(*options.DefaultConfigFlags.KubeConfig, o.parentCommand, tokenStr, *options.DefaultConfigFlags.Context)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get register command, err: %w", err)
|
|
}
|
|
fmt.Fprintln(out, joinCommand)
|
|
} else {
|
|
fmt.Fprintln(out, tokenStr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runListTokens lists details on all existing bootstrap tokens on the server.
|
|
func (o *CommandTokenOptions) runListTokens(client kubeclient.Interface, out io.Writer, errW io.Writer) error {
|
|
// First, build our selector for bootstrap tokens only
|
|
klog.V(1).Infoln("[token] preparing selector for bootstrap token")
|
|
tokenSelector := fields.SelectorFromSet(
|
|
map[string]string{
|
|
"type": string(bootstrapapi.SecretTypeBootstrapToken),
|
|
},
|
|
)
|
|
listOptions := metav1.ListOptions{
|
|
FieldSelector: tokenSelector.String(),
|
|
}
|
|
|
|
klog.V(1).Info("[token] retrieving list of bootstrap tokens")
|
|
secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list bootstrap tokens, err: %w", err)
|
|
}
|
|
|
|
printFlags := get.NewGetPrintFlags()
|
|
|
|
printFlags.SetKind(schema.GroupKind{Group: "output.karmada.io", Kind: "BootstrapToken"})
|
|
|
|
printer, err := printFlags.ToPrinter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// only print token with table format
|
|
printer = &get.TablePrinter{Delegate: printer}
|
|
|
|
tokenTable := &metav1.Table{}
|
|
setColumnDefinition(tokenTable)
|
|
|
|
for idx := range secrets.Items {
|
|
// Get the BootstrapToken struct representation from the Secret object
|
|
token, err := tokenutil.GetBootstrapTokenFromSecret(&secrets.Items[idx])
|
|
if err != nil {
|
|
fmt.Fprintf(errW, "%v", err)
|
|
continue
|
|
}
|
|
|
|
outputToken := tokenutil.BootstrapToken{
|
|
Token: &tokenutil.Token{ID: token.Token.ID, Secret: token.Token.Secret},
|
|
Description: token.Description,
|
|
TTL: token.TTL,
|
|
Expires: token.Expires,
|
|
Usages: token.Usages,
|
|
Groups: token.Groups,
|
|
}
|
|
|
|
tokenTable.Rows = append(tokenTable.Rows, constructTokenTableRow(outputToken))
|
|
}
|
|
|
|
if err := printer.PrintObj(tokenTable, out); err != nil {
|
|
return fmt.Errorf("unable to print tokens, err: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runDeleteTokens removes a bootstrap tokens from the server.
|
|
func (o *CommandTokenOptions) runDeleteTokens(out io.Writer, client kubeclient.Interface, tokenIDsOrTokens []string) error {
|
|
for _, tokenIDOrToken := range tokenIDsOrTokens {
|
|
// Assume this is a token id and try to parse it
|
|
tokenID := tokenIDOrToken
|
|
klog.V(1).Info("[token] parsing token")
|
|
if !bootstraputil.IsValidBootstrapTokenID(tokenIDOrToken) {
|
|
// Okay, the full token with both id and secret was probably passed. Parse it and extract the ID only
|
|
bts, err := tokenutil.NewToken(tokenIDOrToken)
|
|
if err != nil {
|
|
return fmt.Errorf("given token didn't match pattern %q or %q",
|
|
bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern)
|
|
}
|
|
tokenID = bts.ID
|
|
}
|
|
|
|
tokenSecretName := bootstraputil.BootstrapTokenSecretName(tokenID)
|
|
klog.V(1).Infof("[token] deleting token %q", tokenID)
|
|
if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), tokenSecretName, metav1.DeleteOptions{}); err != nil {
|
|
return fmt.Errorf("failed to delete bootstrap token %q, err: %w", tokenID, err)
|
|
}
|
|
fmt.Fprintf(out, "bootstrap token %q deleted\n", tokenID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// constructTokenTableRow construct token table row
|
|
func constructTokenTableRow(token tokenutil.BootstrapToken) metav1.TableRow {
|
|
var row metav1.TableRow
|
|
|
|
tokenString := token.Token.ID + "." + token.Token.Secret
|
|
|
|
ttl := "<forever>"
|
|
expires := "<never>"
|
|
if token.Expires != nil {
|
|
ttl = duration.ShortHumanDuration(time.Until(token.Expires.Time))
|
|
expires = token.Expires.Format(time.RFC3339)
|
|
}
|
|
|
|
usages := strings.Join(token.Usages, ",")
|
|
if len(usages) == 0 {
|
|
usages = "<none>"
|
|
}
|
|
|
|
description := token.Description
|
|
if len(description) == 0 {
|
|
description = "<none>"
|
|
}
|
|
|
|
groups := strings.Join(token.Groups, ",")
|
|
if len(groups) == 0 {
|
|
groups = "<none>"
|
|
}
|
|
|
|
row.Cells = append(row.Cells, tokenString, ttl, expires, usages, description, groups)
|
|
|
|
return row
|
|
}
|
|
|
|
// setColumnDefinition set print ColumnDefinition
|
|
func setColumnDefinition(table *metav1.Table) {
|
|
tokenColumns := []metav1.TableColumnDefinition{
|
|
{Name: "TOKEN", Type: "string", Format: "", Priority: 0},
|
|
{Name: "TTL", Type: "string", Format: "", Priority: 0},
|
|
{Name: "EXPIRES", Type: "string", Format: "", Priority: 0},
|
|
{Name: "USAGES", Type: "string", Format: "", Priority: 0},
|
|
{Name: "DESCRIPTION", Type: "string", Format: "", Priority: 0},
|
|
{Name: "EXTRA GROUPS", Type: "string", Format: "", Priority: 0},
|
|
}
|
|
table.ColumnDefinitions = tokenColumns
|
|
}
|