Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kubeadm: add --print-join-command flag for token create. #56185

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kubeadm/app/cmd/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
79 changes: 76 additions & 3 deletions cmd/kubeadm/app/cmd/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -33,19 +36,26 @@ 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"
tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node"
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
Expand Down Expand Up @@ -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.",
Expand All @@ -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)
},
}
Expand All @@ -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))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users may want to see fmt.Fprintln(out, token) in this case anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention here is that this flag could be useful as part of an automated provisioning step where you might run something like kubeadm token create --print-join-command >> worker_init.sh to create an init script for your worker nodes. Having a second line with just the token that you have to filter out would be annoying in that case.

The token is still there in the join command so if you accidentally ran manually with --print-join-command you could still copy-paste just what you wanted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a second line with just the token that you have to filter out would be annoying in that case.

I didn't meant a second line with just the token is needed here. I meant that when if err != nil is true, we'd better print an error to users and also print the token anyway. (It will return and nothing (UPDATE: no token) will be printed for users for now when if err != nil is true, right?) Feel free to correct me if I misunderstand here :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabriziopandini any ideas here?

return fmt.Errorf("failed to get join command: %v", err)
}
fmt.Fprintln(out, joinCommand)
} else {
fmt.Fprintln(out, token)
}

return nil
}

Expand Down Expand Up @@ -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
}