Skip to content

Commit d0b2075

Browse files
committed
Deregister control plane VM from Kubernetes_API_Server LB rule on CloudStackMachine delete
1 parent 5eca360 commit d0b2075

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

‎controllers/cloudstackmachine_controller.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,45 @@ func (r *CloudStackMachineReconciliationRunner) AddToLBIfNeeded() (retRes ctrl.R
311311
return ctrl.Result{}, nil
312312
}
313313

314+
// RemoveFromLBIfNeeded removes the instance from the API server load balancer rule if it is a
315+
// control plane machine on a non-routed isolated network. Called from ReconcileDelete so the VM
316+
// is detached before being destroyed.
317+
//
318+
// The control-plane check uses the MachineControlPlaneLabel on the CloudStackMachine because the
319+
// CAPI Machine is not loaded on the delete path (see common stages in Reconcile).
320+
func (r *CloudStackMachineReconciliationRunner) RemoveFromLBIfNeeded() (retRes ctrl.Result, reterr error) {
321+
if _, ok := r.ReconciliationSubject.Labels[clusterv1.MachineControlPlaneLabel]; !ok {
322+
return ctrl.Result{}, nil
323+
}
324+
if r.FailureDomain.Spec.Zone.Network.Type != cloud.NetworkTypeIsolated {
325+
return ctrl.Result{}, nil
326+
}
327+
if r.ReconciliationSubject.Spec.InstanceID == nil {
328+
return ctrl.Result{}, nil
329+
}
330+
331+
// IsoNet is not pre-loaded on the delete path; fetch it by the same meta name AddToLBIfNeeded relies on.
332+
if res, err := r.GetObjectByName(
333+
r.IsoNetMetaName(r.FailureDomain.Spec.Zone.Network.Name), r.IsoNet,
334+
)(); r.ShouldReturn(res, err) {
335+
return res, err
336+
}
337+
if r.IsoNet.Spec.Name == "" {
338+
// Isolated network object already gone -- nothing to detach from.
339+
return ctrl.Result{}, nil
340+
}
341+
if r.IsoNet.Status.RoutingMode != "" {
342+
// Routed isolated networks do not use a load balancer.
343+
return ctrl.Result{}, nil
344+
}
345+
346+
r.Log.Info("Removing VM from load balancer rule.", "instance-id", *r.ReconciliationSubject.Spec.InstanceID)
347+
if err := r.CSUser.RemoveVMFromLoadBalancerRule(r.IsoNet, *r.ReconciliationSubject.Spec.InstanceID); err != nil {
348+
return ctrl.Result{}, err
349+
}
350+
return ctrl.Result{}, nil
351+
}
352+
314353
// GetOrCreateMachineStateChecker creates or gets CloudStackMachineStateChecker object.
315354
func (r *CloudStackMachineReconciliationRunner) GetOrCreateMachineStateChecker() (retRes ctrl.Result, reterr error) {
316355
checkerName := r.ReconciliationSubject.Spec.InstanceID
@@ -344,6 +383,14 @@ func (r *CloudStackMachineReconciliationRunner) ReconcileDelete() (retRes ctrl.R
344383
return ctrl.Result{}, err
345384
}
346385
}
386+
387+
// For control plane machines on a non-routed isolated network, remove the VM from the
388+
// API server load balancer rule before destroying the VM, so CloudStack does not keep
389+
// routing API traffic to a VM that's about to be expunged.
390+
if res, err := r.RemoveFromLBIfNeeded(); r.ShouldReturn(res, err) {
391+
return res, err
392+
}
393+
347394
r.Recorder.Eventf(r.ReconciliationSubject, "Normal", "Deleting", CSMachineDeletionMessage, r.ReconciliationSubject.Name)
348395
r.Log.Info("Deleting instance", "instance-id", r.ReconciliationSubject.Spec.InstanceID)
349396
// Use CSClient instead of CSUser here to expunge as admin.

