Skip to content

Commit 090c3e0

Browse files
authored
fix: move responsibility for managing k3s token to control plane controller (#71)
* Move responsibility for creating the token required by nodes to join the cluster to the KThreesControlPlane controller
1 parent f640118 commit 090c3e0

File tree

8 files changed

+411
-66
lines changed

8 files changed

+411
-66
lines changed

.golangci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ linters-settings:
8686
- sigs.k8s.io/cluster-api
8787

8888
- github.com/cluster-api-provider-k3s/cluster-api-k3s
89+
90+
- github.com/google/uuid
8991
gci:
9092
sections:
9193
- standard

bootstrap/controllers/kthreesconfig_controller.go

+6-64
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,13 @@ func (r *KThreesConfigReconciler) joinControlplane(ctx context.Context, scope *S
223223

224224
serverURL := fmt.Sprintf("https://%s", scope.Cluster.Spec.ControlPlaneEndpoint.String())
225225

226-
tokn, err := r.retrieveToken(ctx, scope)
226+
tokn, err := token.Lookup(ctx, r.Client, client.ObjectKeyFromObject(scope.Cluster))
227227
if err != nil {
228228
conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
229229
return err
230230
}
231231

232-
configStruct := k3s.GenerateJoinControlPlaneConfig(serverURL, tokn,
232+
configStruct := k3s.GenerateJoinControlPlaneConfig(serverURL, *tokn,
233233
scope.Cluster.Spec.ControlPlaneEndpoint.Host,
234234
scope.Config.Spec.ServerConfig,
235235
scope.Config.Spec.AgentConfig)
@@ -284,13 +284,13 @@ func (r *KThreesConfigReconciler) joinWorker(ctx context.Context, scope *Scope)
284284

285285
serverURL := fmt.Sprintf("https://%s", scope.Cluster.Spec.ControlPlaneEndpoint.String())
286286

287-
tokn, err := r.retrieveToken(ctx, scope)
287+
tokn, err := token.Lookup(ctx, r.Client, client.ObjectKeyFromObject(scope.Cluster))
288288
if err != nil {
289289
conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
290290
return err
291291
}
292292

293-
configStruct := k3s.GenerateWorkerConfig(serverURL, tokn, scope.Config.Spec.ServerConfig, scope.Config.Spec.AgentConfig)
293+
configStruct := k3s.GenerateWorkerConfig(serverURL, *tokn, scope.Config.Spec.ServerConfig, scope.Config.Spec.AgentConfig)
294294

295295
b, err := kubeyaml.Marshal(configStruct)
296296
if err != nil {
@@ -424,7 +424,7 @@ func (r *KThreesConfigReconciler) handleClusterNotInitialized(ctx context.Contex
424424
}
425425
conditions.MarkTrue(scope.Config, bootstrapv1.CertificatesAvailableCondition)
426426

427-
token, err := r.generateAndStoreToken(ctx, scope)
427+
token, err := token.Lookup(ctx, r.Client, client.ObjectKeyFromObject(scope.Cluster))
428428
if err != nil {
429429
return ctrl.Result{}, err
430430
}
@@ -433,7 +433,7 @@ func (r *KThreesConfigReconciler) handleClusterNotInitialized(ctx context.Contex
433433
// For now just use the etcd option
434434
configStruct := k3s.GenerateInitControlPlaneConfig(
435435
scope.Cluster.Spec.ControlPlaneEndpoint.Host,
436-
token,
436+
*token,
437437
scope.Config.Spec.ServerConfig,
438438
scope.Config.Spec.AgentConfig)
439439

@@ -480,64 +480,6 @@ func (r *KThreesConfigReconciler) handleClusterNotInitialized(ctx context.Contex
480480
return r.reconcileKubeconfig(ctx, scope)
481481
}
482482

483-
func (r *KThreesConfigReconciler) generateAndStoreToken(ctx context.Context, scope *Scope) (string, error) {
484-
tokn, err := token.Random(16)
485-
if err != nil {
486-
return "", err
487-
}
488-
489-
secret := &corev1.Secret{
490-
ObjectMeta: metav1.ObjectMeta{
491-
Name: token.Name(scope.Cluster.Name),
492-
Namespace: scope.Config.Namespace,
493-
Labels: map[string]string{
494-
clusterv1.ClusterNameLabel: scope.Cluster.Name,
495-
},
496-
OwnerReferences: []metav1.OwnerReference{
497-
{
498-
APIVersion: clusterv1.GroupVersion.String(),
499-
Kind: "Cluster",
500-
Name: scope.Cluster.Name,
501-
UID: scope.Cluster.UID,
502-
Controller: pointer.Bool(true),
503-
},
504-
},
505-
},
506-
Data: map[string][]byte{
507-
"value": []byte(tokn),
508-
},
509-
Type: clusterv1.ClusterSecretType,
510-
}
511-
512-
// as secret creation and scope.Config status patch are not atomic operations
513-
// it is possible that secret creation happens but the config.Status patches are not applied
514-
if err := r.Client.Create(ctx, secret); err != nil {
515-
if !apierrors.IsAlreadyExists(err) {
516-
return "", fmt.Errorf("failed to create token for KThreesConfig %s/%s: %w", scope.Config.Namespace, scope.Config.Name, err)
517-
}
518-
// r.Log.Info("bootstrap data secret for KThreesConfig already exists, updating", "secret", secret.Name, "KThreesConfig", scope.Config.Name)
519-
if err := r.Client.Update(ctx, secret); err != nil {
520-
return "", fmt.Errorf("failed to update bootstrap token secret for KThreesConfig %s/%s: %w", scope.Config.Namespace, scope.Config.Name, err)
521-
}
522-
}
523-
524-
return tokn, nil
525-
}
526-
527-
func (r *KThreesConfigReconciler) retrieveToken(ctx context.Context, scope *Scope) (string, error) {
528-
secret := &corev1.Secret{}
529-
obj := client.ObjectKey{
530-
Namespace: scope.Config.Namespace,
531-
Name: token.Name(scope.Cluster.Name),
532-
}
533-
534-
if err := r.Client.Get(ctx, obj, secret); err != nil {
535-
return "", fmt.Errorf("failed to get token for KThreesConfig %s/%s: %w", scope.Config.Namespace, scope.Config.Name, err)
536-
}
537-
538-
return string(secret.Data["value"]), nil
539-
}
540-
541483
func (r *KThreesConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
542484
if r.KThreesInitLock == nil {
543485
r.KThreesInitLock = locking.NewControlPlaneInitMutex(ctrl.Log.WithName("init-locker"), mgr.GetClient())

controlplane/api/v1beta1/condition_consts.go

+8
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,11 @@ const (
120120
// EtcdMemberUnhealthyReason (Severity=Error) documents a Machine's etcd member is unhealthy.
121121
EtcdMemberUnhealthyReason = "EtcdMemberUnhealthy"
122122
)
123+
124+
const (
125+
// TokenAvailableCondition documents whether the token required for nodes to join the cluster is available.
126+
TokenAvailableCondition clusterv1.ConditionType = "TokenAvailable"
127+
128+
// TokenGenerationFailedReason documents that the token required for nodes to join the cluster could not be generated.
129+
TokenGenerationFailedReason = "TokenGenerationFailed"
130+
)

controlplane/controllers/kthreescontrolplane_controller.go

+9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
"github.com/cluster-api-provider-k3s/cluster-api-k3s/pkg/kubeconfig"
5151
"github.com/cluster-api-provider-k3s/cluster-api-k3s/pkg/machinefilters"
5252
"github.com/cluster-api-provider-k3s/cluster-api-k3s/pkg/secret"
53+
"github.com/cluster-api-provider-k3s/cluster-api-k3s/pkg/token"
5354
)
5455

5556
// KThreesControlPlaneReconciler reconciles a KThreesControlPlane object.
@@ -244,6 +245,7 @@ func patchKThreesControlPlane(ctx context.Context, patchHelper *patch.Helper, kc
244245
controlplanev1.MachinesReadyCondition,
245246
controlplanev1.AvailableCondition,
246247
controlplanev1.CertificatesAvailableCondition,
248+
controlplanev1.TokenAvailableCondition,
247249
),
248250
)
249251

@@ -258,6 +260,7 @@ func patchKThreesControlPlane(ctx context.Context, patchHelper *patch.Helper, kc
258260
controlplanev1.MachinesReadyCondition,
259261
controlplanev1.AvailableCondition,
260262
controlplanev1.CertificatesAvailableCondition,
263+
controlplanev1.TokenAvailableCondition,
261264
}},
262265
)
263266
}
@@ -408,6 +411,12 @@ func (r *KThreesControlPlaneReconciler) reconcile(ctx context.Context, cluster *
408411
}
409412
conditions.MarkTrue(kcp, controlplanev1.CertificatesAvailableCondition)
410413

414+
if err := token.Reconcile(ctx, r.Client, client.ObjectKeyFromObject(cluster), kcp); err != nil {
415+
conditions.MarkFalse(kcp, controlplanev1.TokenAvailableCondition, controlplanev1.TokenGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
416+
return reconcile.Result{}, err
417+
}
418+
conditions.MarkTrue(kcp, controlplanev1.TokenAvailableCondition)
419+
411420
// If ControlPlaneEndpoint is not set, return early
412421
if !cluster.Spec.ControlPlaneEndpoint.IsValid() {
413422
logger.Info("Cluster does not yet have a ControlPlaneEndpoint defined")

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/coredns/caddy v1.1.0 // indirect
2727
github.com/davecgh/go-spew v1.1.1 // indirect
2828
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
29+
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
2930
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
3031
github.com/fsnotify/fsnotify v1.6.0 // indirect
3132
github.com/go-logr/zapr v1.2.3 // indirect

go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
102102
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
103103
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
104104
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
105+
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
105106
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
106107
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
107108
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=

pkg/token/token.go

+126-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
11
package token
22

33
import (
4+
"context"
45
cryptorand "crypto/rand"
56
"encoding/hex"
67
"fmt"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
715
)
816

9-
func Random(size int) (string, error) {
17+
func Lookup(ctx context.Context, ctrlclient client.Client, clusterKey client.ObjectKey) (*string, error) {
18+
var s *corev1.Secret
19+
var err error
20+
21+
if s, err = getSecret(ctx, ctrlclient, clusterKey); err != nil {
22+
return nil, fmt.Errorf("failed to lookup token: %v", err)
23+
}
24+
if val, ok := s.Data["value"]; ok {
25+
ret := string(val)
26+
return &ret, nil
27+
}
28+
29+
return nil, fmt.Errorf("found token secret without value")
30+
}
31+
32+
func Reconcile(ctx context.Context, ctrlclient client.Client, clusterKey client.ObjectKey, owner client.Object) error {
33+
var s *corev1.Secret
34+
var err error
35+
36+
// Find the token secret
37+
if s, err = getSecret(ctx, ctrlclient, clusterKey); err != nil {
38+
if apierrors.IsNotFound(err) {
39+
// Secret does not exist, create it
40+
_, err = generateAndStore(ctx, ctrlclient, clusterKey, owner)
41+
return err
42+
}
43+
}
44+
45+
// Secret exists
46+
// Ensure the secret has correct ownership; this is necessary because at one point, the secret was owned by KThreesConfig
47+
if !metav1.IsControlledBy(s, owner) {
48+
upsertControllerRef(s, owner)
49+
if err := ctrlclient.Update(ctx, s); err != nil {
50+
return fmt.Errorf("failed to update ownership of token: %v", err)
51+
}
52+
}
53+
54+
return nil
55+
}
56+
57+
// randomB64 generates a cryptographically secure random byte slice of length size and returns its base64 encoding.
58+
func randomB64(size int) (string, error) {
1059
token := make([]byte, size)
1160
_, err := cryptorand.Read(token)
1261
if err != nil {
@@ -15,6 +64,81 @@ func Random(size int) (string, error) {
1564
return hex.EncodeToString(token), err
1665
}
1766

18-
func Name(clusterName string) string {
67+
// name returns the name of the token secret, computed by convention using the name of the cluster.
68+
func name(clusterName string) string {
1969
return fmt.Sprintf("%s-token", clusterName)
2070
}
71+
72+
func getSecret(ctx context.Context, ctrlclient client.Client, clusterKey client.ObjectKey) (*corev1.Secret, error) {
73+
s := &corev1.Secret{}
74+
key := client.ObjectKey{
75+
Name: name(clusterKey.Name),
76+
Namespace: clusterKey.Namespace,
77+
}
78+
if err := ctrlclient.Get(ctx, key, s); err != nil {
79+
return nil, err
80+
}
81+
82+
return s, nil
83+
}
84+
85+
func generateAndStore(ctx context.Context, ctrlclient client.Client, clusterKey client.ObjectKey, owner client.Object) (*string, error) {
86+
tokn, err := randomB64(16)
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to generate token: %v", err)
89+
}
90+
91+
secret := &corev1.Secret{
92+
ObjectMeta: metav1.ObjectMeta{
93+
Name: name(clusterKey.Name),
94+
Namespace: clusterKey.Namespace,
95+
Labels: map[string]string{
96+
clusterv1.ClusterNameLabel: clusterKey.Name,
97+
},
98+
},
99+
Data: map[string][]byte{
100+
"value": []byte(tokn),
101+
},
102+
Type: clusterv1.ClusterSecretType,
103+
}
104+
105+
//nolint:errcheck
106+
controllerutil.SetControllerReference(owner, secret, ctrlclient.Scheme())
107+
108+
// as secret creation and scope.Config status patch are not atomic operations
109+
// it is possible that secret creation happens but the config.Status patches are not applied
110+
if err := ctrlclient.Create(ctx, secret); err != nil {
111+
return nil, fmt.Errorf("failed to store token: %v", err)
112+
}
113+
114+
return &tokn, nil
115+
}
116+
117+
// upsertControllerRef takes controllee and controller objects, either replaces the existing controller ref
118+
// if one exists or appends the new controller ref if one does not exist, and returns the updated controllee
119+
// This is meant to be used in place of controllerutil.SetControllerReference(...), which would throw an error
120+
// if there were already an existing controller ref.
121+
func upsertControllerRef(controllee client.Object, controller client.Object) {
122+
newControllerRef := metav1.NewControllerRef(controller, controller.GetObjectKind().GroupVersionKind())
123+
124+
// Iterate through existing owner references
125+
var updatedOwnerReferences []metav1.OwnerReference
126+
var controllerRefUpdated bool
127+
for _, ownerRef := range controllee.GetOwnerReferences() {
128+
// Identify and replace the controlling owner reference
129+
if ownerRef.Controller != nil && *ownerRef.Controller {
130+
updatedOwnerReferences = append(updatedOwnerReferences, *newControllerRef)
131+
controllerRefUpdated = true
132+
} else {
133+
// Keep non-controlling owner references intact
134+
updatedOwnerReferences = append(updatedOwnerReferences, ownerRef)
135+
}
136+
}
137+
138+
// If the controlling owner reference was not found, add the new one
139+
if !controllerRefUpdated {
140+
updatedOwnerReferences = append(updatedOwnerReferences, *newControllerRef)
141+
}
142+
143+
controllee.SetOwnerReferences(updatedOwnerReferences)
144+
}

0 commit comments

Comments
 (0)