github.com/openshift/installer@v1.4.17/pkg/infrastructure/azure/azure.go (about)

     1  package azure
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    13  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    14  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
    15  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    16  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
    17  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3"
    18  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4"
    19  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi"
    20  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2"
    21  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
    22  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
    23  	"github.com/coreos/stream-metadata-go/arch"
    24  	"github.com/google/uuid"
    25  	"github.com/sirupsen/logrus"
    26  	"k8s.io/utils/ptr"
    27  	capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  
    30  	"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
    31  	azconfig "github.com/openshift/installer/pkg/asset/installconfig/azure"
    32  	"github.com/openshift/installer/pkg/asset/manifests/capiutils"
    33  	"github.com/openshift/installer/pkg/infrastructure/clusterapi"
    34  	"github.com/openshift/installer/pkg/rhcos"
    35  	"github.com/openshift/installer/pkg/types"
    36  	aztypes "github.com/openshift/installer/pkg/types/azure"
    37  )
    38  
    39  const (
    40  	retryTime  = 10 * time.Second
    41  	retryCount = 6
    42  )
    43  
    44  // Provider implements Azure CAPI installation.
    45  type Provider struct {
    46  	ResourceGroupName    string
    47  	StorageAccountName   string
    48  	StorageURL           string
    49  	StorageAccount       *armstorage.Account
    50  	StorageClientFactory *armstorage.ClientFactory
    51  	StorageAccountKeys   []armstorage.AccountKey
    52  	NetworkClientFactory *armnetwork.ClientFactory
    53  	lbBackendAddressPool *armnetwork.BackendAddressPool
    54  	CloudConfiguration   cloud.Configuration
    55  	TokenCredential      azcore.TokenCredential
    56  	Tags                 map[string]*string
    57  }
    58  
    59  var _ clusterapi.PreProvider = (*Provider)(nil)
    60  var _ clusterapi.InfraReadyProvider = (*Provider)(nil)
    61  var _ clusterapi.PostProvider = (*Provider)(nil)
    62  var _ clusterapi.IgnitionProvider = (*Provider)(nil)
    63  var _ clusterapi.PostDestroyer = (*Provider)(nil)
    64  
    65  // Name returns the name of the provider.
    66  func (p *Provider) Name() string {
    67  	return aztypes.Name
    68  }
    69  
    70  // PublicGatherEndpoint indicates that machine ready checks should NOT wait for an ExternalIP
    71  // in the status and should use the API load balancer when gathering bootstrap log bundles.
    72  func (*Provider) PublicGatherEndpoint() clusterapi.GatherEndpoint { return clusterapi.APILoadBalancer }
    73  
    74  // PreProvision is called before provisioning using CAPI controllers has begun.
    75  func (p *Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionInput) error {
    76  	session, err := in.InstallConfig.Azure.Session()
    77  	if err != nil {
    78  		return fmt.Errorf("failed to get session: %w", err)
    79  	}
    80  
    81  	installConfig := in.InstallConfig.Config
    82  	platform := installConfig.Platform.Azure
    83  	subscriptionID := session.Credentials.SubscriptionID
    84  	cloudConfiguration := session.CloudConfig
    85  	tokenCredential := session.TokenCreds
    86  	resourceGroupName := platform.ClusterResourceGroupName(in.InfraID)
    87  
    88  	userTags := platform.UserTags
    89  	tags := make(map[string]*string, len(userTags)+1)
    90  	tags[fmt.Sprintf("kubernetes.io_cluster.%s", in.InfraID)] = ptr.To("owned")
    91  	for k, v := range userTags {
    92  		tags[k] = ptr.To(v)
    93  	}
    94  	p.Tags = tags
    95  
    96  	// Create resource group
    97  	resourcesClientFactory, err := armresources.NewClientFactory(
    98  		subscriptionID,
    99  		tokenCredential,
   100  		&arm.ClientOptions{
   101  			ClientOptions: policy.ClientOptions{
   102  				Cloud: cloudConfiguration,
   103  			},
   104  		},
   105  	)
   106  	if err != nil {
   107  		return fmt.Errorf("failed to get azure resource groups factory: %w", err)
   108  	}
   109  	resourceGroupsClient := resourcesClientFactory.NewResourceGroupsClient()
   110  	_, err = resourceGroupsClient.CreateOrUpdate(
   111  		ctx,
   112  		resourceGroupName,
   113  		armresources.ResourceGroup{
   114  			Location:  ptr.To(platform.Region),
   115  			ManagedBy: nil,
   116  			Tags:      tags,
   117  		},
   118  		nil,
   119  	)
   120  	if err != nil {
   121  		return fmt.Errorf("error creating resource group %s: %w", resourceGroupName, err)
   122  	}
   123  	resourceGroup, err := resourceGroupsClient.Get(ctx, resourceGroupName, nil)
   124  	if err != nil {
   125  		return fmt.Errorf("error getting resource group %s: %w", resourceGroupName, err)
   126  	}
   127  
   128  	logrus.Debugf("ResourceGroup.ID=%s", *resourceGroup.ID)
   129  	p.ResourceGroupName = resourceGroupName
   130  
   131  	// Create user assigned identity
   132  	userAssignedIdentityName := fmt.Sprintf("%s-identity", in.InfraID)
   133  	armmsiClientFactory, err := armmsi.NewClientFactory(
   134  		subscriptionID,
   135  		tokenCredential,
   136  		&arm.ClientOptions{
   137  			ClientOptions: policy.ClientOptions{
   138  				Cloud: cloudConfiguration,
   139  			},
   140  		},
   141  	)
   142  	if err != nil {
   143  		return fmt.Errorf("failed to create armmsi client: %w", err)
   144  	}
   145  	_, err = armmsiClientFactory.NewUserAssignedIdentitiesClient().CreateOrUpdate(
   146  		ctx,
   147  		resourceGroupName,
   148  		userAssignedIdentityName,
   149  		armmsi.Identity{
   150  			Location: ptr.To(platform.Region),
   151  			Tags:     tags,
   152  		},
   153  		nil,
   154  	)
   155  	if err != nil {
   156  		return fmt.Errorf("failed to create user assigned identity %s: %w", userAssignedIdentityName, err)
   157  	}
   158  	userAssignedIdentity, err := armmsiClientFactory.NewUserAssignedIdentitiesClient().Get(
   159  		ctx,
   160  		resourceGroupName,
   161  		userAssignedIdentityName,
   162  		nil,
   163  	)
   164  	if err != nil {
   165  		return fmt.Errorf("failed to get user assigned identity %s: %w", userAssignedIdentityName, err)
   166  	}
   167  	principalID := *userAssignedIdentity.Properties.PrincipalID
   168  
   169  	logrus.Debugf("UserAssignedIdentity.ID=%s", *userAssignedIdentity.ID)
   170  	logrus.Debugf("PrinciapalID=%s", principalID)
   171  
   172  	clientFactory, err := armauthorization.NewClientFactory(
   173  		subscriptionID,
   174  		tokenCredential,
   175  		&arm.ClientOptions{
   176  			ClientOptions: policy.ClientOptions{
   177  				Cloud: cloudConfiguration,
   178  			},
   179  		},
   180  	)
   181  	if err != nil {
   182  		return fmt.Errorf("failed to create armauthorization client: %w", err)
   183  	}
   184  
   185  	roleDefinitionsClient := clientFactory.NewRoleDefinitionsClient()
   186  
   187  	var contributor *armauthorization.RoleDefinition
   188  	roleDefinitionsPager := roleDefinitionsClient.NewListPager(*resourceGroup.ID, nil)
   189  	for roleDefinitionsPager.More() {
   190  		roleDefinitionsList, err := roleDefinitionsPager.NextPage(ctx)
   191  		if err != nil {
   192  			return fmt.Errorf("failed to find any role definitions: %w", err)
   193  		}
   194  		for _, roleDefinition := range roleDefinitionsList.Value {
   195  			if *roleDefinition.Properties.RoleName == "Contributor" {
   196  				contributor = roleDefinition
   197  				break
   198  			}
   199  		}
   200  	}
   201  	if contributor == nil {
   202  		return fmt.Errorf("failed to find contributor definition")
   203  	}
   204  
   205  	roleAssignmentsClient := clientFactory.NewRoleAssignmentsClient()
   206  	scope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionID, resourceGroupName)
   207  	roleAssignmentUUID := uuid.New().String()
   208  
   209  	// XXX: Azure doesn't like creating an identity and immediately
   210  	// creating a role assignment for the identity. There can be
   211  	// replication delays. So, retry every 10 seconds for a minute until
   212  	// the role assignment gets created.
   213  	//
   214  	// See https://aka.ms/docs-principaltype
   215  	for i := 0; i < retryCount; i++ {
   216  		_, err = roleAssignmentsClient.Create(ctx, scope, roleAssignmentUUID,
   217  			armauthorization.RoleAssignmentCreateParameters{
   218  				Properties: &armauthorization.RoleAssignmentProperties{
   219  					PrincipalID:      ptr.To(principalID),
   220  					RoleDefinitionID: contributor.ID,
   221  				},
   222  			},
   223  			nil,
   224  		)
   225  		if err == nil {
   226  			break
   227  		}
   228  		time.Sleep(retryTime)
   229  	}
   230  	if err != nil {
   231  		return fmt.Errorf("failed to create role assignment: %w", err)
   232  	}
   233  
   234  	// Creating a dummy nsg for existing vnets installation to appease the ingress operator.
   235  	if in.InstallConfig.Config.Azure.VirtualNetwork != "" {
   236  		networkClientFactory, err := armnetwork.NewClientFactory(subscriptionID, tokenCredential, nil)
   237  		if err != nil {
   238  			return fmt.Errorf("failed to create azure network factory: %w", err)
   239  		}
   240  		securityGroupName := in.InstallConfig.Config.Platform.Azure.NetworkSecurityGroupName(in.InfraID)
   241  		securityGroupsClient := networkClientFactory.NewSecurityGroupsClient()
   242  		pollerResp, err := securityGroupsClient.BeginCreateOrUpdate(
   243  			ctx,
   244  			resourceGroupName,
   245  			securityGroupName,
   246  			armnetwork.SecurityGroup{
   247  				Location: to.Ptr(platform.Region),
   248  				Tags:     tags,
   249  			},
   250  			nil)
   251  		if err != nil {
   252  			return fmt.Errorf("failed to create network security group: %w", err)
   253  		}
   254  		nsg, err := pollerResp.PollUntilDone(ctx, nil)
   255  		if err != nil {
   256  			return fmt.Errorf("failed to create network security group: %w", err)
   257  		}
   258  		logrus.Infof("nsg=%s", *nsg.ID)
   259  	}
   260  
   261  	return nil
   262  }
   263  
   264  // InfraReady is called once the installer infrastructure is ready.
   265  func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput) error {
   266  	session, err := in.InstallConfig.Azure.Session()
   267  	if err != nil {
   268  		return fmt.Errorf("failed to get session: %w", err)
   269  	}
   270  
   271  	installConfig := in.InstallConfig.Config
   272  	platform := installConfig.Platform.Azure
   273  	subscriptionID := session.Credentials.SubscriptionID
   274  	cloudConfiguration := session.CloudConfig
   275  
   276  	var architecture armcompute.Architecture
   277  	if installConfig.ControlPlane.Architecture == types.ArchitectureARM64 {
   278  		architecture = armcompute.ArchitectureArm64
   279  	} else {
   280  		architecture = armcompute.ArchitectureX64
   281  	}
   282  
   283  	resourceGroupName := p.ResourceGroupName
   284  	storageAccountName := fmt.Sprintf("%ssa", strings.ReplaceAll(in.InfraID, "-", ""))
   285  	containerName := "vhd"
   286  	blobName := fmt.Sprintf("rhcos%s.vhd", randomString(5))
   287  
   288  	stream, err := rhcos.FetchCoreOSBuild(ctx)
   289  	if err != nil {
   290  		return fmt.Errorf("failed to get rhcos stream: %w", err)
   291  	}
   292  	archName := arch.RpmArch(string(installConfig.ControlPlane.Architecture))
   293  	streamArch, err := stream.GetArchitecture(archName)
   294  	if err != nil {
   295  		return fmt.Errorf("failed to get rhcos architecture: %w", err)
   296  	}
   297  
   298  	azureDisk := streamArch.RHELCoreOSExtensions.AzureDisk
   299  	imageURL := azureDisk.URL
   300  
   301  	rawImageVersion := strings.ReplaceAll(azureDisk.Release, "-", "_")
   302  	imageVersion := rawImageVersion[:len(rawImageVersion)-6]
   303  
   304  	galleryName := fmt.Sprintf("gallery_%s", strings.ReplaceAll(in.InfraID, "-", "_"))
   305  	galleryImageName := in.InfraID
   306  	galleryImageVersionName := imageVersion
   307  	galleryGen2ImageName := fmt.Sprintf("%s-gen2", in.InfraID)
   308  	galleryGen2ImageVersionName := imageVersion
   309  
   310  	headResponse, err := http.Head(imageURL) // nolint:gosec
   311  	if err != nil {
   312  		return fmt.Errorf("failed HEAD request for image URL %s: %w", imageURL, err)
   313  	}
   314  
   315  	imageLength := headResponse.ContentLength
   316  	if imageLength%512 != 0 {
   317  		return fmt.Errorf("image length is not aligned on a 512 byte boundary")
   318  	}
   319  
   320  	userTags := platform.UserTags
   321  	tags := make(map[string]*string, len(userTags)+1)
   322  	tags[fmt.Sprintf("kubernetes.io_cluster.%s", in.InfraID)] = ptr.To("owned")
   323  	for k, v := range userTags {
   324  		tags[k] = ptr.To(v)
   325  	}
   326  
   327  	tokenCredential := session.TokenCreds
   328  	storageURL := fmt.Sprintf("https://%s.blob.core.windows.net", storageAccountName)
   329  	blobURL := fmt.Sprintf("%s/%s/%s", storageURL, containerName, blobName)
   330  
   331  	// Create storage account
   332  	createStorageAccountOutput, err := CreateStorageAccount(ctx, &CreateStorageAccountInput{
   333  		SubscriptionID:     subscriptionID,
   334  		ResourceGroupName:  resourceGroupName,
   335  		StorageAccountName: storageAccountName,
   336  		CloudName:          platform.CloudName,
   337  		Region:             platform.Region,
   338  		Tags:               tags,
   339  		CustomerManagedKey: platform.CustomerManagedKey,
   340  		TokenCredential:    tokenCredential,
   341  		CloudConfiguration: cloudConfiguration,
   342  	})
   343  	if err != nil {
   344  		return err
   345  	}
   346  
   347  	storageAccount := createStorageAccountOutput.StorageAccount
   348  	storageClientFactory := createStorageAccountOutput.StorageClientFactory
   349  	storageAccountKeys := createStorageAccountOutput.StorageAccountKeys
   350  
   351  	logrus.Debugf("StorageAccount.ID=%s", *storageAccount.ID)
   352  
   353  	// Create blob storage container
   354  	publicAccess := armstorage.PublicAccessContainer
   355  	if platform.CustomerManagedKey != nil {
   356  		publicAccess = armstorage.PublicAccessNone
   357  	}
   358  	createBlobContainerOutput, err := CreateBlobContainer(ctx, &CreateBlobContainerInput{
   359  		SubscriptionID:       subscriptionID,
   360  		ResourceGroupName:    resourceGroupName,
   361  		StorageAccountName:   storageAccountName,
   362  		ContainerName:        containerName,
   363  		PublicAccess:         to.Ptr(publicAccess),
   364  		StorageClientFactory: storageClientFactory,
   365  	})
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	blobContainer := createBlobContainerOutput.BlobContainer
   371  	logrus.Debugf("BlobContainer.ID=%s", *blobContainer.ID)
   372  
   373  	// Upload the image to the container
   374  	if _, ok := os.LookupEnv("OPENSHIFT_INSTALL_SKIP_IMAGE_UPLOAD"); !ok {
   375  		_, err = CreatePageBlob(ctx, &CreatePageBlobInput{
   376  			StorageURL:         storageURL,
   377  			BlobURL:            blobURL,
   378  			ImageURL:           imageURL,
   379  			ImageLength:        imageLength,
   380  			StorageAccountName: storageAccountName,
   381  			StorageAccountKeys: storageAccountKeys,
   382  			CloudConfiguration: cloudConfiguration,
   383  		})
   384  		if err != nil {
   385  			return err
   386  		}
   387  
   388  		// Create image gallery
   389  		createImageGalleryOutput, err := CreateImageGallery(ctx, &CreateImageGalleryInput{
   390  			SubscriptionID:     subscriptionID,
   391  			ResourceGroupName:  resourceGroupName,
   392  			GalleryName:        galleryName,
   393  			Region:             platform.Region,
   394  			Tags:               tags,
   395  			TokenCredential:    tokenCredential,
   396  			CloudConfiguration: cloudConfiguration,
   397  		})
   398  		if err != nil {
   399  			return err
   400  		}
   401  
   402  		computeClientFactory := createImageGalleryOutput.ComputeClientFactory
   403  
   404  		// Create gallery images
   405  		_, err = CreateGalleryImage(ctx, &CreateGalleryImageInput{
   406  			ResourceGroupName:    resourceGroupName,
   407  			GalleryName:          galleryName,
   408  			GalleryImageName:     galleryImageName,
   409  			Region:               platform.Region,
   410  			Publisher:            "RedHat",
   411  			Offer:                "rhcos",
   412  			SKU:                  "basic",
   413  			Tags:                 tags,
   414  			TokenCredential:      tokenCredential,
   415  			CloudConfiguration:   cloudConfiguration,
   416  			Architecture:         architecture,
   417  			OSType:               armcompute.OperatingSystemTypesLinux,
   418  			OSState:              armcompute.OperatingSystemStateTypesGeneralized,
   419  			HyperVGeneration:     armcompute.HyperVGenerationV1,
   420  			ComputeClientFactory: computeClientFactory,
   421  		})
   422  		if err != nil {
   423  			return err
   424  		}
   425  
   426  		_, err = CreateGalleryImage(ctx, &CreateGalleryImageInput{
   427  			ResourceGroupName:    resourceGroupName,
   428  			GalleryName:          galleryName,
   429  			GalleryImageName:     galleryGen2ImageName,
   430  			Region:               platform.Region,
   431  			Publisher:            "RedHat-gen2",
   432  			Offer:                "rhcos-gen2",
   433  			SKU:                  "gen2",
   434  			Tags:                 tags,
   435  			TokenCredential:      tokenCredential,
   436  			CloudConfiguration:   cloudConfiguration,
   437  			Architecture:         architecture,
   438  			OSType:               armcompute.OperatingSystemTypesLinux,
   439  			OSState:              armcompute.OperatingSystemStateTypesGeneralized,
   440  			HyperVGeneration:     armcompute.HyperVGenerationV2,
   441  			ComputeClientFactory: computeClientFactory,
   442  		})
   443  		if err != nil {
   444  			return err
   445  		}
   446  
   447  		// Create gallery image versions
   448  		_, err = CreateGalleryImageVersion(ctx, &CreateGalleryImageVersionInput{
   449  			ResourceGroupName:       resourceGroupName,
   450  			StorageAccountID:        *storageAccount.ID,
   451  			GalleryName:             galleryName,
   452  			GalleryImageName:        galleryImageName,
   453  			GalleryImageVersionName: galleryImageVersionName,
   454  			Region:                  platform.Region,
   455  			BlobURL:                 blobURL,
   456  			RegionalReplicaCount:    int32(1),
   457  			ComputeClientFactory:    computeClientFactory,
   458  		})
   459  		if err != nil {
   460  			return err
   461  		}
   462  
   463  		_, err = CreateGalleryImageVersion(ctx, &CreateGalleryImageVersionInput{
   464  			ResourceGroupName:       resourceGroupName,
   465  			StorageAccountID:        *storageAccount.ID,
   466  			GalleryName:             galleryName,
   467  			GalleryImageName:        galleryGen2ImageName,
   468  			GalleryImageVersionName: galleryGen2ImageVersionName,
   469  			Region:                  platform.Region,
   470  			BlobURL:                 blobURL,
   471  			RegionalReplicaCount:    int32(1),
   472  			ComputeClientFactory:    computeClientFactory,
   473  		})
   474  		if err != nil {
   475  			return err
   476  		}
   477  	}
   478  
   479  	networkClientFactory, err := armnetwork.NewClientFactory(subscriptionID, session.TokenCreds,
   480  		&arm.ClientOptions{
   481  			ClientOptions: policy.ClientOptions{
   482  				Cloud: cloudConfiguration,
   483  			},
   484  		},
   485  	)
   486  	if err != nil {
   487  		return fmt.Errorf("error creating network client factory: %w", err)
   488  	}
   489  
   490  	lbClient := networkClientFactory.NewLoadBalancersClient()
   491  	lbInput := &lbInput{
   492  		loadBalancerName:       fmt.Sprintf("%s-internal", in.InfraID),
   493  		infraID:                in.InfraID,
   494  		region:                 platform.Region,
   495  		resourceGroup:          resourceGroupName,
   496  		subscriptionID:         session.Credentials.SubscriptionID,
   497  		frontendIPConfigName:   "public-lb-ip-v4",
   498  		backendAddressPoolName: fmt.Sprintf("%s-internal", in.InfraID),
   499  		idPrefix: fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers",
   500  			session.Credentials.SubscriptionID,
   501  			resourceGroupName,
   502  		),
   503  		lbClient: lbClient,
   504  		tags:     p.Tags,
   505  	}
   506  
   507  	intLoadBalancer, err := updateInternalLoadBalancer(ctx, lbInput)
   508  	if err != nil {
   509  		return fmt.Errorf("failed to update internal load balancer: %w", err)
   510  	}
   511  	logrus.Debugf("updated internal load balancer: %s", *intLoadBalancer.ID)
   512  
   513  	var lbBap *armnetwork.BackendAddressPool
   514  	var extLBFQDN string
   515  	if in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy {
   516  		publicIP, err := createPublicIP(ctx, &pipInput{
   517  			name:          fmt.Sprintf("%s-pip-v4", in.InfraID),
   518  			infraID:       in.InfraID,
   519  			region:        in.InstallConfig.Config.Azure.Region,
   520  			resourceGroup: resourceGroupName,
   521  			pipClient:     networkClientFactory.NewPublicIPAddressesClient(),
   522  			tags:          p.Tags,
   523  		})
   524  		if err != nil {
   525  			return fmt.Errorf("failed to create public ip: %w", err)
   526  		}
   527  		logrus.Debugf("created public ip: %s", *publicIP.ID)
   528  
   529  		lbInput.loadBalancerName = in.InfraID
   530  		lbInput.backendAddressPoolName = in.InfraID
   531  
   532  		var loadBalancer *armnetwork.LoadBalancer
   533  		if platform.OutboundType == aztypes.UserDefinedRoutingOutboundType {
   534  			loadBalancer, err = createAPILoadBalancer(ctx, publicIP, lbInput)
   535  			if err != nil {
   536  				return fmt.Errorf("failed to create API load balancer: %w", err)
   537  			}
   538  		} else {
   539  			loadBalancer, err = updateOutboundLoadBalancerToAPILoadBalancer(ctx, publicIP, lbInput)
   540  			if err != nil {
   541  				return fmt.Errorf("failed to update external load balancer: %w", err)
   542  			}
   543  		}
   544  
   545  		logrus.Debugf("updated external load balancer: %s", *loadBalancer.ID)
   546  		lbBap = loadBalancer.Properties.BackendAddressPools[0]
   547  		extLBFQDN = *publicIP.Properties.DNSSettings.Fqdn
   548  	}
   549  
   550  	// Save context for other hooks
   551  	p.ResourceGroupName = resourceGroupName
   552  	p.StorageAccountName = storageAccountName
   553  	p.StorageURL = storageURL
   554  	p.StorageAccount = storageAccount
   555  	p.StorageAccountKeys = storageAccountKeys
   556  	p.StorageClientFactory = storageClientFactory
   557  	p.NetworkClientFactory = networkClientFactory
   558  	p.lbBackendAddressPool = lbBap
   559  
   560  	if err := createDNSEntries(ctx, in, extLBFQDN, resourceGroupName); err != nil {
   561  		return fmt.Errorf("error creating DNS records: %w", err)
   562  	}
   563  
   564  	return nil
   565  }
   566  
   567  // PostProvision provisions an external Load Balancer (when appropriate), and adds configuration
   568  // for the MCS to the CAPI-provisioned internal LB.
   569  func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisionInput) error {
   570  	ssn, err := in.InstallConfig.Azure.Session()
   571  	if err != nil {
   572  		return fmt.Errorf("error retrieving Azure session: %w", err)
   573  	}
   574  	subscriptionID := ssn.Credentials.SubscriptionID
   575  	cloudConfiguration := ssn.CloudConfig
   576  
   577  	if in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy {
   578  		vmClient, err := armcompute.NewVirtualMachinesClient(subscriptionID, ssn.TokenCreds,
   579  			&arm.ClientOptions{
   580  				ClientOptions: policy.ClientOptions{
   581  					Cloud: cloudConfiguration,
   582  				},
   583  			},
   584  		)
   585  		if err != nil {
   586  			return fmt.Errorf("error creating vm client: %w", err)
   587  		}
   588  
   589  		vmIDs, err := getControlPlaneIDs(in.Client, in.InstallConfig.Config.ControlPlane.Replicas, in.InfraID)
   590  		if err != nil {
   591  			return fmt.Errorf("failed to get control plane VM IDs: %w", err)
   592  		}
   593  
   594  		vmInput := &vmInput{
   595  			infraID:       in.InfraID,
   596  			resourceGroup: p.ResourceGroupName,
   597  			vmClient:      vmClient,
   598  			nicClient:     p.NetworkClientFactory.NewInterfacesClient(),
   599  			ids:           vmIDs,
   600  			bap:           p.lbBackendAddressPool,
   601  		}
   602  
   603  		if err = associateVMToBackendPool(ctx, *vmInput); err != nil {
   604  			return fmt.Errorf("failed to associate control plane VMs with external load balancer: %w", err)
   605  		}
   606  
   607  		sshRuleName := fmt.Sprintf("%s_ssh_in", in.InfraID)
   608  		if err = addSecurityGroupRule(ctx, &securityGroupInput{
   609  			resourceGroupName:    p.ResourceGroupName,
   610  			securityGroupName:    fmt.Sprintf("%s-nsg", in.InfraID),
   611  			securityRuleName:     sshRuleName,
   612  			securityRulePort:     "22",
   613  			securityRulePriority: 220,
   614  			networkClientFactory: p.NetworkClientFactory,
   615  		}); err != nil {
   616  			return fmt.Errorf("failed to add security rule: %w", err)
   617  		}
   618  
   619  		loadBalancerName := in.InfraID
   620  		frontendIPConfigName := "public-lb-ip-v4"
   621  		frontendIPConfigID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s",
   622  			subscriptionID,
   623  			p.ResourceGroupName,
   624  			loadBalancerName,
   625  			frontendIPConfigName,
   626  		)
   627  
   628  		// Create an inbound nat rule that forwards port 22 on the
   629  		// public load balancer to the bootstrap host. This takes 2
   630  		// stages to accomplish. First, the nat rule needs to be added
   631  		// to the frontend IP configuration on the public load
   632  		// balancer. Second, the nat rule needs to be addded to the
   633  		// bootstrap interface with the association to the rule on the
   634  		// public load balancer.
   635  		inboundNatRule, err := addInboundNatRuleToLoadBalancer(ctx, &inboundNatRuleInput{
   636  			resourceGroupName:    p.ResourceGroupName,
   637  			loadBalancerName:     loadBalancerName,
   638  			frontendIPConfigID:   frontendIPConfigID,
   639  			inboundNatRuleName:   sshRuleName,
   640  			inboundNatRulePort:   22,
   641  			networkClientFactory: p.NetworkClientFactory,
   642  		})
   643  		if err != nil {
   644  			return fmt.Errorf("failed to create inbound nat rule: %w", err)
   645  		}
   646  		_, err = associateInboundNatRuleToInterface(ctx, &inboundNatRuleInput{
   647  			resourceGroupName:    p.ResourceGroupName,
   648  			loadBalancerName:     loadBalancerName,
   649  			bootstrapNicName:     fmt.Sprintf("%s-bootstrap-nic", in.InfraID),
   650  			frontendIPConfigID:   frontendIPConfigID,
   651  			inboundNatRuleID:     *inboundNatRule.ID,
   652  			inboundNatRuleName:   sshRuleName,
   653  			inboundNatRulePort:   22,
   654  			networkClientFactory: p.NetworkClientFactory,
   655  		})
   656  		if err != nil {
   657  			return fmt.Errorf("failed to associate inbound nat rule to interface: %w", err)
   658  		}
   659  	}
   660  
   661  	return nil
   662  }
   663  
   664  // PostDestroy removes SSH access from the network security rules and removes
   665  // SSH port forwarding off the public load balancer when the bootstrap machine
   666  // is destroyed.
   667  func (p *Provider) PostDestroy(ctx context.Context, in clusterapi.PostDestroyerInput) error {
   668  	session, err := azconfig.GetSession(in.Metadata.Azure.CloudName, in.Metadata.Azure.ARMEndpoint)
   669  	if err != nil {
   670  		return fmt.Errorf("failed to get session: %w", err)
   671  	}
   672  
   673  	networkClientFactory, err := armnetwork.NewClientFactory(
   674  		session.Credentials.SubscriptionID,
   675  		session.TokenCreds,
   676  		&arm.ClientOptions{
   677  			ClientOptions: policy.ClientOptions{
   678  				Cloud: session.CloudConfig,
   679  			},
   680  		},
   681  	)
   682  	if err != nil {
   683  		return fmt.Errorf("error creating network client factory: %w", err)
   684  	}
   685  
   686  	resourceGroupName := fmt.Sprintf("%s-rg", in.Metadata.InfraID)
   687  	securityGroupName := fmt.Sprintf("%s-nsg", in.Metadata.InfraID)
   688  	sshRuleName := fmt.Sprintf("%s_ssh_in", in.Metadata.InfraID)
   689  
   690  	// See if a security group rule exists with the name ${InfraID}_ssh_in.
   691  	// If it does, this is a private cluster. If it does not, this is a
   692  	// public cluster and we need to delete the SSH forward rule and
   693  	// security group rule.
   694  	_, err = networkClientFactory.NewSecurityRulesClient().Get(ctx,
   695  		resourceGroupName,
   696  		securityGroupName,
   697  		sshRuleName,
   698  		nil,
   699  	)
   700  	if err == nil {
   701  		err = deleteSecurityGroupRule(ctx, &securityGroupInput{
   702  			resourceGroupName:    resourceGroupName,
   703  			securityGroupName:    securityGroupName,
   704  			securityRuleName:     sshRuleName,
   705  			securityRulePort:     "22",
   706  			networkClientFactory: networkClientFactory,
   707  		})
   708  		if err != nil {
   709  			return fmt.Errorf("failed to delete security rule: %w", err)
   710  		}
   711  
   712  		err = deleteInboundNatRule(ctx, &inboundNatRuleInput{
   713  			resourceGroupName:    resourceGroupName,
   714  			loadBalancerName:     in.Metadata.InfraID,
   715  			inboundNatRuleName:   sshRuleName,
   716  			networkClientFactory: networkClientFactory,
   717  		})
   718  		if err != nil {
   719  			return fmt.Errorf("failed to delete inbound nat rule: %w", err)
   720  		}
   721  	}
   722  
   723  	return nil
   724  }
   725  
   726  func getControlPlaneIDs(cl client.Client, replicas *int64, infraID string) ([]string, error) {
   727  	res := []string{}
   728  	total := int64(1)
   729  	if replicas != nil {
   730  		total = *replicas
   731  	}
   732  	for i := int64(0); i < total; i++ {
   733  		machineName := fmt.Sprintf("%s-master-%d", infraID, i)
   734  		key := client.ObjectKey{
   735  			Name:      machineName,
   736  			Namespace: capiutils.Namespace,
   737  		}
   738  		azureMachine := &capz.AzureMachine{}
   739  		if err := cl.Get(context.Background(), key, azureMachine); err != nil {
   740  			return nil, fmt.Errorf("failed to get AzureMachine: %w", err)
   741  		}
   742  		if vmID := azureMachine.Spec.ProviderID; vmID != nil && len(*vmID) != 0 {
   743  			res = append(res, *azureMachine.Spec.ProviderID)
   744  		} else {
   745  			return nil, fmt.Errorf("%s .Spec.ProviderID is empty", machineName)
   746  		}
   747  	}
   748  
   749  	bootstrapName := capiutils.GenerateBoostrapMachineName(infraID)
   750  	key := client.ObjectKey{
   751  		Name:      bootstrapName,
   752  		Namespace: capiutils.Namespace,
   753  	}
   754  	azureMachine := &capz.AzureMachine{}
   755  	if err := cl.Get(context.Background(), key, azureMachine); err != nil {
   756  		return nil, fmt.Errorf("failed to get AzureMachine: %w", err)
   757  	}
   758  	if vmID := azureMachine.Spec.ProviderID; vmID != nil && len(*vmID) != 0 {
   759  		res = append(res, *azureMachine.Spec.ProviderID)
   760  	} else {
   761  		return nil, fmt.Errorf("%s .Spec.ProviderID is empty", bootstrapName)
   762  	}
   763  	return res, nil
   764  }
   765  
   766  func randomString(length int) string {
   767  	source := rand.NewSource(time.Now().UnixNano())
   768  	rng := rand.New(source) // nolint:gosec
   769  	chars := "abcdefghijklmnopqrstuvwxyz0123456789"
   770  
   771  	s := make([]byte, length)
   772  	for i := range s {
   773  		s[i] = chars[rng.Intn(len(chars))]
   774  	}
   775  
   776  	return string(s)
   777  }
   778  
   779  // Ignition provisions the Azure container that holds the bootstrap ignition
   780  // file.
   781  func (p Provider) Ignition(ctx context.Context, in clusterapi.IgnitionInput) ([]byte, error) {
   782  	session, err := in.InstallConfig.Azure.Session()
   783  	if err != nil {
   784  		return nil, fmt.Errorf("failed to get session: %w", err)
   785  	}
   786  
   787  	bootstrapIgnData := in.BootstrapIgnData
   788  	subscriptionID := session.Credentials.SubscriptionID
   789  	cloudConfiguration := session.CloudConfig
   790  
   791  	ignitionContainerName := "ignition"
   792  	blobName := "bootstrap.ign"
   793  	blobURL := fmt.Sprintf("%s/%s/%s", p.StorageURL, ignitionContainerName, blobName)
   794  	publicAccess := armstorage.PublicAccessContainer
   795  	if in.InstallConfig.Config.Azure.CustomerManagedKey != nil {
   796  		publicAccess = armstorage.PublicAccessNone
   797  	}
   798  	// Create ignition blob storage container
   799  	createBlobContainerOutput, err := CreateBlobContainer(ctx, &CreateBlobContainerInput{
   800  		ContainerName:        ignitionContainerName,
   801  		SubscriptionID:       subscriptionID,
   802  		ResourceGroupName:    p.ResourceGroupName,
   803  		StorageAccountName:   p.StorageAccountName,
   804  		PublicAccess:         to.Ptr(publicAccess),
   805  		StorageClientFactory: p.StorageClientFactory,
   806  	})
   807  	if err != nil {
   808  		return nil, err
   809  	}
   810  
   811  	blobIgnitionContainer := createBlobContainerOutput.BlobContainer
   812  	logrus.Debugf("BlobIgnitionContainer.ID=%s", *blobIgnitionContainer.ID)
   813  
   814  	sasURL := ""
   815  
   816  	if in.InstallConfig.Config.Azure.CustomerManagedKey == nil {
   817  		logrus.Debugf("Creating a Block Blob for ignition shim")
   818  		sasURL, err = CreateBlockBlob(ctx, &CreateBlockBlobInput{
   819  			StorageURL:         p.StorageURL,
   820  			BlobURL:            blobURL,
   821  			StorageAccountName: p.StorageAccountName,
   822  			StorageAccountKeys: p.StorageAccountKeys,
   823  			CloudConfiguration: cloudConfiguration,
   824  			BootstrapIgnData:   bootstrapIgnData,
   825  		})
   826  		if err != nil {
   827  			return nil, fmt.Errorf("failed to create BlockBlob for ignition shim: %w", err)
   828  		}
   829  	} else {
   830  		logrus.Debugf("Creating a Page Blob for ignition shim because Customer Managed Key is provided")
   831  		lengthBootstrapFile := int64(len(bootstrapIgnData))
   832  		if lengthBootstrapFile%512 != 0 {
   833  			lengthBootstrapFile = (((lengthBootstrapFile / 512) + 1) * 512)
   834  		}
   835  
   836  		sasURL, err = CreatePageBlob(ctx, &CreatePageBlobInput{
   837  			StorageURL:         p.StorageURL,
   838  			BlobURL:            blobURL,
   839  			ImageURL:           "",
   840  			StorageAccountName: p.StorageAccountName,
   841  			BootstrapIgnData:   bootstrapIgnData,
   842  			ImageLength:        lengthBootstrapFile,
   843  			StorageAccountKeys: p.StorageAccountKeys,
   844  			CloudConfiguration: cloudConfiguration,
   845  		})
   846  		if err != nil {
   847  			return nil, fmt.Errorf("failed to create PageBlob for ignition shim: %w", err)
   848  		}
   849  	}
   850  	ignShim, err := bootstrap.GenerateIgnitionShimWithCertBundleAndProxy(sasURL, in.InstallConfig.Config.AdditionalTrustBundle, in.InstallConfig.Config.Proxy)
   851  	if err != nil {
   852  		return nil, fmt.Errorf("failed to create ignition shim: %w", err)
   853  	}
   854  
   855  	return ignShim, nil
   856  }