-
Notifications
You must be signed in to change notification settings - Fork 39k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64408 from luxas/kubeadm_refactor_bt
Automatic merge from submit-queue (batch tested with PRs 64057, 63223, 64346, 64562, 64408). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. kubeadm: Refactor the Bootstrap Tokens usage in the API types **What this PR does / why we need it**: This PR: - Moves some common, generic Bootstrap Token helpers and constants from `k8s.io/kubernetes/cmd/kubeadm/app/util/token` to `k8s.io/client-go/tools/bootstrap/token/` - Breaks out the top-level Bootstrap Token fields to a dedicated `BootstrapToken` struct with helper functions. - Instead of representing the Bootstrap Token as a plain `string`, there is now a wrapper struct `BootstrapTokenString` that can marshal/unmarshal correctly and supports validation on create, and splitting up the full token in the ID/Secret parts automatically. - Makes kubeadm support multiple Bootstrap Tokens automatically by supporting a slice of `BootstrapToken` in the `MasterConfiguration` API object - Consolidates the place for kubeadm to create token-related flags in an `options` package - Supports automatic conversion from the `v1alpha1` to `v1alpha2` API - Adds support to set token expiration directly instead of setting a TTL (Expiration and TTL are mutually exclusive) - Removes the old `TokenDiscovery` struct we're not using anymore inside of kubeadm **Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: Related to kubernetes/community#2131 **Special notes for your reviewer**: This is work in progress. Please only review the first two commits for now. I will work on splitting up this PR in smaller chunks. I will also write unit tests tomorrow. **Release note**: ```release-note [action required] kubeadm: The Token-related fields in the `MasterConfiguration` object have now been refactored. Instead of the top-level `.Token`, `.TokenTTL`, `.TokenUsages`, `.TokenGroups` fields, there is now a `BootstrapTokens` slice of `BootstrapToken` objects that support the same features under the `.Token`, `.TTL`, `.Usages`, `.Groups` fields. ``` @kubernetes/sig-cluster-lifecycle-pr-reviews @mattmoyer @liztio
- Loading branch information
Showing
55 changed files
with
2,337 additions
and
986 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* | ||
Copyright 2018 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 kubeadm | ||
|
||
import ( | ||
"fmt" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
"k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
bootstrapapi "k8s.io/client-go/tools/bootstrap/token/api" | ||
bootstraputil "k8s.io/client-go/tools/bootstrap/token/util" | ||
) | ||
|
||
// ToSecret converts the given BootstrapToken object to its Secret representation that | ||
// may be submitted to the API Server in order to be stored. | ||
func (bt *BootstrapToken) ToSecret() *v1.Secret { | ||
return &v1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID), | ||
Namespace: metav1.NamespaceSystem, | ||
}, | ||
Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), | ||
Data: encodeTokenSecretData(bt, time.Now()), | ||
} | ||
} | ||
|
||
// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret | ||
// now is passed in order to be able to used in unit testing | ||
func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte { | ||
data := map[string][]byte{ | ||
bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID), | ||
bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret), | ||
} | ||
|
||
if len(token.Description) > 0 { | ||
data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description) | ||
} | ||
|
||
// If for some strange reason both token.TTL and token.Expires would be set | ||
// (they are mutually exlusive in validation so this shouldn't be the case), | ||
// token.Expires has higher priority, as can be seen in the logic here. | ||
if token.Expires != nil { | ||
// Format the expiration date accordingly | ||
// TODO: This maybe should be a helper function in bootstraputil? | ||
expirationString := token.Expires.Time.Format(time.RFC3339) | ||
data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) | ||
|
||
} else if token.TTL != nil && token.TTL.Duration > 0 { | ||
// Only if .Expires is unset, TTL might have an effect | ||
// Get the current time, add the specified duration, and format it accordingly | ||
expirationString := now.Add(token.TTL.Duration).Format(time.RFC3339) | ||
data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) | ||
} | ||
|
||
for _, usage := range token.Usages { | ||
data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true") | ||
} | ||
|
||
if len(token.Groups) > 0 { | ||
data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ",")) | ||
} | ||
return data | ||
} | ||
|
||
// BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret | ||
func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { | ||
// Get the Token ID field from the Secret data | ||
tokenID := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) | ||
if len(tokenID) == 0 { | ||
return nil, fmt.Errorf("Bootstrap Token Secret has no token-id data: %s", secret.Name) | ||
} | ||
|
||
// Enforce the right naming convention | ||
if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) { | ||
return nil, fmt.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q", | ||
bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID)) | ||
} | ||
|
||
tokenSecret := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) | ||
if len(tokenSecret) == 0 { | ||
return nil, fmt.Errorf("Bootstrap Token Secret has no token-secret data: %s", secret.Name) | ||
} | ||
|
||
// Create the BootstrapTokenString object based on the ID and Secret | ||
bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret) | ||
if err != nil { | ||
return nil, fmt.Errorf("Bootstrap Token Secret is invalid and couldn't be parsed: %v", err) | ||
} | ||
|
||
// Get the description (if any) from the Secret | ||
description := getSecretString(secret, bootstrapapi.BootstrapTokenDescriptionKey) | ||
|
||
// Expiration time is optional, if not specified this implies the token | ||
// never expires. | ||
secretExpiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) | ||
var expires *metav1.Time | ||
if len(secretExpiration) > 0 { | ||
expTime, err := time.Parse(time.RFC3339, secretExpiration) | ||
if err != nil { | ||
return nil, fmt.Errorf("can't parse expiration time of bootstrap token %q: %v", secret.Name, err) | ||
} | ||
expires = &metav1.Time{Time: expTime} | ||
} | ||
|
||
// Build an usages string slice from the Secret data | ||
var usages []string | ||
for k, v := range secret.Data { | ||
// Skip all fields that don't include this prefix | ||
if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) { | ||
continue | ||
} | ||
// Skip those that don't have this usage set to true | ||
if string(v) != "true" { | ||
continue | ||
} | ||
usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix)) | ||
} | ||
// Only sort the slice if defined | ||
if usages != nil { | ||
sort.Strings(usages) | ||
} | ||
|
||
// Get the extra groups information from the Secret | ||
// It's done this way to make .Groups be nil in case there is no items, rather than an | ||
// empty slice or an empty slice with a "" string only | ||
var groups []string | ||
groupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) | ||
g := strings.Split(groupsString, ",") | ||
if len(g) > 0 && len(g[0]) > 0 { | ||
groups = g | ||
} | ||
|
||
return &BootstrapToken{ | ||
Token: bts, | ||
Description: description, | ||
Expires: expires, | ||
Usages: usages, | ||
Groups: groups, | ||
}, nil | ||
} | ||
|
||
// getSecretString returns the string value for the given key in the specified Secret | ||
func getSecretString(secret *v1.Secret, key string) string { | ||
if secret.Data == nil { | ||
return "" | ||
} | ||
if val, ok := secret.Data[key]; ok { | ||
return string(val) | ||
} | ||
return "" | ||
} |
Oops, something went wrong.