Skip to content

Commit a911c68

Browse files
Merge tag 'v0.3.12' into spectro-master
v0.3.12
2 parents dbf361c + 9e1dd7e commit a911c68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+599
-285
lines changed

OWNERS

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ approvers:
77
- cluster-api-maintainers
88

99
emeritus_approvers:
10-
- roberthbailey
11-
- kris-nova
1210
- chuckha
11+
- kris-nova
12+
- ncdc
13+
- roberthbailey
14+
15+
emeritus_maintainers:
16+
- ncdc
1317

1418
reviewers:
1519
- cluster-api-maintainers

OWNERS_ALIASES

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ aliases:
2323
cluster-api-maintainers:
2424
- justinsb
2525
- detiber
26-
- ncdc
2726
- vincepri
2827
- CecileRobertMichon
2928

@@ -48,20 +47,18 @@ aliases:
4847
# -----------------------------------------------------------
4948

5049
cluster-api-provider-docker-maintainers:
51-
- ncdc
50+
- fabriziopandini
5251

5352
# -----------------------------------------------------------
5453
# OWNER_ALIASES for bootstrap/kubeadm
5554
# -----------------------------------------------------------
5655

5756
cluster-api-bootstrap-provider-kubeadm-maintainers:
5857
- fabriziopandini
59-
- ncdc
6058
- SataQiu
6159

6260
cluster-api-bootstrap-provider-kubeadm-reviewers:
6361
- fabriziopandini
64-
- ncdc
6562

6663
# -----------------------------------------------------------
6764
# OWNER_ALIASES for cmd/clusterctl

bootstrap/kubeadm/controllers/kubeadmconfig_controller.go

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
148148

149149
// Look up the owner of this KubeConfig if there is one
150150
configOwner, err := bsutil.GetConfigOwner(ctx, r.Client, config)
151-
if apierrors.IsNotFound(err) {
151+
if apierrors.IsNotFound(errors.Cause(err)) {
152152
// Could not find the owner yet, this is not an error and will rereconcile when the owner gets set.
153153
return ctrl.Result{}, nil
154154
}
@@ -169,7 +169,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
169169
return ctrl.Result{}, nil
170170
}
171171

