sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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 := NewAzureClusterCredentialsProvider(ctx, params.Client, params.AzureCluster)
    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.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  	isVnetManaged := s.Vnet().ID == "" || s.Vnet().Tags.HasOwned(s.ClusterName())
   586  	s.cache.isVnetManaged = ptr.To(isVnetManaged)
   587  	return isVnetManaged
   588  }
   589  
   590  // IsIPv6Enabled returns true if IPv6 is enabled.
   591  func (s *ClusterScope) IsIPv6Enabled() bool {
   592  	for _, cidr := range s.AzureCluster.Spec.NetworkSpec.Vnet.CIDRBlocks {
   593  		if net.IsIPv6CIDRString(cidr) {
   594  			return true
   595  		}
   596  	}
   597  	return false
   598  }
   599  
   600  // Subnets returns the cluster subnets.
   601  func (s *ClusterScope) Subnets() infrav1.Subnets {
   602  	return s.AzureCluster.Spec.NetworkSpec.Subnets
   603  }
   604  
   605  // ControlPlaneSubnet returns the cluster control plane subnet.
   606  func (s *ClusterScope) ControlPlaneSubnet() infrav1.SubnetSpec {
   607  	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetControlPlaneSubnet()
   608  	return subnet
   609  }
   610  
   611  // NodeSubnets returns the subnets with the node role.
   612  func (s *ClusterScope) NodeSubnets() []infrav1.SubnetSpec {
   613  	subnets := []infrav1.SubnetSpec{}
   614  	for _, subnet := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   615  		if subnet.Role == infrav1.SubnetNode || subnet.Role == infrav1.SubnetCluster {
   616  			subnets = append(subnets, subnet)
   617  		}
   618  	}
   619  
   620  	return subnets
   621  }
   622  
   623  // Subnet returns the subnet with the provided name.
   624  func (s *ClusterScope) Subnet(name string) infrav1.SubnetSpec {
   625  	for _, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   626  		if sn.Name == name {
   627  			return sn
   628  		}
   629  	}
   630  
   631  	return infrav1.SubnetSpec{}
   632  }
   633  
   634  // SetSubnet sets the subnet spec for the subnet with the same name.
   635  func (s *ClusterScope) SetSubnet(subnetSpec infrav1.SubnetSpec) {
   636  	for i, sn := range s.AzureCluster.Spec.NetworkSpec.Subnets {
   637  		if sn.Name == subnetSpec.Name {
   638  			s.AzureCluster.Spec.NetworkSpec.Subnets[i] = subnetSpec
   639  			return
   640  		}
   641  	}
   642  }
   643  
   644  // SetNatGatewayIDInSubnets sets the NAT Gateway ID in the subnets with the same name.
   645  func (s *ClusterScope) SetNatGatewayIDInSubnets(name string, id string) {
   646  	for _, subnet := range s.Subnets() {
   647  		if subnet.NatGateway.Name == name {
   648  			subnet.NatGateway.ID = id
   649  			s.SetSubnet(subnet)
   650  		}
   651  	}
   652  }
   653  
   654  // UpdateSubnetCIDRs updates the subnet CIDRs for the subnet with the same name.
   655  func (s *ClusterScope) UpdateSubnetCIDRs(name string, cidrBlocks []string) {
   656  	subnetSpecInfra := s.Subnet(name)
   657  	subnetSpecInfra.CIDRBlocks = cidrBlocks
   658  	s.SetSubnet(subnetSpecInfra)
   659  }
   660  
   661  // UpdateSubnetID updates the subnet ID for the subnet with the same name.
   662  func (s *ClusterScope) UpdateSubnetID(name string, id string) {
   663  	subnetSpecInfra := s.Subnet(name)
   664  	subnetSpecInfra.ID = id
   665  	s.SetSubnet(subnetSpecInfra)
   666  }
   667  
   668  // ControlPlaneRouteTable returns the cluster controlplane routetable.
   669  func (s *ClusterScope) ControlPlaneRouteTable() infrav1.RouteTable {
   670  	subnet, _ := s.AzureCluster.Spec.NetworkSpec.GetControlPlaneSubnet()
   671  	return subnet.RouteTable
   672  }
   673  
   674  // APIServerLB returns the cluster API Server load balancer.
   675  func (s *ClusterScope) APIServerLB() *infrav1.LoadBalancerSpec {
   676  	return &s.AzureCluster.Spec.NetworkSpec.APIServerLB
   677  }
   678  
   679  // NodeOutboundLB returns the cluster node outbound load balancer.
   680  func (s *ClusterScope) NodeOutboundLB() *infrav1.LoadBalancerSpec {
   681  	return s.AzureCluster.Spec.NetworkSpec.NodeOutboundLB
   682  }
   683  
   684  // ControlPlaneOutboundLB returns the cluster control plane outbound load balancer.
   685  func (s *ClusterScope) ControlPlaneOutboundLB() *infrav1.LoadBalancerSpec {
   686  	return s.AzureCluster.Spec.NetworkSpec.ControlPlaneOutboundLB
   687  }
   688  
   689  // APIServerLBName returns the API Server LB name.
   690  func (s *ClusterScope) APIServerLBName() string {
   691  	return s.APIServerLB().Name
   692  }
   693  
   694  // IsAPIServerPrivate returns true if the API Server LB is of type Internal.
   695  func (s *ClusterScope) IsAPIServerPrivate() bool {
   696  	return s.APIServerLB().Type == infrav1.Internal
   697  }
   698  
   699  // APIServerPublicIP returns the API Server public IP.
   700  func (s *ClusterScope) APIServerPublicIP() *infrav1.PublicIPSpec {
   701  	return s.APIServerLB().FrontendIPs[0].PublicIP
   702  }
   703  
   704  // APIServerPrivateIP returns the API Server private IP.
   705  func (s *ClusterScope) APIServerPrivateIP() string {
   706  	return s.APIServerLB().FrontendIPs[0].PrivateIPAddress
   707  }
   708  
   709  // GetPrivateDNSZoneName returns the Private DNS Zone from the spec or generate it from cluster name.
   710  func (s *ClusterScope) GetPrivateDNSZoneName() string {
   711  	if len(s.AzureCluster.Spec.NetworkSpec.PrivateDNSZoneName) > 0 {
   712  		return s.AzureCluster.Spec.NetworkSpec.PrivateDNSZoneName
   713  	}
   714  	return azure.GeneratePrivateDNSZoneName(s.ClusterName())
   715  }
   716  
   717  // APIServerLBPoolName returns the API Server LB backend pool name.
   718  func (s *ClusterScope) APIServerLBPoolName() string {
   719  	return s.APIServerLB().BackendPool.Name
   720  }
   721  
   722  // OutboundLB returns the outbound LB.
   723  func (s *ClusterScope) outboundLB(role string) *infrav1.LoadBalancerSpec {
   724  	if role == infrav1.Node {
   725  		return s.NodeOutboundLB()
   726  	}
   727  	if s.IsAPIServerPrivate() {
   728  		return s.ControlPlaneOutboundLB()
   729  	}
   730  	return s.APIServerLB()
   731  }
   732  
   733  // OutboundLBName returns the name of the outbound LB.
   734  func (s *ClusterScope) OutboundLBName(role string) string {
   735  	lb := s.outboundLB(role)
   736  	if lb == nil {
   737  		return ""
   738  	}
   739  	return lb.Name
   740  }
   741  
   742  // OutboundPoolName returns the outbound LB backend pool name.
   743  func (s *ClusterScope) OutboundPoolName(role string) string {
   744  	lb := s.outboundLB(role)
   745  	if lb == nil {
   746  		return ""
   747  	}
   748  	return lb.BackendPool.Name
   749  }
   750  
   751  // ResourceGroup returns the cluster resource group.
   752  func (s *ClusterScope) ResourceGroup() string {
   753  	return s.AzureCluster.Spec.ResourceGroup
   754  }
   755  
   756  // NodeResourceGroup returns the resource group where nodes live.
   757  // For AzureClusters this is the same as the cluster RG.
   758  func (s *ClusterScope) NodeResourceGroup() string {
   759  	return s.ResourceGroup()
   760  }
   761  
   762  // ClusterName returns the cluster name.
   763  func (s *ClusterScope) ClusterName() string {
   764  	return s.Cluster.Name
   765  }
   766  
   767  // Namespace returns the cluster namespace.
   768  func (s *ClusterScope) Namespace() string {
   769  	return s.Cluster.Namespace
   770  }
   771  
   772  // Location returns the cluster location.
   773  func (s *ClusterScope) Location() string {
   774  	return s.AzureCluster.Spec.Location
   775  }
   776  
   777  // AvailabilitySetEnabled informs machines that they should be part of an Availability Set.
   778  func (s *ClusterScope) AvailabilitySetEnabled() bool {
   779  	return len(s.AzureCluster.Status.FailureDomains) == 0
   780  }
   781  
   782  // CloudProviderConfigOverrides returns the cloud provider config overrides for the cluster.
   783  func (s *ClusterScope) CloudProviderConfigOverrides() *infrav1.CloudProviderConfigOverrides {
   784  	return s.AzureCluster.Spec.CloudProviderConfigOverrides
   785  }
   786  
   787  // ExtendedLocationName returns ExtendedLocation name for the cluster.
   788  func (s *ClusterScope) ExtendedLocationName() string {
   789  	if s.ExtendedLocation() == nil {
   790  		return ""
   791  	}
   792  	return s.ExtendedLocation().Name
   793  }
   794  
   795  // ExtendedLocationType returns ExtendedLocation type for the cluster.
   796  func (s *ClusterScope) ExtendedLocationType() string {
   797  	if s.ExtendedLocation() == nil {
   798  		return ""
   799  	}
   800  	return s.ExtendedLocation().Type
   801  }
   802  
   803  // ExtendedLocation returns the cluster extendedLocation.
   804  func (s *ClusterScope) ExtendedLocation() *infrav1.ExtendedLocationSpec {
   805  	return s.AzureCluster.Spec.ExtendedLocation
   806  }
   807  
   808  // GenerateFQDN generates a fully qualified domain name, based on a hash, cluster name and cluster location.
   809  func (s *ClusterScope) GenerateFQDN(ipName string) string {
   810  	h := fnv.New32a()
   811  	if _, err := fmt.Fprintf(h, "%s/%s/%s", s.SubscriptionID(), s.ResourceGroup(), ipName); err != nil {
   812  		return ""
   813  	}
   814  	hash := fmt.Sprintf("%x", h.Sum32())
   815  	return strings.ToLower(fmt.Sprintf("%s-%s.%s.%s", s.ClusterName(), hash, s.Location(), s.AzureClients.ResourceManagerVMDNSSuffix))
   816  }
   817  
   818  // GenerateLegacyFQDN generates an IP name and a fully qualified domain name, based on a hash, cluster name and cluster location.
   819  // Deprecated: use GenerateFQDN instead.
   820  func (s *ClusterScope) GenerateLegacyFQDN() (ip string, domain string) {
   821  	h := fnv.New32a()
   822  	if _, err := fmt.Fprintf(h, "%s/%s/%s", s.SubscriptionID(), s.ResourceGroup(), s.ClusterName()); err != nil {
   823  		return "", ""
   824  	}
   825  	ipName := fmt.Sprintf("%s-%x", s.ClusterName(), h.Sum32())
   826  	fqdn := fmt.Sprintf("%s.%s.%s", ipName, s.Location(), s.AzureClients.ResourceManagerVMDNSSuffix)
   827  	return ipName, fqdn
   828  }
   829  
   830  // ListOptionsLabelSelector returns a ListOptions with a label selector for clusterName.
   831  func (s *ClusterScope) ListOptionsLabelSelector() client.ListOption {
   832  	return client.MatchingLabels(map[string]string{
   833  		clusterv1.ClusterNameLabel: s.Cluster.Name,
   834  	})
   835  }
   836  
   837  // PatchObject persists the cluster configuration and status.
   838  func (s *ClusterScope) PatchObject(ctx context.Context) error {
   839  	ctx, _, done := tele.StartSpanWithLogger(ctx, "scope.ClusterScope.PatchObject")
   840  	defer done()
   841  
   842  	conditions.SetSummary(s.AzureCluster)
   843  
   844  	return s.patchHelper.Patch(
   845  		ctx,
   846  		s.AzureCluster,
   847  		patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
   848  			clusterv1.ReadyCondition,
   849  			infrav1.ResourceGroupReadyCondition,
   850  			infrav1.RouteTablesReadyCondition,
   851  			infrav1.NetworkInfrastructureReadyCondition,
   852  			infrav1.VnetPeeringReadyCondition,
   853  			infrav1.DisksReadyCondition,
   854  			infrav1.NATGatewaysReadyCondition,
   855  			infrav1.LoadBalancersReadyCondition,
   856  			infrav1.BastionHostReadyCondition,
   857  			infrav1.VNetReadyCondition,
   858  			infrav1.SubnetsReadyCondition,
   859  			infrav1.SecurityGroupsReadyCondition,
   860  			infrav1.PrivateDNSZoneReadyCondition,
   861  			infrav1.PrivateDNSLinkReadyCondition,
   862  			infrav1.PrivateDNSRecordReadyCondition,
   863  			infrav1.PrivateEndpointsReadyCondition,
   864  		}})
   865  }
   866  
   867  // Close closes the current scope persisting the cluster configuration and status.
   868  func (s *ClusterScope) Close(ctx context.Context) error {
   869  	return s.PatchObject(ctx)
   870  }
   871  
   872  // AdditionalTags returns AdditionalTags from the scope's AzureCluster.
   873  func (s *ClusterScope) AdditionalTags() infrav1.Tags {
   874  	tags := make(infrav1.Tags)
   875  	if s.AzureCluster.Spec.AdditionalTags != nil {
   876  		tags = s.AzureCluster.Spec.AdditionalTags.DeepCopy()
   877  	}
   878  	return tags
   879  }
   880  
   881  // APIServerPort returns the APIServerPort to use when creating the load balancer.
   882  func (s *ClusterScope) APIServerPort() int32 {
   883  	if s.Cluster.Spec.ClusterNetwork != nil && s.Cluster.Spec.ClusterNetwork.APIServerPort != nil {
   884  		return *s.Cluster.Spec.ClusterNetwork.APIServerPort
   885  	}
   886  	return 6443
   887  }
   888  
   889  // APIServerHost returns the hostname used to reach the API server.
   890  func (s *ClusterScope) APIServerHost() string {
   891  	if s.IsAPIServerPrivate() {
   892  		return azure.GeneratePrivateFQDN(s.GetPrivateDNSZoneName())
   893  	}
   894  	return s.APIServerPublicIP().DNSName
   895  }
   896  
   897  // SetFailureDomain sets a failure domain in a cluster's status by its id.
   898  // The provided failure domain spec may be overridden to false by cluster's spec property.
   899  func (s *ClusterScope) SetFailureDomain(id string, spec clusterv1.FailureDomainSpec) {
   900  	if s.AzureCluster.Status.FailureDomains == nil {
   901  		s.AzureCluster.Status.FailureDomains = make(clusterv1.FailureDomains)
   902  	}
   903  
   904  	if fd, ok := s.AzureCluster.Spec.FailureDomains[id]; ok && !fd.ControlPlane {
   905  		spec.ControlPlane = false
   906  	}
   907  
   908  	s.AzureCluster.Status.FailureDomains[id] = spec
   909  }
   910  
   911  // FailureDomains returns the failure domains for the cluster.
   912  func (s *ClusterScope) FailureDomains() []*string {
   913  	fds := make([]*string, len(s.AzureCluster.Status.FailureDomains))
   914  	i := 0
   915  	for id := range s.AzureCluster.Status.FailureDomains {
   916  		fds[i] = ptr.To(id)
   917  		i++
   918  	}
   919  
   920  	// sort in increasing order restoring the original sort.Strings(fds) behavior
   921  	sort.Slice(fds, func(i, j int) bool {
   922  		return *fds[i] < *fds[j]
   923  	})
   924  
   925  	return fds
   926  }
   927  
   928  // SetControlPlaneSecurityRules sets the default security rules of the control plane subnet.
   929  // Note that this is not done in a webhook as it requires a valid Cluster object to exist to get the API Server port.
   930  func (s *ClusterScope) SetControlPlaneSecurityRules() {
   931  	if s.ControlPlaneSubnet().SecurityGroup.SecurityRules == nil {
   932  		subnet := s.ControlPlaneSubnet()
   933  		subnet.SecurityGroup.SecurityRules = infrav1.SecurityRules{
   934  			infrav1.SecurityRule{
   935  				Name:             "allow_ssh",
   936  				Description:      "Allow SSH",
   937  				Priority:         2200,
   938  				Protocol:         infrav1.SecurityGroupProtocolTCP,
   939  				Direction:        infrav1.SecurityRuleDirectionInbound,
   940  				Source:           ptr.To("*"),
   941  				SourcePorts:      ptr.To("*"),
   942  				Destination:      ptr.To("*"),
   943  				DestinationPorts: ptr.To("22"),
   944  				Action:           infrav1.SecurityRuleActionAllow,
   945  			},
   946  			infrav1.SecurityRule{
   947  				Name:             "allow_apiserver",
   948  				Description:      "Allow K8s API Server",
   949  				Priority:         2201,
   950  				Protocol:         infrav1.SecurityGroupProtocolTCP,
   951  				Direction:        infrav1.SecurityRuleDirectionInbound,
   952  				Source:           ptr.To("*"),
   953  				SourcePorts:      ptr.To("*"),
   954  				Destination:      ptr.To("*"),
   955  				DestinationPorts: ptr.To(strconv.Itoa(int(s.APIServerPort()))),
   956  				Action:           infrav1.SecurityRuleActionAllow,
   957  			},
   958  		}
   959  		s.AzureCluster.Spec.NetworkSpec.UpdateControlPlaneSubnet(subnet)
   960  	}
   961  }
   962  
   963  // SetDNSName sets the API Server public IP DNS name.
   964  // 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.
   965  func (s *ClusterScope) SetDNSName() {
   966  	// for back compat, set the old API Server defaults if no API Server Spec has been set by new webhooks.
   967  	lb := s.APIServerLB()
   968  	if lb == nil || lb.Name == "" {
   969  		lbName := fmt.Sprintf("%s-%s", s.ClusterName(), "public-lb")
   970  		ip, dns := s.GenerateLegacyFQDN()
   971  		lb = &infrav1.LoadBalancerSpec{
   972  			Name: lbName,
   973  			FrontendIPs: []infrav1.FrontendIP{
   974  				{
   975  					Name: azure.GenerateFrontendIPConfigName(lbName),
   976  					PublicIP: &infrav1.PublicIPSpec{
   977  						Name:    ip,
   978  						DNSName: dns,
   979  					},
   980  				},
   981  			},
   982  			LoadBalancerClassSpec: infrav1.LoadBalancerClassSpec{
   983  				SKU:  infrav1.SKUStandard,
   984  				Type: infrav1.Public,
   985  			},
   986  		}
   987  		lb.DeepCopyInto(s.APIServerLB())
   988  	}
   989  	// Generate valid FQDN if not set.
   990  	// Note: this function uses the AzureCluster subscription ID.
   991  	if !s.IsAPIServerPrivate() && s.APIServerPublicIP().DNSName == "" {
   992  		s.APIServerPublicIP().DNSName = s.GenerateFQDN(s.APIServerPublicIP().Name)
   993  	}
   994  }
   995  
   996  // SetLongRunningOperationState will set the future on the AzureCluster status to allow the resource to continue
   997  // in the next reconciliation.
   998  func (s *ClusterScope) SetLongRunningOperationState(future *infrav1.Future) {
   999  	futures.Set(s.AzureCluster, future)
  1000  }
  1001  
  1002  // GetLongRunningOperationState will get the future on the AzureCluster status.
  1003  func (s *ClusterScope) GetLongRunningOperationState(name, service, futureType string) *infrav1.Future {
  1004  	return futures.Get(s.AzureCluster, name, service, futureType)
  1005  }
  1006  
  1007  // DeleteLongRunningOperationState will delete the future from the AzureCluster status.
  1008  func (s *ClusterScope) DeleteLongRunningOperationState(name, service, futureType string) {
  1009  	futures.Delete(s.AzureCluster, name, service, futureType)
  1010  }
  1011  
  1012  // UpdateDeleteStatus updates a condition on the AzureCluster status after a DELETE operation.
  1013  func (s *ClusterScope) UpdateDeleteStatus(condition clusterv1.ConditionType, service string, err error) {
  1014  	switch {
  1015  	case err == nil:
  1016  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletedReason, clusterv1.ConditionSeverityInfo, "%s successfully deleted", service)
  1017  	case azure.IsOperationNotDoneError(err):
  1018  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletingReason, clusterv1.ConditionSeverityInfo, "%s deleting", service)
  1019  	default:
  1020  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.DeletionFailedReason, clusterv1.ConditionSeverityError, "%s failed to delete. err: %s", service, err.Error())
  1021  	}
  1022  }
  1023  
  1024  // UpdatePutStatus updates a condition on the AzureCluster status after a PUT operation.
  1025  func (s *ClusterScope) UpdatePutStatus(condition clusterv1.ConditionType, service string, err error) {
  1026  	switch {
  1027  	case err == nil:
  1028  		conditions.MarkTrue(s.AzureCluster, condition)
  1029  	case azure.IsOperationNotDoneError(err):
  1030  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.CreatingReason, clusterv1.ConditionSeverityInfo, "%s creating or updating", service)
  1031  	default:
  1032  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to create or update. err: %s", service, err.Error())
  1033  	}
  1034  }
  1035  
  1036  // UpdatePatchStatus updates a condition on the AzureCluster status after a PATCH operation.
  1037  func (s *ClusterScope) UpdatePatchStatus(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.UpdatingReason, clusterv1.ConditionSeverityInfo, "%s updating", service)
  1043  	default:
  1044  		conditions.MarkFalse(s.AzureCluster, condition, infrav1.FailedReason, clusterv1.ConditionSeverityError, "%s failed to update. err: %s", service, err.Error())
  1045  	}
  1046  }
  1047  
  1048  // AnnotationJSON returns a map[string]interface from a JSON annotation.
  1049  func (s *ClusterScope) AnnotationJSON(annotation string) (map[string]interface{}, error) {
  1050  	out := map[string]interface{}{}
  1051  	jsonAnnotation := s.AzureCluster.GetAnnotations()[annotation]
  1052  	if jsonAnnotation == "" {
  1053  		return out, nil
  1054  	}
  1055  	err := json.Unmarshal([]byte(jsonAnnotation), &out)
  1056  	if err != nil {
  1057  		return out, err
  1058  	}
  1059  	return out, nil
  1060  }
  1061  
  1062  // UpdateAnnotationJSON updates the `annotation` with
  1063  // `content`. `content` in this case should be a `map[string]interface{}`
  1064  // suitable for turning into JSON. This `content` map will be marshalled into a
  1065  // JSON string before being set as the given `annotation`.
  1066  func (s *ClusterScope) UpdateAnnotationJSON(annotation string, content map[string]interface{}) error {
  1067  	b, err := json.Marshal(content)
  1068  	if err != nil {
  1069  		return err
  1070  	}
  1071  	s.SetAnnotation(annotation, string(b))
  1072  	return nil
  1073  }
  1074  
  1075  // SetAnnotation sets a key value annotation on the AzureCluster.
  1076  func (s *ClusterScope) SetAnnotation(key, value string) {
  1077  	if s.AzureCluster.Annotations == nil {
  1078  		s.AzureCluster.Annotations = map[string]string{}
  1079  	}
  1080  	s.AzureCluster.Annotations[key] = value
  1081  }
  1082  
  1083  // PrivateEndpointSpecs returns the private endpoint specs.
  1084  func (s *ClusterScope) PrivateEndpointSpecs() []azure.ASOResourceSpecGetter[*asonetworkv1api20220701.PrivateEndpoint] {
  1085  	subnetsList := s.AzureCluster.Spec.NetworkSpec.Subnets
  1086  	numberOfSubnets := len(subnetsList)
  1087  	if s.IsAzureBastionEnabled() {
  1088  		subnetsList = append(subnetsList, s.AzureCluster.Spec.BastionSpec.AzureBastion.Subnet)
  1089  		numberOfSubnets++
  1090  	}
  1091  
  1092  	// privateEndpointSpecs will be an empty list if no private endpoints were found.
  1093  	// We pre-allocate the list to avoid unnecessary allocations during append.
  1094  	privateEndpointSpecs := make([]azure.ASOResourceSpecGetter[*asonetworkv1api20220701.PrivateEndpoint], 0, numberOfSubnets)
  1095  
  1096  	for _, subnet := range subnetsList {
  1097  		for _, privateEndpoint := range subnet.PrivateEndpoints {
  1098  			privateEndpointSpec := &privateendpoints.PrivateEndpointSpec{
  1099  				Name:                       privateEndpoint.Name,
  1100  				ResourceGroup:              s.ResourceGroup(),
  1101  				Location:                   privateEndpoint.Location,
  1102  				CustomNetworkInterfaceName: privateEndpoint.CustomNetworkInterfaceName,
  1103  				PrivateIPAddresses:         privateEndpoint.PrivateIPAddresses,
  1104  				SubnetID:                   subnet.ID,
  1105  				ApplicationSecurityGroups:  privateEndpoint.ApplicationSecurityGroups,
  1106  				ManualApproval:             privateEndpoint.ManualApproval,
  1107  				ClusterName:                s.ClusterName(),
  1108  				AdditionalTags:             s.AdditionalTags(),
  1109  			}
  1110  
  1111  			for _, privateLinkServiceConnection := range privateEndpoint.PrivateLinkServiceConnections {
  1112  				pl := privateendpoints.PrivateLinkServiceConnection{
  1113  					PrivateLinkServiceID: privateLinkServiceConnection.PrivateLinkServiceID,
  1114  					Name:                 privateLinkServiceConnection.Name,
  1115  					RequestMessage:       privateLinkServiceConnection.RequestMessage,
  1116  					GroupIDs:             privateLinkServiceConnection.GroupIDs,
  1117  				}
  1118  				privateEndpointSpec.PrivateLinkServiceConnections = append(privateEndpointSpec.PrivateLinkServiceConnections, pl)
  1119  			}
  1120  			privateEndpointSpecs = append(privateEndpointSpecs, privateEndpointSpec)
  1121  		}
  1122  	}
  1123  
  1124  	return privateEndpointSpecs
  1125  }
  1126  
  1127  func (s *ClusterScope) getLastAppliedSecurityRules(nsgName string) map[string]interface{} {
  1128  	// Retrieve the last applied security rules for all NSGs.
  1129  	lastAppliedSecurityRulesAll, err := s.AnnotationJSON(azure.SecurityRuleLastAppliedAnnotation)
  1130  	if err != nil {
  1131  		return map[string]interface{}{}
  1132  	}
  1133  
  1134  	// Retrieve the last applied security rules for this NSG.
  1135  	lastAppliedSecurityRules, ok := lastAppliedSecurityRulesAll[nsgName].(map[string]interface{})
  1136  	if !ok {
  1137  		lastAppliedSecurityRules = map[string]interface{}{}
  1138  	}
  1139  	return lastAppliedSecurityRules
  1140  }