diff --git a/cmd/kubeadm/app/BUILD b/cmd/kubeadm/app/BUILD index ae1a1864c4f..6df668112a3 100644 --- a/cmd/kubeadm/app/BUILD +++ b/cmd/kubeadm/app/BUILD @@ -41,6 +41,7 @@ filegroup( "//cmd/kubeadm/app/phases/apiconfig:all-srcs", "//cmd/kubeadm/app/phases/certs:all-srcs", "//cmd/kubeadm/app/phases/kubeconfig:all-srcs", + "//cmd/kubeadm/app/phases/token:all-srcs", "//cmd/kubeadm/app/preflight:all-srcs", "//cmd/kubeadm/app/util:all-srcs", ], diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index 90292bf275c..74858f0f8c1 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -33,8 +33,10 @@ go_library( "//cmd/kubeadm/app/phases/apiconfig:go_default_library", "//cmd/kubeadm/app/phases/certs:go_default_library", "//cmd/kubeadm/app/phases/kubeconfig:go_default_library", + "//cmd/kubeadm/app/phases/token:go_default_library", "//cmd/kubeadm/app/preflight:go_default_library", "//cmd/kubeadm/app/util:go_default_library", + "//cmd/kubeadm/app/util/token:go_default_library", "//pkg/bootstrap/api:go_default_library", "//pkg/kubectl:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", diff --git a/cmd/kubeadm/app/cmd/defaults.go b/cmd/kubeadm/app/cmd/defaults.go index 4de829c684e..182202619d3 100644 --- a/cmd/kubeadm/app/cmd/defaults.go +++ b/cmd/kubeadm/app/cmd/defaults.go @@ -25,6 +25,7 @@ import ( kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" "github.com/blang/semver" ) @@ -75,13 +76,13 @@ func setInitDynamicDefaults(cfg *kubeadmapi.MasterConfiguration) error { // Validate token if any, otherwise generate if cfg.Discovery.Token != nil { if cfg.Discovery.Token.ID != "" && cfg.Discovery.Token.Secret != "" { - fmt.Printf("[init] A token has been provided, validating [%s]\n", kubeadmutil.BearerToken(cfg.Discovery.Token)) - if valid, err := kubeadmutil.ValidateToken(cfg.Discovery.Token); valid == false { + fmt.Printf("[init] A token has been provided, validating [%s]\n", tokenutil.BearerToken(cfg.Discovery.Token)) + if valid, err := tokenutil.ValidateToken(cfg.Discovery.Token); valid == false { return err } } else { fmt.Println("[init] A token has not been provided, generating one") - if err := kubeadmutil.GenerateToken(cfg.Discovery.Token); err != nil { + if err := tokenutil.GenerateToken(cfg.Discovery.Token); err != nil { return err } } diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index db4a183a2f5..b709c808745 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -38,8 +38,10 @@ import ( apiconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/apiconfig" certphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" + tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/token" "k8s.io/kubernetes/cmd/kubeadm/app/preflight" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" ) var ( @@ -206,7 +208,7 @@ func (i *Init) Run(out io.Writer) error { // TODO: It's not great to have an exception for token here, but necessary because the apiserver doesn't handle this properly in the API yet // but relies on files on disk for now, which is daunting. if i.cfg.Discovery.Token != nil { - if err := kubemaster.CreateTokenAuthFile(kubeadmutil.BearerToken(i.cfg.Discovery.Token)); err != nil { + if err := tokenphase.CreateTokenAuthFile(tokenutil.BearerToken(i.cfg.Discovery.Token)); err != nil { return err } } @@ -235,7 +237,19 @@ func (i *Init) Run(out io.Writer) error { } } - // PHASE 4: Set up various things in the API + // PHASE 4: Set up the bootstrap tokens + if i.cfg.Discovery.Token != nil { + fmt.Printf("[token-discovery] Using token: %s\n", tokenutil.BearerToken(i.cfg.Discovery.Token)) + if err := kubemaster.CreateDiscoveryDeploymentAndSecret(i.cfg, client); err != nil { + return err + } + if err := tokenphase.UpdateOrCreateToken(client, i.cfg.Discovery.Token, kubeadmconstants.DefaultTokenDuration); err != nil { + return err + } + } + + // PHASE 5: Install and deploy all addons, and configure things as necessary + // Create the necessary ServiceAccounts err = apiconfigphase.CreateServiceAccounts(client) if err != nil { @@ -249,17 +263,6 @@ func (i *Init) Run(out io.Writer) error { } } - if i.cfg.Discovery.Token != nil { - fmt.Printf("[token-discovery] Using token: %s\n", kubeadmutil.BearerToken(i.cfg.Discovery.Token)) - if err := kubemaster.CreateDiscoveryDeploymentAndSecret(i.cfg, client); err != nil { - return err - } - if err := kubeadmutil.UpdateOrCreateToken(client, i.cfg.Discovery.Token, kubeadmutil.DefaultTokenDuration); err != nil { - return err - } - } - - // PHASE 5: Deploy essential addons if err := addonsphase.CreateEssentialAddons(i.cfg, client); err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/token.go b/cmd/kubeadm/app/cmd/token.go index aed6e487855..c83ef076ae0 100644 --- a/cmd/kubeadm/app/cmd/token.go +++ b/cmd/kubeadm/app/cmd/token.go @@ -31,9 +31,12 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/pkg/api" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master" "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" + tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/token" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" "k8s.io/kubernetes/pkg/kubectl" ) @@ -69,7 +72,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { }, } createCmd.PersistentFlags().DurationVar(&tokenDuration, - "ttl", kubeadmutil.DefaultTokenDuration, "The duration before the token is automatically deleted.") + "ttl", kubeadmconstants.DefaultTokenDuration, "The duration before the token is automatically deleted. 0 means 'never expires'.") createCmd.PersistentFlags().StringVar( &token, "token", "", "Shared secret used to secure cluster bootstrap. If none is provided, one will be generated for you.", @@ -133,29 +136,29 @@ func RunCreateToken(out io.Writer, cmd *cobra.Command, tokenDuration time.Durati return err } - parsedID, parsedSecret, err := kubeadmutil.ParseToken(token) + parsedID, parsedSecret, err := tokenutil.ParseToken(token) if err != nil { return err } td := &kubeadmapi.TokenDiscovery{ID: parsedID, Secret: parsedSecret} - err = kubeadmutil.UpdateOrCreateToken(client, td, tokenDuration) + err = tokenphase.UpdateOrCreateToken(client, td, tokenDuration) if err != nil { return err } - fmt.Fprintln(out, kubeadmutil.BearerToken(td)) + fmt.Fprintln(out, tokenutil.BearerToken(td)) return nil } func RunGenerateToken(out io.Writer) error { td := &kubeadmapi.TokenDiscovery{} - err := kubeadmutil.GenerateToken(td) + err := tokenutil.GenerateToken(td) if err != nil { return err } - fmt.Fprintln(out, kubeadmutil.BearerToken(td)) + fmt.Fprintln(out, tokenutil.BearerToken(td)) return nil } @@ -183,31 +186,31 @@ func RunListTokens(out io.Writer, errW io.Writer, cmd *cobra.Command) error { w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) fmt.Fprintln(w, "ID\tTOKEN\tTTL") for _, secret := range results.Items { - tokenId, ok := secret.Data["token-id"] + tokenId, ok := secret.Data[bootstrapapi.BootstrapTokenIDKey] if !ok { fmt.Fprintf(errW, "[token] bootstrap token has no token-id data: %s\n", secret.Name) continue } - tokenSecret, ok := secret.Data["token-secret"] + tokenSecret, ok := secret.Data[bootstrapapi.BootstrapTokenSecretKey] if !ok { fmt.Fprintf(errW, "[token] bootstrap token has no token-secret data: %s\n", secret.Name) continue } - token := fmt.Sprintf("%s.%s", tokenId, tokenSecret) + td := &kubeadmapi.TokenDiscovery{ID: string(tokenId), Secret: string(tokenSecret)} // Expiration time is optional, if not specified this implies the token // never expires. expires := "" - secretExpiration, ok := secret.Data["expiration"] + secretExpiration, ok := secret.Data[bootstrapapi.BootstrapTokenExpirationKey] if ok { expireTime, err := time.Parse(time.RFC3339, string(secretExpiration)) if err != nil { - return fmt.Errorf("error parsing expiry time [%v]", err) + return fmt.Errorf("error parsing expiration time [%v]", err) } expires = kubectl.ShortHumanDuration(expireTime.Sub(time.Now())) } - fmt.Fprintf(w, "%s\t%s\t%s\n", tokenId, token, expires) + fmt.Fprintf(w, "%s\t%s\t%s\n", tokenId, tokenutil.BearerToken(td), expires) } w.Flush() @@ -216,7 +219,7 @@ func RunListTokens(out io.Writer, errW io.Writer, cmd *cobra.Command) error { // RunDeleteToken removes a bootstrap token from the server. func RunDeleteToken(out io.Writer, cmd *cobra.Command, tokenId string) error { - if err := kubeadmutil.ParseTokenID(tokenId); err != nil { + if err := tokenutil.ParseTokenID(tokenId); err != nil { return err } @@ -225,7 +228,7 @@ func RunDeleteToken(out io.Writer, cmd *cobra.Command, tokenId string) error { return err } - tokenSecretName := fmt.Sprintf("%s%s", kubeadmutil.BootstrapTokenSecretPrefix, tokenId) + tokenSecretName := fmt.Sprintf("%s%s", kubeadmconstants.BootstrapTokenSecretPrefix, tokenId) if err := client.Secrets(metav1.NamespaceSystem).Delete(tokenSecretName, nil); err != nil { return fmt.Errorf("failed to delete bootstrap token [%v]", err) } diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index 620c46bdea2..7c5b3884f8e 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -67,4 +67,18 @@ const ( // Minimum amount of nodes the Service subnet should allow. // We need at least ten, because the DNS service is always at the tenth cluster clusterIP MinimumAddressesInServiceSubnet = 10 + + // DefaultTokenDuration specifies the default amount of time that a bootstrap token will be valid + DefaultTokenDuration = time.Duration(8) * time.Hour + // BootstrapTokenSecretPrefix the the prefix that will be used for the Secrets that are created as type bootstrap.kubernetes.io/token + BootstrapTokenSecretPrefix = "bootstrap-token-" + + // CSVTokenBootstrapUser is currently the user the bootstrap token in the .csv file + // TODO: This should change to something more official and supported + // TODO: Prefix with kubeadm prefix + CSVTokenBootstrapUser = "kubeadm-node-csr" + // CSVTokenBootstrapGroup specifies the group the tokens in the .csv file will belong to + CSVTokenBootstrapGroup = "kubeadm:kubelet-bootstrap" + // The file name of the tokens file that can be used for bootstrapping + CSVTokenFileName = "tokens.csv" ) diff --git a/cmd/kubeadm/app/discovery/BUILD b/cmd/kubeadm/app/discovery/BUILD index d6b0bce5834..8b8c1270749 100644 --- a/cmd/kubeadm/app/discovery/BUILD +++ b/cmd/kubeadm/app/discovery/BUILD @@ -21,7 +21,7 @@ go_library( "//cmd/kubeadm/app/discovery/https:go_default_library", "//cmd/kubeadm/app/discovery/token:go_default_library", "//cmd/kubeadm/app/node:go_default_library", - "//cmd/kubeadm/app/util:go_default_library", + "//cmd/kubeadm/app/util/token:go_default_library", "//vendor:github.com/spf13/pflag", "//vendor:k8s.io/client-go/tools/clientcmd", "//vendor:k8s.io/client-go/tools/clientcmd/api", diff --git a/cmd/kubeadm/app/discovery/discovery.go b/cmd/kubeadm/app/discovery/discovery.go index 7cf6178b445..ac6118e94fc 100644 --- a/cmd/kubeadm/app/discovery/discovery.go +++ b/cmd/kubeadm/app/discovery/discovery.go @@ -25,7 +25,7 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubenode "k8s.io/kubernetes/cmd/kubeadm/app/node" - kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" ) // For identifies and executes the desired discovery mechanism. @@ -65,7 +65,7 @@ func runHTTPSDiscovery(hd *kubeadmapi.HTTPSDiscovery) (*clientcmdapi.Config, err // runTokenDiscovery executes token-based discovery. func runTokenDiscovery(td *kubeadmapi.TokenDiscovery) (*clientcmdapi.Config, error) { - if valid, err := kubeadmutil.ValidateToken(td); valid == false { + if valid, err := tokenutil.ValidateToken(td); valid == false { return nil, err } diff --git a/cmd/kubeadm/app/master/BUILD b/cmd/kubeadm/app/master/BUILD index 3ff00454381..fb4bf94eff4 100644 --- a/cmd/kubeadm/app/master/BUILD +++ b/cmd/kubeadm/app/master/BUILD @@ -16,7 +16,6 @@ go_library( "manifests.go", "selfhosted.go", "templates.go", - "tokens.go", ], tags = ["automanaged"], deps = [ @@ -31,7 +30,6 @@ go_library( "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/util/intstr", - "//vendor:k8s.io/apimachinery/pkg/util/uuid", "//vendor:k8s.io/apimachinery/pkg/util/wait", "//vendor:k8s.io/client-go/kubernetes", "//vendor:k8s.io/client-go/pkg/api", diff --git a/cmd/kubeadm/app/master/manifests.go b/cmd/kubeadm/app/master/manifests.go index a64aa4ce5e7..7b8e7fc7698 100644 --- a/cmd/kubeadm/app/master/manifests.go +++ b/cmd/kubeadm/app/master/manifests.go @@ -392,7 +392,7 @@ func getControllerManagerCommand(cfg *kubeadmapi.MasterConfiguration, selfHosted "--service-account-private-key-file="+getCertFilePath(kubeadmconstants.ServiceAccountPrivateKeyName), "--cluster-signing-cert-file="+getCertFilePath(kubeadmconstants.CACertName), "--cluster-signing-key-file="+getCertFilePath(kubeadmconstants.CAKeyName), - "--insecure-experimental-approve-all-kubelet-csrs-for-group="+KubeletBootstrapGroup, + "--insecure-experimental-approve-all-kubelet-csrs-for-group="+kubeadmconstants.CSVTokenBootstrapGroup, ) if cfg.CloudProvider != "" { diff --git a/cmd/kubeadm/app/node/BUILD b/cmd/kubeadm/app/node/BUILD index b018c21d2f9..407f71f893e 100644 --- a/cmd/kubeadm/app/node/BUILD +++ b/cmd/kubeadm/app/node/BUILD @@ -19,7 +19,7 @@ go_library( deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/phases/kubeconfig:go_default_library", - "//cmd/kubeadm/app/util:go_default_library", + "//cmd/kubeadm/app/util/token:go_default_library", "//pkg/kubelet/util/csr:go_default_library", "//vendor:github.com/square/go-jose", "//vendor:k8s.io/apimachinery/pkg/types", diff --git a/cmd/kubeadm/app/node/bootstrap.go b/cmd/kubeadm/app/node/bootstrap.go index 3f74ac19639..013a9edc878 100644 --- a/cmd/kubeadm/app/node/bootstrap.go +++ b/cmd/kubeadm/app/node/bootstrap.go @@ -30,7 +30,7 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" - kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" ) // retryTimeout between the subsequent attempts to connect @@ -62,7 +62,7 @@ func EstablishMasterConnection(c *kubeadmapi.TokenDiscovery, clusterInfo *kubead var once sync.Once var wg sync.WaitGroup for _, endpoint := range endpoints { - ac, err := createClients(caCert, endpoint, kubeadmutil.BearerToken(c), nodeName) + ac, err := createClients(caCert, endpoint, tokenutil.BearerToken(c), nodeName) if err != nil { fmt.Printf("[bootstrap] Warning: %s. Skipping endpoint %s\n", err, endpoint) continue diff --git a/cmd/kubeadm/app/phases/apiconfig/BUILD b/cmd/kubeadm/app/phases/apiconfig/BUILD index 8779352d9b2..b1a4fe326b5 100644 --- a/cmd/kubeadm/app/phases/apiconfig/BUILD +++ b/cmd/kubeadm/app/phases/apiconfig/BUILD @@ -16,7 +16,6 @@ go_library( tags = ["automanaged"], deps = [ "//cmd/kubeadm/app/constants:go_default_library", - "//cmd/kubeadm/app/master:go_default_library", "//vendor:k8s.io/apimachinery/pkg/api/errors", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/types", diff --git a/cmd/kubeadm/app/phases/apiconfig/clusterroles.go b/cmd/kubeadm/app/phases/apiconfig/clusterroles.go index b35c801a8ec..20e35d9ed3a 100644 --- a/cmd/kubeadm/app/phases/apiconfig/clusterroles.go +++ b/cmd/kubeadm/app/phases/apiconfig/clusterroles.go @@ -24,7 +24,6 @@ import ( "k8s.io/client-go/pkg/api/v1" rbac "k8s.io/client-go/pkg/apis/rbac/v1beta1" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" - "k8s.io/kubernetes/cmd/kubeadm/app/master" ) const ( @@ -117,7 +116,7 @@ func CreateClusterRoleBindings(clientset *clientset.Clientset) error { Subjects: []rbac.Subject{ { Kind: "Group", - Name: master.KubeletBootstrapGroup, + Name: kubeadmconstants.CSVTokenBootstrapGroup, }, }, }, diff --git a/cmd/kubeadm/app/phases/token/BUILD b/cmd/kubeadm/app/phases/token/BUILD new file mode 100644 index 00000000000..c1e08e6ab6c --- /dev/null +++ b/cmd/kubeadm/app/phases/token/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "bootstrap_test.go", + "csv_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//cmd/kubeadm/app/apis/kubeadm:go_default_library"], +) + +go_library( + name = "go_default_library", + srcs = [ + "bootstrap.go", + "csv.go", + ], + tags = ["automanaged"], + deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/util/token:go_default_library", + "//pkg/bootstrap/api:go_default_library", + "//pkg/kubectl/cmd/util:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/errors", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/util/uuid", + "//vendor:k8s.io/client-go/kubernetes", + "//vendor:k8s.io/client-go/pkg/api/v1", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/cmd/kubeadm/app/phases/token/bootstrap.go b/cmd/kubeadm/app/phases/token/bootstrap.go new file mode 100644 index 00000000000..dd931e78a7b --- /dev/null +++ b/cmd/kubeadm/app/phases/token/bootstrap.go @@ -0,0 +1,99 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/api/v1" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" +) + +const ( + tokenCreateRetries = 5 +) + +// UpdateOrCreateToken attempts to update a token with the given ID, or create if it does +// not already exist. +func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscovery, tokenDuration time.Duration) error { + // Let's make sure the token is valid + if valid, err := tokenutil.ValidateToken(d); !valid { + return err + } + secretName := fmt.Sprintf("%s%s", kubeadmconstants.BootstrapTokenSecretPrefix, d.ID) + var lastErr error + for i := 0; i < tokenCreateRetries; i++ { + secret, err := client.Secrets(metav1.NamespaceSystem).Get(secretName, metav1.GetOptions{}) + if err == nil { + // Secret with this ID already exists, update it: + secret.Data = encodeTokenSecretData(d, tokenDuration) + if _, err := client.Secrets(metav1.NamespaceSystem).Update(secret); err == nil { + return nil + } else { + lastErr = err + } + continue + } + + // Secret does not already exist: + if apierrors.IsNotFound(err) { + secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), + Data: encodeTokenSecretData(d, tokenDuration), + } + if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil { + return nil + } else { + lastErr = err + } + + continue + } + + } + return fmt.Errorf( + "unable to create bootstrap token after %d attempts [%v]", + tokenCreateRetries, + lastErr, + ) +} + +// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret +func encodeTokenSecretData(d *kubeadmapi.TokenDiscovery, duration time.Duration) map[string][]byte { + data := map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(d.ID), + bootstrapapi.BootstrapTokenSecretKey: []byte(d.Secret), + bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"), + } + + if duration > 0 { + // Get the current time, add the specified duration, and format it accordingly + durationString := time.Now().Add(duration).Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(durationString) + } + return data +} diff --git a/cmd/kubeadm/app/phases/token/bootstrap_test.go b/cmd/kubeadm/app/phases/token/bootstrap_test.go new file mode 100644 index 00000000000..4975af69271 --- /dev/null +++ b/cmd/kubeadm/app/phases/token/bootstrap_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "bytes" + "testing" + "time" + + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" +) + +func TestEncodeTokenSecretData(t *testing.T) { + var tests = []struct { + token *kubeadmapi.TokenDiscovery + t time.Duration + }{ + {token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}}, // should use default + {token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}, t: time.Second}, // should use default + } + for _, rt := range tests { + actual := encodeTokenSecretData(rt.token, rt.t) + if !bytes.Equal(actual["token-id"], []byte(rt.token.ID)) { + t.Errorf( + "failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s", + rt.token.ID, + actual["token-id"], + ) + } + if !bytes.Equal(actual["token-secret"], []byte(rt.token.Secret)) { + t.Errorf( + "failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s", + rt.token.Secret, + actual["token-secret"], + ) + } + if rt.t > 0 { + if actual["expiration"] == nil { + t.Errorf( + "failed EncodeTokenSecretData, duration was not added to time", + ) + } + } + } +} diff --git a/cmd/kubeadm/app/master/tokens.go b/cmd/kubeadm/app/phases/token/csv.go similarity index 74% rename from cmd/kubeadm/app/master/tokens.go rename to cmd/kubeadm/app/phases/token/csv.go index 458f3315361..914e6b79930 100644 --- a/cmd/kubeadm/app/master/tokens.go +++ b/cmd/kubeadm/app/phases/token/csv.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package master +package token import ( "bytes" @@ -24,22 +24,17 @@ import ( "k8s.io/apimachinery/pkg/util/uuid" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) -const ( - // TODO: prefix with kubeadm prefix - KubeletBootstrapUser = "kubeadm-node-csr" - - KubeletBootstrapGroup = "kubeadm:kubelet-bootstrap" -) - +// CreateTokenAuthFile creates the CSV file that can be used for allowing users with tokens access to the API Server func CreateTokenAuthFile(bt string) error { - tokenAuthFilePath := path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, "tokens.csv") + tokenAuthFilePath := path.Join(kubeadmapi.GlobalEnvParams.HostPKIPath, kubeadmconstants.CSVTokenFileName) if err := os.MkdirAll(kubeadmapi.GlobalEnvParams.HostPKIPath, 0700); err != nil { return fmt.Errorf("failed to create directory %q [%v]", kubeadmapi.GlobalEnvParams.HostPKIPath, err) } - serialized := []byte(fmt.Sprintf("%s,%s,%s,%s\n", bt, KubeletBootstrapUser, uuid.NewUUID(), KubeletBootstrapGroup)) + serialized := []byte(fmt.Sprintf("%s,%s,%s,%s\n", bt, kubeadmconstants.CSVTokenBootstrapUser, uuid.NewUUID(), kubeadmconstants.CSVTokenBootstrapGroup)) // DumpReaderToFile create a file with mode 0600 if err := cmdutil.DumpReaderToFile(bytes.NewReader(serialized), tokenAuthFilePath); err != nil { return fmt.Errorf("failed to save token auth file (%q) [%v]", tokenAuthFilePath, err) diff --git a/cmd/kubeadm/app/phases/token/csv_test.go b/cmd/kubeadm/app/phases/token/csv_test.go new file mode 100644 index 00000000000..abadcab9d08 --- /dev/null +++ b/cmd/kubeadm/app/phases/token/csv_test.go @@ -0,0 +1,17 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 74b61a882ec..21cd36b4092 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -13,20 +13,10 @@ go_library( srcs = [ "error.go", "template.go", - "tokens.go", "version.go", ], tags = ["automanaged"], - deps = [ - "//cmd/kubeadm/app/apis/kubeadm:go_default_library", - "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", - "//cmd/kubeadm/app/preflight:go_default_library", - "//pkg/bootstrap/api:go_default_library", - "//vendor:k8s.io/apimachinery/pkg/api/errors", - "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", - "//vendor:k8s.io/client-go/kubernetes", - "//vendor:k8s.io/client-go/pkg/api/v1", - ], + deps = ["//cmd/kubeadm/app/preflight:go_default_library"], ) go_test( @@ -34,15 +24,11 @@ go_test( srcs = [ "error_test.go", "template_test.go", - "tokens_test.go", "version_test.go", ], library = ":go_default_library", tags = ["automanaged"], - deps = [ - "//cmd/kubeadm/app/apis/kubeadm:go_default_library", - "//cmd/kubeadm/app/preflight:go_default_library", - ], + deps = ["//cmd/kubeadm/app/preflight:go_default_library"], ) filegroup( @@ -54,6 +40,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//cmd/kubeadm/app/util/token:all-srcs", + ], tags = ["automanaged"], ) diff --git a/cmd/kubeadm/app/util/token/BUILD b/cmd/kubeadm/app/util/token/BUILD new file mode 100644 index 00000000000..2e71b54f460 --- /dev/null +++ b/cmd/kubeadm/app/util/token/BUILD @@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["tokens_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//cmd/kubeadm/app/apis/kubeadm:go_default_library"], +) + +go_library( + name = "go_default_library", + srcs = ["tokens.go"], + tags = ["automanaged"], + deps = ["//cmd/kubeadm/app/apis/kubeadm:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/cmd/kubeadm/app/util/token/tokens.go b/cmd/kubeadm/app/util/token/tokens.go new file mode 100644 index 00000000000..dcba423c8da --- /dev/null +++ b/cmd/kubeadm/app/util/token/tokens.go @@ -0,0 +1,100 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "regexp" + "strings" + + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" +) + +const ( + TokenIDBytes = 3 + TokenSecretBytes = 8 +) + +var ( + tokenIDRegexpString = "^([a-z0-9]{6})$" + tokenIDRegexp = regexp.MustCompile(tokenIDRegexpString) + tokenRegexpString = "^([a-z0-9]{6})\\:([a-z0-9]{16})$" + tokenRegexp = regexp.MustCompile(tokenRegexpString) +) + +func randBytes(length int) (string, error) { + b := make([]byte, length) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +// GenerateToken generates a new token with a token ID that is valid as a +// Kubernetes DNS label. +// For more info, see kubernetes/pkg/util/validation/validation.go. +func GenerateToken(d *kubeadmapi.TokenDiscovery) error { + tokenID, err := randBytes(TokenIDBytes) + if err != nil { + return err + } + + token, err := randBytes(TokenSecretBytes) + if err != nil { + return err + } + + d.ID = strings.ToLower(tokenID) + d.Secret = strings.ToLower(token) + return nil +} + +// ParseTokenID tries and parse a valid token ID from a string. +// An error is returned in case of failure. +func ParseTokenID(s string) error { + if !tokenIDRegexp.MatchString(s) { + return fmt.Errorf("token ID [%q] was not of form [%q]", s, tokenIDRegexpString) + } + return nil +} + +// ParseToken tries and parse a valid token from a string. +// A token ID and token secret are returned in case of success, an error otherwise. +func ParseToken(s string) (string, string, error) { + split := tokenRegexp.FindStringSubmatch(s) + if len(split) != 3 { + return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString) + } + return split[1], split[2], nil +} + +// BearerToken returns a string representation of the passed token. +func BearerToken(d *kubeadmapi.TokenDiscovery) string { + return fmt.Sprintf("%s:%s", d.ID, d.Secret) +} + +// ValidateToken validates whether a token is well-formed. +// In case it's not, the corresponding error is returned as well. +func ValidateToken(d *kubeadmapi.TokenDiscovery) (bool, error) { + if _, _, err := ParseToken(d.ID + ":" + d.Secret); err != nil { + return false, err + } + return true, nil +} diff --git a/cmd/kubeadm/app/util/tokens_test.go b/cmd/kubeadm/app/util/token/tokens_test.go similarity index 74% rename from cmd/kubeadm/app/util/tokens_test.go rename to cmd/kubeadm/app/util/token/tokens_test.go index f9a02e989f2..794a9545586 100644 --- a/cmd/kubeadm/app/util/tokens_test.go +++ b/cmd/kubeadm/app/util/token/tokens_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package token import ( - "bytes" "testing" - "time" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" ) @@ -151,28 +149,6 @@ func TestRandBytes(t *testing.T) { } } -func TestDiscoveryPort(t *testing.T) { - var tests = []struct { - token *kubeadmapi.TokenDiscovery - expected int32 - }{ - {token: &kubeadmapi.TokenDiscovery{}, expected: 9898}, // should use default - {token: &kubeadmapi.TokenDiscovery{Addresses: []string{"foobar:1234"}}, expected: 1234}, - {token: &kubeadmapi.TokenDiscovery{Addresses: []string{"doesnothaveport"}}, expected: 9898}, // should use default - {token: &kubeadmapi.TokenDiscovery{Addresses: []string{"foorbar:abcd"}}, expected: 9898}, // since abcd isn't an int, should use default - } - for _, rt := range tests { - actual := DiscoveryPort(rt.token) - if actual != rt.expected { - t.Errorf( - "failed DiscoveryPort:\n\texpected: %d\n\t actual: %d", - rt.expected, - actual, - ) - } - } -} - func TestBearerToken(t *testing.T) { var tests = []struct { token *kubeadmapi.TokenDiscovery @@ -191,37 +167,3 @@ func TestBearerToken(t *testing.T) { } } } - -func TestEncodeTokenSecretData(t *testing.T) { - var tests = []struct { - token *kubeadmapi.TokenDiscovery - t time.Duration - }{ - {token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}}, // should use default - {token: &kubeadmapi.TokenDiscovery{ID: "foo", Secret: "bar"}, t: time.Second}, // should use default - } - for _, rt := range tests { - actual := encodeTokenSecretData(rt.token, rt.t) - if !bytes.Equal(actual["token-id"], []byte(rt.token.ID)) { - t.Errorf( - "failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s", - rt.token.ID, - actual["token-id"], - ) - } - if !bytes.Equal(actual["token-secret"], []byte(rt.token.Secret)) { - t.Errorf( - "failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s", - rt.token.Secret, - actual["token-secret"], - ) - } - if rt.t > 0 { - if actual["expiration"] == nil { - t.Errorf( - "failed EncodeTokenSecretData, duration was not added to time", - ) - } - } - } -} diff --git a/cmd/kubeadm/app/util/tokens.go b/cmd/kubeadm/app/util/tokens.go deleted file mode 100644 index d7cb39e199b..00000000000 --- a/cmd/kubeadm/app/util/tokens.go +++ /dev/null @@ -1,196 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "regexp" - "strconv" - "strings" - "time" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clientset "k8s.io/client-go/kubernetes" - v1 "k8s.io/client-go/pkg/api/v1" - kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" - kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" - bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" -) - -const ( - TokenIDBytes = 3 - TokenSecretBytes = 8 - BootstrapTokenSecretPrefix = "bootstrap-token-" - DefaultTokenDuration = time.Duration(8) * time.Hour - tokenCreateRetries = 5 -) - -var ( - tokenIDRegexpString = "^([a-z0-9]{6})$" - tokenIDRegexp = regexp.MustCompile(tokenIDRegexpString) - tokenRegexpString = "^([a-z0-9]{6})\\:([a-z0-9]{16})$" - tokenRegexp = regexp.MustCompile(tokenRegexpString) -) - -func randBytes(length int) (string, error) { - b := make([]byte, length) - _, err := rand.Read(b) - if err != nil { - return "", err - } - return hex.EncodeToString(b), nil -} - -// GenerateToken generates a new token with a token ID that is valid as a -// Kubernetes DNS label. -// For more info, see kubernetes/pkg/util/validation/validation.go. -func GenerateToken(d *kubeadmapi.TokenDiscovery) error { - tokenID, err := randBytes(TokenIDBytes) - if err != nil { - return err - } - - token, err := randBytes(TokenSecretBytes) - if err != nil { - return err - } - - d.ID = strings.ToLower(tokenID) - d.Secret = strings.ToLower(token) - return nil -} - -// ParseTokenID tries and parse a valid token ID from a string. -// An error is returned in case of failure. -func ParseTokenID(s string) error { - if !tokenIDRegexp.MatchString(s) { - return fmt.Errorf("token ID [%q] was not of form [%q]", s, tokenIDRegexpString) - } - return nil - -} - -// ParseToken tries and parse a valid token from a string. -// A token ID and token secret are returned in case of success, an error otherwise. -func ParseToken(s string) (string, string, error) { - split := tokenRegexp.FindStringSubmatch(s) - if len(split) != 3 { - return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString) - } - return split[1], split[2], nil - -} - -// BearerToken returns a string representation of the passed token. -func BearerToken(d *kubeadmapi.TokenDiscovery) string { - return fmt.Sprintf("%s:%s", d.ID, d.Secret) -} - -// ValidateToken validates whether a token is well-formed. -// In case it's not, the corresponding error is returned as well. -func ValidateToken(d *kubeadmapi.TokenDiscovery) (bool, error) { - if _, _, err := ParseToken(d.ID + ":" + d.Secret); err != nil { - return false, err - } - return true, nil -} - -func DiscoveryPort(d *kubeadmapi.TokenDiscovery) int32 { - if len(d.Addresses) == 0 { - return kubeadmapiext.DefaultDiscoveryBindPort - } - split := strings.Split(d.Addresses[0], ":") - if len(split) == 1 { - return kubeadmapiext.DefaultDiscoveryBindPort - } - if i, err := strconv.Atoi(split[1]); err == nil { - return int32(i) - } - return kubeadmapiext.DefaultDiscoveryBindPort -} - -// UpdateOrCreateToken attempts to update a token with the given ID, or create if it does -// not already exist. -func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscovery, tokenDuration time.Duration) error { - // Let's make sure - if valid, err := ValidateToken(d); !valid { - return err - } - secretName := fmt.Sprintf("%s%s", BootstrapTokenSecretPrefix, d.ID) - var lastErr error - for i := 0; i < tokenCreateRetries; i++ { - secret, err := client.Secrets(metav1.NamespaceSystem).Get(secretName, metav1.GetOptions{}) - if err == nil { - // Secret with this ID already exists, update it: - secret.Data = encodeTokenSecretData(d, tokenDuration) - if _, err := client.Secrets(metav1.NamespaceSystem).Update(secret); err == nil { - return nil - } else { - lastErr = err - } - continue - } - - // Secret does not already exist: - if apierrors.IsNotFound(err) { - secret = &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - }, - // TODO(jbeda): convert kubeadm to client-go - // https://github.com/kubernetes/kubeadm/issues/52 - Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), - Data: encodeTokenSecretData(d, tokenDuration), - } - if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil { - return nil - } else { - lastErr = err - } - - continue - } - - } - return fmt.Errorf( - "unable to create bootstrap token after %d attempts [%v]", - tokenCreateRetries, - lastErr, - ) -} - -func encodeTokenSecretData(d *kubeadmapi.TokenDiscovery, duration time.Duration) map[string][]byte { - var ( - data = map[string][]byte{} - ) - - data["token-id"] = []byte(d.ID) - data["token-secret"] = []byte(d.Secret) - - data["usage-bootstrap-signing"] = []byte("true") - - if duration > 0 { - t := time.Now() - t = t.Add(duration) - data["expiration"] = []byte(t.Format(time.RFC3339)) - } - - return data -}