172-
if apierrors.IsNotFound(err) {
172+
if apierrors.IsNotFound(errors.Cause(err)) {
173173
log.Info("Cluster does not exist yet, waiting until it is created")
174174
return ctrl.Result{}, nil
175175
}
@@ -225,10 +225,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
225225
return ctrl.Result{}, nil
226226
// Migrate plaintext data to secret.
227227
case config.Status.BootstrapData != nil && config.Status.DataSecretName == nil:
228-
if err := r.storeBootstrapData(ctx, scope, config.Status.BootstrapData); err != nil {
229-
return ctrl.Result{}, err
230-
}
231-
return ctrl.Result{}, nil
228+
return ctrl.Result{}, r.storeBootstrapData(ctx, scope, config.Status.BootstrapData)
232229
// Reconcile status for machines that already have a secret reference, but our status isn't up to date.
233230
// This case solves the pivoting scenario (or a backup restore) which doesn't preserve the status subresource on objects.
234231
case configOwner.DataSecretName() != nil && (!config.Status.Ready || config.Status.DataSecretName == nil):
@@ -240,26 +237,14 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
240237
case config.Status.Ready:
241238
// If the BootstrapToken has been generated for a join and the infrastructure is not ready.
242239
// This indicates the token in the join config has not been consumed and it may need a refresh.
243-
if (config.Spec.JoinConfiguration != nil && config.Spec.JoinConfiguration.Discovery.BootstrapToken != nil) && !configOwner.IsInfrastructureReady() {
244-
245-
token := config.Spec.JoinConfiguration.Discovery.BootstrapToken.Token
246-
247-
remoteClient, err := r.remoteClientGetter(ctx, r.Client, util.ObjectKey(cluster), r.scheme)
248-
if err != nil {
249-
log.Error(err, "Error creating remote cluster client")
250-
return ctrl.Result{}, err
251-
}
252-
253-
log.Info("Refreshing token until the infrastructure has a chance to consume it")
254-
err = refreshToken(remoteClient, token)
255-
if err != nil {
256-
// It would be nice to re-create the bootstrap token if the error was "not found", but we have no way to update the Machine's bootstrap data
257-
return ctrl.Result{}, errors.Wrapf(err, "failed to refresh bootstrap token")
240+
if config.Spec.JoinConfiguration != nil && config.Spec.JoinConfiguration.Discovery.BootstrapToken != nil {
241+
if !configOwner.IsInfrastructureReady() {
242+
return r.refreshBootstrapToken(ctx, log, cluster, config)
243+
} else if (config.Spec.JoinConfiguration != nil && config.Spec.JoinConfiguration.Discovery.BootstrapToken != nil) && configOwner.IsMachinePool() {
244+
// If the BootstrapToken has been generated and infrastructure is ready but the configOwner is a MachinePool,
245+
// we rotate the token to keep it fresh for future scale ups.
246+
return r.rotateMachinePoolBootstrapToken(ctx, log, cluster, config, scope)
258247
}
259-
// NB: this may not be sufficient to keep the token live if we don't see it before it expires, but when we generate a config we will set the status to "ready" which should generate an update event
260-
return ctrl.Result{
261-
RequeueAfter: DefaultTokenTTL / 2,
262-
}, nil
263248
}
264249
// In any other case just return as the config is already generated and need not be generated again.
265250
return ctrl.Result{}, nil
@@ -290,6 +275,54 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
290275
return r.joinWorker(ctx, scope)
291276
}
292277

278+
func (r *KubeadmConfigReconciler) refreshBootstrapToken(ctx context.Context, log logr.Logger, cluster *clusterv1.Cluster, config *bootstrapv1.KubeadmConfig) (ctrl.Result, error) {
279+
token := config.Spec.JoinConfiguration.Discovery.BootstrapToken.Token
280+
281+
remoteClient, err := r.remoteClientGetter(ctx, r.Client, util.ObjectKey(cluster), r.scheme)
282+
if err != nil {
283+
log.Error(err, "Error creating remote cluster client")
284+
return ctrl.Result{}, err
285+
}
286+
287+
log.Info("Refreshing token until the infrastructure has a chance to consume it")
288+
if err := refreshToken(remoteClient, token); err != nil {
289+
return ctrl.Result{}, errors.Wrapf(err, "failed to refresh bootstrap token")
290+
}
291+
return ctrl.Result{
292+
RequeueAfter: DefaultTokenTTL / 2,
293+
}, nil
294+
}
295+
296+
func (r *KubeadmConfigReconciler) rotateMachinePoolBootstrapToken(ctx context.Context, log logr.Logger, cluster *clusterv1.Cluster, config *bootstrapv1.KubeadmConfig, scope *Scope) (ctrl.Result, error) {
297+
log.V(2).Info("Config is owned by a MachinePool, checking if token should be rotated")
298+
remoteClient, err := r.remoteClientGetter(ctx, r.Client, util.ObjectKey(cluster), r.scheme)
299+
if err != nil {
300+
return ctrl.Result{}, err
301+
}
302+
303+
token := config.Spec.JoinConfiguration.Discovery.BootstrapToken.Token
304+
shouldRotate, err := shouldRotate(remoteClient, token)
305+
if err != nil {
306+
return ctrl.Result{}, err
307+
}
308+
if shouldRotate {
309+
log.V(2).Info("Creating new bootstrap token")
310+
token, err := createToken(remoteClient)
311+
if err != nil {
312+
return ctrl.Result{}, errors.Wrapf(err, "failed to create new bootstrap token")
313+
}
314+
315+
config.Spec.JoinConfiguration.Discovery.BootstrapToken.Token = token
316+
log.Info("Altering JoinConfiguration.Discovery.BootstrapToken", "Token", token)
317+
318+
// update the bootstrap data
319+
return r.joinWorker(ctx, scope)
320+
}
321+
return ctrl.Result{
322+
RequeueAfter: DefaultTokenTTL / 3,
323+
}, nil
324+
}
325+
293326
func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Context, scope *Scope) (_ ctrl.Result, reterr error) {
294327
// initialize the DataSecretAvailableCondition if missing.
295328
// this is required in order to avoid the condition's LastTransitionTime to flicker in case of errors surfacing
@@ -839,7 +872,10 @@ func (r *KubeadmConfigReconciler) storeBootstrapData(ctx context.Context, scope
839872
if !apierrors.IsAlreadyExists(err) {
840873
return errors.Wrapf(err, "failed to create bootstrap data secret for KubeadmConfig %s/%s", scope.Config.Namespace, scope.Config.Name)
841874
}
842-
r.Log.Info("bootstrap data secret for KubeadmConfig already exists", "secret", secret.Name, "KubeadmConfig", scope.Config.Name)
875+
r.Log.Info("bootstrap data secret for KubeadmConfig already exists, updating", "secret", secret.Name, "KubeadmConfig", scope.Config.Name)
876+
if err := r.Client.Update(ctx, secret); err != nil {
877+
return errors.Wrapf(err, "failed to update bootstrap data secret for KubeadmConfig %s/%s", scope.Config.Namespace, scope.Config.Name)
878+
}
843879
}
844880
scope.Config.Status.DataSecretName = pointer.StringPtr(secret.Name)
845881
scope.Config.Status.Ready = true

bootstrap/kubeadm/controllers/kubeadmconfig_controller_test.go

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfKubeadmConfigIsReady(t *
134134
}
135135

136136
// Reconcile returns an error in this case because the owning machine should not go away before the things it owns.
137-
func TestKubeadmConfigReconciler_Reconcile_ReturnErrorIfReferencedMachineIsNotFound(t *testing.T) {
137+
func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfReferencedMachineIsNotFound(t *testing.T) {
138138
g := NewWithT(t)
139139

140140
machine := newMachine(nil, "machine")
@@ -158,7 +158,7 @@ func TestKubeadmConfigReconciler_Reconcile_ReturnErrorIfReferencedMachineIsNotFo
158158
},
159159
}
160160
_, err := k.Reconcile(request)
161-
g.Expect(err).To(HaveOccurred())
161+
g.Expect(err).To(BeNil())
162162
}
163163

164164
// If the machine has bootstrap data secret reference, there is no need to generate more bootstrap data.
@@ -944,6 +944,148 @@ func TestBootstrapTokenTTLExtension(t *testing.T) {
944944
}
945945
}
946946

