Skip to content

Commit 676e42d

Browse files
corioliskrafttuunit
authored andcommitted
fix: report a missing instance as a final state
When the server of an already provisioned machine was gone, the reconcile used the reason InstanceError and returned an error, so the controller retried for ever and wrote an error line for a state that no retry changes. The provider now reports that state with its own error value. The reconcile turns it into the reason InstanceNotFound on the conditions and into a warning event, and it gives no error back to the controller runtime, so the retry stops. It still creates no replacement server. test: cover the error return of the machine reconcile The regression test for the gone server asserted this before. It now asserts the opposite, so a rejected VM creation covers it instead. test: cover the deletion of a machine whose server is gone The spec drives a provisioned control plane machine into the terminal state of a missing instance, then deletes it while the cloud answers the server delete with not found. The load balancer target and the finalizer must go. The spec is the fourth caller of updateMachineControlPlaneLabel, so unparam reports the namespace parameter that always receives "default". The parameter has been refactored away, and the helper writes the namespace like the helper above it. test: cover a failed server delete during machine deletion The delete path removes the finalizer only after the cloud reports the server as deleted or as already gone. The other exit had no spec: a deletion that answers with any other error must keep the object. The new spec injects a transient error into the server delete, then asserts that the reconcile returns that error, that the server is still there, and that the finalizer still holds the object, so the next attempt can find the instance ID. test: drop the excessive comment of the failed delete spec
1 parent 1e3885d commit 676e42d

3 files changed

Lines changed: 125 additions & 11 deletions

File tree

‎controller/controller_test_helpers_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@ func updateMachineBootstrapSecret(ctx context.Context, name, bootstrapSecretName
130130
Expect(k8sClient.Update(ctx, machine)).To(Succeed())
131131
}
132132

133-
func updateMachineControlPlaneLabel(ctx context.Context, name, namespace string) {
133+
func updateMachineControlPlaneLabel(ctx context.Context, name string) {
134134
machine := &clusterv1.Machine{}
135-
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, machine)).To(Succeed())
135+
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: "default"}, machine)).To(Succeed())
136136
if machine.Labels == nil {
137137
machine.Labels = map[string]string{}
138138
}

‎controller/stackitmachine_controller_test.go‎

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ var _ = Describe("StackitMachine Controller", func() {
154154
Expect(fakeCloud.ServerCount()).To(Equal(0))
155155

156156
By("reconciling again")
157-
_, err = reconciler.Reconcile(ctx, request)
158-
Expect(err).To(HaveOccurred(), "reconcile must surface the missing server instead of papering over it")
157+
result, err := reconciler.Reconcile(ctx, request)
158+
Expect(err).NotTo(HaveOccurred())
159+
Expect(result).To(Equal(reconcile.Result{}))
159160

160161
Expect(fakeCloud.CreateServerCalls).To(Equal(1),
161162
"a replacement server was created for an already-provisioned machine")
@@ -164,7 +165,8 @@ var _ = Describe("StackitMachine Controller", func() {
164165
By("reporting a consistent readiness state")
165166
degraded := &infrav1.StackitMachine{}
166167
Expect(k8sClient.Get(ctx, stackitKey, degraded)).To(Succeed())
167-
expectCondition(degraded.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "InstanceError")
168+
expectCondition(degraded.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "InstanceNotFound")
169+
expectCondition(degraded.Status.Conditions, infrav1.MachineInstanceReadyCondition, metav1.ConditionFalse, "InstanceNotFound")
168170
Expect(degraded.Status.Ready).To(BeFalse(),
169171
"legacy status.ready must follow the Ready condition, not contradict it")
170172
})
@@ -328,9 +330,25 @@ var _ = Describe("StackitMachine Controller", func() {
328330
expectCondition(got.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "InstanceError")
329331
})
330332

333+
It("returns an error when VM creation is rejected", func() {
334+
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
335+
createBootstrapSecret(ctx, bootstrapName)
336+
fakeCloud.FailNextCreateServer = fmt.Errorf("create server rejected: %w", cloud.ErrInvalidInput)
337+
338+
result, err := reconciler.Reconcile(ctx, request)
339+
Expect(err).To(HaveOccurred())
340+
Expect(result).To(Equal(reconcile.Result{}))
341+
Expect(fakeCloud.ServerCount()).To(Equal(0))
342+
343+
got := &infrav1.StackitMachine{}
344+
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
345+
expectCondition(got.Status.Conditions, infrav1.MachineInstanceReadyCondition, metav1.ConditionFalse, "InstanceError")
346+
expectCondition(got.Status.Conditions, infrav1.MachineReadyCondition, metav1.ConditionFalse, "InstanceError")
347+
})
348+
331349
It("registers control plane VMs as API server load balancer targets", func() {
332350
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
333-
updateMachineControlPlaneLabel(ctx, machineName, namespace)
351+
updateMachineControlPlaneLabel(ctx, machineName)
334352
enableStackitClusterLoadBalancer(ctx, clusterName, namespace)
335353
reconcileStackitClusterOnce(ctx, clusterName, namespace, fakeCloud)
336354
createBootstrapSecret(ctx, bootstrapName)
@@ -350,7 +368,7 @@ var _ = Describe("StackitMachine Controller", func() {
350368

351369
It("requeues when load balancer target registration returns a transient error", func() {
352370
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
353-
updateMachineControlPlaneLabel(ctx, machineName, namespace)
371+
updateMachineControlPlaneLabel(ctx, machineName)
354372
createBootstrapSecret(ctx, bootstrapName)
355373
loadBalancerID := createAPIServerLoadBalancer(ctx, fakeCloud)
356374
updateStackitClusterLoadBalancer(ctx, clusterName, namespace, loadBalancerID)
@@ -367,7 +385,7 @@ var _ = Describe("StackitMachine Controller", func() {
367385

368386
It("deletes the VM and removes the finalizer", func() {
369387
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
370-
updateMachineControlPlaneLabel(ctx, machineName, namespace)
388+
updateMachineControlPlaneLabel(ctx, machineName)
371389
enableStackitClusterLoadBalancer(ctx, clusterName, namespace)
372390
reconcileStackitClusterOnce(ctx, clusterName, namespace, fakeCloud)
373391
createBootstrapSecret(ctx, bootstrapName)
@@ -416,6 +434,63 @@ var _ = Describe("StackitMachine Controller", func() {
416434
"the server was created while the API server had no finalizer to clean it up")
417435
})
418436

437+
It("removes the finalizer when the server is already gone at deletion time", func() {
438+
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
439+
updateMachineControlPlaneLabel(ctx, machineName)
440+
enableStackitClusterLoadBalancer(ctx, clusterName, namespace)
441+
reconcileStackitClusterOnce(ctx, clusterName, namespace, fakeCloud)
442+
createBootstrapSecret(ctx, bootstrapName)
443+
444+
_, err := reconciler.Reconcile(ctx, request)
445+
Expect(err).NotTo(HaveOccurred())
446+
Expect(fakeCloud.ServerCount()).To(Equal(1))
447+
448+
stackitCluster := &infrav1.StackitCluster{}
449+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, stackitCluster)).To(Succeed())
450+
loadBalancerID := stackitCluster.Status.APIServerLoadBalancerID
451+
Expect(loadBalancerID).NotTo(BeEmpty())
452+
Expect(fakeCloud.LoadBalancerTargetCount(loadBalancerID)).To(Equal(1))
453+
454+
provisioned := &infrav1.StackitMachine{}
455+
Expect(k8sClient.Get(ctx, stackitKey, provisioned)).To(Succeed())
456+
instanceID := provisioned.Status.InstanceID
457+
Expect(instanceID).NotTo(BeEmpty())
458+
459+
By("removing the server behind the provider's back")
460+
Expect(fakeCloud.DeleteServer(ctx, instanceID)).To(Succeed())
461+
Expect(fakeCloud.ServerCount()).To(Equal(0))
462+
463+
By("reconciling into the terminal state")
464+
_, err = reconciler.Reconcile(ctx, request)
465+
Expect(err).NotTo(HaveOccurred())
466+
467+
degraded := &infrav1.StackitMachine{}
468+
Expect(k8sClient.Get(ctx, stackitKey, degraded)).To(Succeed())
469+
expectCondition(degraded.Status.Conditions, infrav1.MachineInstanceReadyCondition, metav1.ConditionFalse, "InstanceNotFound")
470+
Expect(degraded.Status.InstanceID).To(Equal(instanceID))
471+
Expect(fakeCloud.LoadBalancerTargetCount(loadBalancerID)).To(Equal(1),
472+
"the deletion must still find the load balancer target to remove")
473+
474+
By("deleting the object while the cloud reports the server as gone")
475+
// The fake deletes an unknown ID without an error, so the not-found answer
476+
// of the cloud must be injected.
477+
fakeCloud.FailNextDeleteServer = fmt.Errorf("delete server: %w", cloud.ErrNotFound)
478+
479+
Expect(k8sClient.Delete(ctx, degraded)).To(Succeed())
480+
481+
_, err = reconciler.Reconcile(ctx, request)
482+
Expect(err).NotTo(HaveOccurred())
483+
// The reconcile clears FailNextDeleteServer when it calls DeleteServer,
484+
// so a nil field proves the call happened.
485+
Expect(fakeCloud.FailNextDeleteServer).ToNot(HaveOccurred(),
486+
"the machine deletion did not ask the cloud to remove the server")
487+
Expect(fakeCloud.LoadBalancerTargetCount(loadBalancerID)).To(Equal(0))
488+
Eventually(func() bool {
489+
err := k8sClient.Get(ctx, stackitKey, &infrav1.StackitMachine{})
490+
return apierrors.IsNotFound(err)
491+
}).Should(BeTrue())
492+
})
493+
419494
// A lost status patch leaves a running, tagged server that no field on the
420495
// object names, so an empty status.instanceID is not proof that none exists.
421496
It("deletes a tagged server whose instance ID was lost from the status", func() {
@@ -442,7 +517,6 @@ var _ = Describe("StackitMachine Controller", func() {
442517

443518
_, err = reconciler.Reconcile(ctx, request)
444519
Expect(err).NotTo(HaveOccurred())
445-
446520
Expect(fakeCloud.ServerCount()).To(Equal(0),
447521
"the tagged server leaked because deletion trusted the empty status")
448522
Eventually(func() bool {
@@ -451,6 +525,28 @@ var _ = Describe("StackitMachine Controller", func() {
451525
}).Should(BeTrue())
452526
})
453527

528+
It("keeps the finalizer when the server deletion fails", func() {
529+
updateMachineBootstrapSecret(ctx, machineName, bootstrapName)
530+
createBootstrapSecret(ctx, bootstrapName)
531+
532+
_, err := reconciler.Reconcile(ctx, request)
533+
Expect(err).NotTo(HaveOccurred())
534+
Expect(fakeCloud.ServerCount()).To(Equal(1))
535+
536+
got := &infrav1.StackitMachine{}
537+
Expect(k8sClient.Get(ctx, stackitKey, got)).To(Succeed())
538+
fakeCloud.FailNextDeleteServer = fmt.Errorf("delete server: %w", cloud.ErrTransient)
539+
540+
Expect(k8sClient.Delete(ctx, got)).To(Succeed())
541+
_, err = reconciler.Reconcile(ctx, request)
542+
543+
Expect(err).To(MatchError(cloud.ErrTransient))
544+
Expect(fakeCloud.ServerCount()).To(Equal(1))
545+
stillThere := &infrav1.StackitMachine{}
546+
Expect(k8sClient.Get(ctx, stackitKey, stillThere)).To(Succeed())
547+
Expect(stillThere.Finalizers).To(ContainElement(infrav1.MachineFinalizer))
548+
})
549+
454550
It("maps owning Machine events to StackitMachine reconcile requests", func() {
455551
machine := &clusterv1.Machine{}
456552
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: machineName, Namespace: namespace}, machine)).To(Succeed())

‎controller/stackitmachine_infrastructure.go‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package controller
1212

1313
import (
1414
"context"
15+
"errors"
1516
"fmt"
1617
"time"
1718

@@ -32,6 +33,8 @@ import (
3233
"github.com/stackitcloud/cluster-api-provider-stackit/util"
3334
)
3435

36+
var errInstanceGone = errors.New("instance gone")
37+
3538
func (r *StackitMachineReconciler) reconcileNormal(ctx context.Context, machineScope *scope.MachineScope) (ctrl.Result, error) {
3639
log := logf.FromContext(ctx)
3740
stackitMachine := machineScope.StackitMachine
@@ -83,6 +86,21 @@ func (r *StackitMachineReconciler) reconcileNormal(ctx context.Context, machineS
8386
machineScope.SetConditions(metav1.ConditionTrue, "Available", "", infrav1.MachineCredentialsReadyCondition)
8487

8588
server, created, err := r.ensureServer(ctx, cloudClient, machineScope, bootstrapData)
89+
if errors.Is(err, errInstanceGone) {
90+
machineScope.SetNotReady(
91+
"InstanceNotFound",
92+
err.Error(),
93+
infrav1.MachineInstanceReadyCondition,
94+
infrav1.MachineReadyCondition,
95+
)
96+
if r.Recorder != nil {
97+
r.Recorder.Eventf(
98+
stackitMachine, nil, corev1.EventTypeWarning, "InstanceNotFound", "Reconcile",
99+
"Server %s no longer exists; the Machine must be replaced", stackitMachine.Status.InstanceID,
100+
)
101+
}
102+
return ctrl.Result{}, nil
103+
}
86104
if err != nil {
87105
machineScope.SetNotReady(
88106
"InstanceError",
@@ -285,8 +303,8 @@ func (r *StackitMachineReconciler) ensureServer(
285303
// identity, so surface the loss and let Cluster API replace the Machine.
286304
if stackitMachine.Status.Initialization.Provisioned {
287305
return nil, false, fmt.Errorf(
288-
"%w: server %s for already-provisioned machine no longer exists; the Machine must be replaced",
289-
cloud.ErrNotFound, stackitMachine.Status.InstanceID,
306+
"%w: server %s; the Machine must be replaced",
307+
errInstanceGone, stackitMachine.Status.InstanceID,
290308
)
291309
}
292310

0 commit comments

Comments
 (0)