diff --git a/cmd/api/api.go b/cmd/api/api.go index 7c7815bb9817..5685556c6f6d 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -17,6 +17,7 @@ limitations under the License. package api import ( + "bytes" "context" "crypto/tls" "encoding/json" @@ -26,6 +27,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "time" @@ -37,8 +39,11 @@ import ( "github.com/k0sproject/k0s/pkg/etcd" kubeutil "github.com/k0sproject/k0s/pkg/kubernetes" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + tokenutil "k8s.io/cluster-bootstrap/token/util" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -90,12 +95,12 @@ func (c *command) start() (err error) { // Only mount the etcd handler if we're running on internal etcd storage // by default the mux will return 404 back which the caller should handle mux.Handle(prefix+"/etcd/members", mw.AllowMethods(http.MethodPost)( - c.authMiddleware(c.etcdHandler(), "usage-controller-join"))) + c.authMiddleware(c.etcdHandler(), "controller-join"))) } if storage.IsJoinable() { mux.Handle(prefix+"/ca", mw.AllowMethods(http.MethodGet)( - c.authMiddleware(c.caHandler(), "usage-controller-join"))) + c.authMiddleware(c.caHandler(), "controller-join"))) } srv := &http.Server{ @@ -216,54 +221,54 @@ func (c *command) caHandler() http.Handler { // We need to validate: // - that we find a secret with the ID // - that the token matches whats inside the secret -func (c *command) isValidToken(ctx context.Context, token string, usage string) bool { - parts := strings.Split(token, ".") - logrus.Debugf("token parts: %v", parts) - if len(parts) != 2 { +func (c *command) isValidToken(ctx context.Context, rawTokenString string, usage string) bool { + tokenString, err := bootstraptokenv1.NewBootstrapTokenString(rawTokenString) + if err != nil { return false } - secretName := "bootstrap-token-" + parts[0] + secretName := tokenutil.BootstrapTokenSecretName(tokenString.ID) secret, err := c.client.CoreV1().Secrets("kube-system").Get(ctx, secretName, metav1.GetOptions{}) if err != nil { - logrus.Errorf("failed to get bootstrap token: %s", err.Error()) + if !apierrors.IsNotFound(err) { + logrus.WithError(err).Error("Failed to get bootstrap token with ID ", tokenString.ID) + } return false } - if string(secret.Data["token-secret"]) != parts[1] { + token, err := bootstraptokenv1.BootstrapTokenFromSecret(secret) + if err != nil { + logrus.WithError(err).Errorf("Bootstrap token with ID %s is malformed", tokenString.ID) return false } - usageValue, ok := secret.Data[usage] - if !ok || string(usageValue) != "true" { + if token.Expires != nil && !time.Now().Before(token.Expires.Time) { return false } - return true + if *token.Token != *tokenString { + return false + } + + switch { + case slices.Contains(token.Usages, usage): + return true // usage found + case bytes.Equal(secret.Data["usage-"+usage], []byte("true")): + return true // usage found in its legacy form + default: + return false // usage not found + } } func (c *command) authMiddleware(next http.Handler, usage string) http.Handler { unauthorizedErr := errors.New("go away") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - if auth == "" { - sendError(unauthorizedErr, w, http.StatusUnauthorized) - return - } - - parts := strings.Split(auth, "Bearer ") - if len(parts) == 2 { - token := parts[1] - if !c.isValidToken(r.Context(), token, usage) { - sendError(unauthorizedErr, w, http.StatusUnauthorized) - return - } + token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") + if ok && c.isValidToken(r.Context(), token, usage) { + next.ServeHTTP(w, r) } else { sendError(unauthorizedErr, w, http.StatusUnauthorized) - return } - - next.ServeHTTP(w, r) }) } diff --git a/cmd/token/list.go b/cmd/token/list.go index 493d8d6318f7..8239cf21b1cc 100644 --- a/cmd/token/list.go +++ b/cmd/token/list.go @@ -47,7 +47,7 @@ func tokenListCmd() *cobra.Command { return err } - tokens, err := manager.List(cmd.Context(), listTokenRole) + tokens, err := manager.List(cmd.Context()) if err != nil { return err } @@ -70,7 +70,9 @@ func tokenListCmd() *cobra.Command { table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) for _, t := range tokens { - table.Append(t.ToArray()) + if listTokenRole == "" || listTokenRole == t.Role { + table.Append(t.ToArray()) + } } table.Render() diff --git a/cmd/token/preshared.go b/cmd/token/preshared.go index 8bb62d778b70..6b81ebcb085c 100644 --- a/cmd/token/preshared.go +++ b/cmd/token/preshared.go @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/client-go/kubernetes/scheme" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" "github.com/k0sproject/k0s/internal/pkg/file" "github.com/k0sproject/k0s/pkg/config" @@ -87,10 +88,10 @@ func preSharedCmd() *cobra.Command { return cmd } -func createSecret(role string, validity time.Duration, outDir string) (string, error) { +func createSecret(role string, validity time.Duration, outDir string) (*bootstraptokenv1.BootstrapTokenString, error) { secret, token, err := token.RandomBootstrapSecret(role, validity) if err != nil { - return "", fmt.Errorf("failed to generate bootstrap secret: %w", err) + return nil, fmt.Errorf("failed to generate bootstrap secret: %w", err) } if err := file.WriteAtomically(filepath.Join(outDir, secret.Name+".yaml"), 0640, func(unbuffered io.Writer) error { @@ -102,13 +103,13 @@ func createSecret(role string, validity time.Duration, outDir string) (string, e } return w.Flush() }); err != nil { - return "", fmt.Errorf("failed to save bootstrap secret: %w", err) + return nil, fmt.Errorf("failed to save bootstrap secret: %w", err) } return token, nil } -func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error { +func createKubeConfig(tok *bootstraptokenv1.BootstrapTokenString, role, joinURL, certPath, outDir string) error { caCert, err := os.ReadFile(certPath) if err != nil { return fmt.Errorf("error reading certificate: %w", err) @@ -123,7 +124,7 @@ func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error default: return fmt.Errorf("unknown role: %s", role) } - kubeconfig, err := token.GenerateKubeconfig(joinURL, caCert, userName, tokenString) + kubeconfig, err := token.GenerateKubeconfig(joinURL, caCert, userName, tok) if err != nil { return fmt.Errorf("error generating kubeconfig: %w", err) } @@ -133,7 +134,7 @@ func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error return fmt.Errorf("error encoding token: %w", err) } - err = file.WriteContentAtomically(filepath.Join(outDir, "token_"+tokenString), []byte(encodedToken), 0640) + err = file.WriteContentAtomically(filepath.Join(outDir, "token_"+tok.ID), []byte(encodedToken), 0640) if err != nil { return fmt.Errorf("error writing kubeconfig: %w", err) } diff --git a/go.mod b/go.mod index 06ed4f55af68..e5e58c9c7027 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( k8s.io/cli-runtime v0.31.3 k8s.io/client-go v0.31.3 k8s.io/cloud-provider v0.31.3 + k8s.io/cluster-bootstrap v0.31.3 k8s.io/component-base v0.31.3 k8s.io/component-helpers v0.31.3 k8s.io/cri-api v0.31.3 diff --git a/go.sum b/go.sum index 410979dc5e23..6249ad14296e 100644 --- a/go.sum +++ b/go.sum @@ -851,6 +851,8 @@ k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= k8s.io/cloud-provider v0.31.3 h1:7C3CHQUUwnv/HWWVIaibZH06iPg663RYQ6C6Zy4FnO8= k8s.io/cloud-provider v0.31.3/go.mod h1:c7csKppoVb9Ej6upJ28AvHy4B3BtlRMzXfgezsDdPKw= +k8s.io/cluster-bootstrap v0.31.3 h1:O1Yxk1bLaxZvmQCXLaJjj5iJD+lVMfJdRUuKgbUHPlA= +k8s.io/cluster-bootstrap v0.31.3/go.mod h1:TI6TCsQQB4FfcryWgNO3SLXSKWBqHjx4DfyqSFwixj8= k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ= k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU= k8s.io/component-helpers v0.31.3 h1:0zGPD2PrekhFWgmz85XxlMEl7dfhlKC1tERZDe3onQc= diff --git a/internal/autopilot/pkg/random/random.go b/internal/autopilot/pkg/random/random.go deleted file mode 100644 index 861d33dfb21a..000000000000 --- a/internal/autopilot/pkg/random/random.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021 k0s 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 random - -import ( - "crypto/rand" -) - -var letters = "abcdefghijklmnopqrstuvwxyz0123456789" - -// String generates a random string with given length -func String(length int) string { - - bytes := make([]byte, length) - if _, err := rand.Read(bytes); err != nil { - // Not much we can do on broken system - panic("random is broken: " + err.Error()) - } - - for i, b := range bytes { - bytes[i] = letters[b%byte(len(letters))] - } - return string(bytes) -} diff --git a/internal/pkg/random/random.go b/internal/pkg/random/random.go deleted file mode 100644 index fe69bfbfcc9f..000000000000 --- a/internal/pkg/random/random.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2021 k0s 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 random - -import "crypto/rand" - -var letters = "abcdefghijklmnopqrstuvwxyz0123456789" - -// String generates a random string with given length -func String(length int) string { - - bytes := make([]byte, length) - if _, err := rand.Read(bytes); err != nil { - // Not much we can do on broken system - panic("random is broken: " + err.Error()) - } - - for i, b := range bytes { - bytes[i] = letters[b%byte(len(letters))] - } - return string(bytes) -} diff --git a/pkg/token/joinclient_test.go b/pkg/token/joinclient_test.go index 0b2ef274eb9d..dbbf3133839c 100644 --- a/pkg/token/joinclient_test.go +++ b/pkg/token/joinclient_test.go @@ -31,6 +31,8 @@ import ( k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/k0sproject/k0s/pkg/token" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" + "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/initca" "github.com/stretchr/testify/assert" @@ -42,13 +44,13 @@ func TestJoinClient_GetCA(t *testing.T) { joinURL, certData := startFakeJoinServer(t, func(res http.ResponseWriter, req *http.Request) { assert.Equal(t, "/some/sub/path/v1beta1/ca", req.RequestURI) - assert.Equal(t, []string{"Bearer the-token"}, req.Header["Authorization"]) + assert.Equal(t, []string{"Bearer the-id.the-secret"}, req.Header["Authorization"]) _, err := res.Write([]byte("{}")) assert.NoError(t, err) }) joinURL.Path = "/some/sub/path" - kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), "the-token") + kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), &bootstraptokenv1.BootstrapTokenString{ID: "the-id", Secret: "the-secret"}) require.NoError(t, err) tok, err := token.JoinEncode(bytes.NewReader(kubeconfig)) require.NoError(t, err) @@ -66,7 +68,7 @@ func TestJoinClient_JoinEtcd(t *testing.T) { joinURL, certData := startFakeJoinServer(t, func(res http.ResponseWriter, req *http.Request) { assert.Equal(t, "/some/sub/path/v1beta1/etcd/members", req.RequestURI) - assert.Equal(t, []string{"Bearer the-token"}, req.Header["Authorization"]) + assert.Equal(t, []string{"Bearer the-id.the-secret"}, req.Header["Authorization"]) if body, err := io.ReadAll(req.Body); assert.NoError(t, err) { var data map[string]string @@ -83,7 +85,7 @@ func TestJoinClient_JoinEtcd(t *testing.T) { }) joinURL.Path = "/some/sub/path" - kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), "the-token") + kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, t.Name(), &bootstraptokenv1.BootstrapTokenString{ID: "the-id", Secret: "the-secret"}) require.NoError(t, err) tok, err := token.JoinEncode(bytes.NewReader(kubeconfig)) require.NoError(t, err) @@ -124,7 +126,7 @@ func TestJoinClient_Cancellation(t *testing.T) { <-req.Context().Done() // block forever }) - kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, "", "") + kubeconfig, err := token.GenerateKubeconfig(joinURL.String(), certData, "", &bootstraptokenv1.BootstrapTokenString{}) require.NoError(t, err) tok, err := token.JoinEncode(bytes.NewReader(kubeconfig)) require.NoError(t, err) diff --git a/pkg/token/kubeconfig.go b/pkg/token/kubeconfig.go index 58657340124b..6882a556c57c 100644 --- a/pkg/token/kubeconfig.go +++ b/pkg/token/kubeconfig.go @@ -29,6 +29,7 @@ import ( "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" ) const ( @@ -61,7 +62,7 @@ func CreateKubeletBootstrapToken(ctx context.Context, api *v1beta1.APISpec, k0sV return JoinEncode(bytes.NewReader(kubeconfig)) } -func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token string) ([]byte, error) { +func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token *bootstraptokenv1.BootstrapTokenString) ([]byte, error) { const k0sContextName = "k0s" kubeconfig, err := clientcmd.Write(clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{k0sContextName: { @@ -74,7 +75,7 @@ func GenerateKubeconfig(joinURL string, caCert []byte, userName string, token st }}, CurrentContext: k0sContextName, AuthInfos: map[string]*clientcmdapi.AuthInfo{userName: { - Token: token, + Token: token.String(), }}, }) return kubeconfig, err @@ -101,10 +102,10 @@ func loadCACert(k0sVars *config.CfgVars) ([]byte, error) { return caCert, nil } -func loadToken(ctx context.Context, k0sVars *config.CfgVars, role string, expiry time.Duration) (string, error) { +func loadToken(ctx context.Context, k0sVars *config.CfgVars, role string, expiry time.Duration) (*bootstraptokenv1.BootstrapTokenString, error) { manager, err := NewManager(k0sVars.AdminKubeConfigPath) if err != nil { - return "", err + return nil, err } return manager.Create(ctx, expiry, role) } diff --git a/pkg/token/kubeconfig_test.go b/pkg/token/kubeconfig_test.go index 4e67f34856ba..6cb8da87176c 100644 --- a/pkg/token/kubeconfig_test.go +++ b/pkg/token/kubeconfig_test.go @@ -19,6 +19,8 @@ package token import ( "testing" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -41,10 +43,11 @@ preferences: {} users: - name: the user user: - token: the token + token: abcdef.0123456789abcdef ` - kubeconfig, err := GenerateKubeconfig("the join URL", []byte("the cert"), "the user", "the token") + tok := bootstraptokenv1.BootstrapTokenString{ID: "abcdef", Secret: "0123456789abcdef"} + kubeconfig, err := GenerateKubeconfig("the join URL", []byte("the cert"), "the user", &tok) require.NoError(t, err) assert.Equal(t, expected, string(kubeconfig)) } diff --git a/pkg/token/manager.go b/pkg/token/manager.go index 194330040a5a..b853732bde6d 100644 --- a/pkg/token/manager.go +++ b/pkg/token/manager.go @@ -17,19 +17,23 @@ limitations under the License. package token import ( + "bytes" "context" "fmt" + "slices" "time" - "github.com/sirupsen/logrus" + k8sutil "github.com/k0sproject/k0s/pkg/kubernetes" + corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/kubernetes" + tokenutil "k8s.io/cluster-bootstrap/token/util" + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" - "github.com/k0sproject/k0s/internal/pkg/random" - k8sutil "github.com/k0sproject/k0s/pkg/kubernetes" + "github.com/sirupsen/logrus" ) type Token struct { @@ -66,87 +70,105 @@ type Manager struct { client kubernetes.Interface } -func RandomBootstrapSecret(role string, valid time.Duration) (*corev1.Secret, string, error) { - tokenID := random.String(6) - tokenSecret := random.String(16) - - s := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "bootstrap-token-" + tokenID, - Namespace: "kube-system", - }, - Type: corev1.SecretTypeBootstrapToken, - StringData: map[string]string{ - "token-id": tokenID, - "token-secret": tokenSecret, - }, +func RandomBootstrapSecret(role string, ttl time.Duration) (*corev1.Secret, *bootstraptokenv1.BootstrapTokenString, error) { + token := bootstraptokenv1.BootstrapToken{ + TTL: &metav1.Duration{Duration: ttl}, } - if valid != 0 { - exp := time.Now().Add(valid).UTC().Format(time.RFC3339) - s.StringData["expiration"] = exp - logrus.Debug("Set expiry to ", exp) + var err error + token.Token, err = generateBootstrapToken() + if err != nil { + return nil, nil, fmt.Errorf("failed to generate bootstrap token: %w", err) } + var legacyUsages []string // legacy usages for backwards compatibility + switch role { - case "worker": - s.StringData["description"] = "Worker bootstrap token generated by k0s" - s.StringData["usage-bootstrap-authentication"] = "true" - case "controller": - s.StringData["description"] = "Controller bootstrap token generated by k0s" - s.StringData["usage-controller-join"] = "true" + case RoleWorker: + token.Description = "Worker bootstrap token generated by k0s" + token.Usages = append(token.Usages, "authentication") + case RoleController: + token.Description = "Controller bootstrap token generated by k0s" + token.Usages = append(token.Usages, "controller-join") + legacyUsages = append(legacyUsages, "controller-join") default: - return nil, "", fmt.Errorf("unsupported role %q", role) + return nil, nil, fmt.Errorf("unsupported role %q", role) + } + + secret := bootstraptokenv1.BootstrapTokenToSecret(&token) + for _, usage := range legacyUsages { + // Add the usages also in their legacy form. + secret.Data["usage-"+usage] = []byte("true") } - return &s, fmt.Sprintf("%s.%s", tokenID, tokenSecret), nil + return secret, token.Token, nil } // Create creates a new bootstrap token -func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) (string, error) { +func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) (*bootstraptokenv1.BootstrapTokenString, error) { secret, token, err := RandomBootstrapSecret(role, valid) if err != nil { - return "", err + return nil, err } - _, err = m.client.CoreV1().Secrets("kube-system").Create(ctx, secret, metav1.CreateOptions{}) + _, err = m.client.CoreV1().Secrets(metav1.NamespaceSystem).Create(ctx, secret, metav1.CreateOptions{}) if err != nil { - return "", err + return nil, err } return token, nil } -// List returns all the join tokens for given role. If role == "" then it returns all join tokens -func (m *Manager) List(ctx context.Context, role string) ([]Token, error) { - tokenList, err := m.client.CoreV1().Secrets("kube-system").List(ctx, metav1.ListOptions{ +// List returns all the join tokens. +func (m *Manager) List(ctx context.Context) (tokens []Token, _ error) { + secrets, err := m.client.CoreV1().Secrets(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{ FieldSelector: fields.OneTermEqualSelector("type", string(corev1.SecretTypeBootstrapToken)).String(), }) if err != nil { return nil, err } - tokens := make([]Token, 0, len(tokenList.Items)) - for _, t := range tokenList.Items { - r := "worker" - if string(t.Data["usage-controller-join"]) == "true" { - r = "controller" + for _, secret := range secrets.Items { + parsed, err := bootstraptokenv1.BootstrapTokenFromSecret(&secret) + if err != nil { + continue // ignore invalid tokens } - if r == role || role == "" { - tokens = append(tokens, Token{ - ID: string(t.Data["token-id"]), - Role: r, - Expiry: string(t.Data["expiration"]), - }) + + token := Token{ID: parsed.Token.ID} + + if slices.Contains(parsed.Usages, "controller-join") { + token.Role = "controller" + } else if bytes.Equal(secret.Data["usage-controller-join"], []byte("true")) { + // Legacy form of token usage + token.Role = "controller" + } else if slices.Contains(parsed.Usages, "authentication") { + token.Role = "worker" + } + + if parsed.Expires != nil { + token.Expiry = parsed.Expires.UTC().Format(time.RFC3339) } + + tokens = append(tokens, token) } + return tokens, nil } func (m *Manager) Remove(ctx context.Context, tokenID string) error { - err := m.client.CoreV1().Secrets("kube-system").Delete(ctx, "bootstrap-token-"+tokenID, metav1.DeleteOptions{}) - if errors.IsNotFound(err) { + err := m.client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(ctx, tokenutil.BootstrapTokenSecretName(tokenID), metav1.DeleteOptions{}) + if apierrors.IsNotFound(err) { return nil } return err } + +// Generates a new, random Bootstrap Token. +func generateBootstrapToken() (*bootstraptokenv1.BootstrapTokenString, error) { + token, err := tokenutil.GenerateBootstrapToken() + if err != nil { + return nil, err + } + + return bootstraptokenv1.NewBootstrapTokenString(token) +}