947+
func TestBootstrapTokenRotationMachinePool(t *testing.T) {
948+
_ = feature.MutableGates.Set("MachinePool=true")
949+
g := NewWithT(t)
950+
951+
cluster := newCluster("cluster")
952+
cluster.Status.InfrastructureReady = true
953+
cluster.Status.ControlPlaneInitialized = true
954+
cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443}
955+
956+
controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
957+
initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-config")
958+
workerMachinePool := newWorkerMachinePool(cluster)
959+
workerJoinConfig := newWorkerPoolJoinKubeadmConfig(workerMachinePool)
960+
objects := []runtime.Object{
961+
cluster,
962+
workerMachinePool,
963+
workerJoinConfig,
964+
}
965+
966+
objects = append(objects, createSecrets(t, cluster, initConfig)...)
967+
myclient := helpers.NewFakeClientWithScheme(setupScheme(), objects...)
968+
k := &KubeadmConfigReconciler{
969+
Log: log.Log,
970+
Client: myclient,
971+
KubeadmInitLock: &myInitLocker{},
972+
remoteClientGetter: fakeremote.NewClusterClient,
973+
}
974+
request := ctrl.Request{
975+
NamespacedName: client.ObjectKey{
976+
Namespace: "default",
977+
Name: "workerpool-join-cfg",
978+
},
979+
}
980+
result, err := k.Reconcile(request)
981+
g.Expect(err).NotTo(HaveOccurred())
982+
g.Expect(result.Requeue).To(BeFalse())
983+
g.Expect(result.RequeueAfter).To(Equal(time.Duration(0)))
984+
985+
cfg, err := getKubeadmConfig(myclient, "workerpool-join-cfg")
986+
g.Expect(err).NotTo(HaveOccurred())
987+
g.Expect(cfg.Status.Ready).To(BeTrue())
988+
g.Expect(cfg.Status.DataSecretName).NotTo(BeNil())
989+
g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil())
990+
991+
l := &corev1.SecretList{}
992+
err = myclient.List(context.Background(), l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))
993+
g.Expect(err).NotTo(HaveOccurred())
994+
g.Expect(len(l.Items)).To(Equal(1))
995+
996+
// ensure that the token is refreshed...
997+
tokenExpires := make([][]byte, len(l.Items))
998+
999+
for i, item := range l.Items {
1000+
tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey]
1001+
}
1002+
1003+
<-time.After(1 * time.Second)
1004+
1005+
for _, req := range []ctrl.Request{
1006+
{
1007+
NamespacedName: client.ObjectKey{
1008+
Namespace: "default",
1009+
Name: "workerpool-join-cfg",
1010+
},
1011+
},
1012+
} {
1013+
1014+
result, err := k.Reconcile(req)
1015+
g.Expect(err).NotTo(HaveOccurred())
1016+
g.Expect(result.RequeueAfter).NotTo(BeNumerically(">=", DefaultTokenTTL))
1017+
}
1018+
1019+
l = &corev1.SecretList{}
1020+
err = myclient.List(context.Background(), l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))
1021+
g.Expect(err).NotTo(HaveOccurred())
1022+
g.Expect(len(l.Items)).To(Equal(1))
1023+
1024+
for i, item := range l.Items {
1025+
g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeFalse())
1026+
tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey]
1027+
}
1028+
1029+
// ...until the infrastructure is marked "ready"
1030+
workerMachinePool.Status.InfrastructureReady = true
1031+
err = myclient.Update(context.Background(), workerMachinePool)
1032+
g.Expect(err).NotTo(HaveOccurred())
1033+
1034+
<-time.After(1 * time.Second)
1035+
1036+
request = ctrl.Request{
1037+
NamespacedName: client.ObjectKey{
1038+
Namespace: "default",
1039+
Name: "workerpool-join-cfg",
1040+
},
1041+
}
1042+
result, err = k.Reconcile(request)
1043+
g.Expect(err).NotTo(HaveOccurred())
1044+
g.Expect(result.RequeueAfter).To(Equal(DefaultTokenTTL / 3))
1045+
1046+
l = &corev1.SecretList{}
1047+
err = myclient.List(context.Background(), l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))
1048+
g.Expect(err).NotTo(HaveOccurred())
1049+
g.Expect(len(l.Items)).To(Equal(1))
1050+
1051+
for i, item := range l.Items {
1052+
g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeTrue())
1053+
}
1054+
1055+
// before token expires, it should rotate it
1056+
tokenExpires[0] = []byte(time.Now().UTC().Add(DefaultTokenTTL / 5).Format(time.RFC3339))
1057+
l.Items[0].Data[bootstrapapi.BootstrapTokenExpirationKey] = tokenExpires[0]
1058+
err = myclient.Update(context.TODO(), &l.Items[0])
1059+
g.Expect(err).NotTo(HaveOccurred())
1060+
1061+
request = ctrl.Request{
1062+
NamespacedName: client.ObjectKey{
1063+
Namespace: "default",
1064+
Name: "workerpool-join-cfg",
1065+
},
1066+
}
1067+
result, err = k.Reconcile(request)
1068+
g.Expect(err).NotTo(HaveOccurred())
1069+
g.Expect(result.RequeueAfter).To(Equal(time.Duration(0)))
1070+
1071+
l = &corev1.SecretList{}
1072+
err = myclient.List(context.Background(), l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))
1073+
g.Expect(err).NotTo(HaveOccurred())
1074+
g.Expect(len(l.Items)).To(Equal(2))
1075+
foundOld := false
1076+
foundNew := true
1077+
for _, item := range l.Items {
1078+
if bytes.Equal(item.Data[bootstrapapi.BootstrapTokenExpirationKey], tokenExpires[0]) {
1079+
foundOld = true
1080+
} else {
1081+
g.Expect(string(item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(Equal(time.Now().UTC().Add(DefaultTokenTTL).Format(time.RFC3339)))
1082+
foundNew = true
1083+
}
1084+
}
1085+
g.Expect(foundOld).To(BeTrue())
1086+
g.Expect(foundNew).To(BeTrue())
1087+
}
1088+
9471089
// Ensure the discovery portion of the JoinConfiguration gets generated correctly.
9481090
func TestKubeadmConfigReconciler_Reconcile_DiscoveryReconcileBehaviors(t *testing.T) {
9491091
k := &KubeadmConfigReconciler{

bootstrap/kubeadm/controllers/token.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,47 @@ func createToken(c client.Client) (string, error) {
7171
return token, nil
7272
}
7373

74-
// refreshToken extends the TTL for an existing token
75-
func refreshToken(c client.Client, token string) error {
74+
// getToken fetches the token Secret and returns an error if it is invalid.
75+
func getToken(c client.Client, token string) (*v1.Secret, error) {
7676
substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token)
7777
if len(substrs) != 3 {
78-
return errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern)
78+
return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern)
7979
}
8080
tokenID := substrs[1]
8181

8282
secretName := bootstraputil.BootstrapTokenSecretName(tokenID)
8383
secret := &v1.Secret{}
8484
if err := c.Get(context.TODO(), client.ObjectKey{Name: secretName, Namespace: metav1.NamespaceSystem}, secret); err != nil {
85-
return err
85+
return secret, err
8686
}
8787

8888
if secret.Data == nil {
89-
return errors.Errorf("Invalid bootstrap secret %q, remove the token from the kubadm config to re-create", secretName)
89+
return nil, errors.Errorf("Invalid bootstrap secret %q, remove the token from the kubadm config to re-create", secretName)
90+
}
91+
return secret, nil
92+
}
93+
94+
// refreshToken extends the TTL for an existing token.
95+
func refreshToken(c client.Client, token string) error {
96+
secret, err := getToken(c, token)
97+
if err != nil {
98+
return err
9099
}
91100
secret.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(DefaultTokenTTL).Format(time.RFC3339))
92101

93102
return c.Update(context.TODO(), secret)
94103
}
104+
105+
// shouldRotate returns true if an existing token is past half of its TTL and should to be rotated.
106+
func shouldRotate(c client.Client, token string) (bool, error) {
107+
secret, err := getToken(c, token)
108+
if err != nil {
109+
return false, err
110+
}
111+
112+
expiration, err := time.Parse(time.RFC3339, string(secret.Data[bootstrapapi.BootstrapTokenExpirationKey]))
113+
if err != nil {
114+
return false, err
115+
}
116+
return expiration.Before(time.Now().UTC().Add(DefaultTokenTTL / 2)), nil
117+
}

0 commit comments

Comments
 (0)