...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v1
    18  
    19  import (
    20  	"fmt"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/pkg/errors"
    26  
    27  	v1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	bootstrapapi "k8s.io/cluster-bootstrap/token/api"
    30  	bootstraputil "k8s.io/cluster-bootstrap/token/util"
    31  	bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
    32  )
    33  
    34  const (
    35  	// When a token is matched with 'BootstrapTokenPattern', the size of validated substrings returned by
    36  	// regexp functions which contains 'Submatch' in their names will be 3.
    37  	// Submatch 0 is the match of the entire expression, submatch 1 is
    38  	// the match of the first parenthesized subexpression, and so on.
    39  	// e.g.:
    40  	// result := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch("abcdef.1234567890123456")
    41  	// result == []string{"abcdef.1234567890123456","abcdef","1234567890123456"}
    42  	// len(result) == 3
    43  	validatedSubstringsSize = 3
    44  )
    45  
    46  // MarshalJSON implements the json.Marshaler interface.
    47  func (bts BootstrapTokenString) MarshalJSON() ([]byte, error) {
    48  	return []byte(fmt.Sprintf(`"%s"`, bts.String())), nil
    49  }
    50  
    51  // UnmarshalJSON implements the json.Unmarshaller interface.
    52  func (bts *BootstrapTokenString) UnmarshalJSON(b []byte) error {
    53  	// If the token is represented as "", just return quickly without an error
    54  	if len(b) == 0 {
    55  		return nil
    56  	}
    57  
    58  	// Remove unnecessary " characters coming from the JSON parser
    59  	token := strings.Replace(string(b), `"`, ``, -1)
    60  	// Convert the string Token to a BootstrapTokenString object
    61  	newbts, err := NewBootstrapTokenString(token)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	bts.ID = newbts.ID
    66  	bts.Secret = newbts.Secret
    67  	return nil
    68  }
    69  
    70  // String returns the string representation of the BootstrapTokenString
    71  func (bts BootstrapTokenString) String() string {
    72  	if len(bts.ID) > 0 && len(bts.Secret) > 0 {
    73  		return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret)
    74  	}
    75  	return ""
    76  }
    77  
    78  // NewBootstrapTokenString converts the given Bootstrap Token as a string
    79  // to the BootstrapTokenString object used for serialization/deserialization
    80  // and internal usage. It also automatically validates that the given token
    81  // is of the right format
    82  func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) {
    83  	substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token)
    84  	if len(substrs) != validatedSubstringsSize {
    85  		return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern)
    86  	}
    87  
    88  	return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil
    89  }
    90  
    91  // NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString
    92  // that allows the caller to specify the ID and Secret separately
    93  func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) {
    94  	return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret))
    95  }
    96  
    97  // BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that
    98  // may be submitted to the API Server in order to be stored.
    99  func BootstrapTokenToSecret(bt *BootstrapToken) *v1.Secret {
   100  	return &v1.Secret{
   101  		ObjectMeta: metav1.ObjectMeta{
   102  			Name:      bootstraputil.BootstrapTokenSecretName(bt.Token.ID),
   103  			Namespace: metav1.NamespaceSystem,
   104  		},
   105  		Type: bootstrapapi.SecretTypeBootstrapToken,
   106  		Data: encodeTokenSecretData(bt, time.Now()),
   107  	}
   108  }
   109  
   110  // encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret
   111  // now is passed in order to be able to used in unit testing
   112  func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte {
   113  	data := map[string][]byte{
   114  		bootstrapapi.BootstrapTokenIDKey:     []byte(token.Token.ID),
   115  		bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret),
   116  	}
   117  
   118  	if len(token.Description) > 0 {
   119  		data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description)
   120  	}
   121  
   122  	// If for some strange reason both token.TTL and token.Expires would be set
   123  	// (they are mutually exclusive in validation so this shouldn't be the case),
   124  	// token.Expires has higher priority, as can be seen in the logic here.
   125  	if token.Expires != nil {
   126  		// Format the expiration date accordingly
   127  		// TODO: This maybe should be a helper function in bootstraputil?
   128  		expirationString := token.Expires.Time.UTC().Format(time.RFC3339)
   129  		data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString)
   130  
   131  	} else if token.TTL != nil && token.TTL.Duration > 0 {
   132  		// Only if .Expires is unset, TTL might have an effect
   133  		// Get the current time, add the specified duration, and format it accordingly
   134  		expirationString := now.Add(token.TTL.Duration).UTC().Format(time.RFC3339)
   135  		data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString)
   136  	}
   137  
   138  	for _, usage := range token.Usages {
   139  		data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true")
   140  	}
   141  
   142  	if len(token.Groups) > 0 {
   143  		data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ","))
   144  	}
   145  	return data
   146  }
   147  
   148  // BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret
   149  func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
   150  	// Get the Token ID field from the Secret data
   151  	tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
   152  	if len(tokenID) == 0 {
   153  		return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name)
   154  	}
   155  
   156  	// Enforce the right naming convention
   157  	if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) {
   158  		return nil, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q",
   159  			bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID))
   160  	}
   161  
   162  	tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
   163  	if len(tokenSecret) == 0 {
   164  		return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name)
   165  	}
   166  
   167  	// Create the BootstrapTokenString object based on the ID and Secret
   168  	bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret)
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed")
   171  	}
   172  
   173  	// Get the description (if any) from the Secret
   174  	description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey)
   175  
   176  	// Expiration time is optional, if not specified this implies the token
   177  	// never expires.
   178  	secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey)
   179  	var expires *metav1.Time
   180  	if len(secretExpiration) > 0 {
   181  		expTime, err := time.Parse(time.RFC3339, secretExpiration)
   182  		if err != nil {
   183  			return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name)
   184  		}
   185  		expires = &metav1.Time{Time: expTime}
   186  	}
   187  
   188  	// Build an usages string slice from the Secret data
   189  	var usages []string
   190  	for k, v := range secret.Data {
   191  		// Skip all fields that don't include this prefix
   192  		if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) {
   193  			continue
   194  		}
   195  		// Skip those that don't have this usage set to true
   196  		if string(v) != "true" {
   197  			continue
   198  		}
   199  		usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix))
   200  	}
   201  	// Only sort the slice if defined
   202  	if usages != nil {
   203  		sort.Strings(usages)
   204  	}
   205  
   206  	// Get the extra groups information from the Secret
   207  	// It's done this way to make .Groups be nil in case there is no items, rather than an
   208  	// empty slice or an empty slice with a "" string only
   209  	var groups []string
   210  	groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
   211  	g := strings.Split(groupsString, ",")
   212  	if len(g) > 0 && len(g[0]) > 0 {
   213  		groups = g
   214  	}
   215  
   216  	return &BootstrapToken{
   217  		Token:       bts,
   218  		Description: description,
   219  		Expires:     expires,
   220  		Usages:      usages,
   221  		Groups:      groups,
   222  	}, nil
   223  }
   224  

View as plain text