@@ -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 ())
0 commit comments