sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/scope/cluster.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package scope
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"hash/fnv"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  
    28  	asonetworkv1api20201101 "github.com/Azure/azure-service-operator/v2/api/network/v1api20201101"
    29  	asonetworkv1api20220701 "github.com/Azure/azure-service-operator/v2/api/network/v1api20220701"
    30  	asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601"
    31  	"github.com/pkg/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/utils/net"
    34  	"k8s.io/utils/ptr"
    35  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    37  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/bastionhosts"
    38  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/groups"
    39  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/loadbalancers"
    40  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/natgateways"
    41  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/privatedns"
    42  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/privateendpoints"
    43  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/publicips"
    44  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/routetables"
    45  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/securitygroups"
    46  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/subnets"
    47  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/virtualnetworks"
    48  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/vnetpeerings"
    49  	"sigs.k8s.io/cluster-api-provider-azure/util/futures"
    50  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    51  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    52  	"sigs.k8s.io/cluster-api/util/conditions"
    53  	"sigs.k8s.io/cluster-api/util/patch"
    54  	"sigs.k8s.io/controller-runtime/pkg/client"
    55  )
    56  
    57  // ClusterScopeParams defines the input parameters used to create a new Scope.
    58  type ClusterScopeParams struct {
    59  	AzureClients
    60  	Client       client.Client
    61  	Cluster      *clusterv1.Cluster
    62  	AzureCluster *infrav1.AzureCluster
    63  	Cache        *ClusterCache
    64  	Timeouts     azure.AsyncReconciler
    65  }
    66  
    67  // NewClusterScope creates a new Scope from the supplied parameters.
    68  // This is meant to be called for each reconcile iteration.
    69  func NewClusterScope(ctx context.Context, params ClusterScopeParams) (*ClusterScope, error) {
    70  	ctx, _, done := tele.StartSpanWithLogger(ctx, "azure.clusterScope.NewClusterScope")
    71  	defer done()
    72  
    73  	if params.Cluster == nil {
    74  		return nil, errors.New("failed to generate new scope from nil Cluster")
    75  	}
    76  	if params.AzureCluster == nil {
    77  		return nil, errors.New("failed to generate new scope from nil AzureCluster")
    78  	}
    79  
    80  	credentialsProvider, err := NewAzureCredentialsProvider(ctx, params.Client, params.AzureCluster.Spec.IdentityRef, params.AzureCluster.Namespace)
    81  	if err != nil {
    82  		return nil, errors.Wrap(err, "failed to init credentials provider")
    83  	}
    84  	err = params.AzureClients.setCredentialsWithProvider(ctx, params.AzureCluster.Spec.SubscriptionID, params.AzureCluster.Spec.AzureEnvironment, credentialsProvider)
    85  	if err != nil {
    86  		return nil, errors.Wrap(err, "failed to configure azure settings and credentials for Identity")
    87  	}
    88  
    89  	if params.Cache == nil {
    90  		params.Cache = &ClusterCache{}
    91  	}
    92  
    93  	helper, err := patch.NewHelper(params.AzureCluster, params.Client)
    94  	if err != nil {
    95  		return nil, errors.Errorf("failed to init patch helper: %v", err)
    96  	}
    97  
    98  	return &ClusterScope{
    99  		Client:          params.Client,
   100  		AzureClients:    params.AzureClients,
   101  		Cluster:         params.Cluster,
   102  		AzureCluster:    params.AzureCluster,
   103  		patchHelper:     helper,
   104  		cache:           params.Cache,
   105  		AsyncReconciler: params.Timeouts,
   106  	}, nil
   107  }
   108  
   109  // ClusterScope defines the basic context for an actuator to operate upon.
   110  type ClusterScope struct {
   111  	Client      client.Client
   112  	patchHelper *patch.Helper
   113  	cache       *ClusterCache
   114  
   115  	AzureClients
   116  	Cluster      *clusterv1.Cluster
   117  	AzureCluster *infrav1.AzureCluster
   118  	azure.AsyncReconciler
   119  }
   120  
   121  // ClusterCache stores ClusterCache data locally so we don't have to hit the API multiple times within the same reconcile loop.
   122  type ClusterCache struct {
   123  	isVnetManaged *bool
   124  }
   125  
   126  // BaseURI returns the Azure ResourceManagerEndpoint.
   127  func (s *ClusterScope) BaseURI() string {
   128  	return s.ResourceManagerEndpoint
   129  }
   130  
   131  // GetClient returns the controller-runtime client.
   132  func (s *ClusterScope) GetClient() client.Client {
   133  	return s.Client
   134  }
   135  
   136  // GetDeletionTimestamp returns the deletion timestamp of the Cluster.
   137  func (s *ClusterScope) GetDeletionTimestamp() *metav1.Time {
   138  	return s.Cluster.DeletionTimestamp
   139  }
   140  
   141  // ASOOwner implements aso.Scope.
   142  func (s *ClusterScope) ASOOwner() client.Object {
   143  	return s.AzureCluster
   144  }
   145  
   146  // PublicIPSpecs returns the public IP specs.
   147  func (s *ClusterScope) PublicIPSpecs() []azure.ResourceSpecGetter {
   148  	var publicIPSpecs []azure.ResourceSpecGetter
   149  
   150  	// Public IP specs for control plane lb
   151  	var controlPlaneOutboundIPSpecs []azure.ResourceSpecGetter
   152  	if s.IsAPIServerPrivate() {
   153  		// Public IP specs for control plane outbound lb
   154  		if s.ControlPlaneOutboundLB() != nil {
   155  			for _, ip := range s.ControlPlaneOutboundLB().FrontendIPs {
   156  				controlPlaneOutboundIPSpecs = append(controlPlaneOutboundIPSpecs, &publicips.PublicIPSpec{
   157  					Name:             ip.PublicIP.Name,
   158  					ResourceGroup:    s.ResourceGroup(),
   159  					ClusterName:      s.ClusterName(),
   160  					DNSName:          "",    // Set to default value
   161  					IsIPv6:           false, // Set to default value
   162  					Location:         s.Location(),
   163  					ExtendedLocation: s.ExtendedLocation(),
   164  					FailureDomains:   s.FailureDomains(),
   165  					AdditionalTags:   s.AdditionalTags(),
   166  				})
   167  			}
   168  		}
   169  	} else {
   170  		controlPlaneOutboundIPSpecs = []azure.ResourceSpecGetter{
   171  			&publicips.PublicIPSpec{
   172  				Name:             s.APIServerPublicIP().Name,
   173  				ResourceGroup:    s.ResourceGroup(),
   174  				DNSName:          s.APIServerPublicIP().DNSName,
   175  				IsIPv6:           false, // Currently azure requires an IPv4 lb rule to enable IPv6
   176  				ClusterName:      s.ClusterName(),
   177  				Location:         s.Location(),
   178  				ExtendedLocation: s.ExtendedLocation(),
   179  				FailureDomains:   s.FailureDomains(),
   180  				AdditionalTags:   s.AdditionalTags(),
   181  				IPTags:           s.APIServerPublicIP().IPTags,
   182  			},
   183  		}
   184  	}
   185  	publicIPSpecs = append(publicIPSpecs, controlPlaneOutboundIPSpecs...)
   186  
   187  	// Public IP specs for node outbound lb
   188  	if s.NodeOutboundLB() != nil {
   189  		for _, ip := range s.NodeOutboundLB().FrontendIPs {
   190  			publicIPSpecs = append(publicIPSpecs, &publicips.PublicIPSpec{
   191  				Name:             ip.PublicIP.Name,
   192  				ResourceGroup:    s.ResourceGroup(),
   193  				ClusterName:      s.ClusterName(),
   194  				DNSName:          "",    // Set to default value
   195  				IsIPv6:           false, // Set to default value
   196  				Location:         s.Location(),
   197  				ExtendedLocation: s.ExtendedLocation(),
   198  				FailureDomains:   s.FailureDomains(),
   199  				AdditionalTags:   s.AdditionalTags(),
   200  			})
   201  		}
   202  	}
   203  
   204  	// Public IP specs for node NAT gateways
   205  	var nodeNatGatewayIPSpecs []azure.ResourceSpecGetter
   206  	for _, subnet := range s.NodeSubnets() {
   207  		if subnet.IsNatGatewayEnabled() {
   208  			nodeNatGatewayIPSpecs = append(nodeNatGatewayIPSpecs, &publicips.PublicIPSpec{
   209  				Name:           subnet.NatGateway.NatGatewayIP.Name,
   210  				ResourceGroup:  s.ResourceGroup(),
   211  				DNSName:        subnet.NatGateway.NatGatewayIP.DNSName,
   212  				IsIPv6:         false, // Public IP is IPv4 by default
   213  				ClusterName:    s.ClusterName(),
   214  				Location:       s.Location(),
   215  				FailureDomains: s.FailureDomains(),
   216  				AdditionalTags: s.AdditionalTags(),
   217  				IPTags:         subnet.NatGateway.NatGatewayIP.IPTags,
   218  			})
   219  		}
   220  		publicIPSpecs = append(publicIPSpecs, nodeNatGatewayIPSpecs...)
   221  	}
   222  
   223  	if azureBastion := s.AzureBastion(); azureBastion != nil {
   224  		// public IP for Azure Bastion.
   225  		azureBastionPublicIP := &publicips.PublicIPSpec{
   226  			Name:           azureBastion.PublicIP.Name,
   227  			ResourceGroup:  s.ResourceGroup(),
   228  			DNSName:        azureBastion.PublicIP.DNSName,
   229  			IsIPv6:         false, // Public IP is IPv4 by default
   230  			ClusterName:    s.ClusterName(),
   231  			Location:       s.Location(),
   232  			FailureDomains: s.FailureDomains(),
   233  			AdditionalTags: s.AdditionalTags(),
   234  			IPTags:         azureBastion.PublicIP.IPTags,
   235  		}
   236  		publicIPSpecs = append(publicIPSpecs, azureBastionPublicIP)
   237  	}
   238  
   239  	return publicIPSpecs
   240  }
   241  
   242  // LBSpecs returns the load balancer specs.
   243  func (s *ClusterScope) LBSpecs() []azure.ResourceSpecGetter {
   244  	specs := []azure.ResourceSpecGetter{
   245  		&loadbalancers.LBSpec{
   246  			// API Server LB
   247  			Name:                 s.APIServerLB().Name,
   248  			ResourceGroup:        s.ResourceGroup(),
   249  			SubscriptionID:       s.SubscriptionID(),
   250  			ClusterName:          s.ClusterName(),
   251  			Location:             s.Location(),
   252  			ExtendedLocation:     s.ExtendedLocation(),
   253  			VNetName:             s.Vnet().Name,
   254  			VNetResourceGroup:    s.Vnet().ResourceGroup,
   255  			SubnetName:           s.ControlPlaneSubnet().Name,
   256  			FrontendIPConfigs:    s.APIServerLB().FrontendIPs,
   257  			APIServerPort:        s.APIServerPort(),
   258  			Type:                 s.APIServerLB().Type,
   259  			SKU:                  s.APIServerLB().SKU,
   260  			Role:                 infrav1.APIServerRole,
   261  			BackendPoolName:      s.APIServerLB().BackendPool.Name,
   262  			IdleTimeoutInMinutes: s.APIServerLB().IdleTimeoutInMinutes,
   263  			AdditionalTags:       s.AdditionalTags(),
   264  		},
   265  	}
   266  
   267  	// Node outbound LB
   268  	if s.NodeOutboundLB() != nil {
   269  		specs = append(specs, &loadbalancers.LBSpec{
   270  			Name:                 s.NodeOutboundLB().Name,
   271  			ResourceGroup:        s.ResourceGroup(),
   272  			SubscriptionID:       s.SubscriptionID(),
   273  			ClusterName:          s.ClusterName(),
   274  			Location:             s.Location(),
   275  			ExtendedLocation:     s.ExtendedLocation(),
   276  			VNetName:             s.Vnet().Name,
   277  			VNetResourceGroup:    s.Vnet().ResourceGroup,
   278  			FrontendIPConfigs:    s.NodeOutboundLB().FrontendIPs,
   279  			Type:                 s.NodeOutboundLB().Type,
   280  			SKU:                  s.NodeOutboundLB().SKU,
   281  			BackendPoolName:      s.NodeOutboundLB().BackendPool.Name,
   282  			IdleTimeoutInMinutes: s.NodeOutboundLB().IdleTimeoutInMinutes,
   283  			Role:                 infrav1.NodeOutboundRole,
   284  			AdditionalTags:       s.AdditionalTags(),
   285  		})
   286  	}
   287  
   288  	// Control Plane Outbound LB
   289  	if s.ControlPlaneOutboundLB() != nil {
   290  		specs = append(specs, &loadbalancers.LBSpec{
   291  			Name:                 s.ControlPlaneOutboundLB().Name,
   292  			ResourceGroup:        s.ResourceGroup(),
   293  			SubscriptionID:       s.SubscriptionID(),
   294  			ClusterName:          s.ClusterName(),
   295  			Location:             s.Location(),
   296  			ExtendedLocation:     s.ExtendedLocation(),
   297  			VNetName:             s.Vnet().Name,
   298  			VNetResourceGroup:    s.Vnet().ResourceGroup,
   299  			FrontendIPConfigs:    s.ControlPlaneOutboundLB().FrontendIPs,
   300  			Type:                 s.ControlPlaneOutboundLB().Type,
   301  			SKU:                  s.ControlPlaneOutboundLB().SKU,
   302  			BackendPoolName:      s.ControlPlaneOutboundLB().BackendPool.Name,
   303  			IdleTimeoutInMinutes: s.ControlPlaneOutboundLB().IdleTimeoutInMinutes,
   304  			Role:                 infrav1.ControlPlaneOutboundRole,
   305  			AdditionalTags:       s.AdditionalTags(),
   306  		})
   307  	}
   308  
   309  	return specs
   310  }
   311  
   312  // RouteTableSpecs returns the subnet route tables.
   313  func (s *ClusterScope) RouteTableSpecs() []azure.ResourceSpecGetter {
   314  	var specs []azure.ResourceSpecGetter
   315  	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   316  		if subnet.RouteTable.Name != "" {
   317  			specs = append(specs, &routetables.RouteTableSpec{
   318  				Name:           subnet.RouteTable.Name,
   319  				Location:       s.Location(),
   320  				ResourceGroup:  s.Vnet().ResourceGroup,
   321  				ClusterName:    s.ClusterName(),
   322  				AdditionalTags: s.AdditionalTags(),
   323  			})
   324  		}
   325  	}
   326  
   327  	return specs
   328  }
   329  
   330  // NatGatewaySpecs returns the node NAT gateway.
   331  func (s *ClusterScope) NatGatewaySpecs() []azure.ASOResourceSpecGetter[*asonetworkv1api20220701.NatGateway] {
   332  	natGatewaySet := make(map[string]struct{})
   333  	var natGateways []azure.ASOResourceSpecGetter[*asonetworkv1api20220701.NatGateway]
   334  
   335  	// We ignore the control plane NAT gateway, as we will always use a LB to enable egress on the control plane.
   336  	for _, subnet := range s.NodeSubnets() {
   337  		if subnet.IsNatGatewayEnabled() {
   338  			if _, ok := natGatewaySet[subnet.NatGateway.Name]; !ok {
   339  				natGatewaySet[subnet.NatGateway.Name] = struct{}{} // empty struct to represent hash set
   340  				natGateways = append(natGateways, &natgateways.NatGatewaySpec{
   341  					Name:           subnet.NatGateway.Name,
   342  					ResourceGroup:  s.ResourceGroup(),
   343  					SubscriptionID: s.SubscriptionID(),
   344  					Location:       s.Location(),
   345  					ClusterName:    s.ClusterName(),
   346  					NatGatewayIP: infrav1.PublicIPSpec{
   347  						Name: subnet.NatGateway.NatGatewayIP.Name,
   348  					},
   349  					AdditionalTags: s.AdditionalTags(),
   350  					// We need to know if the VNet is managed to decide if this NAT Gateway was-managed or not.
   351  					IsVnetManaged: s.IsVnetManaged(),
   352  				})
   353  			}
   354  		}
   355  	}
   356  
   357  	return natGateways
   358  }
   359  
   360  // NSGSpecs returns the security group specs.
   361  func (s *ClusterScope) NSGSpecs() []azure.ResourceSpecGetter {
   362  	nsgspecs := make([]azure.ResourceSpecGetter, len(s.AzureCluster.Spec.NetworkSpec.Subnets))
   363  	for i, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   364  		nsgspecs[i] = &securitygroups.NSGSpec{
   365  			Name:                     subnet.SecurityGroup.Name,
   366  			SecurityRules:            subnet.SecurityGroup.SecurityRules,
   367  			ResourceGroup:            s.Vnet().ResourceGroup,
   368  			Location:                 s.Location(),
   369  			ClusterName:              s.ClusterName(),
   370  			AdditionalTags:           s.AdditionalTags(),
   371  			LastAppliedSecurityRules: s.getLastAppliedSecurityRules(subnet.SecurityGroup.Name),
   372  		}
   373  	}
   374  
   375  	return nsgspecs
   376  }
   377  
   378  // SubnetSpecs returns the subnets specs.
   379  func (s *ClusterScope) SubnetSpecs() []azure.ASOResourceSpecGetter[*asonetworkv1api20201101.VirtualNetworksSubnet] {
   380  	numberOfSubnets := len(s.AzureCluster.Spec.NetworkSpec.Subnets)
   381  	if s.IsAzureBastionEnabled() {
   382  		numberOfSubnets++
   383  	}
   384  
   385  	subnetSpecs := make([]azure.ASOResourceSpecGetter[*asonetworkv1api20201101.VirtualNetworksSubnet], 0, numberOfSubnets)
   386  
   387  	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   388  		subnetSpec := &subnets.SubnetSpec{
   389  			Name:              subnet.Name,
   390  			ResourceGroup:     s.ResourceGroup(),
   391  			SubscriptionID:    s.SubscriptionID(),
   392  			CIDRs:             subnet.CIDRBlocks,
   393  			VNetName:          s.Vnet().Name,
   394  			VNetResourceGroup: s.Vnet().ResourceGroup,
   395  			IsVNetManaged:     s.IsVnetManaged(),
   396  			RouteTableName:    subnet.RouteTable.Name,
   397  			SecurityGroupName: subnet.SecurityGroup.Name,
   398  			NatGatewayName:    subnet.NatGateway.Name,
   399  			ServiceEndpoints:  subnet.ServiceEndpoints,
   400  		}
   401  		subnetSpecs = append(subnetSpecs, subnetSpec)
   402  	}
   403  
   404  	if s.IsAzureBastionEnabled() {
   405  		azureBastionSubnet := s.AzureCluster.Spec.BastionSpec.AzureBastion.Subnet
   406  		subnetSpecs = append(subnetSpecs, &subnets.SubnetSpec{
   407  			Name:              azureBastionSubnet.Name,
   408  			ResourceGroup:     s.ResourceGroup(),
   409  			SubscriptionID:    s.SubscriptionID(),
   410  			CIDRs:             azureBastionSubnet.CIDRBlocks,
   411  			VNetName:          s.Vnet().Name,
   412  			VNetResourceGroup: s.Vnet().ResourceGroup,
   413  			IsVNetManaged:     s.IsVnetManaged(),
   414  			SecurityGroupName: azureBastionSubnet.SecurityGroup.Name,
   415  			RouteTableName:    azureBastionSubnet.RouteTable.Name,
   416  			ServiceEndpoints:  azureBastionSubnet.ServiceEndpoints,
   417  		})
   418  	}
   419  
   420  	return subnetSpecs
   421  }
   422  
   423  // GroupSpecs returns the resource group spec.
   424  func (s *ClusterScope) GroupSpecs() []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] {
   425  	specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   426  		&groups.GroupSpec{
   427  			Name:           s.ResourceGroup(),
   428  			AzureName:      s.ResourceGroup(),
   429  			Location:       s.Location(),
   430  			ClusterName:    s.ClusterName(),
   431  			AdditionalTags: s.AdditionalTags(),
   432  		},
   433  	}
   434  	if s.Vnet().ResourceGroup != "" && s.Vnet().ResourceGroup != s.ResourceGroup() {
   435  		specs = append(specs, &groups.GroupSpec{
   436  			Name:           azure.GetNormalizedKubernetesName(s.Vnet().ResourceGroup),
   437  			AzureName:      s.Vnet().ResourceGroup,
   438  			Location:       s.Location(),
   439  			ClusterName:    s.ClusterName(),
   440  			AdditionalTags: s.AdditionalTags(),
   441  		})
   442  	}
   443  	return specs
   444  }
   445  
   446  // VnetPeeringSpecs returns the virtual network peering specs.
   447  func (s *ClusterScope) VnetPeeringSpecs() []azure.ResourceSpecGetter {
   448  	peeringSpecs := make([]azure.ResourceSpecGetter, 2*len(s.Vnet().Peerings))
   449  	for i, peering := range s.Vnet().Peerings {
   450  		forwardPeering := &vnetpeerings.VnetPeeringSpec{
   451  			PeeringName:               azure.GenerateVnetPeeringName(s.Vnet().Name, peering.RemoteVnetName),
   452  			SourceVnetName:            s.Vnet().Name,
   453  			SourceResourceGroup:       s.Vnet().ResourceGroup,
   454  			RemoteVnetName:            peering.RemoteVnetName,
   455  			RemoteResourceGroup:       peering.ResourceGroup,
   456  			SubscriptionID:            s.SubscriptionID(),
   457  			AllowForwardedTraffic:     peering.ForwardPeeringProperties.AllowForwardedTraffic,
   458  			AllowGatewayTransit:       peering.ForwardPeeringProperties.AllowGatewayTransit,
   459  			AllowVirtualNetworkAccess: peering.ForwardPeeringProperties.AllowVirtualNetworkAccess,
   460  			UseRemoteGateways:         peering.ForwardPeeringProperties.UseRemoteGateways,
   461  		}
   462  		reversePeering := &vnetpeerings.VnetPeeringSpec{
   463  			PeeringName:               azure.GenerateVnetPeeringName(peering.RemoteVnetName, s.Vnet().Name),
   464  			SourceVnetName:            peering.RemoteVnetName,
   465  			SourceResourceGroup:       peering.ResourceGroup,
   466  			RemoteVnetName:            s.Vnet().Name,
   467  			RemoteResourceGroup:       s.Vnet().ResourceGroup,
   468  			SubscriptionID:            s.SubscriptionID(),
   469  			AllowForwardedTraffic:     peering.ReversePeeringProperties.AllowForwardedTraffic,
   470  			AllowGatewayTransit:       peering.ReversePeeringProperties.AllowGatewayTransit,
   471  			AllowVirtualNetworkAccess: peering.ReversePeeringProperties.AllowVirtualNetworkAccess,
   472  			UseRemoteGateways:         peering.ReversePeeringProperties.UseRemoteGateways,
   473  		}
   474  		peeringSpecs[i*2] = forwardPeering
   475  		peeringSpecs[i*2+1] = reversePeering
   476  	}
   477  
   478  	return peeringSpecs
   479  }
   480  
   481  // VNetSpec returns the virtual network spec.
   482  func (s *ClusterScope) VNetSpec() azure.ASOResourceSpecGetter[*asonetworkv1api20201101.VirtualNetwork] {
   483  	return &virtualnetworks.VNetSpec{
   484  		ResourceGroup:    s.Vnet().ResourceGroup,
   485  		Name:             s.Vnet().Name,
   486  		CIDRs:            s.Vnet().CIDRBlocks,
   487  		ExtendedLocation: s.ExtendedLocation(),
   488  		Location:         s.Location(),
   489  		ClusterName:      s.ClusterName(),
   490  		AdditionalTags:   s.AdditionalTags(),
   491  	}
   492  }
   493  
   494  // PrivateDNSSpec returns the private dns zone spec.
   495  func (s *ClusterScope) PrivateDNSSpec() (zoneSpec azure.ResourceSpecGetter, linkSpec, recordSpec []azure.ResourceSpecGetter) {
   496  	if s.IsAPIServerPrivate() {
   497  		zone := privatedns.ZoneSpec{
   498  			Name:           s.GetPrivateDNSZoneName(),
   499  			ResourceGroup:  s.ResourceGroup(),
   500  			ClusterName:    s.ClusterName(),
   501  			AdditionalTags: s.AdditionalTags(),
   502  		}
   503  
   504  		links := make([]azure.ResourceSpecGetter, 1+len(s.Vnet().Peerings))
   505  		links[0] = privatedns.LinkSpec{
   506  			Name:              azure.GenerateVNetLinkName(s.Vnet().Name),
   507  			ZoneName:          s.GetPrivateDNSZoneName(),
   508  			SubscriptionID:    s.SubscriptionID(),
   509  			VNetResourceGroup: s.Vnet().ResourceGroup,
   510  			VNetName:          s.Vnet().Name,
   511  			ResourceGroup:     s.ResourceGroup(),
   512  			ClusterName:       s.ClusterName(),
   513  			AdditionalTags:    s.AdditionalTags(),
   514  		}
   515  		for i, peering := range s.Vnet().Peerings {
   516  			links[i+1] = privatedns.LinkSpec{
   517  				Name:              azure.GenerateVNetLinkName(peering.RemoteVnetName),
   518  				ZoneName:          s.GetPrivateDNSZoneName(),
   519  				SubscriptionID:    s.SubscriptionID(),
   520  				VNetResourceGroup: peering.ResourceGroup,
   521  				VNetName:          peering.RemoteVnetName,
   522  				ResourceGroup:     s.ResourceGroup(),
   523  				ClusterName:       s.ClusterName(),
   524  				AdditionalTags:    s.AdditionalTags(),
   525  			}
   526  		}
   527  
   528  		records := make([]azure.ResourceSpecGetter, 1)
   529  		records[0] = privatedns.RecordSpec{
   530  			Record: infrav1.AddressRecord{
   531  				Hostname: azure.PrivateAPIServerHostname,
   532  				IP:       s.APIServerPrivateIP(),
   533  			},
   534  			ZoneName:      s.GetPrivateDNSZoneName(),
   535  			ResourceGroup: s.ResourceGroup(),
   536  		}
   537  
   538  		return zone, links, records
   539  	}
   540  
   541  	return nil, nil, nil
   542  }
   543  
   544  // IsAzureBastionEnabled returns true if the azure bastion is enabled.
   545  func (s *ClusterScope) IsAzureBastionEnabled() bool {
   546  	return s.AzureCluster.Spec.BastionSpec.AzureBastion != nil
   547  }
   548  
   549  // AzureBastion returns the cluster AzureBastion.
   550  func (s *ClusterScope) AzureBastion() *infrav1.AzureBastion {
   551  	return s.AzureCluster.Spec.BastionSpec.AzureBastion
   552  }
   553  
   554  // AzureBastionSpec returns the bastion spec.
   555  func (s *ClusterScope) AzureBastionSpec() azure.ASOResourceSpecGetter[*asonetworkv1api20220701.BastionHost] {
   556  	if s.IsAzureBastionEnabled() {
   557  		subnetID := azure.SubnetID(s.SubscriptionID(), s.Vnet().ResourceGroup, s.Vnet().Name, s.AzureBastion().Subnet.Name)
   558  		publicIPID := azure.PublicIPID(s.SubscriptionID(), s.ResourceGroup(), s.AzureBastion().PublicIP.Name)
   559  
   560  		return &bastionhosts.AzureBastionSpec{
   561  			Name:            s.AzureBastion().Name,
   562  			ResourceGroup:   s.ResourceGroup(),
   563  			Location:        s.Location(),
   564  			ClusterName:     s.ClusterName(),
   565  			SubnetID:        subnetID,
   566  			PublicIPID:      publicIPID,
   567  			Sku:             s.AzureBastion().Sku,
   568  			EnableTunneling: s.AzureBastion().EnableTunneling,
   569  		}
   570  	}
   571  
   572  	return nil
   573  }
   574  
   575  // Vnet returns the cluster Vnet.
   576  func (s *ClusterScope) Vnet() *infrav1.VnetSpec {
   577  	return &s.AzureCluster.Spec.NetworkSpec.Vnet
   578  }
   579  
   580  // IsVnetManaged returns true if the vnet is managed.
   581  func (s *ClusterScope) IsVnetManaged() bool {
   582  	if s.cache.isVnetManaged != nil {
   583  		return ptr.Deref(s.cache.isVnetManaged, false)
   584  	}
   585  	ctx := context.Background()
   586  	ctx, log, done := tele.StartSpanWithLogger(ctx, "scope.ClusterScope.IsVnetManaged")
   587  	defer done()
   588  
   589  	vnet := s.VNetSpec().ResourceRef()
   590  	vnet.SetNamespace(s.ASOOwner().GetNamespace())
   591  	err := s.Client.Get(ctx, client.ObjectKeyFromObject(vnet), vnet)
   592  	if err != nil {
   593  		log.Error(err, "Unable to determine if ClusterScope VNET is managed by capz, assuming unmanaged", "AzureCluster", s.ClusterName())
   594  		return false
   595  	}
   596  
   597  	isManaged := infrav1.Tags(vnet.Status.Tags).HasOwned(s.ClusterName())
   598  	s.cache.isVnetManaged = ptr.To(isManaged)
   599  	return isManaged
   600  }
   601  
   602  // IsIPv6Enabled returns true if IPv6 is enabled.
   603  func (s *ClusterScope) IsIPv6Enabled() bool {
   604  	for _, cidr := range s.AzureCluster.Spec.NetworkSpec.Vnet.CIDRBlocks {
   605  		if net.IsIPv6CIDRString(cidr) {
   606  			return true
   607  		}
   608  	}
   609  	return false
   610  }
   611  
   612  // Subnets returns the cluster subnets.
   613  func (s *ClusterScope) Subnets() infrav1.Subnets {
   614  	return s.AzureCluster.Spec.NetworkSpec.Subnets
   615  }
   616  
   617  // ControlPlaneSubnet returns the cluster control plane subnet.
   618  func (s *ClusterScope) ControlPlaneSubnet() infrav1.SubnetSpec {
   619  	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetControlPlaneSubnet()
   620  	return subnet
   621  }
   622  
   623  // NodeSubnets returns the subnets with the node role.
   624  func (s *ClusterScope) NodeSubnets() []infrav1.SubnetSpec {
   625  	subnets := []infrav1.SubnetSpec{}
   626  	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   627  		if subnet.Role == infrav1.SubnetNode || subnet.Role == infrav1.SubnetCluster {
   628  			subnets = append(subnets, subnet)
   629  		}
   630  	}
   631  
   632  	return subnets
   633  }
   634  
   635  // Subnet returns the subnet with the provided name.
   636  func (s *ClusterScope) Subnet(name string) infrav1.SubnetSpec {
   637  	for _, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   638  		if sn.Name == name {
   639  			return sn
   640  		}
   641  	}
   642  
   643  	return infrav1.SubnetSpec{}
   644  }
   645  
   646  // SetSubnet sets the subnet spec for the subnet with the same name.
   647  func (s *ClusterScope) SetSubnet(subnetSpec infrav1.SubnetSpec) {
   648  	for i, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   649  		if sn.Name == subnetSpec.Name {
   650  			s.AzureCluster.Spec.NetworkSpec.Subnets[i] = subnetSpec
   651  			return
   652  		}
   653  	}
   654  }
   655  
   656  // SetNatGatewayIDInSubnets sets the NAT Gateway ID in the subnets with the same name.
   657  func (s *ClusterScope) SetNatGatewayIDInSubnets(name string, id string) {
   658  	for _, subnet := range s.Subnets() {
   659  		if subnet.NatGateway.Name == name {
   660  			subnet.NatGateway.ID = id
   661  			s.SetSubnet(subnet)
   662  		}
   663  	}
   664  }
   665  
   666  // UpdateSubnetCIDRs updates the subnet CIDRs for the subnet with the same name.
   667  func (s *ClusterScope) UpdateSubnetCIDRs(name string, cidrBlocks []string) {
   668  	subnetSpecInfra := s.Subnet(name)
   669  	subnetSpecInfra.CIDRBlocks = cidrBlocks
   670  	s.SetSubnet(subnetSpecInfra)
   671  }
   672  
   673  // UpdateSubnetID updates the subnet ID for the subnet with the same name.
   674  func (s *ClusterScope) UpdateSubnetID(name string, id string) {
   675  	subnetSpecInfra := s.Subnet(name)
   676  	subnetSpecInfra.ID = id
   677  	s.SetSubnet(subnetSpecInfra)
   678  }
   679  
   680  // ControlPlaneRouteTable returns the cluster controlplane routetable.
   681  func (s *ClusterScope) ControlPlaneRouteTable() infrav1.RouteTable {
   682  	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetControlPlaneSubnet()
   683  	return subnet.RouteTable
   684  }
   685  
   686  // APIServerLB returns the cluster API Server load balancer.
   687  func (s *ClusterScope) APIServerLB() *infrav1.LoadBalancerSpec {
   688  	return &s.AzureCluster.Spec.NetworkSpec.APIServerLB
   689  }
   690  
   691  // NodeOutboundLB returns the cluster node outbound load balancer.
   692  func (s *ClusterScope) NodeOutboundLB() *infrav1.LoadBalancerSpec {
   693  	return s.AzureCluster.Spec.NetworkSpec.NodeOutboundLB
   694  }
   695  
   696  // ControlPlaneOutboundLB returns the cluster control plane outbound load balancer.
   697  func (s *ClusterScope) ControlPlaneOutboundLB() *infrav1.LoadBalancerSpec {
   698  	return s.AzureCluster.Spec.NetworkSpec.ControlPlaneOutboundLB
   699  }
   700  
   701  // APIServerLBName returns the API Server LB name.
   702  func (s *ClusterScope) APIServerLBName() string {
   703  	return s.APIServerLB().Name
   704  }
   705  
   706  // IsAPIServerPrivate returns true if the API Server LB is of type Internal.
   707  func (s *ClusterScope) IsAPIServerPrivate() bool {
   708  	return s.APIServerLB().Type == infrav1.Internal
   709  }
   710  
   711  // APIServerPublicIP returns the API Server public IP.
   712  func (s *ClusterScope) APIServerPublicIP() *infrav1.PublicIPSpec {
   713  	return s.APIServerLB().FrontendIPs[0].PublicIP
   714  }
   715  
   716  // APIServerPrivateIP returns the API Server private IP.
   717  func (s *ClusterScope) APIServerPrivateIP() string {
   718  	return s.APIServerLB().FrontendIPs[0].PrivateIPAddress
   719  }
   720  
   721  // GetPrivateDNSZoneName returns the Private DNS Zone from the spec or generate it from cluster name.
   722  func (s *ClusterScope) GetPrivateDNSZoneName() string {
   723  	if len(s.AzureCluster.Spec.NetworkSpec.PrivateDNSZoneName) > 0 {
   724  		return s.AzureCluster.Spec.NetworkSpec.PrivateDNSZoneName
   725  	}
   726  	return azure.GeneratePrivateDNSZoneName(s.ClusterName())
   727  }
   728  
   729  // APIServerLBPoolName returns the API Server LB backend pool name.
   730  func (s *ClusterScope) APIServerLBPoolName() string {
   731  	return s.APIServerLB().BackendPool.Name
   732  }
   733  
   734  // OutboundLB returns the outbound LB.
   735  func (s *ClusterScope) outboundLB(role string) *infrav1.LoadBalancerSpec {
   736  	if role == infrav1.Node {
   737  		return s.NodeOutboundLB()
   738  	}
   739  	if s.IsAPIServerPrivate() {
   740  		return s.ControlPlaneOutboundLB()
   741  	}
   742  	return s.APIServerLB()
   743  }
   744  
   745  // OutboundLBName returns the name of the outbound LB.
   746  func (s *ClusterScope) OutboundLBName(role string) string {
   747  	lb := s.outboundLB(role)
   748  	if lb == nil {
   749  		return ""
   750  	}
   751  	return lb.Name
   752  }
   753  
   754  // OutboundPoolName returns the outbound LB backend pool name.
   755  func (s *ClusterScope) OutboundPoolName(role string) string {
   756  	lb := s.outboundLB(role)
   757  	if lb == nil {
   758  		return ""
   759  	}
   760  	return lb.BackendPool.Name
   761  }
   762  
   763  // ResourceGroup returns the cluster resource group.
   764  func (s *ClusterScope) ResourceGroup() string {
   765  	return s.AzureCluster.Spec.ResourceGroup
   766  }
   767  
   768  // NodeResourceGroup returns the resource group where nodes live.
   769  // For AzureClusters this is the same as the cluster RG.
   770  func (s *ClusterScope) NodeResourceGroup() string {
   771  	return s.ResourceGroup()
   772  }
   773  
   774  // ClusterName returns the cluster name.
   775  func (s *ClusterScope) ClusterName() string {
   776  	return s.Cluster.Name
   777  }
   778  
   779  // Namespace returns the cluster namespace.
   780  func (s *ClusterScope) Namespace() string {
   781  	return s.Cluster.Namespace
   782  }
   783  
   784  // Location returns the cluster location.
   785  func (s *ClusterScope) Location() string {
   786  	return s.AzureCluster.Spec.Location
   787  }
   788  
   789  // AvailabilitySetEnabled informs machines that they should be part of an Availability Set.
   790  func (s *ClusterScope) AvailabilitySetEnabled() bool {
   791  	return len(s.AzureCluster.Status.FailureDomains) == 0
   792  }
   793  
   794  // CloudProviderConfigOverrides returns the cloud provider config overrides for the cluster.
   795  func (s *ClusterScope) CloudProviderConfigOverrides() *infrav1.CloudProviderConfigOverrides {
   796  	return s.AzureCluster.Spec.CloudProviderConfigOverrides
   797  }
   798  
   799  // ExtendedLocationName returns ExtendedLocation name for the cluster.
   800  func (s *ClusterScope) ExtendedLocationName() string {
   801  	if s.ExtendedLocation() == nil {
   802  		return ""
   803  	}
   804  	return s.ExtendedLocation().Name
   805  }
   806  
   807  // ExtendedLocationType returns ExtendedLocation type for the cluster.
   808  func (s *ClusterScope) ExtendedLocationType() string {
   809  	if s.ExtendedLocation() == nil {
   810  		return ""
   811  	}
   812  	return s.ExtendedLocation().Type
   813  }
   814  
   815  // ExtendedLocation returns the cluster extendedLocation.
   816  func (s *ClusterScope) ExtendedLocation() *infrav1.ExtendedLocationSpec {
   817  	return s.AzureCluster.Spec.ExtendedLocation
   818  }
   819  
   820  // GenerateFQDN generates a fully qualified domain name, based on a hash, cluster name and cluster location.
   821  func (s *ClusterScope) GenerateFQDN(ipName string) string {
   822  	h := fnv.New32a()
   823  	if _, err := fmt.Fprintf(h, "%s/%s/%s", s.SubscriptionID(), s.ResourceGroup(), ipName); err != nil {
   824  		return ""
   825  	}
   826  	hash := fmt.Sprintf("%x", h.Sum32())
   827  	return strings.ToLower(fmt.Sprintf("%s-%s.%s.%s", s.ClusterName(), hash, s.Location(), s.AzureClients.ResourceManagerVMDNSSuffix))
   828  }
   829  
   830  // GenerateLegacyFQDN generates an IP name and a fully qualified domain name, based on a hash, cluster name and cluster location.
   831  // Deprecated: use GenerateFQDN instead.
   832  func (s *ClusterScope) GenerateLegacyFQDN() (ip string, domain string) {
   833  	h := fnv.New32a()
   834  	if _, err := fmt.Fprintf(h, "%s/%s/%s", s.SubscriptionID(), s.ResourceGroup(), s.ClusterName()); err != nil {
   835  		return "", ""
   836  	}
   837  	ipName := fmt.Sprintf("%s-%x", s.ClusterName(), h.Sum32())
   838  	fqdn := fmt.Sprintf("%s.%s.%s", ipName, s.Location(), s.AzureClients.ResourceManagerVMDNSSuffix)
   839  	return ipName, fqdn
   840  }
   841  
   842  // ListOptionsLabelSelector returns a ListOptions with a label selector for clusterName.
   843  func (s *ClusterScope) ListOptionsLabelSelector() client.ListOption {
   844  	return client.MatchingLabels(map[string]string{
   845  		clusterv1.ClusterNameLabel: s.Cluster.Name,
   846  	})
   847  }
   848  
   849  // PatchObject persists the cluster configuration and status.
   850  func (s *ClusterScope) PatchObject(ctx context.Context) error {
   851  	ctx, _, done := tele.StartSpanWithLogger(ctx, "scope.ClusterScope.PatchObject")
   852  	defer done()
   853  
   854  	conditions.SetSummary(s.AzureCluster)
   855  
   856  	return s.patchHelper.Patch(
   857  		ctx,
   858  		s.AzureCluster,
   859  		patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
   860  			clusterv1.ReadyCondition,
   861  			infrav1.ResourceGroupReadyCondition,
   862  			infrav1.RouteTablesReadyCondition,
   863  			infrav1.NetworkInfrastructureReadyCondition,
   864  			infrav1.VnetPeeringReadyCondition,
   865  			infrav1.DisksReadyCondition,
   866  			infrav1.NATGatewaysReadyCondition,
   867  			infrav1.LoadBalancersReadyCondition,
   868  			infrav1.BastionHostReadyCondition,
   869  			infrav1.VNetReadyCondition,
   870  			infrav1.SubnetsReadyCondition,
   871  			infrav1.SecurityGroupsReadyCondition,
   872  			infrav1.PrivateDNSZoneReadyCondition,
   873  			infrav1.PrivateDNSLinkReadyCondition,
   874  			infrav1.PrivateDNSRecordReadyCondition,
   875  			infrav1.PrivateEndpointsReadyCondition,
   876  		}})
   877  }
   878  
   879  // Close closes the current scope persisting the cluster configuration and status.
   880  func (s *ClusterScope) Close(ctx context.Context) error {
   881  	return s.PatchObject(ctx)
   882  }
   883  
   884  // AdditionalTags returns AdditionalTags from the scope's AzureCluster.
   885  func (s *ClusterScope) AdditionalTags() infrav1.Tags {
   886  	tags := make(infrav1.Tags)
   887  	if s.AzureCluster.Spec.AdditionalTags != nil {
   888  		tags = s.AzureCluster.Spec.AdditionalTags.DeepCopy()
   889  	}
   890  	return tags
   891  }
   892  
   893  // APIServerPort returns the APIServerPort to use when creating the load balancer.
   894  func (s *ClusterScope) APIServerPort() int32 {
   895  	if s.Cluster.Spec.ClusterNetwork != nil && s.Cluster.Spec.ClusterNetwork.APIServerPort != nil {
   896  		return *s.Cluster.Spec.ClusterNetwork.APIServerPort
   897  	}
   898  	return 6443
   899  }
   900  
   901  // APIServerHost returns the hostname used to reach the API server.
   902  func (s *ClusterScope) APIServerHost() string {
   903  	if s.IsAPIServerPrivate() {
   904  		return azure.GeneratePrivateFQDN(s.GetPrivateDNSZoneName())
   905  	}
   906  	return s.APIServerPublicIP().DNSName
   907  }
   908  
   909  // SetFailureDomain sets a failure domain in a cluster's status by its id.
   910  // The provided failure domain spec may be overridden to false by cluster's spec property.
   911  func (s *ClusterScope) SetFailureDomain(id string, spec clusterv1.FailureDomainSpec) {
   912  	if s.AzureCluster.Status.FailureDomains == nil {
   913  		s.AzureCluster.Status.FailureDomains = make(clusterv1.FailureDomains)
   914  	}
   915  
   916  	if fd, ok := s.AzureCluster.Spec.FailureDomains[id]; ok && !fd.ControlPlane {
   917  		spec.ControlPlane = false
   918  	}
   919  
   920  	s.AzureCluster.Status.FailureDomains[id] = spec
   921  }
   922  
   923  // FailureDomains returns the failure domains for the cluster.
   924  func (s *ClusterScope) FailureDomains() []*string {
   925  	fds := make([]*string, len(s.AzureCluster.Status.FailureDomains))
   926  	i := 0
   927  	for id := range s.AzureCluster.Status.FailureDomains {
   928  		fds[i] = ptr.To(id)
   929  		i++
   930  	}
   931  
   932  	// sort in increasing order restoring the original sort.Strings(fds) behavior
   933  	sort.Slice(fds, func(i, j int) bool {
   934  		return *fds[i] < *fds[j]
   935  	})
   936  
   937  	return fds
   938  }
   939  
   940  // SetControlPlaneSecurityRules sets the default security rules of the control plane subnet.
   941  // Note that this is not done in a webhook as it requires a valid Cluster object to exist to get the API Server port.
   942  func (s *ClusterScope) SetControlPlaneSecurityRules() {
   943  	if s.ControlPlaneSubnet().SecurityGroup.SecurityRules == nil {
   944  		subnet := s.ControlPlaneSubnet()
   945  		subnet.SecurityGroup.SecurityRules = infrav1.SecurityRules{
   946  			infrav1.SecurityRule{
   947  				Name:             "allow_ssh",
   948  				Description:      "Allow SSH",
   949  				Priority:         2200,
   950  				Protocol:         infrav1.SecurityGroupProtocolTCP,
   951  				Direction:        infrav1.SecurityRuleDirectionInbound,
   952  				Source:           ptr.To("*"),
   953  				SourcePorts:      ptr.To("*"),
   954  				Destination:      ptr.To("*"),
   955  				DestinationPorts: ptr.To("22"),
   956  				Action:           infrav1.SecurityRuleActionAllow,
   957  			},
   958  			infrav1.SecurityRule{
   959  				Name:             "allow_apiserver",
   960  				Description:      "Allow K8s API Server",
   961  				Priority:         2201,
   962  				Protocol:         infrav1.SecurityGroupProtocolTCP,
   963  				Direction:        infrav1.SecurityRuleDirectionInbound,
   964  				Source:           ptr.To("*"),
   965  				SourcePorts:      ptr.To("*"),
   966  				Destination:      ptr.To("*"),
   967  				DestinationPorts: ptr.To(strconv.Itoa(int(s.APIServerPort()))),
   968  				Action:           infrav1.SecurityRuleActionAllow,
   969  			},
   970  		}
   971  		s.AzureCluster.Spec.NetworkSpec.UpdateControlPlaneSubnet(subnet)
   972  	}
   973  }
   974  
   975  // SetDNSName sets the API Server public IP DNS name.
   976  // Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without an APIServerLB, and should be removed in the future.
   977  func (s *ClusterScope) SetDNSName() {
   978  	// for back compat, set the old API Server defaults if no API Server Spec has been set by new webhooks.
   979  	lb := s.APIServerLB()
   980  	if lb == nil || lb.Name == "" {
   981  		lbName := fmt.Sprintf("%s-%s", s.ClusterName(), "public-lb")
   982  		ip, dns := s.GenerateLegacyFQDN()
   983  		lb = &infrav1.LoadBalancerSpec{
   984  			Name: lbName,
   985  			FrontendIPs: []infrav1.FrontendIP{
   986  				{
   987  					Name: azure.GenerateFrontendIPConfigName(lbName),
   988  					PublicIP: &infrav1.PublicIPSpec{
   989  						Name:    ip,
   990  						DNSName: dns,
   991  					},
   992  				},
   993  			},
   994  			LoadBalancerClassSpec: infrav1.LoadBalancerClassSpec{
   995  				SKU:  infrav1.SKUStandard,
   996  				Type: infrav1.Public,
   997  			},
   998  		}
   999  		lb.DeepCopyInto(s.APIServerLB())
  1000  	}
  1001  	// Generate valid FQDN if not set.
  1002  	// Note: this function uses the AzureCluster subscription ID.
  1003  	if !s.IsAPIServerPrivate() && s.APIServerPublicIP().DNSName == "" {
  1004  		s.APIServerPublicIP().DNSName = s.GenerateFQDN(s.APIServerPublicIP().Name)
  1005  	}
  1006  }
  1007  
  1008  // SetLongRunningOperationState will set the future on the AzureCluster status to allow the resource to continue
  1009  // in the next reconciliation.
  1010  func (s *ClusterScope) SetLongRunningOperationState(future *infrav1.Future) {
  1011  	futures.Set(s.AzureCluster, future)
  1012  }
  1013  
  1014  // GetLongRunningOperationState will get the future on the AzureCluster status.
  1015  func (s *ClusterScope) GetLongRunningOperationState(name, service, futureType string) *infrav1.Future {
  1016  	return futures.Get(s.AzureCluster, name, service, futureType)
  1017  }
  1018  
  1019  // DeleteLongRunningOperationState will delete the future from the AzureCluster status.
  1020  func (s *ClusterScope) DeleteLongRunningOperationState(name, service, futureType string) {
  1021  	futures.Delete(s.AzureCluster, name, service, futureType)
  1022  }
  1023  
  1024  // UpdateDeleteStatus updates a condition on the AzureCluster status after a DELETE operation.
  1025  func (s *ClusterScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) {
  1026  	switch {
  1027  	case err == nil:
  1028  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service)
  1029  	case azure.IsOperationNotDoneError(err):
  1030  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service)
  1031  	default:
  1032  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error())
  1033  	}
  1034  }
  1035  
  1036  // UpdatePutStatus updates a condition on the AzureCluster status after a PUT operation.
  1037  func (s *ClusterScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) {
  1038  	switch {
  1039  	case err == nil:
  1040  		conditions.MarkTrue(s.AzureCluster, condition)
  1041  	case azure.IsOperationNotDoneError(err):
  1042  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service)
  1043  	default:
  1044  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error())
  1045  	}
  1046  }
  1047  
  1048  // UpdatePatchStatus updates a condition on the AzureCluster status after a PATCH operation.
  1049  func (s *ClusterScope) UpdatePatchStatus(condition clusterv1.ConditionType, service string, err error) {
  1050  	switch {
  1051  	case err == nil:
  1052  		conditions.MarkTrue(s.AzureCluster, condition)
  1053  	case azure.IsOperationNotDoneError(err):
  1054  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service)
  1055  	default:
  1056  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error())
  1057  	}
  1058  }
  1059  
  1060  // AnnotationJSON returns a map[string]interface from a JSON annotation.
  1061  func (s *ClusterScope) AnnotationJSON(annotation string) (map[string]interface{}, error) {
  1062  	out := map[string]interface{}{}
  1063  	jsonAnnotation := s.AzureCluster.GetAnnotations()[annotation]
  1064  	if jsonAnnotation == "" {
  1065  		return out, nil
  1066  	}
  1067  	err := json.Unmarshal([]byte(jsonAnnotation), &out)
  1068  	if err != nil {
  1069  		return out, err
  1070  	}
  1071  	return out, nil
  1072  }
  1073  
  1074  // UpdateAnnotationJSON updates the `annotation` with
  1075  // `content`. `content` in this case should be a `map[string]interface{}`
  1076  // suitable for turning into JSON. This `content` map will be marshalled into a
  1077  // JSON string before being set as the given `annotation`.
  1078  func (s *ClusterScope) UpdateAnnotationJSON(annotation string, content map[string]interface{}) error {
  1079  	b, err := json.Marshal(content)
  1080  	if err != nil {
  1081  		return err
  1082  	}
  1083  	s.SetAnnotation(annotation, string(b))
  1084  	return nil
  1085  }
  1086  
  1087  // SetAnnotation sets a key value annotation on the AzureCluster.
  1088  func (s *ClusterScope) SetAnnotation(key, value string) {
  1089  	if s.AzureCluster.Annotations == nil {
  1090  		s.AzureCluster.Annotations = map[string]string{}
  1091  	}
  1092  	s.AzureCluster.Annotations[key] = value
  1093  }
  1094  
  1095  // PrivateEndpointSpecs returns the private endpoint specs.
  1096  func (s *ClusterScope) PrivateEndpointSpecs() []azure.ASOResourceSpecGetter[*asonetworkv1api20220701.PrivateEndpoint] {
  1097  	subnetsList := s.AzureCluster.Spec.NetworkSpec.Subnets
  1098  	numberOfSubnets := len(subnetsList)
  1099  	if s.IsAzureBastionEnabled() {
  1100  		subnetsList = append(subnetsList, s.AzureCluster.Spec.BastionSpec.AzureBastion.Subnet)
  1101  		numberOfSubnets++
  1102  	}
  1103  
  1104  	// privateEndpointSpecs will be an empty list if no private endpoints were found.
  1105  	// We pre-allocate the list to avoid unnecessary allocations during append.
  1106  	privateEndpointSpecs := make([]azure.ASOResourceSpecGetter[*asonetworkv1api20220701.PrivateEndpoint], 0, numberOfSubnets)
  1107  
  1108  	for _, subnet := range subnetsList {
  1109  		for _, privateEndpoint := range subnet.PrivateEndpoints {
  1110  			privateEndpointSpec := &privateendpoints.PrivateEndpointSpec{
  1111  				Name:                       privateEndpoint.Name,
  1112  				ResourceGroup:              s.ResourceGroup(),
  1113  				Location:                   privateEndpoint.Location,
  1114  				CustomNetworkInterfaceName: privateEndpoint.CustomNetworkInterfaceName,
  1115  				PrivateIPAddresses:         privateEndpoint.PrivateIPAddresses,
  1116  				SubnetID:                   subnet.ID,
  1117  				ApplicationSecurityGroups:  privateEndpoint.ApplicationSecurityGroups,
  1118  				ManualApproval:             privateEndpoint.ManualApproval,
  1119  				ClusterName:                s.ClusterName(),
  1120  				AdditionalTags:             s.AdditionalTags(),
  1121  			}
  1122  
  1123  			for _, privateLinkServiceConnection := range privateEndpoint.PrivateLinkServiceConnections {
  1124  				pl := privateendpoints.PrivateLinkServiceConnection{
  1125  					PrivateLinkServiceID: privateLinkServiceConnection.PrivateLinkServiceID,
  1126  					Name:                 privateLinkServiceConnection.Name,
  1127  					RequestMessage:       privateLinkServiceConnection.RequestMessage,
  1128  					GroupIDs:             privateLinkServiceConnection.GroupIDs,
  1129  				}
  1130  				privateEndpointSpec.PrivateLinkServiceConnections = append(privateEndpointSpec.PrivateLinkServiceConnections, pl)
  1131  			}
  1132  			privateEndpointSpecs = append(privateEndpointSpecs, privateEndpointSpec)
  1133  		}
  1134  	}
  1135  
  1136  	return privateEndpointSpecs
  1137  }
  1138  
  1139  func (s *ClusterScope) getLastAppliedSecurityRules(nsgName string) map[string]interface{} {
  1140  	// Retrieve the last applied security rules for all NSGs.
  1141  	lastAppliedSecurityRulesAll, err := s.AnnotationJSON(azure.SecurityRuleLastAppliedAnnotation)
  1142  	if err != nil {
  1143  		return map[string]interface{}{}
  1144  	}
  1145  
  1146  	// Retrieve the last applied security rules for this NSG.
  1147  	lastAppliedSecurityRules, ok := lastAppliedSecurityRulesAll[nsgName].(map[string]interface{})
  1148  	if !ok {
  1149  		lastAppliedSecurityRules = map[string]interface{}{}
  1150  	}
  1151  	return lastAppliedSecurityRules
  1152  }