‎docs/book/src/topics/cloudstack-permissions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The account that CAPC runs under must minimally be a User type account with a ro
3434
* listVolumes
3535
* listZones
3636
* queryAsyncJobResult
37+
* removeFromLoadBalancerRule
3738
* startVirtualMachine
3839
* stopVirtualMachine
3940
* updateVMAffinityGroup

‎pkg/cloud/isolated_network.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type IsoNetworkIface interface {
3535
ResolveLoadBalancerRuleDetails(*infrav1.CloudStackIsolatedNetwork) error
3636

3737
AssignVMToLoadBalancerRule(isoNet *infrav1.CloudStackIsolatedNetwork, instanceID string) error
38+
RemoveVMFromLoadBalancerRule(isoNet *infrav1.CloudStackIsolatedNetwork, instanceID string) error
3839
DeleteNetwork(infrav1.Network) error
3940
DisposeIsoNetResources(*infrav1.CloudStackIsolatedNetwork, *infrav1.CloudStackCluster) error
4041
}
@@ -465,6 +466,39 @@ func (c *client) AssignVMToLoadBalancerRule(isoNet *infrav1.CloudStackIsolatedNe
465466
return retErr
466467
}
467468

469+
// RemoveVMFromLoadBalancerRule removes a VM instance from the load balancer rule referenced by
470+
// isoNet.Status.LBRuleID. The call is idempotent: if the rule is unknown or the VM is not currently
471+
// a member, it returns nil.
472+
func (c *client) RemoveVMFromLoadBalancerRule(isoNet *infrav1.CloudStackIsolatedNetwork, instanceID string) error {
473+
if isoNet.Status.LBRuleID == "" {
474+
return nil
475+
}
476+
477+
lbRuleInstances, err := c.cs.LoadBalancer.ListLoadBalancerRuleInstances(
478+
c.cs.LoadBalancer.NewListLoadBalancerRuleInstancesParams(isoNet.Status.LBRuleID))
479+
if err != nil {
480+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
481+
return err
482+
}
483+
484+
found := false
485+
for _, instance := range lbRuleInstances.LoadBalancerRuleInstances {
486+
if instance.Id == instanceID {
487+
found = true
488+
break
489+
}
490+
}
491+
if !found {
492+
return nil
493+
}
494+
495+
p := c.cs.LoadBalancer.NewRemoveFromLoadBalancerRuleParams(isoNet.Status.LBRuleID)
496+
p.SetVirtualmachineids([]string{instanceID})
497+
_, err = c.cs.LoadBalancer.RemoveFromLoadBalancerRule(p)
498+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
499+
return err
500+
}
501+
468502
// DeleteNetwork deletes an isolated network.
469503
func (c *client) DeleteNetwork(net infrav1.Network) error {
470504
_, err := c.cs.Network.DeleteNetwork(c.cs.Network.NewDeleteNetworkParams(net.ID))

‎pkg/cloud/isolated_network_test.go‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,66 @@ var _ = ginkgo.Describe("Network", func() {
423423
})
424424
})
425425

