Add support for `k3s token` command

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
This commit is contained in:
Brad Davidson 2022-12-07 01:44:53 +00:00 committed by Brad Davidson
parent 7d49202721
commit 373df1c8b0
14 changed files with 653 additions and 5 deletions

View File

@ -37,6 +37,7 @@ func main() {
return
}
tokenCommand := internalCLIAction(version.Program+"-"+cmds.TokenCommand, dataDir, os.Args)
etcdsnapshotCommand := internalCLIAction(version.Program+"-"+cmds.EtcdSnapshotCommand, dataDir, os.Args)
secretsencryptCommand := internalCLIAction(version.Program+"-"+cmds.SecretsEncryptCommand, dataDir, os.Args)
certCommand := internalCLIAction(version.Program+"-"+cmds.CertCommand, dataDir, os.Args)
@ -51,6 +52,12 @@ func main() {
cmds.NewCRICTL(externalCLIAction("crictl", dataDir)),
cmds.NewCtrCommand(externalCLIAction("ctr", dataDir)),
cmds.NewCheckConfigCommand(externalCLIAction("check-config", dataDir)),
cmds.NewTokenCommands(
tokenCommand,
tokenCommand,
tokenCommand,
tokenCommand,
),
cmds.NewEtcdSnapshotCommands(
etcdsnapshotCommand,
etcdsnapshotCommand,

View File

@ -17,6 +17,7 @@ import (
"github.com/k3s-io/k3s/pkg/cli/kubectl"
"github.com/k3s-io/k3s/pkg/cli/secretsencrypt"
"github.com/k3s-io/k3s/pkg/cli/server"
"github.com/k3s-io/k3s/pkg/cli/token"
"github.com/k3s-io/k3s/pkg/configfilearg"
"github.com/k3s-io/k3s/pkg/containerd"
ctr2 "github.com/k3s-io/k3s/pkg/ctr"
@ -48,6 +49,12 @@ func main() {
cmds.NewKubectlCommand(kubectl.Run),
cmds.NewCRICTL(crictl.Run),
cmds.NewCtrCommand(ctr.Run),
cmds.NewTokenCommands(
token.Create,
token.Delete,
token.Generate,
token.List,
),
cmds.NewEtcdSnapshotCommands(
etcdsnapshot.Run,
etcdsnapshot.Delete,

29
cmd/token/main.go Normal file
View File

@ -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)
}
}

2
go.mod
View File

@ -152,6 +152,7 @@ require (
k8s.io/apiserver v0.26.1
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible
k8s.io/cloud-provider v0.26.1
k8s.io/cluster-bootstrap v0.0.0
k8s.io/component-base v0.26.1
k8s.io/component-helpers v0.26.1
k8s.io/cri-api v0.26.1
@ -396,7 +397,6 @@ require (
honnef.co/go/tools v0.2.2 // indirect
k8s.io/apiextensions-apiserver v0.25.4 // indirect
k8s.io/cli-runtime v0.22.2 // indirect
k8s.io/cluster-bootstrap v0.0.0 // indirect
k8s.io/code-generator v0.25.4 // indirect
k8s.io/controller-manager v0.25.4 // indirect
k8s.io/csi-translation-lib v0.0.0 // indirect

97
pkg/cli/cmds/token.go Normal file
View File

@ -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,
},
},
}
}

224
pkg/cli/token/token.go Normal file
View File

@ -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
}

View File

