Skip to content

Commit 4306e61

Browse files
committed
merge from upstream
2 parents 064a773 + 1de7a5d commit 4306e61

9 files changed

+206
-34
lines changed

api/v1beta2/cloudstackcluster_webhook.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package v1beta2
1818

1919
import (
2020
"fmt"
21-
"reflect"
2221

2322
"k8s.io/apimachinery/pkg/api/errors"
2423
"k8s.io/apimachinery/pkg/runtime"
@@ -95,13 +94,12 @@ func (r *CloudStackCluster) ValidateUpdate(old runtime.Object) error {
9594
}
9695
oldSpec := oldCluster.Spec
9796

98-
// No spec fields may be updated.
9997
errorList := field.ErrorList(nil)
10098

101-
if !reflect.DeepEqual(oldSpec.FailureDomains, spec.FailureDomains) {
102-
errorList = append(errorList, field.Forbidden(
103-
field.NewPath("spec", "FailureDomains"), "FailureDomains and sub-attributes may not be modified after creation"))
99+
if err := ValidateFailureDomainUpdates(oldSpec.FailureDomains, spec.FailureDomains); err != nil {
100+
errorList = append(errorList, err)
104101
}
102+
105103
if oldSpec.ControlPlaneEndpoint.Host != "" { // Need to allow one time endpoint setting via CAPC cluster controller.
106104
errorList = webhookutil.EnsureStringFieldsAreEqual(
107105
spec.ControlPlaneEndpoint.Host, oldSpec.ControlPlaneEndpoint.Host, "controlplaneendpoint.host", errorList)
@@ -113,6 +111,43 @@ func (r *CloudStackCluster) ValidateUpdate(old runtime.Object) error {
113111
return webhookutil.AggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, errorList)
114112
}
115113

114+
// ValidateFailureDomainUpdates verifies that at least one failure domain has not been deleted, and
115+
// failure domains that are held over have not been modified.
116+
func ValidateFailureDomainUpdates(oldFDs, newFDs []CloudStackFailureDomainSpec) *field.Error {
117+
newFDsByName := map[string]CloudStackFailureDomainSpec{}
118+
for _, newFD := range newFDs {
119+
newFDsByName[newFD.Name] = newFD
120+
}
121+
122+
atLeastOneRemains := false
123+
for _, oldFD := range oldFDs {
124+
if newFD, present := newFDsByName[oldFD.Name]; present {
125+
atLeastOneRemains = true
126+
if !FailureDomainsEqual(newFD, oldFD) {
127+
return field.Forbidden(field.NewPath("spec", "FailureDomains"),
128+
fmt.Sprintf("Cannot change FailureDomain %s", oldFD.Name))
129+
}
130+
}
131+
}
132+
if !atLeastOneRemains {
133+
return field.Forbidden(field.NewPath("spec", "FailureDomains"), "At least one FailureDomain must be unchanged on udpate.")
134+
}
135+
return nil
136+
}
137+
138+
// FailureDomainsEqual is a manual deep equal on failure domains.
139+
func FailureDomainsEqual(fd1, fd2 CloudStackFailureDomainSpec) bool {
140+
return fd1.Name == fd2.Name &&
141+
fd1.ACSEndpoint == fd2.ACSEndpoint &&
142+
fd1.Account == fd2.Account &&
143+
fd1.Domain == fd2.Domain &&
144+
fd1.Zone.Name == fd2.Zone.Name &&
145+
fd1.Zone.ID == fd2.Zone.ID &&
146+
fd1.Zone.Network.Name == fd2.Zone.Network.Name &&
147+
fd1.Zone.Network.ID == fd2.Zone.Network.ID &&
148+
fd1.Zone.Network.Type == fd2.Zone.Network.Type
149+
}
150+
116151
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
117152
func (r *CloudStackCluster) ValidateDelete() error {
118153
cloudstackclusterlog.V(1).Info("entered validate delete webhook", "api resource name", r.Name)

api/v1beta2/cloudstackfailuredomain_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
const (
2525
FailureDomainFinalizer = "cloudstackfailuredomain.infrastructure.cluster.x-k8s.io"
26+
FailureDomainLabelName = "cloudstackfailuredomain.infrastructure.cluster.x-k8s.io/name"
2627
NetworkTypeIsolated = "Isolated"
2728
NetworkTypeShared = "Shared"
2829
)

controllers/cloudstackcluster_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func (r *CloudStackClusterReconciliationRunner) Reconcile() (res ctrl.Result, re
9090
r.SetFailureDomainsStatusMap,
9191
r.CreateFailureDomains(r.ReconciliationSubject.Spec.FailureDomains),
9292
r.GetFailureDomains(r.FailureDomains),
93+
r.RemoveExtraneousFailureDomains(r.FailureDomains),
9394
r.VerifyFailureDomainCRDs,
9495
r.SetReady)
9596
}

controllers/cloudstackfailuredomain_controller.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"context"
2121

2222
"github.com/pkg/errors"
23+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2324
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
2426
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2527

2628
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta2"
@@ -105,21 +107,49 @@ func (r *CloudStackFailureDomainReconciliationRunner) Reconcile() (retRes ctrl.R
105107
}
106108

107109
// ReconcileDelete on the ReconciliationRunner attempts to delete the reconciliation subject.
108-
func (r *CloudStackFailureDomainReconciliationRunner) ReconcileDelete() (retRes ctrl.Result, retErr error) {
110+
func (r *CloudStackFailureDomainReconciliationRunner) ReconcileDelete() (ctrl.Result, error) {
109111
r.Log.Info("Deleting CloudStackFailureDomain")
110-
// Address Isolated Networks.
111-
if r.ReconciliationSubject.Spec.Zone.Network.Type == infrav1.NetworkTypeIsolated {
112-
netName := r.ReconciliationSubject.Spec.Zone.Network.Name
113-
if res, err := r.GetObjectByName(r.IsoNetMetaName(netName), r.IsoNet)(); r.ShouldReturn(res, err) {
114-
return res, err
115-
}
116-
if r.IsoNet.Name != "" {
117-
if err := r.K8sClient.Delete(r.RequestCtx, r.IsoNet); err != nil {
118-
return ctrl.Result{}, err
112+
113+
return r.RunReconciliationStages(
114+
r.ClearMachines,
115+
r.DeleteOwnedObjects(
116+
infrav1.GroupVersion.WithKind("CloudStackAffinityGroup"),
117+
infrav1.GroupVersion.WithKind("CloudStackIsolatedNetwork")),
118+
r.CheckOwnedObjectsDeleted(
119+
infrav1.GroupVersion.WithKind("CloudStackAffinityGroup"),
120+
infrav1.GroupVersion.WithKind("CloudStackIsolatedNetwork")),
121+
r.RemoveFinalizer,
122+
)
123+
}
124+
125+
// ClearMachines checks for any machines in failure domain, deletes the CAPI machine for any still in FailureDomain,
126+
// and requeus until all CloudStack machines are cleared from the FailureDomain.
127+
func (r *CloudStackFailureDomainReconciliationRunner) ClearMachines() (ctrl.Result, error) {
128+
machines := &infrav1.CloudStackMachineList{}
129+
if err := r.K8sClient.List(r.RequestCtx, machines, client.MatchingLabels{infrav1.FailureDomainLabelName: r.ReconciliationSubject.Name}); err != nil {
130+
return ctrl.Result{}, err
131+
}
132+
// Deleted CAPI machines for CloudStack machines found.
133+
for _, machine := range machines.Items {
134+
for _, ref := range machine.OwnerReferences {
135+
if ref.Kind == "Machine" {
136+
machine := &clusterv1.Machine{}
137+
machine.Name = ref.Name
138+
machine.Namespace = r.ReconciliationSubject.Namespace
139+
if err := r.K8sClient.Delete(r.RequestCtx, machine); err != nil {
140+
return ctrl.Result{}, err
141+
}
119142
}
120-
return r.RequeueWithMessage("Child IsolatedNetwork still present, requeueing.")
121143
}
122144
}
145+
if len(machines.Items) > 0 {
146+
return r.RequeueWithMessage("FailureDomain still has machine in it.")
147+
}
148+
return ctrl.Result{}, nil
149+
}
150+
151+
// RemoveFinalizer just removes the finalizer from the failure domain.
152+
func (r *CloudStackFailureDomainReconciliationRunner) RemoveFinalizer() (ctrl.Result, error) {
123153
controllerutil.RemoveFinalizer(r.ReconciliationSubject, infrav1.FailureDomainFinalizer)
124154
return ctrl.Result{}, nil
125155
}

controllers/cloudstackmachine_controller.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,23 @@ func (r *CloudStackMachineReconciliationRunner) ConsiderAffinity() (ctrl.Result,
121121
r.Log.Info("getting affinity group name", err)
122122
}
123123

124+
// Set failure domain name and owners.
124125
r.AffinityGroup.Spec.FailureDomainName = r.ReconciliationSubject.Spec.FailureDomainName
125-
if res, err := r.GetOrCreateAffinityGroup(agName, r.ReconciliationSubject.Spec.Affinity, r.AffinityGroup)(); r.ShouldReturn(res, err) {
126+
if res, err := r.GetOrCreateAffinityGroup(
127+
agName, r.ReconciliationSubject.Spec.Affinity, r.AffinityGroup, r.FailureDomain)(); r.ShouldReturn(res, err) {
126128
return res, err
127129
}
128130
if !r.AffinityGroup.Status.Ready {
129131
return r.RequeueWithMessage("Required affinity group not ready.")
130132
}
133+
131134
return ctrl.Result{}, nil
132135
}
133136

134137
// SetFailureDomainOnCSMachine sets the failure domain the machine should launch in.
135138
func (r *CloudStackMachineReconciliationRunner) SetFailureDomainOnCSMachine() (retRes ctrl.Result, reterr error) {
136-
if r.ReconciliationSubject.Spec.FailureDomainName == "" { // Needs random FD, but not yet set.
137-
name := ""
139+
if r.ReconciliationSubject.Spec.FailureDomainName == "" {
140+
var name string
138141
if r.CAPIMachine.Spec.FailureDomain != nil &&
139142
(util.IsControlPlaneMachine(r.CAPIMachine) || // Is control plane machine -- CAPI will specify.
140143
*r.CAPIMachine.Spec.FailureDomain != "") { // Or potentially another machine controller specified.
@@ -148,6 +151,7 @@ func (r *CloudStackMachineReconciliationRunner) SetFailureDomainOnCSMachine() (r
148151
name = name + "-" + r.CAPICluster.Name
149152
}
150153
r.ReconciliationSubject.Spec.FailureDomainName = name
154+
r.ReconciliationSubject.Labels[infrav1.FailureDomainLabelName] = r.ReconciliationSubject.Spec.FailureDomainName
151155
}
152156
return ctrl.Result{}, nil
153157
}

controllers/utils/affinity_group.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ import (
2828
"sigs.k8s.io/controller-runtime/pkg/client"
2929
)
3030

31-
// GenerateIsolatedNetwork of the passed name that's owned by the ReconciliationSubject.
32-
func (r *ReconciliationRunner) GetOrCreateAffinityGroup(name string, affinityType string, ag *infrav1.CloudStackAffinityGroup) CloudStackReconcilerMethod {
33-
return func() (ctrl.Result, error) {
31+
// GetOrCreateAffinityGroup of the passed name that's owned by the failure domain of the reconciliation subject and
32+
// the control plane that manages it.
33+
func (r *ReconciliationRunner) GetOrCreateAffinityGroup(
34+
name string,
35+
affinityType string,
36+
ag *infrav1.CloudStackAffinityGroup,
37+
fd *infrav1.CloudStackFailureDomain) CloudStackReconcilerMethod {
3438

39+
return func() (ctrl.Result, error) {
3540
// Start by attempting a fetch.
3641
lowerName := strings.ToLower(name)
3742
namespace := r.ReconciliationSubject.GetNamespace()
@@ -56,7 +61,7 @@ func (r *ReconciliationRunner) GetOrCreateAffinityGroup(name string, affinityTyp
5661
ag.Spec.Name = name
5762
ag.ObjectMeta = r.NewChildObjectMeta(lowerName)
5863

59-
// Replace owner reference with controller of CAPI and CloudStack machines.
64+
// Replace owner reference with controller of CAPI and CloudStack machines and FailureDomain.
6065
for _, ref := range r.ReconciliationSubject.GetOwnerReferences() {
6166
if strings.EqualFold(ref.Kind, "EtcdadmCluster") ||
6267
strings.EqualFold(ref.Kind, "KubeadmControlPlane") ||
@@ -65,6 +70,13 @@ func (r *ReconciliationRunner) GetOrCreateAffinityGroup(name string, affinityTyp
6570
break
6671
}
6772
}
73+
ag.OwnerReferences = append(ag.OwnerReferences,
74+
metav1.OwnerReference{
75+
Name: fd.Name,
76+
Kind: fd.Kind,
77+
APIVersion: fd.APIVersion,
78+
UID: fd.UID,
79+
})
6880

6981
if err := r.K8sClient.Create(r.RequestCtx, ag); err != nil && !ContainsAlreadyExistsSubstring(err) {
7082
return r.ReturnWrappedError(err, "creating affinity group CRD")

controllers/utils/base_reconciler.go

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,11 @@ func (r *ReconciliationRunner) CheckOwnedCRDsForReadiness(gvks ...schema.GroupVe
232232
} else if !found {
233233
return r.RequeueWithMessage(
234234
fmt.Sprintf(
235-
"Owned object of kind %s with name %s not found, requeueing.",
235+
"Owned object of kind %s with name %s not found, requeuing.",
236236
gvk.Kind,
237237
owned.GetName()))
238238
} else {
239-
r.Log.Info(fmt.Sprintf("Owned object %s of kind %s not ready, requeueing", name, gvk.Kind))
239+
r.Log.Info(fmt.Sprintf("Owned object %s of kind %s not ready, requeuing", name, gvk.Kind))
240240
return ctrl.Result{RequeueAfter: RequeueTimeout}, nil
241241
}
242242
}
@@ -246,6 +246,66 @@ func (r *ReconciliationRunner) CheckOwnedCRDsForReadiness(gvks ...schema.GroupVe
246246
}
247247
}
248248

249+
// CheckOwnedObjectsDeleted queries for the presence of owned objects and requeues if any are still present. Primarily
250+
// used to prevent deletions of owners before dependents.
251+
func (r *ReconciliationRunner) DeleteOwnedObjects(gvks ...schema.GroupVersionKind) CloudStackReconcilerMethod {
252+
return func() (ctrl.Result, error) {
253+
// For each GroupVersionKind...
254+
for _, gvk := range gvks {
255+
// Query to find objects of this kind.
256+
potentiallyOnwedObjs := &unstructured.UnstructuredList{}
257+
potentiallyOnwedObjs.SetGroupVersionKind(gvk)
258+
err := r.K8sClient.List(r.RequestCtx, potentiallyOnwedObjs)
259+
if err != nil {
260+
return ctrl.Result{}, errors.Wrapf(err, "requesting owned objects with gvk %s", gvk)
261+
}
262+
263+
// Filter objects not actually owned by reconciliation subject via owner reference UID.
264+
for _, pOwned := range potentiallyOnwedObjs.Items {
265+
_pOwned := pOwned
266+
refs := pOwned.GetOwnerReferences()
267+
for _, ref := range refs {
268+
if ref.UID == r.ReconciliationSubject.GetUID() {
269+
if err := r.K8sClient.Delete(r.RequestCtx, &_pOwned); err != nil {
270+
return ctrl.Result{}, err
271+
}
272+
}
273+
}
274+
}
275+
}
276+
return ctrl.Result{}, nil
277+
}
278+
}
279+
280+
// CheckOwnedObjectsDeleted queries for the presence of owned objects and requeues if any are still present. Primarily
281+
// used to prevent deletions of owners before dependents.
282+
func (r *ReconciliationRunner) CheckOwnedObjectsDeleted(gvks ...schema.GroupVersionKind) CloudStackReconcilerMethod {
283+
return func() (ctrl.Result, error) {
284+
// For each GroupVersionKind...
285+
for _, gvk := range gvks {
286+
// Query to find objects of this kind.
287+
potentiallyOnwedObjs := &unstructured.UnstructuredList{}
288+
potentiallyOnwedObjs.SetGroupVersionKind(gvk)
289+
err := r.K8sClient.List(r.RequestCtx, potentiallyOnwedObjs)
290+
if err != nil {
291+
return ctrl.Result{}, errors.Wrapf(err, "requesting owned objects with gvk %s", gvk)
292+
}
293+
294+
// Filter objects not actually owned by reconciliation subject via owner reference UID.
295+
for _, pOwned := range potentiallyOnwedObjs.Items {
296+
refs := pOwned.GetOwnerReferences()
297+
for _, ref := range refs {
298+
if ref.UID == r.ReconciliationSubject.GetUID() {
299+
return r.RequeueWithMessage(
300+
fmt.Sprintf("Owned object %s of kind %s not yet deleted", pOwned.GetKind(), pOwned.GetName()))
301+
}
302+
}
303+
}
304+
}
305+
return ctrl.Result{}, nil
306+
}
307+
}
308+
249309
// RequeueIfCloudStackClusterNotReady requeues the reconciliation request if the CloudStackCluster is not ready.
250310
func (r *ReconciliationRunner) RequeueIfCloudStackClusterNotReady() (ctrl.Result, error) {
251311
if !r.CSCluster.Status.Ready {
@@ -272,9 +332,9 @@ func (r *ReconciliationRunner) PatchChangesBackToAPI() (res ctrl.Result, retErr
272332

273333
// RequeueWithMessage is a convenience method to log requeue message and then return a result with RequeueAfter set.
274334
func (r *ReconciliationRunner) RequeueWithMessage(msg string, keysAndValues ...interface{}) (ctrl.Result, error) {
275-
// Add requeueing to message if not present. Might turn this into a lint check later.
276-
if !strings.Contains(strings.ToLower(msg), "requeue") {
277-
msg = msg + " Requeueing."
335+
// Add requeuing to message if not present. Might turn this into a lint check later.
336+
if !strings.Contains(strings.ToLower(msg), "requeu") {
337+
msg = msg + " Requeuing."
278338
}
279339
r.Log.Info(msg, keysAndValues...)
280340
return ctrl.Result{RequeueAfter: RequeueTimeout}, nil
@@ -284,7 +344,7 @@ func (r *ReconciliationRunner) RequeueWithMessage(msg string, keysAndValues ...i
284344
func (r *ReconciliationRunner) RequeueWithMessageStage(msg string, keysAndValues ...interface{}) CloudStackReconcilerMethod {
285345
return func() (ctrl.Result, error) {
286346
if !strings.Contains(strings.ToLower(msg), "requeue") {
287-
msg = msg + " Requeueing."
347+
msg = msg + " Requeuing."
288348
}
289349
r.Log.Info(msg, keysAndValues...)
290350
return ctrl.Result{RequeueAfter: RequeueTimeout}, nil
@@ -381,6 +441,7 @@ func (r *ReconciliationRunner) GetReconciliationSubject() (res ctrl.Result, rete
381441
r.Log.V(1).Info("Getting reconciliation subject.")
382442
err := client.IgnoreNotFound(r.K8sClient.Get(r.RequestCtx, r.Request.NamespacedName, r.ReconciliationSubject))
383443
if r.ReconciliationSubject.GetName() == "" { // Resource does not exist. No need to reconcile.
444+
r.Log.V(1).Info("Resource not found. Exiting reconciliation.")
384445
r.SetReturnEarly()
385446
}
386447
if err != nil {
@@ -428,7 +489,7 @@ func (r *ReconciliationRunner) NewChildObjectMeta(name string) metav1.ObjectMeta
428489
Name: strings.ToLower(name),
429490
Namespace: r.Request.Namespace,
430491
Labels: map[string]string{clusterv1.ClusterLabelName: r.CAPICluster.Name,
431-
infrav1.CloudStackClusterLabelName: r.CSCluster.ClusterName},
492+
infrav1.CloudStackClusterLabelName: r.CSCluster.Name},
432493
OwnerReferences: []metav1.OwnerReference{
433494
*metav1.NewControllerRef(r.ReconciliationSubject, ownerGVK),
434495
},
@@ -438,11 +499,11 @@ func (r *ReconciliationRunner) NewChildObjectMeta(name string) metav1.ObjectMeta
438499
// RequeueIfMissingBaseCRs checks that the ReconciliationSubject, CAPI Cluster, and CloudStackCluster objects were
439500
// actually fetched and reques if not. The base reconciliation stages will continue even if not so as to allow deletion.
440501
func (r *ReconciliationRunner) RequeueIfMissingBaseCRs() (ctrl.Result, error) {
441-
r.Log.V(1).Info("Requeueing if missing ReconciliationSubject, CloudStack cluster, or CAPI cluster.")
502+
r.Log.V(1).Info("Requeuing if missing ReconciliationSubject, CloudStack cluster, or CAPI cluster.")
442503
if r.CSCluster.GetName() == "" {
443-
return r.RequeueWithMessage("CloudStackCluster wasn't found. Requeueing.")
504+
return r.RequeueWithMessage("CloudStackCluster wasn't found. Requeuing.")
444505
} else if r.CAPICluster.GetName() == "" {
445-
return r.RequeueWithMessage("CAPI Cluster wasn't found. Requeueing.")
506+
return r.RequeueWithMessage("CAPI Cluster wasn't found. Requeuing.")
446507
}
447508
return ctrl.Result{}, nil
448509
}

0 commit comments

Comments
 (0)