kubeadm: Implement the kubeadm token command fully and move it out of the experimental subsection

parent 70a26852
......@@ -53,7 +53,9 @@ go_library(
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/util/net",
"//vendor:k8s.io/apiserver/pkg/util/flag",
"//vendor:k8s.io/client-go/kubernetes",
"//vendor:k8s.io/client-go/pkg/api",
"//vendor:k8s.io/client-go/pkg/api/v1",
"//vendor:k8s.io/client-go/util/cert",
],
)
......
......@@ -83,13 +83,13 @@ func NewKubeadmCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
cmds.AddCommand(NewCmdJoin(out))
cmds.AddCommand(NewCmdReset(out))
cmds.AddCommand(NewCmdVersion(out))
cmds.AddCommand(NewCmdToken(out, err))
// Wrap not yet usable/supported commands in experimental sub-command:
// Wrap not yet fully supported commands in an alpha subcommand
experimentalCmd := &cobra.Command{
Use: "ex",
Use: "alpha",
Short: "Experimental sub-commands not yet fully functional.",
}
experimentalCmd.AddCommand(NewCmdToken(out, err))
experimentalCmd.AddCommand(phases.NewCmdPhase(out))
cmds.AddCommand(experimentalCmd)
......
......@@ -233,7 +233,8 @@ func (i *Init) Run(out io.Writer) error {
if err := kubemaster.CreateDiscoveryDeploymentAndSecret(i.cfg, client); err != nil {
return err
}
if err := tokenphase.UpdateOrCreateToken(client, i.cfg.Discovery.Token, kubeadmconstants.DefaultTokenDuration); err != nil {
tokenDescription := "The default bootstrap token generated by 'kubeadm init'."
if err := tokenphase.UpdateOrCreateToken(client, i.cfg.Discovery.Token, false, kubeadmconstants.DefaultTokenDuration, kubeadmconstants.DefaultTokenUsages, tokenDescription); err != nil {
return err
}
}
......
......@@ -80,7 +80,8 @@ const (
MinimumAddressesInServiceSubnet = 10
// DefaultTokenDuration specifies the default amount of time that a bootstrap token will be valid
DefaultTokenDuration = time.Duration(8) * time.Hour
// Default behaviour is "never expire" == 0
DefaultTokenDuration = 0
// LabelNodeRoleMaster specifies that a node is a master
// It's copied over to kubeadm until it's merged in core: https://github.com/kubernetes/kubernetes/pull/39112
......@@ -109,4 +110,7 @@ var (
AuthorizationPolicyPath = path.Join(KubernetesDir, "abac_policy.json")
AuthorizationWebhookConfigPath = path.Join(KubernetesDir, "webhook_authz.conf")
// DefaultTokenUsages specifies the default functions a token will get
DefaultTokenUsages = []string{"signing", "authentication"}
)
......@@ -29,13 +29,15 @@ import (
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
)
const (
tokenCreateRetries = 5
)
const tokenCreateRetries = 5
// CreateNewToken tries to create a token and fails if one with the same ID already exists
func CreateNewToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscovery, tokenDuration time.Duration, usages []string, description string) error {
return UpdateOrCreateToken(client, d, true, tokenDuration, usages, description)
}
// 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 {
// 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, failIfExists bool, tokenDuration time.Duration, usages []string, description string) error {
// Let's make sure the token is valid
if valid, err := tokenutil.ValidateToken(d); !valid {
return err
......@@ -45,8 +47,11 @@ func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscove
for i := 0; i < tokenCreateRetries; i++ {
secret, err := client.Secrets(metav1.NamespaceSystem).Get(secretName, metav1.GetOptions{})
if err == nil {
if failIfExists {
return fmt.Errorf("a token with id %q already exists", d.ID)
}
// Secret with this ID already exists, update it:
secret.Data = encodeTokenSecretData(d, tokenDuration)
secret.Data = encodeTokenSecretData(d, tokenDuration, usages, description)
if _, err := client.Secrets(metav1.NamespaceSystem).Update(secret); err == nil {
return nil
} else {
......@@ -62,14 +67,13 @@ func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscove
Name: secretName,
},
Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken),
Data: encodeTokenSecretData(d, tokenDuration),
Data: encodeTokenSecretData(d, tokenDuration, usages, description),
}
if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil {
return nil
} else {
lastErr = err
}
continue
}
......@@ -82,11 +86,10 @@ func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscove
}
// 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 {
func encodeTokenSecretData(d *kubeadmapi.TokenDiscovery, duration time.Duration, usages []string, description string) map[string][]byte {
data := map[string][]byte{
bootstrapapi.BootstrapTokenIDKey: []byte(d.ID),
bootstrapapi.BootstrapTokenSecretKey: []byte(d.Secret),
bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"),
bootstrapapi.BootstrapTokenIDKey: []byte(d.ID),
bootstrapapi.BootstrapTokenSecretKey: []byte(d.Secret),
}
if duration > 0 {
......@@ -94,5 +97,12 @@ func encodeTokenSecretData(d *kubeadmapi.TokenDiscovery, duration time.Duration)
durationString := time.Now().Add(duration).Format(time.RFC3339)
data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(durationString)
}
if len(description) > 0 {
data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(description)
}
for _, usage := range usages {
// TODO: Validate the usage string here before
data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true")
}
return data
}
......@@ -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, rt.t)
actual := encodeTokenSecretData(rt.token, rt.t, []string{}, "")
if !bytes.Equal(actual["token-id"], []byte(rt.token.ID)) {
t.Errorf(
"failed EncodeTokenSecretData:\n\texpected: %s\n\t actual: %s",
......
......@@ -32,10 +32,10 @@ const (
)
var (
tokenIDRegexpString = "^([a-z0-9]{6})$"
tokenIDRegexp = regexp.MustCompile(tokenIDRegexpString)
tokenRegexpString = "^([a-z0-9]{6})\\.([a-z0-9]{16})$"
tokenRegexp = regexp.MustCompile(tokenRegexpString)
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) {
......@@ -69,8 +69,8 @@ func GenerateToken(d *kubeadmapi.TokenDiscovery) error {
// 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)
if !TokenIDRegexp.MatchString(s) {
return fmt.Errorf("token ID [%q] was not of form [%q]", s, TokenIDRegexpString)
}
return nil
}
......@@ -78,9 +78,9 @@ func ParseTokenID(s string) error {
// 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)
split := TokenRegexp.FindStringSubmatch(s)
if len(split) != 3 {
return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString)
return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, TokenRegexpString)
}
return split[1], split[2], nil
}
......
......@@ -36,17 +36,17 @@ func TestCmdTokenGenerate(t *testing.T) {
t.Log("kubeadm cmd tests being skipped")
t.Skip()
}
stdout, _, err := RunCmd(*kubeadmPath, "ex", "token", "generate")
stdout, _, err := RunCmd(*kubeadmPath, "token", "generate")
if err != nil {
t.Fatalf("'kubeadm ex token generate' exited uncleanly: %v", err)
t.Fatalf("'kubeadm token generate' exited uncleanly: %v", err)
}
matched, err := regexp.MatchString(TokenExpectedRegex, stdout)
if err != nil {
t.Fatalf("encountered an error while trying to match 'kubeadm ex token generate' stdout: %v", err)
t.Fatalf("encountered an error while trying to match 'kubeadm token generate' stdout: %v", err)
}
if !matched {
t.Errorf("'kubeadm ex token generate' stdout did not match expected regex; wanted: [%q], got: [%s]", TokenExpectedRegex, stdout)
t.Errorf("'kubeadm token generate' stdout did not match expected regex; wanted: [%q], got: [%s]", TokenExpectedRegex, stdout)
}
}
......@@ -54,7 +54,7 @@ func TestCmdTokenGenerateTypoError(t *testing.T) {
/*
Since we expect users to do things like this:
$ TOKEN=$(kubeadm ex token generate)
$ TOKEN=$(kubeadm token generate)
we want to make sure that if they have a typo in their command, we exit
with a non-zero status code after showing the command's usage, so that
......@@ -65,9 +65,9 @@ func TestCmdTokenGenerateTypoError(t *testing.T) {
t.Skip()
}
_, _, err := RunCmd(*kubeadmPath, "ex", "token", "genorate") // subtle typo
_, _, err := RunCmd(*kubeadmPath, "token", "genorate") // subtle typo
if err == nil {
t.Error("'kubeadm ex token genorate' (a deliberate typo) exited without an error when we expected non-zero exit status")
t.Error("'kubeadm token genorate' (a deliberate typo) exited without an error when we expected non-zero exit status")
}
}
func TestCmdTokenDelete(t *testing.T) {
......@@ -85,10 +85,10 @@ func TestCmdTokenDelete(t *testing.T) {
}
for _, rt := range tests {
_, _, actual := RunCmd(*kubeadmPath, "ex", "token", "delete", rt.args)
_, _, actual := RunCmd(*kubeadmPath, "token", "delete", rt.args)
if (actual == nil) != rt.expected {
t.Errorf(
"failed CmdTokenDelete running 'kubeadm ex token %s' with an error: %v\n\texpected: %t\n\t actual: %t",
"failed CmdTokenDelete running 'kubeadm token %s' with an error: %v\n\texpected: %t\n\t actual: %t",
rt.args,
actual,
rt.expected,
......
......@@ -47,6 +47,14 @@ const (
// should be considered invalid. Optional.
BootstrapTokenExpirationKey = "expiration"
// BootstrapTokenDescriptionKey is a description in human-readable format that
// describes what the bootstrap token is used for. Optional.
BootstrapTokenDescriptionKey = "description"
// BootstrapTokenUsagePrefix is the prefix for the other usage constants that specifies different
// functions of a bootstrap token
BootstrapTokenUsagePrefix = "usage-bootstrap-"
// BootstrapTokenUsageSigningKey signals that this token should be used to
// sign configs as part of the bootstrap process. Value must be "true". Any
// other value is assumed to be false. Optional.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment