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 }