diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index 195f639a333..182f1b1a5ff 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -74,6 +74,7 @@ go_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", + "//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", "//vendor/k8s.io/client-go/util/cert:go_default_library", "//vendor/k8s.io/utils/exec:go_default_library", ], diff --git a/cmd/kubeadm/app/cmd/token.go b/cmd/kubeadm/app/cmd/token.go index fb608befb55..ee2aba0ffb8 100644 --- a/cmd/kubeadm/app/cmd/token.go +++ b/cmd/kubeadm/app/cmd/token.go @@ -17,12 +17,15 @@ limitations under the License. package cmd import ( + "bytes" + "crypto/x509" "fmt" "io" "os" "sort" "strings" "text/tabwriter" + "text/template" "time" "github.com/renstrom/dedent" @@ -33,6 +36,8 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/sets" clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + clientcertutil "k8s.io/client-go/util/cert" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" @@ -40,12 +45,17 @@ import ( kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pubkeypin" tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token" api "k8s.io/kubernetes/pkg/apis/core" bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" "k8s.io/kubernetes/pkg/printers" ) +var joinCommandTemplate = template.Must(template.New("join").Parse(`` + + `kubeadm join --token {{.Token}} {{.MasterHostPort}}{{range $h := .CAPubKeyPins}} --discovery-token-ca-cert-hash {{$h}}{{end}}`, +)) + // NewCmdToken returns cobra.Command for token management func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { var kubeConfigFile string @@ -89,6 +99,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { var extraGroups []string var tokenDuration time.Duration var description string + var printJoinCommand bool createCmd := &cobra.Command{ Use: "create [token]", Short: "Create bootstrap tokens on the server.", @@ -108,7 +119,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { client, err := getClientset(kubeConfigFile, dryRun) kubeadmutil.CheckErr(err) - err = RunCreateToken(out, client, token, tokenDuration, usages, extraGroups, description) + err = RunCreateToken(out, client, token, tokenDuration, usages, extraGroups, description, printJoinCommand, kubeConfigFile) kubeadmutil.CheckErr(err) }, } @@ -121,6 +132,8 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { 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.") + createCmd.Flags().BoolVar(&printJoinCommand, + "print-join-command", false, "Instead of printing only the token, print the full 'kubeadm join' flag needed to join the cluster using the token.") tokenCmd.AddCommand(createCmd) tokenCmd.AddCommand(NewCmdTokenGenerate(out)) @@ -190,7 +203,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, extraGroups []string, description string) error { +func RunCreateToken(out io.Writer, client clientset.Interface, token string, tokenDuration time.Duration, usages []string, extraGroups []string, description string, printJoinCommand bool, kubeConfigFile string) error { if len(token) == 0 { var err error token, err = tokenutil.GenerateToken() @@ -228,7 +241,18 @@ func RunCreateToken(out io.Writer, client clientset.Interface, token string, tok return err } - fmt.Fprintln(out, token) + // if --print-join-command was specified, print the full `kubeadm join` command + // otherwise, just print the token + if printJoinCommand { + joinCommand, err := getJoinCommand(token, kubeConfigFile) + if err != nil { + return fmt.Errorf("failed to get join command: %v", err) + } + fmt.Fprintln(out, joinCommand) + } else { + fmt.Fprintln(out, token) + } + return nil } @@ -369,3 +393,52 @@ func getClientset(file string, dryRun bool) (clientset.Interface, error) { client, err := kubeconfigutil.ClientSetFromFile(file) return client, err } + +func getJoinCommand(token string, kubeConfigFile string) (string, error) { + // load the kubeconfig file to get the CA certificate and endpoint + config, err := clientcmd.LoadFromFile(kubeConfigFile) + if err != nil { + return "", fmt.Errorf("failed to load kubeconfig: %v", err) + } + + // load the default cluster config + clusterConfig := kubeconfigutil.GetClusterFromKubeConfig(config) + if clusterConfig == nil { + return "", fmt.Errorf("failed to get default cluster config") + } + + // load CA certificates from the kubeconfig (either from PEM data or by file path) + var caCerts []*x509.Certificate + if clusterConfig.CertificateAuthorityData != nil { + caCerts, err = clientcertutil.ParseCertsPEM(clusterConfig.CertificateAuthorityData) + if err != nil { + return "", fmt.Errorf("failed to parse CA certificate from kubeconfig: %v", err) + } + } else if clusterConfig.CertificateAuthority != "" { + caCerts, err = clientcertutil.CertsFromFile(clusterConfig.CertificateAuthority) + if err != nil { + return "", fmt.Errorf("failed to load CA certificate referenced by kubeconfig: %v", err) + } + } else { + return "", fmt.Errorf("no CA certificates found in kubeconfig") + } + + // hash all the CA certs and include their public key pins as trusted values + publicKeyPins := make([]string, 0, len(caCerts)) + for _, caCert := range caCerts { + publicKeyPins = append(publicKeyPins, pubkeypin.Hash(caCert)) + } + + ctx := map[string]interface{}{ + "Token": token, + "CAPubKeyPins": publicKeyPins, + "MasterHostPort": strings.Replace(clusterConfig.Server, "https://", "", -1), + } + + var out bytes.Buffer + err = joinCommandTemplate.Execute(&out, ctx) + if err != nil { + return "", fmt.Errorf("failed to render join command template: %v", err) + } + return out.String(), nil +}