mirror of https://github.com/k3s-io/k3s
Signed-off-by: Brad Davidson <brad.davidson@rancher.com>pull/6922/head
parent
7d49202721
commit
373df1c8b0
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/token"
|
||||||
|
"github.com/k3s-io/k3s/pkg/configfilearg"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cmds.NewApp()
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cmds.NewTokenCommands(
|
||||||
|
token.Create,
|
||||||
|
token.Delete,
|
||||||
|
token.Generate,
|
||||||
|
token.List,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(configfilearg.MustParse(os.Args)); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package cmds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TokenCommand = "token"
|
||||||
|
|
||||||
|
// Config holds CLI values for the token subcommands
|
||||||
|
type Token struct {
|
||||||
|
Description string
|
||||||
|
Kubeconfig string
|
||||||
|
Token string
|
||||||
|
Output string
|
||||||
|
Groups cli.StringSlice
|
||||||
|
Usages cli.StringSlice
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenConfig = Token{}
|
||||||
|
TokenFlags = []cli.Flag{
|
||||||
|
DataDirFlag,
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "kubeconfig",
|
||||||
|
Usage: "(cluster) Server to connect to",
|
||||||
|
EnvVar: "KUBECONFIG",
|
||||||
|
Destination: &TokenConfig.Kubeconfig,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTokenCommands(create, delete, generate, list func(ctx *cli.Context) error) cli.Command {
|
||||||
|
return cli.Command{
|
||||||
|
Name: TokenCommand,
|
||||||
|
Usage: "Manage bootstrap tokens",
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create bootstrap tokens on the server",
|
||||||
|
Flags: append(TokenFlags, &cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "A human friendly description of how this token is used",
|
||||||
|
Destination: &TokenConfig.Description,
|
||||||
|
}, &cli.StringSliceFlag{
|
||||||
|
Name: "groups",
|
||||||
|
Usage: "Extra groups that this token will authenticate as when used for authentication",
|
||||||
|
Value: &TokenConfig.Groups,
|
||||||
|
}, &cli.DurationFlag{
|
||||||
|
Name: "ttl",
|
||||||
|
Usage: "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). If set to '0', the token will never expire",
|
||||||
|
Value: time.Hour * 24,
|
||||||
|
Destination: &TokenConfig.TTL,
|
||||||
|
}, &cli.StringSliceFlag{
|
||||||
|
Name: "usages",
|
||||||
|
Usage: "Describes the ways in which this token can be used.",
|
||||||
|
Value: &TokenConfig.Usages,
|
||||||
|
}),
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: create,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "Delete bootstrap tokens on the server",
|
||||||
|
Flags: TokenFlags,
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "generate",
|
||||||
|
Usage: "Generate and print a bootstrap token, but do not create it on the server",
|
||||||
|
Flags: TokenFlags,
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: generate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "List bootstrap tokens on the server",
|
||||||
|
Flags: append(TokenFlags, &cli.StringFlag{
|
||||||
|
Name: "output,o",
|
||||||
|
Value: "text",
|
||||||
|
Destination: &TokenConfig.Output,
|
||||||
|
}),
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: list,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,224 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/util"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
||||||
|
"github.com/k3s-io/k3s/pkg/version"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||||
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
NodeBootstrapTokenAuthGroup = "system:bootstrappers:" + version.Program + ":default-node-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDefaults ensures that the default values are set on the token configuration.
|
||||||
|
// These are set here, rather than in the default Token struct, to avoid
|
||||||
|
// importing the cluster-bootstrap packages into the CLI.
|
||||||
|
func SetDefaults(clx *cli.Context, cfg *cmds.Token) error {
|
||||||
|
if !clx.IsSet("groups") {
|
||||||
|
cfg.Groups = []string{NodeBootstrapTokenAuthGroup}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clx.IsSet("usages") {
|
||||||
|
cfg.Usages = bootstrapapi.KnownTokenUsages
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Output == "" {
|
||||||
|
cfg.Output = "text"
|
||||||
|
} else {
|
||||||
|
switch cfg.Output {
|
||||||
|
case "text", "json", "yaml":
|
||||||
|
default:
|
||||||
|
return errors.New("invalid output format: " + cfg.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := clx.Args()
|
||||||
|
if len(args) > 0 {
|
||||||
|
cfg.Token = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Token == "" {
|
||||||
|
var err error
|
||||||
|
cfg.Token, err = bootstraputil.GenerateBootstrapToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kubeadm bootstrap token types cribbed from:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/types.go
|
||||||
|
// Copying these instead of importing from kubeadm saves about 4mb of binary size.
|
||||||
|
|
||||||
|
// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster
|
||||||
|
type BootstrapToken struct {
|
||||||
|
// Token is used for establishing bidirectional trust between nodes and control-planes.
|
||||||
|
// Used for joining nodes in the cluster.
|
||||||
|
Token *BootstrapTokenString `json:"token" datapolicy:"token"`
|
||||||
|
// Description sets a human-friendly message why this token exists and what it's used
|
||||||
|
// for, so other administrators can know its purpose.
|
||||||
|
// +optional
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// TTL defines the time to live for this token. Defaults to 24h.
|
||||||
|
// Expires and TTL are mutually exclusive.
|
||||||
|
// +optional
|
||||||
|
TTL *metav1.Duration `json:"ttl,omitempty"`
|
||||||
|
// Expires specifies the timestamp when this token expires. Defaults to being set
|
||||||
|
// dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive.
|
||||||
|
// +optional
|
||||||
|
Expires *metav1.Time `json:"expires,omitempty"`
|
||||||
|
// Usages describes the ways in which this token can be used. Can by default be used
|
||||||
|
// for establishing bidirectional trust, but that can be changed here.
|
||||||
|
// +optional
|
||||||
|
Usages []string `json:"usages,omitempty"`
|
||||||
|
// Groups specifies the extra groups that this token will authenticate as when/if
|
||||||
|
// used for authentication
|
||||||
|
// +optional
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used
|
||||||
|
// for both validation of the identity of the API server from a joining node's point
|
||||||
|
// of view and as an authentication method for the node. This token is and should be
|
||||||
|
// short-lived.
|
||||||
|
type BootstrapTokenString struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Secret string `json:"-" datapolicy:"token"`
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||||
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
||||||
|
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kubeadm bootstrap token utilities cribbed from:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go
|
||||||
|
// Copying these instead of importing from kubeadm saves about 4mb of binary size.
|
||||||
|
|
||||||
|
// String returns the string representation of the BootstrapTokenString
|
||||||
|
func (bts BootstrapTokenString) String() string {
|
||||||
|
if len(bts.ID) > 0 && len(bts.Secret) > 0 {
|
||||||
|
return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBootstrapTokenString converts the given Bootstrap Token as a string
|
||||||
|
// to the BootstrapTokenString object used for serialization/deserialization
|
||||||
|
// and internal usage. It also automatically validates that the given token
|
||||||
|
// is of the right format
|
||||||
|
func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) {
|
||||||
|
substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token)
|
||||||
|
if len(substrs) != 3 {
|
||||||
|
return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString
|
||||||
|
// that allows the caller to specify the ID and Secret separately
|
||||||
|
func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) {
|
||||||
|
return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that
|
||||||
|
// may be submitted to the API Server in order to be stored.
|
||||||
|
func BootstrapTokenToSecret(bt *BootstrapToken) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID),
|
||||||
|
Namespace: metav1.NamespaceSystem,
|
||||||
|
},
|
||||||
|
Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken),
|
||||||
|
Data: encodeTokenSecretData(bt, time.Now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret
|
||||||
|
// now is passed in order to be able to used in unit testing
|
||||||
|
func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte {
|
||||||
|
data := map[string][]byte{
|
||||||
|
bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID),
|
||||||
|
bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(token.Description) > 0 {
|
||||||
|
data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If for some strange reason both token.TTL and token.Expires would be set
|
||||||
|
// (they are mutually exclusive in validation so this shouldn't be the case),
|
||||||
|
// token.Expires has higher priority, as can be seen in the logic here.
|
||||||
|
if token.Expires != nil {
|
||||||
|
// Format the expiration date accordingly
|
||||||
|
// TODO: This maybe should be a helper function in bootstraputil?
|
||||||
|
expirationString := token.Expires.Time.UTC().Format(time.RFC3339)
|
||||||
|
data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString)
|
||||||
|
|
||||||
|
} else if token.TTL != nil && token.TTL.Duration > 0 {
|
||||||
|
// Only if .Expires is unset, TTL might have an effect
|
||||||
|
// Get the current time, add the specified duration, and format it accordingly
|
||||||
|
expirationString := now.Add(token.TTL.Duration).UTC().Format(time.RFC3339)
|
||||||
|
data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, usage := range token.Usages {
|
||||||
|
data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(token.Groups) > 0 {
|
||||||
|
data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ","))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret
|
||||||
|
func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
|
||||||
|
// Get the Token ID field from the Secret data
|
||||||
|
tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
|
||||||
|
if len(tokenID) == 0 {
|
||||||
|
return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce the right naming convention
|
||||||
|
if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) {
|
||||||
|
return nil, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q",
|
||||||
|
bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
|
||||||
|
if len(tokenSecret) == 0 {
|
||||||
|
return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the BootstrapTokenString object based on the ID and Secret
|
||||||
|
bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the description (if any) from the Secret
|
||||||
|
description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey)
|
||||||
|
|
||||||
|
// Expiration time is optional, if not specified this implies the token
|
||||||
|
// never expires.
|
||||||
|
secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey)
|
||||||
|
var expires *metav1.Time
|
||||||
|
if len(secretExpiration) > 0 {
|
||||||
|
expTime, err := time.Parse(time.RFC3339, secretExpiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name)
|
||||||
|
}
|
||||||
|
expires = &metav1.Time{Time: expTime}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build an usages string slice from the Secret data
|
||||||
|
var usages []string
|
||||||
|
for k, v := range secret.Data {
|
||||||
|
// Skip all fields that don't include this prefix
|
||||||
|
if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip those that don't have this usage set to true
|
||||||
|
if string(v) != "true" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix))
|
||||||
|
}
|
||||||
|
// Only sort the slice if defined
|
||||||
|
if usages != nil {
|
||||||
|
sort.Strings(usages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the extra groups information from the Secret
|
||||||
|
// It's done this way to make .Groups be nil in case there is no items, rather than an
|
||||||
|
// empty slice or an empty slice with a "" string only
|
||||||
|
var groups []string
|
||||||
|
groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
|
||||||
|
g := strings.Split(groupsString, ",")
|
||||||
|
if len(g) > 0 && len(g[0]) > 0 {
|
||||||
|
groups = g
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BootstrapToken{
|
||||||
|
Token: bts,
|
||||||
|
Description: description,
|
||||||
|
Expires: expires,
|
||||||
|
Usages: usages,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
Loading…
Reference in new issue