mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
Signed-off-by: haoyun <yun.hao@daocloud.io> Co-authored-by: Lubomir I. Ivanov <neolit123@gmail.com>
224 lines
8.3 KiB
Go
224 lines
8.3 KiB
Go
/*
|
|
Copyright 2021 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 v1
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
|
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
|
|
)
|
|
|
|
const (
|
|
// When a token is matched with 'BootstrapTokenPattern', the size of validated substrings returned by
|
|
// regexp functions which contains 'Submatch' in their names will be 3.
|
|
// Submatch 0 is the match of the entire expression, submatch 1 is
|
|
// the match of the first parenthesized subexpression, and so on.
|
|
// e.g.:
|
|
// result := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch("abcdef.1234567890123456")
|
|
// result == []string{"abcdef.1234567890123456","abcdef","1234567890123456"}
|
|
// len(result) == 3
|
|
validatedSubstringsSize = 3
|
|
)
|
|
|
|
// MarshalJSON implements the json.Marshaler interface.
|
|
func (bts BootstrapTokenString) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf(`"%s"`, bts.String())), nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaller interface.
|
|
func (bts *BootstrapTokenString) UnmarshalJSON(b []byte) error {
|
|
// If the token is represented as "", just return quickly without an error
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Remove unnecessary " characters coming from the JSON parser
|
|
token := strings.Replace(string(b), `"`, ``, -1)
|
|
// Convert the string Token to a BootstrapTokenString object
|
|
newbts, err := NewBootstrapTokenString(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bts.ID = newbts.ID
|
|
bts.Secret = newbts.Secret
|
|
return nil
|
|
}
|
|
|
|
// String returns the string representation of the BootstrapTokenString
|
|
func (bts BootstrapTokenString) String() string {
|
|
if len(bts.ID) > 0 && len(bts.Secret) > 0 {
|
|
return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NewBootstrapTokenString converts the given Bootstrap Token as a string
|
|
// to the BootstrapTokenString object used for serialization/deserialization
|
|
// and internal usage. It also automatically validates that the given token
|
|
// is of the right format
|
|
func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) {
|
|
substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token)
|
|
if len(substrs) != validatedSubstringsSize {
|
|
return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern)
|
|
}
|
|
|
|
return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil
|
|
}
|
|
|
|
// NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString
|
|
// that allows the caller to specify the ID and Secret separately
|
|
func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) {
|
|
return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret))
|
|
}
|
|
|
|
// BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that
|
|
// may be submitted to the API Server in order to be stored.
|
|
func BootstrapTokenToSecret(bt *BootstrapToken) *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 exclusive 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.UTC().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).UTC().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 := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
|
|
if len(tokenID) == 0 {
|
|
return nil, errors.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, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q",
|
|
bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID))
|
|
}
|
|
|
|
tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
|
|
if len(tokenSecret) == 0 {
|
|
return nil, errors.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, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed")
|
|
}
|
|
|
|
// Get the description (if any) from the Secret
|
|
description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey)
|
|
|
|
// Expiration time is optional, if not specified this implies the token
|
|
// never expires.
|
|
secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey)
|
|
var expires *metav1.Time
|
|
if len(secretExpiration) > 0 {
|
|
expTime, err := time.Parse(time.RFC3339, secretExpiration)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name)
|
|
}
|
|
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 := bootstrapsecretutil.GetData(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
|
|
}
|