mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
kubeadm: introduce apis/bootstraptoken/v1
Package bootstraptoken contains an API and utilities wrapping the "bootstrap.kubernetes.io/token" Secret type to ease its usage in kubeadm. The API is released as v1, since these utilities have been part of a GA workflow for 10+ releases. The "bootstrap.kubernetes.io/token" Secret type is also GA.
This commit is contained in:
parent
3334703eb2
commit
5b7bda90c0
21
cmd/kubeadm/app/apis/bootstraptoken/doc.go
Normal file
21
cmd/kubeadm/app/apis/bootstraptoken/doc.go
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// +groupName=bootstraptoken.kubeadm.k8s.io
|
||||
|
||||
// Package bootstraptoken contains an API and utilities wrapping the
|
||||
// "bootstrap.kubernetes.io/token" Secret type to ease its usage in kubeadm.
|
||||
package bootstraptoken // import "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken"
|
58
cmd/kubeadm/app/apis/bootstraptoken/v1/types.go
Normal file
58
cmd/kubeadm/app/apis/bootstraptoken/v1/types.go
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster
|
||||
// +k8s:deepcopy-gen=true
|
||||
type BootstrapToken struct {
|
||||
// Token is used for establishing bidirectional trust between nodes and control-planes.
|
||||
// Used for joining nodes in the cluster.
|
||||
Token *BootstrapTokenString `json:"token" datapolicy:"token"`
|
||||
// Description sets a human-friendly message why this token exists and what it's used
|
||||
// for, so other administrators can know its purpose.
|
||||
// +optional
|
||||
Description string `json:"description,omitempty"`
|
||||
// TTL defines the time to live for this token. Defaults to 24h.
|
||||
// Expires and TTL are mutually exclusive.
|
||||
// +optional
|
||||
TTL *metav1.Duration `json:"ttl,omitempty"`
|
||||
// Expires specifies the timestamp when this token expires. Defaults to being set
|
||||
// dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive.
|
||||
// +optional
|
||||
Expires *metav1.Time `json:"expires,omitempty"`
|
||||
// Usages describes the ways in which this token can be used. Can by default be used
|
||||
// for establishing bidirectional trust, but that can be changed here.
|
||||
// +optional
|
||||
Usages []string `json:"usages,omitempty"`
|
||||
// Groups specifies the extra groups that this token will authenticate as when/if
|
||||
// used for authentication
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used
|
||||
// for both validation of the practically of the API server from a joining node's point
|
||||
// of view and as an authentication method for the node in the bootstrap phase of
|
||||
// "kubeadm join". This token is and should be short-lived
|
||||
type BootstrapTokenString struct {
|
||||
ID string `json:"-"`
|
||||
Secret string `json:"-" datapolicy:"token"`
|
||||
}
|
212
cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go
Normal file
212
cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// TODO: Add a constant for the 3 value here, and explain better why it's needed (other than because how the regexp parsin works)
|
||||
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
|
||||
}
|
680
cmd/kubeadm/app/apis/bootstraptoken/v1/utils_test.go
Normal file
680
cmd/kubeadm/app/apis/bootstraptoken/v1/utils_test.go
Normal file
@ -0,0 +1,680 @@
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
var tests = []struct {
|
||||
bts BootstrapTokenString
|
||||
expected string
|
||||
}{
|
||||
{BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, `"abcdef.abcdef0123456789"`},
|
||||
{BootstrapTokenString{ID: "foo", Secret: "bar"}, `"foo.bar"`},
|
||||
{BootstrapTokenString{ID: "h", Secret: "b"}, `"h.b"`},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.bts.ID, func(t *testing.T) {
|
||||
b, err := json.Marshal(rt.bts)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal returned an unexpected error: %v", err)
|
||||
}
|
||||
if string(b) != rt.expected {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenString.MarshalJSON:\n\texpected: %s\n\t actual: %s",
|
||||
rt.expected,
|
||||
string(b),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalJSON(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input string
|
||||
bts *BootstrapTokenString
|
||||
expectedError bool
|
||||
}{
|
||||
{`"f.s"`, &BootstrapTokenString{}, true},
|
||||
{`"abcdef."`, &BootstrapTokenString{}, true},
|
||||
{`"abcdef:abcdef0123456789"`, &BootstrapTokenString{}, true},
|
||||
{`abcdef.abcdef0123456789`, &BootstrapTokenString{}, true},
|
||||
{`"abcdef.abcdef0123456789`, &BootstrapTokenString{}, true},
|
||||
{`"abcdef.ABCDEF0123456789"`, &BootstrapTokenString{}, true},
|
||||
{`"abcdef.abcdef0123456789"`, &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, false},
|
||||
{`"123456.aabbccddeeffgghh"`, &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}, false},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.input, func(t *testing.T) {
|
||||
newbts := &BootstrapTokenString{}
|
||||
err := json.Unmarshal([]byte(rt.input), newbts)
|
||||
if (err != nil) != rt.expectedError {
|
||||
t.Errorf("failed BootstrapTokenString.UnmarshalJSON:\n\texpected error: %t\n\t actual error: %v", rt.expectedError, err)
|
||||
} else if !reflect.DeepEqual(rt.bts, newbts) {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenString.UnmarshalJSON:\n\texpected: %v\n\t actual: %v",
|
||||
rt.bts,
|
||||
newbts,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONRoundtrip(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input string
|
||||
bts *BootstrapTokenString
|
||||
}{
|
||||
{`"abcdef.abcdef0123456789"`, nil},
|
||||
{"", &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.input, func(t *testing.T) {
|
||||
if err := roundtrip(rt.input, rt.bts); err != nil {
|
||||
t.Errorf("failed BootstrapTokenString JSON roundtrip with error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func roundtrip(input string, bts *BootstrapTokenString) error {
|
||||
var b []byte
|
||||
var err error
|
||||
newbts := &BootstrapTokenString{}
|
||||
// If string input was specified, roundtrip like this: string -> (unmarshal) -> object -> (marshal) -> string
|
||||
if len(input) > 0 {
|
||||
if err := json.Unmarshal([]byte(input), newbts); err != nil {
|
||||
return errors.Wrap(err, "expected no unmarshal error, got error")
|
||||
}
|
||||
if b, err = json.Marshal(newbts); err != nil {
|
||||
return errors.Wrap(err, "expected no marshal error, got error")
|
||||
}
|
||||
if input != string(b) {
|
||||
return errors.Errorf(
|
||||
"expected token: %s\n\t actual: %s",
|
||||
input,
|
||||
string(b),
|
||||
)
|
||||
}
|
||||
} else { // Otherwise, roundtrip like this: object -> (marshal) -> string -> (unmarshal) -> object
|
||||
if b, err = json.Marshal(bts); err != nil {
|
||||
return errors.Wrap(err, "expected no marshal error, got error")
|
||||
}
|
||||
if err := json.Unmarshal(b, newbts); err != nil {
|
||||
return errors.Wrap(err, "expected no unmarshal error, got error")
|
||||
}
|
||||
if !reflect.DeepEqual(bts, newbts) {
|
||||
return errors.Errorf(
|
||||
"expected object: %v\n\t actual: %v",
|
||||
bts,
|
||||
newbts,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTokenFromIDAndSecret(t *testing.T) {
|
||||
var tests = []struct {
|
||||
bts BootstrapTokenString
|
||||
expected string
|
||||
}{
|
||||
{BootstrapTokenString{ID: "foo", Secret: "bar"}, "foo.bar"},
|
||||
{BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, "abcdef.abcdef0123456789"},
|
||||
{BootstrapTokenString{ID: "h", Secret: "b"}, "h.b"},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.bts.ID, func(t *testing.T) {
|
||||
actual := rt.bts.String()
|
||||
if actual != rt.expected {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenString.String():\n\texpected: %s\n\t actual: %s",
|
||||
rt.expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBootstrapTokenString(t *testing.T) {
|
||||
var tests = []struct {
|
||||
token string
|
||||
expectedError bool
|
||||
bts *BootstrapTokenString
|
||||
}{
|
||||
{token: "", expectedError: true, bts: nil},
|
||||
{token: ".", expectedError: true, bts: nil},
|
||||
{token: "1234567890123456789012", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{token: "12345.1234567890123456", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{token: ".1234567890123456", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{token: "123456.", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{token: "123456:1234567890.123456", expectedError: true, bts: nil}, // invalid separation
|
||||
{token: "abcdef:1234567890123456", expectedError: true, bts: nil}, // invalid separation
|
||||
{token: "Abcdef.1234567890123456", expectedError: true, bts: nil}, // invalid token id
|
||||
{token: "123456.AABBCCDDEEFFGGHH", expectedError: true, bts: nil}, // invalid token secret
|
||||
{token: "123456.AABBCCD-EEFFGGHH", expectedError: true, bts: nil}, // invalid character
|
||||
{token: "abc*ef.1234567890123456", expectedError: true, bts: nil}, // invalid character
|
||||
{token: "abcdef.1234567890123456", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "1234567890123456"}},
|
||||
{token: "123456.aabbccddeeffgghh", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}},
|
||||
{token: "abcdef.abcdef0123456789", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}},
|
||||
{token: "123456.1234560123456789", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "1234560123456789"}},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.token, func(t *testing.T) {
|
||||
actual, err := NewBootstrapTokenString(rt.token)
|
||||
if (err != nil) != rt.expectedError {
|
||||
t.Errorf(
|
||||
"failed NewBootstrapTokenString for the token %q\n\texpected error: %t\n\t actual error: %v",
|
||||
rt.token,
|
||||
rt.expectedError,
|
||||
err,
|
||||
)
|
||||
} else if !reflect.DeepEqual(actual, rt.bts) {
|
||||
t.Errorf(
|
||||
"failed NewBootstrapTokenString for the token %q\n\texpected: %v\n\t actual: %v",
|
||||
rt.token,
|
||||
rt.bts,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBootstrapTokenStringFromIDAndSecret(t *testing.T) {
|
||||
var tests = []struct {
|
||||
id, secret string
|
||||
expectedError bool
|
||||
bts *BootstrapTokenString
|
||||
}{
|
||||
{id: "", secret: "", expectedError: true, bts: nil},
|
||||
{id: "1234567890123456789012", secret: "", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{id: "12345", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{id: "", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{id: "123456", secret: "", expectedError: true, bts: nil}, // invalid parcel size
|
||||
{id: "Abcdef", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid token id
|
||||
{id: "123456", secret: "AABBCCDDEEFFGGHH", expectedError: true, bts: nil}, // invalid token secret
|
||||
{id: "123456", secret: "AABBCCD-EEFFGGHH", expectedError: true, bts: nil}, // invalid character
|
||||
{id: "abc*ef", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid character
|
||||
{id: "abcdef", secret: "1234567890123456", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "1234567890123456"}},
|
||||
{id: "123456", secret: "aabbccddeeffgghh", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}},
|
||||
{id: "abcdef", secret: "abcdef0123456789", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}},
|
||||
{id: "123456", secret: "1234560123456789", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "1234560123456789"}},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.id, func(t *testing.T) {
|
||||
actual, err := NewBootstrapTokenStringFromIDAndSecret(rt.id, rt.secret)
|
||||
if (err != nil) != rt.expectedError {
|
||||
t.Errorf(
|
||||
"failed NewBootstrapTokenStringFromIDAndSecret for the token with id %q and secret %q\n\texpected error: %t\n\t actual error: %v",
|
||||
rt.id,
|
||||
rt.secret,
|
||||
rt.expectedError,
|
||||
err,
|
||||
)
|
||||
} else if !reflect.DeepEqual(actual, rt.bts) {
|
||||
t.Errorf(
|
||||
"failed NewBootstrapTokenStringFromIDAndSecret for the token with id %q and secret %q\n\texpected: %v\n\t actual: %v",
|
||||
rt.id,
|
||||
rt.secret,
|
||||
rt.bts,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This timestamp is used as the reference value when computing expiration dates based on TTLs in these unit tests
|
||||
var refTime = time.Date(1970, time.January, 1, 1, 1, 1, 0, time.UTC)
|
||||
|
||||
func TestBootstrapTokenToSecret(t *testing.T) {
|
||||
var tests = []struct {
|
||||
bt *BootstrapToken
|
||||
secret *v1.Secret
|
||||
}{
|
||||
{
|
||||
&BootstrapToken{ // all together
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
Usages: []string{"signing", "authentication"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bootstrap-token-abcdef",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Type: v1.SecretType("bootstrap.kubernetes.io/token"),
|
||||
Data: map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"description": []byte("foo"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
"auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.bt.Token.ID, func(t *testing.T) {
|
||||
actual := BootstrapTokenToSecret(rt.bt)
|
||||
if !reflect.DeepEqual(actual, rt.secret) {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenToSecret():\n\texpected: %v\n\t actual: %v",
|
||||
rt.secret,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapTokenToSecretRoundtrip(t *testing.T) {
|
||||
var tests = []struct {
|
||||
bt *BootstrapToken
|
||||
}{
|
||||
{
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.bt.Token.ID, func(t *testing.T) {
|
||||
actual, err := BootstrapTokenFromSecret(BootstrapTokenToSecret(rt.bt))
|
||||
if err != nil {
|
||||
t.Errorf("failed BootstrapToken to Secret roundtrip with error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, rt.bt) {
|
||||
t.Errorf(
|
||||
"failed BootstrapToken to Secret roundtrip:\n\texpected: %v\n\t actual: %v",
|
||||
rt.bt,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeTokenSecretData(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
bt *BootstrapToken
|
||||
data map[string][]byte
|
||||
}{
|
||||
{
|
||||
"the minimum amount of information needed to be specified",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds description",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"description": []byte("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds ttl",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
TTL: &metav1.Duration{
|
||||
Duration: mustParseDuration("2h", t),
|
||||
},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"expiration": []byte(refTime.Add(mustParseDuration("2h", t)).Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds expiration",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds ttl and expiration, should favor expiration",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
TTL: &metav1.Duration{
|
||||
Duration: mustParseDuration("2h", t),
|
||||
},
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds usages",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"adds groups",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"all together",
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
TTL: &metav1.Duration{
|
||||
Duration: mustParseDuration("2h", t),
|
||||
},
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"description": []byte("foo"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
"auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.name, func(t *testing.T) {
|
||||
actual := encodeTokenSecretData(rt.bt, refTime)
|
||||
if !reflect.DeepEqual(actual, rt.data) {
|
||||
t.Errorf(
|
||||
"failed encodeTokenSecretData:\n\texpected: %v\n\t actual: %v",
|
||||
rt.data,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseDuration(durationStr string, t *testing.T) time.Duration {
|
||||
d, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't parse duration %q: %v", durationStr, err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func TestBootstrapTokenFromSecret(t *testing.T) {
|
||||
var tests = []struct {
|
||||
desc string
|
||||
name string
|
||||
data map[string][]byte
|
||||
bt *BootstrapToken
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
"minimum information",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid token id",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdeF"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid secret naming",
|
||||
"foo",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid token secret",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("ABCDEF0123456789"),
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"adds description",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"description": []byte("foo"),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"adds expiration",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid expiration",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"expiration": []byte("invalid date"),
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"adds usages",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"should ignore usages that aren't set to true",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
"usage-bootstrap-foo": []byte("false"),
|
||||
"usage-bootstrap-bar": []byte(""),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"adds groups",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"all fields set",
|
||||
"bootstrap-token-abcdef",
|
||||
map[string][]byte{
|
||||
"token-id": []byte("abcdef"),
|
||||
"token-secret": []byte("abcdef0123456789"),
|
||||
"description": []byte("foo"),
|
||||
"expiration": []byte(refTime.Format(time.RFC3339)),
|
||||
"usage-bootstrap-signing": []byte("true"),
|
||||
"usage-bootstrap-authentication": []byte("true"),
|
||||
"auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"),
|
||||
},
|
||||
&BootstrapToken{
|
||||
Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"},
|
||||
Description: "foo",
|
||||
Expires: &metav1.Time{
|
||||
Time: refTime,
|
||||
},
|
||||
Usages: []string{"authentication", "signing"},
|
||||
Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, rt := range tests {
|
||||
t.Run(rt.desc, func(t *testing.T) {
|
||||
actual, err := BootstrapTokenFromSecret(&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: rt.name,
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Type: v1.SecretType("bootstrap.kubernetes.io/token"),
|
||||
Data: rt.data,
|
||||
})
|
||||
if (err != nil) != rt.expectedError {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenFromSecret\n\texpected error: %t\n\t actual error: %v",
|
||||
rt.expectedError,
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
if actual == nil && rt.bt == nil {
|
||||
// if both pointers are nil, it's okay, just continue
|
||||
return
|
||||
}
|
||||
// If one of the pointers is defined but the other isn't, throw error. If both pointers are defined but unequal, throw error
|
||||
if (actual == nil && rt.bt != nil) || (actual != nil && rt.bt == nil) || !reflect.DeepEqual(*actual, *rt.bt) {
|
||||
t.Errorf(
|
||||
"failed BootstrapTokenFromSecret\n\texpected: %s\n\t actual: %s",
|
||||
jsonMarshal(rt.bt),
|
||||
jsonMarshal(actual),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func jsonMarshal(bt *BootstrapToken) string {
|
||||
b, _ := json.Marshal(*bt)
|
||||
return string(b)
|
||||
}
|
65
cmd/kubeadm/app/apis/bootstraptoken/v1/zz_generated.deepcopy.go
generated
Normal file
65
cmd/kubeadm/app/apis/bootstraptoken/v1/zz_generated.deepcopy.go
generated
Normal file
@ -0,0 +1,65 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BootstrapToken) DeepCopyInto(out *BootstrapToken) {
|
||||
*out = *in
|
||||
if in.Token != nil {
|
||||
in, out := &in.Token, &out.Token
|
||||
*out = new(BootstrapTokenString)
|
||||
**out = **in
|
||||
}
|
||||
if in.TTL != nil {
|
||||
in, out := &in.TTL, &out.TTL
|
||||
*out = new(metav1.Duration)
|
||||
**out = **in
|
||||
}
|
||||
if in.Expires != nil {
|
||||
in, out := &in.Expires, &out.Expires
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Usages != nil {
|
||||
in, out := &in.Usages, &out.Usages
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Groups != nil {
|
||||
in, out := &in.Groups, &out.Groups
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapToken.
|
||||
func (in *BootstrapToken) DeepCopy() *BootstrapToken {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BootstrapToken)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
Loading…
Reference in New Issue
Block a user