From 77f1b72a404b90326263fc55bef2f951b77db5f0 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 22 Aug 2017 16:13:24 -0500 Subject: [PATCH] kubeadm: add `--groups` flag for `kubeadm token create`. This adds support for creating a bootstrap token that authenticates with extra `system:bootstrappers:*` groups in addition to `system:bootstrappers`. --- cmd/kubeadm/app/cmd/BUILD | 1 + cmd/kubeadm/app/cmd/init.go | 2 +- cmd/kubeadm/app/cmd/token.go | 25 ++++++++++++++++--- .../app/phases/bootstraptoken/node/token.go | 17 ++++++++----- .../phases/bootstraptoken/node/token_test.go | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index 4249096ded3..6c261ac2c68 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -66,6 +66,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/version:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/flag:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 1fdbd1cf82b..d6f08cf471c 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -339,7 +339,7 @@ func (i *Init) Run(out io.Writer) error { // Create the default node bootstrap token tokenDescription := "The default bootstrap token generated by 'kubeadm init'." - if err := nodebootstraptokenphase.UpdateOrCreateToken(client, i.cfg.Token, false, i.cfg.TokenTTL, kubeadmconstants.DefaultTokenUsages, tokenDescription); err != nil { + if err := nodebootstraptokenphase.UpdateOrCreateToken(client, i.cfg.Token, false, i.cfg.TokenTTL, kubeadmconstants.DefaultTokenUsages, []string{}, tokenDescription); err != nil { return err } // Create RBAC rules that makes the bootstrap tokens able to post CSRs diff --git a/cmd/kubeadm/app/cmd/token.go b/cmd/kubeadm/app/cmd/token.go index d3bbda1041d..1aab3f196e0 100644 --- a/cmd/kubeadm/app/cmd/token.go +++ b/cmd/kubeadm/app/cmd/token.go @@ -31,6 +31,7 @@ import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/sets" clientset "k8s.io/client-go/kubernetes" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" @@ -87,6 +88,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { "dry-run", dryRun, "Whether to enable dry-run mode or not") var usages []string + var extraGroups []string var tokenDuration time.Duration var description string createCmd := &cobra.Command{ @@ -114,7 +116,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { fmt.Fprintln(errW, "[kubeadm] WARNING: starting in 1.8, tokens expire after 24 hours by default (if you require a non-expiring token use --ttl 0)") } - err = RunCreateToken(out, client, token, tokenDuration, usages, description) + err = RunCreateToken(out, client, token, tokenDuration, usages, extraGroups, description) kubeadmutil.CheckErr(err) }, } @@ -122,6 +124,9 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { "ttl", kubeadmconstants.DefaultTokenDuration, "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). 0 means 'never expires'.") createCmd.Flags().StringSliceVar(&usages, "usages", kubeadmconstants.DefaultTokenUsages, "The ways in which this token can be used. Valid options: [signing,authentication].") + createCmd.Flags().StringSliceVar(&extraGroups, + "groups", []string{}, + fmt.Sprintf("Extra groups that this token will authenticate as when used for authentication. Must match %q.", bootstrapapi.BootstrapGroupPattern)) createCmd.Flags().StringVar(&description, "description", "", "A human friendly description of how this token is used.") tokenCmd.AddCommand(createCmd) @@ -192,7 +197,7 @@ func NewCmdTokenGenerate(out io.Writer) *cobra.Command { } // RunCreateToken generates a new bootstrap token and stores it as a secret on the server. -func RunCreateToken(out io.Writer, client clientset.Interface, token string, tokenDuration time.Duration, usages []string, description string) error { +func RunCreateToken(out io.Writer, client clientset.Interface, token string, tokenDuration time.Duration, usages []string, extraGroups []string, description string) error { if len(token) == 0 { var err error @@ -207,8 +212,22 @@ func RunCreateToken(out io.Writer, client clientset.Interface, token string, tok } } + // adding groups only makes sense for authentication + var usagesSet sets.String + usagesSet.Insert(usages...) + if len(extraGroups) > 0 && !usagesSet.Has("authentication") { + return fmt.Errorf("--groups cannot be specified unless --usages includes \"authentication\"") + } + + // validate any extra group names + for _, group := range extraGroups { + if err := bootstrapapi.ValidateBootstrapGroupName(group); err != nil { + return err + } + } + // TODO: Validate usages here so we don't allow something unsupported - err := tokenphase.CreateNewToken(client, token, tokenDuration, usages, description) + err := tokenphase.CreateNewToken(client, token, tokenDuration, usages, extraGroups, description) if err != nil { return err } diff --git a/cmd/kubeadm/app/phases/bootstraptoken/node/token.go b/cmd/kubeadm/app/phases/bootstraptoken/node/token.go index e4c29912e4a..4b59d81fcc0 100644 --- a/cmd/kubeadm/app/phases/bootstraptoken/node/token.go +++ b/cmd/kubeadm/app/phases/bootstraptoken/node/token.go @@ -18,6 +18,7 @@ package node import ( "fmt" + "strings" "time" "k8s.io/api/core/v1" @@ -33,12 +34,12 @@ const tokenCreateRetries = 5 // TODO(mattmoyer): Move CreateNewToken, UpdateOrCreateToken and encodeTokenSecretData out of this package to client-go for a generic abstraction and client for a Bootstrap Token // CreateNewToken tries to create a token and fails if one with the same ID already exists -func CreateNewToken(client clientset.Interface, token string, tokenDuration time.Duration, usages []string, description string) error { - return UpdateOrCreateToken(client, token, true, tokenDuration, usages, description) +func CreateNewToken(client clientset.Interface, token string, tokenDuration time.Duration, usages []string, extraGroups []string, description string) error { + return UpdateOrCreateToken(client, token, true, tokenDuration, usages, extraGroups, description) } // UpdateOrCreateToken attempts to update a token with the given ID, or create if it does not already exist. -func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists bool, tokenDuration time.Duration, usages []string, description string) error { +func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists bool, tokenDuration time.Duration, usages []string, extraGroups []string, description string) error { tokenID, tokenSecret, err := tokenutil.ParseToken(token) if err != nil { return err @@ -52,7 +53,7 @@ func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists return fmt.Errorf("a token with id %q already exists", tokenID) } // Secret with this ID already exists, update it: - secret.Data = encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, description) + secret.Data = encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, extraGroups, description) if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Update(secret); err == nil { return nil } @@ -67,7 +68,7 @@ func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists Name: secretName, }, Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), - Data: encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, description), + Data: encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, extraGroups, description), } if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(secret); err == nil { return nil @@ -85,12 +86,16 @@ func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists } // encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret -func encodeTokenSecretData(tokenID, tokenSecret string, duration time.Duration, usages []string, description string) map[string][]byte { +func encodeTokenSecretData(tokenID, tokenSecret string, duration time.Duration, usages []string, extraGroups []string, description string) map[string][]byte { data := map[string][]byte{ bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), } + if len(extraGroups) > 0 { + data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(extraGroups, ",")) + } + if duration > 0 { // Get the current time, add the specified duration, and format it accordingly durationString := time.Now().Add(duration).Format(time.RFC3339) diff --git a/cmd/kubeadm/app/phases/bootstraptoken/node/token_test.go b/cmd/kubeadm/app/phases/bootstraptoken/node/token_test.go index 48a6f80e982..7af575e01d1 100644 --- a/cmd/kubeadm/app/phases/bootstraptoken/node/token_test.go +++ b/cmd/kubeadm/app/phases/bootstraptoken/node/token_test.go @@ -33,7 +33,7 @@ func TestEncodeTokenSecretData(t *testing.T) { {token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}, t: time.Second}, // should use default } for _, rt := range tests { - actual := encodeTokenSecretData(rt.token.ID, rt.token.Secret, rt.t, []string{}, "") + actual := encodeTokenSecretData(rt.token.ID, rt.token.Secret, rt.t, []string{}, []string{}, "") if !bytes.Equal(actual["token-id"], []byte(rt.token.ID)) { t.Errorf( "failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s",