426+
ginkgo.Context("Remove VM from Load Balancer rule", func() {
427+
ginkgo.It("no-op when LBRuleID is empty", func() {
428+
dummies.CSISONet1.Status.LBRuleID = ""
429+
430+
gomega.Ω(client.RemoveVMFromLoadBalancerRule(dummies.CSISONet1, *dummies.CSMachine1.Spec.InstanceID)).Should(gomega.Succeed())
431+
})
432+
433+
ginkgo.It("no-op when VM is not a member of the rule", func() {
434+
dummies.CSISONet1.Status.LBRuleID = "lbruleid"
435+
lbip := &csapi.ListLoadBalancerRuleInstancesParams{}
436+
lbs.EXPECT().NewListLoadBalancerRuleInstancesParams(dummies.CSISONet1.Status.LBRuleID).Return(lbip)
437+
lbs.EXPECT().ListLoadBalancerRuleInstances(lbip).Return(&csapi.ListLoadBalancerRuleInstancesResponse{}, nil)
438+
439+
gomega.Ω(client.RemoveVMFromLoadBalancerRule(dummies.CSISONet1, *dummies.CSMachine1.Spec.InstanceID)).Should(gomega.Succeed())
440+
})
441+
442+
ginkgo.It("removes the VM when it is a member of the rule", func() {
443+
dummies.CSISONet1.Status.LBRuleID = "lbruleid"
444+
lbip := &csapi.ListLoadBalancerRuleInstancesParams{}
445+
rfp := &csapi.RemoveFromLoadBalancerRuleParams{}
446+
lbs.EXPECT().NewListLoadBalancerRuleInstancesParams(dummies.CSISONet1.Status.LBRuleID).Return(lbip)
447+
lbs.EXPECT().ListLoadBalancerRuleInstances(lbip).Return(&csapi.ListLoadBalancerRuleInstancesResponse{
448+
Count: 1,
449+
LoadBalancerRuleInstances: []*csapi.VirtualMachine{{
450+
Id: *dummies.CSMachine1.Spec.InstanceID,
451+
}},
452+
}, nil)
453+
lbs.EXPECT().NewRemoveFromLoadBalancerRuleParams(dummies.CSISONet1.Status.LBRuleID).Return(rfp)
454+
lbs.EXPECT().RemoveFromLoadBalancerRule(rfp).Return(&csapi.RemoveFromLoadBalancerRuleResponse{}, nil)
455+
456+
gomega.Ω(client.RemoveVMFromLoadBalancerRule(dummies.CSISONet1, *dummies.CSMachine1.Spec.InstanceID)).Should(gomega.Succeed())
457+
})
458+
459+
ginkgo.It("returns an error when listing rule instances fails", func() {
460+
dummies.CSISONet1.Status.LBRuleID = "lbruleid"
461+
lbip := &csapi.ListLoadBalancerRuleInstancesParams{}
462+
lbs.EXPECT().NewListLoadBalancerRuleInstancesParams(dummies.CSISONet1.Status.LBRuleID).Return(lbip)
463+
lbs.EXPECT().ListLoadBalancerRuleInstances(lbip).Return(nil, fakeError)
464+
465+
gomega.Ω(client.RemoveVMFromLoadBalancerRule(dummies.CSISONet1, *dummies.CSMachine1.Spec.InstanceID)).ShouldNot(gomega.Succeed())
466+
})
467+
468+
ginkgo.It("returns an error when CloudStack rejects the removal", func() {
469+
dummies.CSISONet1.Status.LBRuleID = "lbruleid"
470+
lbip := &csapi.ListLoadBalancerRuleInstancesParams{}
471+
rfp := &csapi.RemoveFromLoadBalancerRuleParams{}
472+
lbs.EXPECT().NewListLoadBalancerRuleInstancesParams(dummies.CSISONet1.Status.LBRuleID).Return(lbip)
473+
lbs.EXPECT().ListLoadBalancerRuleInstances(lbip).Return(&csapi.ListLoadBalancerRuleInstancesResponse{
474+
Count: 1,
475+
LoadBalancerRuleInstances: []*csapi.VirtualMachine{{
476+
Id: *dummies.CSMachine1.Spec.InstanceID,
477+
}},
478+
}, nil)
479+
lbs.EXPECT().NewRemoveFromLoadBalancerRuleParams(dummies.CSISONet1.Status.LBRuleID).Return(rfp)
480+
lbs.EXPECT().RemoveFromLoadBalancerRule(rfp).Return(nil, fakeError)
481+
482+
gomega.Ω(client.RemoveVMFromLoadBalancerRule(dummies.CSISONet1, *dummies.CSMachine1.Spec.InstanceID)).ShouldNot(gomega.Succeed())
483+
})
484+
})
485+
426486
ginkgo.Context("load balancer rule does not exist", func() {
427487
ginkgo.It("calls cloudstack to create a new load balancer rule.", func() {
428488
lbs.EXPECT().NewListLoadBalancerRulesParams().Return(&csapi.ListLoadBalancerRulesParams{})

0 commit comments

Comments
 (0)