From c28d96ed7024588a61c2b6aa9755e1187680fa16 Mon Sep 17 00:00:00 2001 From: Tom Wieczorek Date: Mon, 5 Feb 2024 13:04:24 +0100 Subject: [PATCH 1/2] Extract BootstrapToken secret generation in its own function This allows the secret to be created without creating it in the cluster. Signed-off-by: Tom Wieczorek --- pkg/token/manager.go | 78 +++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/pkg/token/manager.go b/pkg/token/manager.go index 5fe01dc7c788..dcea57727bd5 100644 --- a/pkg/token/manager.go +++ b/pkg/token/manager.go @@ -22,9 +22,10 @@ import ( "time" "github.com/sirupsen/logrus" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/kubernetes" "github.com/k0sproject/k0s/internal/pkg/random" @@ -65,48 +66,59 @@ type Manager struct { client kubernetes.Interface } -// Create creates a new bootstrap token -func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) (string, error) { +func RandomBootstrapSecret(role string, valid time.Duration) (*corev1.Secret, string, error) { tokenID := random.String(6) tokenSecret := random.String(16) - token := fmt.Sprintf("%s.%s", tokenID, tokenSecret) + s := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("bootstrap-token-%s", tokenID), + Namespace: "kube-system", + }, + Type: corev1.SecretTypeBootstrapToken, + StringData: map[string]string{ + "token-id": tokenID, + "token-secret": tokenSecret, + + // This "usage-" is shared for all roles of the token which allows + // them to execute calls to the k0s API. This is done because we + // need to call the k0s API from windows workers during the join + // step. + "usage-bootstrap-api-auth": "true", + }, + } - data := make(map[string]string) - data["token-id"] = tokenID - data["token-secret"] = tokenSecret if valid != 0 { - data["expiration"] = time.Now().Add(valid).UTC().Format(time.RFC3339) - logrus.Debugf("Set expiry to %s", data["expiration"]) + exp := time.Now().Add(valid).UTC().Format(time.RFC3339) + s.StringData["expiration"] = exp + logrus.Debug("Set expiry to ", exp) } - // This "usage-" is shared for both roles of the token - // which allows both roles execute calls to the k0s api. - // this is done because we need to call k0s api from - // windows workers during the join step - data["usage-bootstrap-api-auth"] = "true" - - if role == "worker" { - data["description"] = "Worker bootstrap token generated by k0s" - data["usage-bootstrap-authentication"] = "true" - data["usage-bootstrap-api-worker-calls"] = "true" - } else { - data["description"] = "Controller bootstrap token generated by k0s" - data["usage-bootstrap-authentication"] = "false" - data["usage-bootstrap-signing"] = "false" - data["usage-controller-join"] = "true" + switch role { + case "worker": + s.StringData["description"] = "Worker bootstrap token generated by k0s" + s.StringData["usage-bootstrap-authentication"] = "true" + s.StringData["usage-bootstrap-api-worker-calls"] = "true" + case "controller": + s.StringData["description"] = "Controller bootstrap token generated by k0s" + s.StringData["usage-bootstrap-authentication"] = "false" + s.StringData["usage-bootstrap-signing"] = "false" + s.StringData["usage-controller-join"] = "true" + default: + return nil, "", fmt.Errorf("unsupported role %q", role) } - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("bootstrap-token-%s", tokenID), - Namespace: "kube-system", - }, - Type: v1.SecretTypeBootstrapToken, - StringData: data, + return &s, fmt.Sprintf("%s.%s", tokenID, tokenSecret), nil +} + +// Create creates a new bootstrap token +func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) (string, error) { + secret, token, err := RandomBootstrapSecret(role, valid) + if err != nil { + return "", err } - _, err := m.client.CoreV1().Secrets("kube-system").Create(ctx, secret, metav1.CreateOptions{}) + _, err = m.client.CoreV1().Secrets("kube-system").Create(ctx, secret, metav1.CreateOptions{}) if err != nil { return "", err } @@ -117,7 +129,7 @@ func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) // 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{ - FieldSelector: "type=bootstrap.kubernetes.io/token", + FieldSelector: fields.OneTermEqualSelector("type", string(corev1.SecretTypeBootstrapToken)).String(), }) if err != nil { return nil, err From c2c801ca4738d25dfaa3793c4a28fe521b07e678 Mon Sep 17 00:00:00 2001 From: Tom Wieczorek Date: Mon, 5 Feb 2024 15:33:11 +0100 Subject: [PATCH 2/2] Use RandomBootstrapSecret to generate preshared tokens Replace the fake client workaround for the missing API and use a straight-forward approach to get the secret and token. Signed-off-by: Tom Wieczorek --- cmd/token/preshared.go | 58 +++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/cmd/token/preshared.go b/cmd/token/preshared.go index 07d3d66f2c0d..4e25a534be0f 100644 --- a/cmd/token/preshared.go +++ b/cmd/token/preshared.go @@ -17,22 +17,23 @@ limitations under the License. package token import ( + "bufio" "bytes" - "context" "fmt" + "io" "os" "path/filepath" "time" - "github.com/spf13/cobra" - v1 "k8s.io/api/core/v1" - fakeclientset "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/testing" - "sigs.k8s.io/yaml" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/client-go/kubernetes/scheme" "github.com/k0sproject/k0s/internal/pkg/file" "github.com/k0sproject/k0s/pkg/config" "github.com/k0sproject/k0s/pkg/token" + + "github.com/spf13/cobra" ) func preSharedCmd() *cobra.Command { @@ -85,43 +86,24 @@ func preSharedCmd() *cobra.Command { } func createSecret(role string, validity time.Duration, outDir string) (string, error) { - fakeClient := fakeclientset.NewSimpleClientset() - - manager, err := token.NewManagerForClient(fakeClient) - if err != nil { - return "", fmt.Errorf("error creating token manager: %w", err) - } - - t, err := manager.Create(context.Background(), validity, role) + secret, token, err := token.RandomBootstrapSecret(role, validity) if err != nil { - return "", fmt.Errorf("error creating token: %w", err) + return "", fmt.Errorf("failed to generate bootstrap secret: %w", err) } - // Get created Secret from the fake client and write it as a file - for _, action := range fakeClient.Actions() { - a, ok := action.(testing.CreateActionImpl) - if !ok { - continue - } - - secret, ok := a.GetObject().(*v1.Secret) - if !ok { - continue - } - secret.APIVersion = "v1" - secret.Kind = "Secret" - - b, err := yaml.Marshal(secret) - if err != nil { - return "", fmt.Errorf("error marshailling secret: %w", err) - } - - err = file.WriteContentAtomically(filepath.Join(outDir, secret.Name+".yaml"), b, 0640) - if err != nil { - return "", fmt.Errorf("error writing secret: %w", err) + if err := file.WriteAtomically(filepath.Join(outDir, secret.Name+".yaml"), 0640, func(unbuffered io.Writer) error { + serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme) + encoder := scheme.Codecs.EncoderForVersion(serializer, corev1.SchemeGroupVersion) + w := bufio.NewWriter(unbuffered) + if err := encoder.Encode(secret, w); err != nil { + return err } + return w.Flush() + }); err != nil { + return "", fmt.Errorf("failed to save bootstrap secret: %w", err) } - return t, nil + + return token, nil } func createKubeConfig(tokenString, role, joinURL, certPath, outDir string) error {