| /* |
| 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 ( |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/pkg/errors" |
| |
| "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" |
| ) |
| |
| // 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, 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 := getSecretString(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 := 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, 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 := 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 "" |
| } |