k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go (about) 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 }