mirror of https://github.com/k3s-io/k3s
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
6.7 KiB
173 lines
6.7 KiB
package kubeadm |
|
|
|
import ( |
|
"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" |
|
) |
|
|
|
// kubeadm bootstrap token utilities cribbed from: |
|
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go |
|
// Copying these instead of importing from kubeadm saves about 4mb of binary size. |
|
|
|
// 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) != 3 { |
|
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 |
|
}
|
|
|