Skip to content

Commit b9df899

Browse files
committed
fix: requeue invalid credentials instead of watching the Secret
1 parent 087fb45 commit b9df899

7 files changed

Lines changed: 46 additions & 90 deletions

‎controller/constants.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ const (
1919

2020
retryableErrorRequeueAfter = 5 * time.Second
2121

22-
// deleteRequeueAfter paces the wait for dependent objects to disappear
23-
// during deletion. Matches what Cluster API and the other infrastructure
24-
// providers use for the same purpose.
22+
// deleteRequeueAfter paces the wait for dependent objects to disappear during deletion.
2523
deleteRequeueAfter = 5 * time.Second
24+
25+
// credentialsRetryRequeueAfter paces retries of invalid credentials, which
26+
// need an operator to fix the Secret before they can succeed.
27+
credentialsRetryRequeueAfter = time.Minute
2628
)

‎controller/stackitcluster_controller.go‎

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -150,52 +150,13 @@ func (r *StackitClusterReconciler) stackitClusterRequestsForCloudInitRef(ctx con
150150
return requests
151151
}
152152

153-
// stackitClusterRequestsForCredentialsSecret enqueues every StackitCluster whose
154-
// credentialsSecretRef points at the given Secret.
155-
//
156-
// Without it, correcting an invalid credentials Secret never reaches the
157-
// cluster: CredentialFailureResult deliberately returns without a requeue,
158-
// because retrying invalid credentials in a hot loop helps nobody — which only
159-
// works if fixing them triggers a reconcile.
160-
func (r *StackitClusterReconciler) stackitClusterRequestsForCredentialsSecret(ctx context.Context, obj client.Object) []reconcile.Request {
161-
secret, ok := obj.(*corev1.Secret)
162-
if !ok {
163-
return nil
164-
}
165-
166-
// Listed across all namespaces on purpose: CredentialsSecretRef.Namespace is
167-
// optional, so the Secret may well live somewhere other than the
168-
// StackitCluster that references it.
169-
clusters := &infrav1.StackitClusterList{}
170-
if err := r.List(ctx, clusters); err != nil {
171-
logf.FromContext(ctx).Error(err, "Failed to list StackitClusters for credentials Secret watch", "secret", client.ObjectKeyFromObject(secret))
172-
return nil
173-
}
174-
175-
secretKey := client.ObjectKeyFromObject(secret)
176-
requests := make([]reconcile.Request, 0, len(clusters.Items))
177-
for _, cluster := range clusters.Items {
178-
if util.CredentialsSecretKey(&cluster) != secretKey {
179-
continue
180-
}
181-
requests = append(requests, reconcile.Request{
182-
NamespacedName: types.NamespacedName{
183-
Namespace: cluster.Namespace,
184-
Name: cluster.Name,
185-
},
186-
})
187-
}
188-
return requests
189-
}
190-
191153
// SetupWithManager registers the controller with the manager.
192154
func (r *StackitClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
193155
return ctrl.NewControllerManagedBy(mgr).
194156
For(&infrav1.StackitCluster{}).
195157
Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCluster)).
196158
Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCloudInitRef)).
197159
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCloudInitRef)).
198-
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCredentialsSecret)).
199160
Named("stackitcluster").
200161
Complete(r)
201162
}

‎controller/stackitcluster_controller_test.go‎

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2323
"k8s.io/apimachinery/pkg/types"
2424
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
25-
"sigs.k8s.io/controller-runtime/pkg/client"
2625
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2726

2827
infrav1 "github.com/stackitcloud/cluster-api-provider-stackit/api/v1alpha1"
@@ -500,19 +499,34 @@ var _ = Describe("StackitCluster Controller", func() {
500499
expectCondition(got.Status.Conditions, infrav1.ClusterReadyCondition, metav1.ConditionFalse, "NetworkNotFound")
501500
})
502501

