From fd0b16e07fdc5a72d6c228a2f360179b9f751894 Mon Sep 17 00:00:00 2001 From: Alexander Eldeib Date: Wed, 27 May 2020 19:05:55 -0700 Subject: [PATCH] :sparkles: enable BYO IP address for control plane Signed-off-by: Alexander Eldeib --- api/v1alpha2/azurecluster_conversion.go | 1 + api/v1alpha2/zz_generated.conversion.go | 3 +- api/v1alpha3/azurecluster_validation.go | 29 +++++---- api/v1alpha3/azurecluster_webhook.go | 46 ++++++++++++-- api/v1alpha3/types.go | 16 ++++- api/v1alpha3/zz_generated.deepcopy.go | 20 ++++++ cloud/scope/cluster.go | 33 +++++++++- cloud/scope/machine.go | 13 ++-- cloud/services/publicips/publicips.go | 63 ++++++++++--------- cloud/services/publicips/publicips_test.go | 12 ++-- ...ucture.cluster.x-k8s.io_azureclusters.yaml | 16 +++++ controllers/azurecluster_controller.go | 2 +- controllers/azurecluster_reconciler.go | 21 +------ 13 files changed, 198 insertions(+), 77 deletions(-) diff --git a/api/v1alpha2/azurecluster_conversion.go b/api/v1alpha2/azurecluster_conversion.go index 052d7590c64..e0b10d27a4c 100644 --- a/api/v1alpha2/azurecluster_conversion.go +++ b/api/v1alpha2/azurecluster_conversion.go @@ -59,6 +59,7 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint } dst.Status.FailureDomains = restored.Status.FailureDomains + dst.Spec.NetworkSpec.PublicIP = restored.Spec.NetworkSpec.PublicIP for _, restoredSubnet := range restored.Spec.NetworkSpec.Subnets { if restoredSubnet != nil { diff --git a/api/v1alpha2/zz_generated.conversion.go b/api/v1alpha2/zz_generated.conversion.go index 85b91581509..167d4641db2 100644 --- a/api/v1alpha2/zz_generated.conversion.go +++ b/api/v1alpha2/zz_generated.conversion.go @@ -943,6 +943,7 @@ func autoConvert_v1alpha2_NetworkSpec_To_v1alpha3_NetworkSpec(in *NetworkSpec, o } func autoConvert_v1alpha3_NetworkSpec_To_v1alpha2_NetworkSpec(in *v1alpha3.NetworkSpec, out *NetworkSpec, s conversion.Scope) error { + // WARNING: in.PublicIP requires manual conversion: does not exist in peer-type if err := Convert_v1alpha3_VnetSpec_To_v1alpha2_VnetSpec(&in.Vnet, &out.Vnet, s); err != nil { return err } @@ -1004,9 +1005,9 @@ func Convert_v1alpha2_PublicIP_To_v1alpha3_PublicIP(in *PublicIP, out *v1alpha3. func autoConvert_v1alpha3_PublicIP_To_v1alpha2_PublicIP(in *v1alpha3.PublicIP, out *PublicIP, s conversion.Scope) error { out.ID = in.ID - out.Name = in.Name out.IPAddress = in.IPAddress out.DNSName = in.DNSName + out.Name = in.Name return nil } diff --git a/api/v1alpha3/azurecluster_validation.go b/api/v1alpha3/azurecluster_validation.go index b7d566b73ae..9f8f0a61770 100644 --- a/api/v1alpha3/azurecluster_validation.go +++ b/api/v1alpha3/azurecluster_validation.go @@ -20,8 +20,6 @@ import ( "fmt" "regexp" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -34,16 +32,8 @@ const ( ) // validateCluster validates a cluster -func (c *AzureCluster) validateCluster() error { - var allErrs field.ErrorList - allErrs = append(allErrs, c.validateClusterSpec()...) - if len(allErrs) == 0 { - return nil - } - - return apierrors.NewInvalid( - schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "AzureCluster"}, - c.Name, allErrs) +func (c *AzureCluster) validateCluster() field.ErrorList { + return c.validateClusterSpec() } // validateClusterSpec validates a ClusterSpec @@ -156,6 +146,21 @@ func validateIngressRule(ingressRule *IngressRule, fldPath *field.Path) *field.E return field.Invalid(fldPath, ingressRule.Priority, fmt.Sprintf("ingress priorities should be between 100 and 4096")) } + return nil +} +func validateControlPlaneIP(old, new *PublicIPSpec, fldPath *field.Path) *field.Error { + if old == nil && new != nil { + return field.Invalid(fldPath, new, fmt.Sprintf("setting control plane endpoint after cluster creation is not allowed")) + } + if old != nil && new == nil { + return field.Invalid(fldPath, new, fmt.Sprintf("removing control plane endpoint after cluster creation is not allowed")) + } + if old != nil && new != nil && old.Name != new.Name { + return field.Invalid(fldPath, new, fmt.Sprintf("changing control plane endpoint after cluster creation is not allowed")) + } + if new != nil && new.Name == "" { + return field.Invalid(fldPath, new, fmt.Sprintf("control plane endpoint IP name must be non-empty")) + } return nil } diff --git a/api/v1alpha3/azurecluster_webhook.go b/api/v1alpha3/azurecluster_webhook.go index e1b30359ae3..bde022e4931 100644 --- a/api/v1alpha3/azurecluster_webhook.go +++ b/api/v1alpha3/azurecluster_webhook.go @@ -17,7 +17,9 @@ limitations under the License. package v1alpha3 import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -48,15 +50,51 @@ func (c *AzureCluster) Default() { // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (c *AzureCluster) ValidateCreate() error { clusterlog.Info("validate create", "name", c.Name) - - return c.validateCluster() + allErrs := c.validateCluster() + + // validation is smart/lazy enough to work with the same object compared to itself + // it works well for update to take both objects. + if err := validateControlPlaneIP( + c.Spec.NetworkSpec.PublicIP, + c.Spec.NetworkSpec.PublicIP, + field.NewPath("spec").Child("networkSpec").Child("publicIp"), + ); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(GroupVersion.WithKind("AzureCluster").GroupKind(), c.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (c *AzureCluster) ValidateUpdate(old runtime.Object) error { +func (c *AzureCluster) ValidateUpdate(oldRaw runtime.Object) error { clusterlog.Info("validate update", "name", c.Name) - return c.validateCluster() + old := oldRaw.(*AzureCluster) + + var allErrs field.ErrorList + + // validate cluster may return a list of errors + if errs := c.validateCluster(); errs != nil { + allErrs = append(allErrs, errs...) + } + + if err := validateControlPlaneIP( + old.Spec.NetworkSpec.PublicIP, + c.Spec.NetworkSpec.PublicIP, + field.NewPath("spec").Child("networkSpec").Child("publicIp"), + ); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(GroupVersion.WithKind("AzureCluster").GroupKind(), c.Name, allErrs) } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/api/v1alpha3/types.go b/api/v1alpha3/types.go index b8c9c7cb7d2..242314cc79e 100644 --- a/api/v1alpha3/types.go +++ b/api/v1alpha3/types.go @@ -38,6 +38,12 @@ type Network struct { // NetworkSpec specifies what the Azure networking resources should look like. type NetworkSpec struct { + // PublicIP is the public IP to attach to the Azure load balanacer. + // If users provide this field, we expect name to be set and the IP to be in the cluster resource group. + // We will expect the IP to exist and not create it. + // +optional + PublicIP *PublicIPSpec `json:"publicIp,omitempty"` + // Vnet is the configuration for the Azure virtual network. // +optional Vnet VnetSpec `json:"vnet,omitempty"` @@ -141,9 +147,17 @@ type IngressRules []*IngressRule // PublicIP defines an Azure public IP address. type PublicIP struct { ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` IPAddress string `json:"ipAddress,omitempty"` DNSName string `json:"dnsName,omitempty"` + Name string `json:"name,omitempty"` +} + +// PublicIPSpec defines the inputs to create an Azure public IP address. +type PublicIPSpec struct { + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // +kubebuilder:validation:MinLength=1 + DNSName string `json:"dnsName"` } // LoadBalancer defines an Azure load balancer. diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index 26e953fceee..fe1ca90b1fa 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -679,6 +679,11 @@ func (in *Network) DeepCopy() *Network { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) { *out = *in + if in.PublicIP != nil { + in, out := &in.PublicIP, &out.PublicIP + *out = new(PublicIPSpec) + **out = **in + } in.Vnet.DeepCopyInto(&out.Vnet) if in.Subnets != nil { in, out := &in.Subnets, &out.Subnets @@ -734,6 +739,21 @@ func (in *PublicIP) DeepCopy() *PublicIP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicIPSpec) DeepCopyInto(out *PublicIPSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicIPSpec. +func (in *PublicIPSpec) DeepCopy() *PublicIPSpec { + if in == nil { + return nil + } + out := new(PublicIPSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RouteTable) DeepCopyInto(out *RouteTable) { *out = *in diff --git a/cloud/scope/cluster.go b/cloud/scope/cluster.go index 5c58d177dbb..cffcd726252 100644 --- a/cloud/scope/cluster.go +++ b/cloud/scope/cluster.go @@ -19,6 +19,8 @@ package scope import ( "context" "fmt" + "hash/fnv" + "github.com/Azure/go-autorest/autorest" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -104,8 +106,18 @@ func (s *ClusterScope) Network() *infrav1.Network { return &s.AzureCluster.Status.Network } -// PublicIPSpec returns the public IP specs. +// PublicIPSpecs returns the public IP specs. func (s *ClusterScope) PublicIPSpecs() []azure.PublicIPSpec { + // If user provides an API server IP, do not return it return it here. + // We expect it to be created by them and will not reconcile it. + if s.AzureCluster.Spec.NetworkSpec.PublicIP != nil && s.AzureCluster.Spec.NetworkSpec.PublicIP.Name != "" { + return []azure.PublicIPSpec{ + { + Name: azure.GenerateNodeOutboundIPName(s.ClusterName()), + }, + } + } + return []azure.PublicIPSpec{ { Name: azure.GenerateNodeOutboundIPName(s.ClusterName()), @@ -231,3 +243,22 @@ func (s *ClusterScope) SetFailureDomain(id string, spec clusterv1.FailureDomainS } s.AzureCluster.Status.FailureDomains[id] = spec } + +// SetAPIServerIP will set the spec for a for a given key +func (s *ClusterScope) SetAPIServerIP() error { + // If IP provided by name, expect to exist and don't precreate. + if s.AzureCluster.Spec.NetworkSpec.PublicIP != nil { + s.AzureCluster.Status.Network.APIServerIP.Name = s.AzureCluster.Spec.NetworkSpec.PublicIP.Name + s.AzureCluster.Status.Network.APIServerIP.DNSName = s.AzureCluster.Spec.NetworkSpec.PublicIP.DNSName + } else if s.Network().APIServerIP.Name == "" { + // otherwise, generate + h := fnv.New32a() + if _, err := h.Write([]byte(fmt.Sprintf("%s/%s/%s", s.SubscriptionID(), s.ResourceGroup(), s.ClusterName()))); err != nil { + return errors.Wrapf(err, "failed to write hash sum for api server ip") + } + s.AzureCluster.Status.Network.APIServerIP.Name = azure.GeneratePublicIPName(s.ClusterName(), fmt.Sprintf("%x", h.Sum32())) + s.AzureCluster.Status.Network.APIServerIP.DNSName = s.GenerateFQDN() + } + + return nil +} diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 0c0ab21d992..79c2ef9633f 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -88,13 +88,14 @@ type MachineScope struct { // PublicIPSpec returns the public IP specs. func (m *MachineScope) PublicIPSpecs() []azure.PublicIPSpec { - var spec []azure.PublicIPSpec - if m.AzureMachine.Spec.AllocatePublicIP == true { - spec = append(spec, azure.PublicIPSpec{ - Name: azure.GenerateNodePublicIPName(m.Name()), - }) + if m.AzureMachine.Spec.AllocatePublicIP { + return []azure.PublicIPSpec{ + { + Name: azure.GenerateNodePublicIPName(azure.GenerateNICName(m.Name())), + }, + } } - return spec + return nil } // NICSpecs returns the network interface specs. diff --git a/cloud/services/publicips/publicips.go b/cloud/services/publicips/publicips.go index b598e4642cc..fca9e534a10 100644 --- a/cloud/services/publicips/publicips.go +++ b/cloud/services/publicips/publicips.go @@ -28,32 +28,33 @@ import ( // Reconcile gets/creates/updates a public ip. func (s *Service) Reconcile(ctx context.Context) error { - for _, ip := range s.Scope.PublicIPSpecs() { - s.Scope.V(2).Info("creating public IP", "public ip", ip.Name) - err := s.Client.CreateOrUpdate( - ctx, - s.Scope.ResourceGroup(), - ip.Name, - network.PublicIPAddress{ - Sku: &network.PublicIPAddressSku{Name: network.PublicIPAddressSkuNameStandard}, - Name: to.StringPtr(ip.Name), - Location: to.StringPtr(s.Scope.Location()), - PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: network.IPv4, - PublicIPAllocationMethod: network.Static, - DNSSettings: &network.PublicIPAddressDNSSettings{ - DomainNameLabel: to.StringPtr(strings.ToLower(ip.Name)), - Fqdn: to.StringPtr(ip.DNSName), - }, + for _, publicIPSpec := range s.Scope.PublicIPSpecs() { + s.Scope.V(2).Info("creating public IP", "public-ip", publicIPSpec.Name) + + ipSpec := network.PublicIPAddress{ + Sku: &network.PublicIPAddressSku{Name: network.PublicIPAddressSkuNameStandard}, + Name: to.StringPtr(publicIPSpec.Name), + Location: to.StringPtr(s.Scope.Location()), + PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ + PublicIPAddressVersion: network.IPv4, + PublicIPAllocationMethod: network.Static, + DNSSettings: &network.PublicIPAddressDNSSettings{ + DomainNameLabel: to.StringPtr(strings.ToLower(publicIPSpec.Name)), + Fqdn: to.StringPtr(publicIPSpec.DNSName), }, }, - ) + } - if err != nil { + if err := s.Client.CreateOrUpdate( + ctx, + s.Scope.ResourceGroup(), + publicIPSpec.Name, + ipSpec, + ); err != nil { return errors.Wrap(err, "cannot create public IP") } - s.Scope.V(2).Info("successfully created public IP", "public ip", ip.Name) + s.Scope.V(2).Info("successfully created public IP", "public ip", publicIPSpec.Name) } return nil @@ -61,18 +62,22 @@ func (s *Service) Reconcile(ctx context.Context) error { // Delete deletes the public IP with the provided scope. func (s *Service) Delete(ctx context.Context) error { - for _, ip := range s.Scope.PublicIPSpecs() { - s.Scope.V(2).Info("deleting public IP", "public ip", ip.Name) - err := s.Client.Delete(ctx, s.Scope.ResourceGroup(), ip.Name) - if err != nil && azure.ResourceNotFound(err) { - // already deleted - return nil + for _, publicIPSpec := range s.Scope.PublicIPSpecs() { + s.Scope.V(2).Info("deleting public IP", "public ip", publicIPSpec.Name) + + err := s.Client.Delete(ctx, s.Scope.ResourceGroup(), publicIPSpec.Name) + if err == nil { + s.Scope.V(2).Info("deleted public IP", "public ip", publicIPSpec.Name) + continue } - if err != nil { - return errors.Wrapf(err, "failed to delete public IP %s in resource group %s", ip.Name, s.Scope.ResourceGroup()) + + if azure.ResourceNotFound(err) { + s.Scope.V(2).Info("tried to delete public IP which didn't exist", "public ip", publicIPSpec.Name) + continue } - s.Scope.V(2).Info("deleted public IP", "public ip", ip.Name) + return errors.Wrapf(err, "failed to delete public IP %s in resource group %s", publicIPSpec.Name, s.Scope.ResourceGroup()) } + return nil } diff --git a/cloud/services/publicips/publicips_test.go b/cloud/services/publicips/publicips_test.go index 04bede10526..623b5141ceb 100644 --- a/cloud/services/publicips/publicips_test.go +++ b/cloud/services/publicips/publicips_test.go @@ -65,6 +65,7 @@ func TestReconcilePublicIP(t *testing.T) { }) s.ResourceGroup().AnyTimes().Return("my-rg") s.Location().AnyTimes().Return("testlocation") + s.ClusterName().AnyTimes().Return("foo") m.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip", gomock.AssignableToTypeOf(network.PublicIPAddress{})) m.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip-2", gomock.AssignableToTypeOf(network.PublicIPAddress{})) m.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip-3", gomock.AssignableToTypeOf(network.PublicIPAddress{})) @@ -108,7 +109,7 @@ func TestReconcilePublicIP(t *testing.T) { err := s.Reconcile(context.TODO()) if tc.expectedError != "" { g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(MatchError(tc.expectedError)) + g.Expect(err.Error()).To(Equal(tc.expectedError)) } else { g.Expect(err).NotTo(HaveOccurred()) } @@ -136,8 +137,10 @@ func TestDeletePublicIP(t *testing.T) { }, }) s.ResourceGroup().AnyTimes().Return("my-rg") - m.Delete(context.TODO(), "my-rg", "my-publicip") - m.Delete(context.TODO(), "my-rg", "my-publicip-2") + + m.Delete(context.TODO(), "my-rg", "my-publicip").Return(nil) + + m.Delete(context.TODO(), "my-rg", "my-publicip-2").Return(nil) }, }, { @@ -173,6 +176,7 @@ func TestDeletePublicIP(t *testing.T) { } for _, tc := range testcases { + tc := tc t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) @@ -192,7 +196,7 @@ func TestDeletePublicIP(t *testing.T) { err := s.Delete(context.TODO()) if tc.expectedError != "" { g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(MatchError(tc.expectedError)) + g.Expect(err.Error()).To(Equal(tc.expectedError)) } else { g.Expect(err).NotTo(HaveOccurred()) } diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml index 80415cf1462..3570ca635aa 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml @@ -465,6 +465,22 @@ spec: description: NetworkSpec encapsulates all things related to Azure network. properties: + publicIp: + description: PublicIP is the public IP to attach to the Azure + load balanacer. If users provide this field, we expect name + to be set and the IP to be in the cluster resource group. We + will expect the IP to exist and not create it. + properties: + dnsName: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - dnsName + - name + type: object subnets: description: Subnets is the configuration for the control-plane subnet and the node subnet. diff --git a/controllers/azurecluster_controller.go b/controllers/azurecluster_controller.go index 1aa87073be9..1393c0f95fe 100644 --- a/controllers/azurecluster_controller.go +++ b/controllers/azurecluster_controller.go @@ -168,7 +168,7 @@ func (r *AzureClusterReconciler) reconcileNormal(ctx context.Context, clusterSco if azureCluster.Status.Network.APIServerIP.DNSName == "" { clusterScope.Info("Waiting for Load Balancer to exist") - conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, infrav1.LoadBalancerProvisioningReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, infrav1.LoadBalancerProvisioningReason, clusterv1.ConditionSeverityWarning, infrav1.LoadBalancerProvisioningReason) return reconcile.Result{RequeueAfter: 15 * time.Second}, nil } diff --git a/controllers/azurecluster_reconciler.go b/controllers/azurecluster_reconciler.go index d8cf81ea55e..cc0e6e1d1e4 100644 --- a/controllers/azurecluster_reconciler.go +++ b/controllers/azurecluster_reconciler.go @@ -18,8 +18,6 @@ package controllers import ( "context" - "fmt" - "hash/fnv" "strconv" "github.com/Azure/go-autorest/autorest/to" @@ -72,8 +70,9 @@ func newAzureClusterReconciler(scope *scope.ClusterScope) *azureClusterReconcile // Reconcile reconciles all the services in pre determined order func (r *azureClusterReconciler) Reconcile(ctx context.Context) error { klog.V(2).Infof("reconciling cluster %s", r.scope.ClusterName()) - if err := r.createOrUpdateNetworkAPIServerIP(); err != nil { - return errors.Wrapf(err, "failed to create or update network API server IP for cluster %s in location %s", r.scope.ClusterName(), r.scope.Location()) + + if err := r.scope.SetAPIServerIP(); err != nil { + return errors.Wrap(err, "failed to set api server IP") } if err := r.setFailureDomainsForLocation(ctx); err != nil { @@ -237,20 +236,6 @@ func (r *azureClusterReconciler) deleteNSG(ctx context.Context) error { return nil } -// CreateOrUpdateNetworkAPIServerIP creates or updates public ip name and dns name -func (r *azureClusterReconciler) createOrUpdateNetworkAPIServerIP() error { - if r.scope.Network().APIServerIP.Name == "" { - h := fnv.New32a() - if _, err := h.Write([]byte(fmt.Sprintf("%s/%s/%s", r.scope.SubscriptionID(), r.scope.ResourceGroup(), r.scope.ClusterName()))); err != nil { - return errors.Wrapf(err, "failed to write hash sum for api server ip") - } - r.scope.Network().APIServerIP.Name = azure.GeneratePublicIPName(r.scope.ClusterName(), fmt.Sprintf("%x", h.Sum32())) - } - - r.scope.Network().APIServerIP.DNSName = r.scope.GenerateFQDN() - return nil -} - func (r *azureClusterReconciler) setFailureDomainsForLocation(ctx context.Context) error { spec := &availabilityzones.Spec{} zonesInterface, err := r.availabilityZonesSvc.Get(ctx, spec)