@ -369,7 +369,7 @@ func put(u string, body []byte, client *http.Client, username, password string)
return nil
}
// FormatToken takes a username:password string, and a path to a certificate bundle, and
// FormatToken takes a username:password string or join token, and a path to a certificate bundle, and
// returns a string containing the full K10 format token string. If the credentials are
// empty, an empty token is returned. If the certificate bundle does not exist or does not
// contain a valid bundle, an error is returned.
@ -382,6 +382,15 @@ func FormatToken(creds, certFile string) (string, error) {
if err != nil {
return "", err
}
return FormatTokenBytes(creds, b)
}
// FormatTokenBytes has the same interface as FormatToken, but accepts a byte slice instead
// of file path.
func FormatTokenBytes(creds string, b []byte) (string, error) {
if len(creds) == 0 {
return "", nil
}
digest, err := hashCA(b)
if err != nil {

View File

@ -90,6 +90,7 @@ func Server(ctx context.Context, cfg *config.Control) error {
func controllerManager(ctx context.Context, cfg *config.Control) error {
runtime := cfg.Runtime
argsMap := map[string]string{
"controllers": "*,tokencleaner",
"feature-gates": "JobTrackingWithFinalizers=true",
"kubeconfig": runtime.KubeConfigController,
"authorization-kubeconfig": runtime.KubeConfigController,
@ -117,7 +118,7 @@ func controllerManager(ctx context.Context, cfg *config.Control) error {
}
if !cfg.DisableCCM {
argsMap["configure-cloud-routes"] = "false"
argsMap["controllers"] = "*,-service,-route,-cloud-node-lifecycle"
argsMap["controllers"] = argsMap["controllers"] + ",-service,-route,-cloud-node-lifecycle"
}
args := config.GetArgs(argsMap, cfg.ExtraControllerArgs)
@ -158,6 +159,7 @@ func apiServer(ctx context.Context, cfg *config.Control) error {
argsMap["cert-dir"] = certDir
argsMap["allow-privileged"] = "true"
argsMap["enable-bootstrap-token-auth"] = "true"
argsMap["authorization-mode"] = strings.Join([]string{modes.ModeNode, modes.ModeRBAC}, ",")
argsMap["service-account-signing-key-file"] = runtime.ServiceCurrentKey
argsMap["service-cluster-ip-range"] = util.JoinIPNets(cfg.ServiceIPRanges)

52
pkg/kubeadm/token.go Normal file
View File

@ -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
}

45
pkg/kubeadm/types.go Normal file
View File

@ -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"`
}

173
pkg/kubeadm/utils.go Normal file
View File

@ -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
}

View File

@ -32,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
)
const (
@ -44,7 +45,7 @@ func router(ctx context.Context, config *Config, cfg *cmds.Server) http.Handler
prefix := "/v1-" + version.Program
authed := mux.NewRouter().SkipClean(true)
authed.Use(authMiddleware(serverConfig, version.Program+":agent"))
authed.Use(authMiddleware(serverConfig, version.Program+":agent", user.NodesGroup, bootstrapapi.BootstrapDefaultGroup))
authed.Path(prefix + "/serving-kubelet.crt").Handler(servingKubeletCert(serverConfig, serverConfig.Runtime.ServingKubeletKey, nodeAuth))
authed.Path(prefix + "/client-kubelet.crt").Handler(clientKubeletCert(serverConfig, serverConfig.Runtime.ClientKubeletKey, nodeAuth))
authed.Path(prefix + "/client-kube-proxy.crt").Handler(fileHandler(serverConfig.Runtime.ClientKubeProxyCert, serverConfig.Runtime.ClientKubeProxyKey))

View File

@ -92,6 +92,7 @@ fi
rm -f \
bin/k3s-agent \
bin/k3s-server \
bin/k3s-token \
bin/k3s-etcd-snapshot \
bin/k3s-secrets-encrypt \
bin/k3s-certificate \
@ -127,6 +128,7 @@ echo Building k3s
CGO_ENABLED=1 "${GO}" build -tags "$TAGS" -gcflags="all=${GCFLAGS}" -ldflags "$VERSIONFLAGS $LDFLAGS $STATIC" -o bin/k3s ./cmd/server/main.go
ln -s k3s ./bin/k3s-agent
ln -s k3s ./bin/k3s-server
ln -s k3s ./bin/k3s-token
ln -s k3s ./bin/k3s-etcd-snapshot
ln -s k3s ./bin/k3s-secrets-encrypt
ln -s k3s ./bin/k3s-certificate

View File

@ -7,7 +7,7 @@ cd $(dirname $0)/..
GO=${GO-go}
for i in crictl kubectl k3s-agent k3s-server k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do
for i in crictl kubectl k3s-agent k3s-server k3s-token k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do
rm -f bin/$i
ln -s k3s bin/$i
done