503-
It("marks credentials invalid without requeueing on unauthorized credentials", func() {
502+
// The requeue is what lets a corrected Secret take effect: nothing else
503+
// enqueues the cluster once the credentials are rejected.
504+
It("requeues on unauthorized credentials and recovers once they are corrected", func() {
504505
reconciler.CloudClientFactory = func(context.Context, cloud.Credentials) (cloud.Client, error) {
505506
return nil, fmt.Errorf("authenticate: %w", cloud.ErrUnauthorized)
506507
}
507508

508509
result, err := reconciler.Reconcile(ctx, request)
509510
Expect(err).NotTo(HaveOccurred())
510-
Expect(result).To(Equal(reconcile.Result{}))
511+
Expect(result.RequeueAfter).To(Equal(credentialsRetryRequeueAfter))
511512

512513
got := &infrav1.StackitCluster{}
513514
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
515+
Expect(got.Status.Ready).To(BeFalse())
514516
expectCondition(got.Status.Conditions, infrav1.ClusterCredentialsReadyCondition, metav1.ConditionFalse, "CredentialsInvalid")
515517
expectCondition(got.Status.Conditions, infrav1.ClusterReadyCondition, metav1.ConditionFalse, "CredentialsInvalid")
518+
519+
By("correcting the credentials")
520+
reconciler.CloudClientFactory = func(context.Context, cloud.Credentials) (cloud.Client, error) {
521+
return fakeCloud, nil
522+
}
523+
524+
_, err = reconciler.Reconcile(ctx, request)
525+
Expect(err).NotTo(HaveOccurred())
526+
527+
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
528+
Expect(got.Status.Ready).To(BeTrue())
529+
expectCondition(got.Status.Conditions, infrav1.ClusterCredentialsReadyCondition, metav1.ConditionTrue, "Available")
516530
})
517531

518532
It("does not call the cloud API when the owning Cluster is paused", func() {
@@ -723,48 +737,6 @@ var _ = Describe("StackitCluster Controller", func() {
723737
Expect(requests).To(Equal([]reconcile.Request{request}))
724738
})
725739

726-
It("maps credentials Secret events to StackitCluster reconcile requests", func() {
727-
secret := &corev1.Secret{}
728-
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: credentials, Namespace: namespace}, secret)).To(Succeed())
729-
730-
requests := reconciler.stackitClusterRequestsForCredentialsSecret(ctx, secret)
731-
Expect(requests).To(Equal([]reconcile.Request{request}))
732-
})
733-
734-
It("maps credentials Secret events from a different namespace than the StackitCluster", func() {
735-
otherNamespace := "credentials-elsewhere"
736-
Expect(client.IgnoreAlreadyExists(k8sClient.Create(ctx, &corev1.Namespace{
737-
ObjectMeta: metav1.ObjectMeta{Name: otherNamespace},
738-
}))).To(Succeed())
739-
740-
otherCredentials := credentials + "-elsewhere"
741-
createCredentialsSecret(ctx, otherCredentials, otherNamespace, testProjectID)
742-
DeferCleanup(func() {
743-
deleteIfExists(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: otherCredentials, Namespace: otherNamespace}})
744-
})
745-
746-
got := &infrav1.StackitCluster{}
747-
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
748-
got.Spec.CredentialsSecretRef = corev1.SecretReference{Name: otherCredentials, Namespace: otherNamespace}
749-
Expect(k8sClient.Update(ctx, got)).To(Succeed())
750-
751-
secret := &corev1.Secret{}
752-
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: otherCredentials, Namespace: otherNamespace}, secret)).To(Succeed())
753-
754-
requests := reconciler.stackitClusterRequestsForCredentialsSecret(ctx, secret)
755-
Expect(requests).To(Equal([]reconcile.Request{request}),
756-
"credentialsSecretRef.namespace is optional, so the mapper must not be limited to the Secret's own namespace")
757-
})
758-
759-
It("ignores Secret events that no StackitCluster uses as credentials", func() {
760-
secret := &corev1.Secret{}
761-
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: credentials, Namespace: namespace}, secret)).To(Succeed())
762-
secret.Name = credentials + "-unused"
763-
764-
requests := reconciler.stackitClusterRequestsForCredentialsSecret(ctx, secret)
765-
Expect(requests).To(BeEmpty())
766-
})
767-
768740
It("ignores Cluster events for other infrastructure providers", func() {
769741
cluster := &clusterv1.Cluster{
770742
ObjectMeta: metav1.ObjectMeta{Name: "other", Namespace: namespace},

‎controller/stackitcluster_infrastructure.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func (r *StackitClusterReconciler) reconcileNormal(ctx context.Context, clusterS
5757
&stackitCluster.Status.Conditions,
5858
stackitCluster.Generation,
5959
err,
60+
credentialsRetryRequeueAfter,
6061
infrav1.ClusterCredentialsReadyCondition,
6162
infrav1.ClusterReadyCondition,
6263
)

‎controller/stackitmachine_controller_test.go‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ var _ = Describe("StackitMachine Controller", func() {
220220
expectCondition(got.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "InvalidFailureDomain")
221221
})
222222

223-
It("marks credentials invalid without requeueing on unauthorized credentials", func() {
223+
It("requeues on unauthorized credentials and recovers once they are corrected", func() {
224224
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
225225
createBootstrapSecret(ctx, bootstrapName)
226226
reconciler.CloudClientFactory = func(context.Context, cloud.Credentials) (cloud.Client, error) {
@@ -229,13 +229,25 @@ var _ = Describe("StackitMachine Controller", func() {
229229

230230
result, err := reconciler.Reconcile(ctx, request)
231231
Expect(err).NotTo(HaveOccurred())
232-
Expect(result).To(Equal(reconcile.Result{}))
232+
Expect(result.RequeueAfter).To(Equal(credentialsRetryRequeueAfter))
233233
Expect(fakeCloud.ServerCount()).To(Equal(0))
234234

235235
got := &infrav1.StackitMachine{}
236236
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
237237
expectCondition(got.Status.Conditions, infrav1.MachineCredentialsReadyCondition, metav1.ConditionFalse, "CredentialsInvalid")
238238
expectCondition(got.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "CredentialsInvalid")
239+
240+
By("correcting the credentials")
241+
reconciler.CloudClientFactory = func(context.Context, cloud.Credentials) (cloud.Client, error) {
242+
return fakeCloud, nil
243+
}
244+
245+
_, err = reconciler.Reconcile(ctx, request)
246+
Expect(err).NotTo(HaveOccurred())
247+
Expect(fakeCloud.ServerCount()).To(Equal(1))
248+
249+
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
250+
expectCondition(got.Status.Conditions, infrav1.MachineCredentialsReadyCondition, metav1.ConditionTrue, "Available")
239251
})
240252

241253
It("does not call the cloud API when the owning Cluster is paused", func() {

‎controller/stackitmachine_infrastructure.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func (r *StackitMachineReconciler) reconcileNormal(ctx context.Context, machineS
7878
&stackitMachine.Status.Conditions,
7979
stackitMachine.Generation,
8080
err,
81+
credentialsRetryRequeueAfter,
8182
infrav1.MachineCredentialsReadyCondition,
8283
infrav1.MachineReadyCondition,
8384
)
@@ -193,6 +194,7 @@ func (r *StackitMachineReconciler) reconcileDelete(ctx context.Context, machineS
193194
&stackitMachine.Status.Conditions,
194195
stackitMachine.Generation,
195196
err,
197+
credentialsRetryRequeueAfter,
196198
infrav1.MachineCredentialsReadyCondition,
197199
)
198200
return resultErr

‎util/conditions.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,21 @@ func SetConditions(
8181
}
8282
}
8383

84+
// CredentialFailureResult records the failure and decides how to retry.
85+
//
86+
// Invalid credentials need an operator to fix the Secret, so they requeue on a
87+
// slow timer rather than returning an error and spinning through the controller
88+
// backoff. The reconcile reads the Secret again, which is what picks up the fix.
8489
func CredentialFailureResult(
8590
conditions *[]metav1.Condition,
8691
generation int64,
8792
err error,
93+
requeueAfter time.Duration,
8894
conditionTypes ...string,
8995
) (ctrl.Result, error) {
9096
SetConditions(conditions, generation, metav1.ConditionFalse, "CredentialsInvalid", err.Error(), conditionTypes...)
9197
if cloud.IsUnauthorized(err) || cloud.IsInvalidInput(err) || errors.Is(err, ErrCredentialsInvalid) {
92-
return ctrl.Result{}, nil
98+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
9399
}
94100
return ctrl.Result{}, err
95101
}

0 commit comments

Comments
 (0)