sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/scalesets/spec.go (about)

     1  /*
     2  Copyright 2023 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 scalesets
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"strconv"
    24  
    25  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    26  	"github.com/pkg/errors"
    27  	"k8s.io/utils/ptr"
    28  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    29  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    30  	"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
    31  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus"
    32  	"sigs.k8s.io/cluster-api-provider-azure/util/generators"
    33  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    34  )
    35  
    36  // ScaleSetSpec defines the specification for a Scale Set.
    37  type ScaleSetSpec struct {
    38  	Name                         string
    39  	ResourceGroup                string
    40  	Size                         string
    41  	Capacity                     int64
    42  	SSHKeyData                   string
    43  	OSDisk                       infrav1.OSDisk
    44  	DataDisks                    []infrav1.DataDisk
    45  	SubnetName                   string
    46  	VNetName                     string
    47  	VNetResourceGroup            string
    48  	PublicLBName                 string
    49  	PublicLBAddressPoolName      string
    50  	AcceleratedNetworking        *bool
    51  	TerminateNotificationTimeout *int
    52  	Identity                     infrav1.VMIdentity
    53  	UserAssignedIdentities       []infrav1.UserAssignedIdentity
    54  	SecurityProfile              *infrav1.SecurityProfile
    55  	SpotVMOptions                *infrav1.SpotVMOptions
    56  	AdditionalCapabilities       *infrav1.AdditionalCapabilities
    57  	DiagnosticsProfile           *infrav1.Diagnostics
    58  	FailureDomains               []string
    59  	VMExtensions                 []infrav1.VMExtension
    60  	NetworkInterfaces            []infrav1.NetworkInterface
    61  	IPv6Enabled                  bool
    62  	OrchestrationMode            infrav1.OrchestrationModeType
    63  	Location                     string
    64  	SubscriptionID               string
    65  	SKU                          resourceskus.SKU
    66  	VMSSExtensionSpecs           []azure.ResourceSpecGetter
    67  	VMImage                      *infrav1.Image
    68  	BootstrapData                string
    69  	VMSSInstances                []armcompute.VirtualMachineScaleSetVM
    70  	MaxSurge                     int
    71  	ClusterName                  string
    72  	ShouldPatchCustomData        bool
    73  	HasReplicasExternallyManaged bool
    74  	AdditionalTags               infrav1.Tags
    75  	PlatformFaultDomainCount     *int32
    76  	ZoneBalance                  *bool
    77  }
    78  
    79  // ResourceName returns the name of the Scale Set.
    80  func (s *ScaleSetSpec) ResourceName() string {
    81  	return s.Name
    82  }
    83  
    84  // ResourceGroupName returns the name of the resource group for this Scale Set.
    85  func (s *ScaleSetSpec) ResourceGroupName() string {
    86  	return s.ResourceGroup
    87  }
    88  
    89  // OwnerResourceName is a no-op for Scale Sets.
    90  func (s *ScaleSetSpec) OwnerResourceName() string {
    91  	return ""
    92  }
    93  
    94  func (s *ScaleSetSpec) existingParameters(ctx context.Context, existing interface{}) (parameters interface{}, err error) {
    95  	existingVMSS, ok := existing.(armcompute.VirtualMachineScaleSet)
    96  	if !ok {
    97  		return nil, errors.Errorf("%T is not an armcompute.VirtualMachineScaleSet", existing)
    98  	}
    99  
   100  	existingInfraVMSS := converters.SDKToVMSS(existingVMSS, s.VMSSInstances)
   101  
   102  	params, err := s.Parameters(ctx, nil)
   103  	if err != nil {
   104  		return nil, errors.Wrapf(err, "failed to generate scale set update parameters for %s", s.Name)
   105  	}
   106  
   107  	vmss, ok := params.(armcompute.VirtualMachineScaleSet)
   108  	if !ok {
   109  		return nil, errors.Errorf("%T is not an armcompute.VirtualMachineScaleSet", existing)
   110  	}
   111  
   112  	vmss.Properties.VirtualMachineProfile.NetworkProfile = nil
   113  	vmss.ID = existingVMSS.ID
   114  
   115  	hasModelChanges := hasModelModifyingDifferences(&existingInfraVMSS, vmss)
   116  	isFlex := s.OrchestrationMode == infrav1.FlexibleOrchestrationMode
   117  	updated := true
   118  	if !isFlex {
   119  		updated = existingInfraVMSS.HasEnoughLatestModelOrNotMixedModel()
   120  	}
   121  	if s.MaxSurge > 0 && (hasModelChanges || !updated) && !s.HasReplicasExternallyManaged {
   122  		// surge capacity with the intention of lowering during instance reconciliation
   123  		surge := s.Capacity + int64(s.MaxSurge)
   124  		vmss.SKU.Capacity = ptr.To[int64](surge)
   125  	}
   126  
   127  	// If there are no model changes and no increase in the replica count, do not update the VMSS.
   128  	// Decreases in replica count is handled by deleting AzureMachinePoolMachine instances in the MachinePoolScope
   129  	if *vmss.SKU.Capacity <= existingInfraVMSS.Capacity && !hasModelChanges && !s.ShouldPatchCustomData {
   130  		// up to date, nothing to do
   131  		return nil, nil
   132  	}
   133  
   134  	return vmss, nil
   135  }
   136  
   137  // Parameters returns the parameters for the Scale Set.
   138  func (s *ScaleSetSpec) Parameters(ctx context.Context, existing interface{}) (parameters interface{}, err error) {
   139  	if existing != nil {
   140  		return s.existingParameters(ctx, existing)
   141  	}
   142  
   143  	if s.AcceleratedNetworking == nil {
   144  		// set accelerated networking to the capability of the VMSize
   145  		accelNet := s.SKU.HasCapability(resourceskus.AcceleratedNetworking)
   146  		s.AcceleratedNetworking = &accelNet
   147  	}
   148  
   149  	extensions, err := s.generateExtensions(ctx)
   150  	if err != nil {
   151  		return armcompute.VirtualMachineScaleSet{}, err
   152  	}
   153  
   154  	storageProfile, err := s.generateStorageProfile(ctx)
   155  	if err != nil {
   156  		return armcompute.VirtualMachineScaleSet{}, err
   157  	}
   158  
   159  	securityProfile, err := s.getSecurityProfile()
   160  	if err != nil {
   161  		return armcompute.VirtualMachineScaleSet{}, err
   162  	}
   163  
   164  	priority, evictionPolicy, billingProfile, err := converters.GetSpotVMOptions(s.SpotVMOptions, s.OSDisk.DiffDiskSettings)
   165  	if err != nil {
   166  		return armcompute.VirtualMachineScaleSet{}, errors.Wrapf(err, "failed to get Spot VM options")
   167  	}
   168  
   169  	diagnosticsProfile := converters.GetDiagnosticsProfile(s.DiagnosticsProfile)
   170  
   171  	osProfile, err := s.generateOSProfile(ctx)
   172  	if err != nil {
   173  		return armcompute.VirtualMachineScaleSet{}, err
   174  	}
   175  
   176  	orchestrationMode := converters.GetOrchestrationMode(s.OrchestrationMode)
   177  
   178  	vmss := armcompute.VirtualMachineScaleSet{
   179  		Location: ptr.To(s.Location),
   180  		SKU: &armcompute.SKU{
   181  			Name:     ptr.To(s.Size),
   182  			Tier:     ptr.To("Standard"),
   183  			Capacity: ptr.To[int64](s.Capacity),
   184  		},
   185  		Zones: azure.PtrSlice(&s.FailureDomains),
   186  		Plan:  s.generateImagePlan(ctx),
   187  		Properties: &armcompute.VirtualMachineScaleSetProperties{
   188  			OrchestrationMode:    ptr.To(orchestrationMode),
   189  			SinglePlacementGroup: ptr.To(false),
   190  			VirtualMachineProfile: &armcompute.VirtualMachineScaleSetVMProfile{
   191  				OSProfile:          osProfile,
   192  				StorageProfile:     storageProfile,
   193  				SecurityProfile:    securityProfile,
   194  				DiagnosticsProfile: diagnosticsProfile,
   195  				NetworkProfile: &armcompute.VirtualMachineScaleSetNetworkProfile{
   196  					NetworkInterfaceConfigurations: azure.PtrSlice(s.getVirtualMachineScaleSetNetworkConfiguration()),
   197  				},
   198  				Priority:       priority,
   199  				EvictionPolicy: evictionPolicy,
   200  				BillingProfile: billingProfile,
   201  				ExtensionProfile: &armcompute.VirtualMachineScaleSetExtensionProfile{
   202  					Extensions: azure.PtrSlice(&extensions),
   203  				},
   204  			},
   205  		},
   206  	}
   207  
   208  	// Set properties specific to VMSS orchestration mode
   209  	// See https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-orchestration-modes for more details
   210  	switch orchestrationMode {
   211  	case armcompute.OrchestrationModeUniform: // Uniform VMSS
   212  		vmss.Properties.Overprovision = ptr.To(false)
   213  		vmss.Properties.UpgradePolicy = &armcompute.UpgradePolicy{Mode: ptr.To(armcompute.UpgradeModeManual)}
   214  	case armcompute.OrchestrationModeFlexible: // VMSS Flex, VMs are treated as individual virtual machines
   215  		vmss.Properties.VirtualMachineProfile.NetworkProfile.NetworkAPIVersion =
   216  			ptr.To(armcompute.NetworkAPIVersionTwoThousandTwenty1101)
   217  		vmss.Properties.PlatformFaultDomainCount = ptr.To[int32](1)
   218  	}
   219  
   220  	if s.PlatformFaultDomainCount != nil {
   221  		vmss.Properties.PlatformFaultDomainCount = ptr.To[int32](*s.PlatformFaultDomainCount)
   222  	}
   223  
   224  	if s.ZoneBalance != nil {
   225  		vmss.Properties.ZoneBalance = s.ZoneBalance
   226  	}
   227  
   228  	// Assign Identity to VMSS
   229  	if s.Identity == infrav1.VMIdentitySystemAssigned {
   230  		vmss.Identity = &armcompute.VirtualMachineScaleSetIdentity{
   231  			Type: ptr.To(armcompute.ResourceIdentityTypeSystemAssigned),
   232  		}
   233  	} else if s.Identity == infrav1.VMIdentityUserAssigned {
   234  		userIdentitiesMap, err := converters.UserAssignedIdentitiesToVMSSSDK(s.UserAssignedIdentities)
   235  		if err != nil {
   236  			return vmss, errors.Wrapf(err, "failed to assign identity %q", s.Name)
   237  		}
   238  		vmss.Identity = &armcompute.VirtualMachineScaleSetIdentity{
   239  			Type:                   ptr.To(armcompute.ResourceIdentityTypeUserAssigned),
   240  			UserAssignedIdentities: userIdentitiesMap,
   241  		}
   242  	}
   243  
   244  	// Provisionally detect whether there is any Data Disk defined which uses UltraSSDs.
   245  	// If that's the case, enable the UltraSSD capability.
   246  	for _, dataDisk := range s.DataDisks {
   247  		if dataDisk.ManagedDisk != nil && dataDisk.ManagedDisk.StorageAccountType == string(armcompute.StorageAccountTypesUltraSSDLRS) {
   248  			vmss.Properties.AdditionalCapabilities = &armcompute.AdditionalCapabilities{
   249  				UltraSSDEnabled: ptr.To(true),
   250  			}
   251  		}
   252  	}
   253  
   254  	// Set Additional Capabilities if any is present on the spec.
   255  	if s.AdditionalCapabilities != nil {
   256  		// Set UltraSSDEnabled if a specific value is set on the spec for it.
   257  		if s.AdditionalCapabilities.UltraSSDEnabled != nil {
   258  			vmss.Properties.AdditionalCapabilities.UltraSSDEnabled = s.AdditionalCapabilities.UltraSSDEnabled
   259  		}
   260  	}
   261  
   262  	if s.TerminateNotificationTimeout != nil {
   263  		vmss.Properties.VirtualMachineProfile.ScheduledEventsProfile = &armcompute.ScheduledEventsProfile{
   264  			TerminateNotificationProfile: &armcompute.TerminateNotificationProfile{
   265  				NotBeforeTimeout: ptr.To(fmt.Sprintf("PT%dM", *s.TerminateNotificationTimeout)),
   266  				Enable:           ptr.To(true),
   267  			},
   268  		}
   269  	}
   270  
   271  	tags := infrav1.Build(infrav1.BuildParams{
   272  		ClusterName: s.ClusterName,
   273  		Lifecycle:   infrav1.ResourceLifecycleOwned,
   274  		Name:        ptr.To(s.Name),
   275  		Role:        ptr.To(infrav1.Node),
   276  		Additional:  s.AdditionalTags,
   277  	})
   278  
   279  	vmss.Tags = converters.TagsToMap(tags)
   280  	return vmss, nil
   281  }
   282  
   283  func hasModelModifyingDifferences(infraVMSS *azure.VMSS, vmss armcompute.VirtualMachineScaleSet) bool {
   284  	other := converters.SDKToVMSS(vmss, []armcompute.VirtualMachineScaleSetVM{})
   285  	return infraVMSS.HasModelChanges(other)
   286  }
   287  
   288  func (s *ScaleSetSpec) generateExtensions(ctx context.Context) ([]armcompute.VirtualMachineScaleSetExtension, error) {
   289  	extensions := make([]armcompute.VirtualMachineScaleSetExtension, len(s.VMSSExtensionSpecs))
   290  	for i, extensionSpec := range s.VMSSExtensionSpecs {
   291  		extensionSpec := extensionSpec
   292  		parameters, err := extensionSpec.Parameters(ctx, nil)
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  		vmssextension, ok := parameters.(armcompute.VirtualMachineScaleSetExtension)
   297  		if !ok {
   298  			return nil, errors.Errorf("%T is not an armcompute.VirtualMachineScaleSetExtension", parameters)
   299  		}
   300  		extensions[i] = vmssextension
   301  	}
   302  
   303  	return extensions, nil
   304  }
   305  
   306  func (s *ScaleSetSpec) getVirtualMachineScaleSetNetworkConfiguration() *[]armcompute.VirtualMachineScaleSetNetworkConfiguration {
   307  	var backendAddressPools []armcompute.SubResource
   308  	if s.PublicLBName != "" {
   309  		if s.PublicLBAddressPoolName != "" {
   310  			backendAddressPools = append(backendAddressPools,
   311  				armcompute.SubResource{
   312  					ID: ptr.To(azure.AddressPoolID(s.SubscriptionID, s.ResourceGroup, s.PublicLBName, s.PublicLBAddressPoolName)),
   313  				})
   314  		}
   315  	}
   316  	nicConfigs := []armcompute.VirtualMachineScaleSetNetworkConfiguration{}
   317  	for i, n := range s.NetworkInterfaces {
   318  		nicConfig := armcompute.VirtualMachineScaleSetNetworkConfiguration{}
   319  		nicConfig.Properties = &armcompute.VirtualMachineScaleSetNetworkConfigurationProperties{}
   320  		nicConfig.Name = ptr.To(s.Name + "-nic-" + strconv.Itoa(i))
   321  		nicConfig.Properties.EnableIPForwarding = ptr.To(true)
   322  		if n.AcceleratedNetworking != nil {
   323  			nicConfig.Properties.EnableAcceleratedNetworking = n.AcceleratedNetworking
   324  		} else {
   325  			// If AcceleratedNetworking is not specified, use the value from the VMSS spec.
   326  			// It will be set to true if the VMSS SKU supports it.
   327  			nicConfig.Properties.EnableAcceleratedNetworking = s.AcceleratedNetworking
   328  		}
   329  
   330  		// Create IPConfigs
   331  		ipconfigs := []armcompute.VirtualMachineScaleSetIPConfiguration{}
   332  		for j := 0; j < n.PrivateIPConfigs; j++ {
   333  			ipconfig := armcompute.VirtualMachineScaleSetIPConfiguration{
   334  				Name: ptr.To(fmt.Sprintf("ipConfig" + strconv.Itoa(j))),
   335  				Properties: &armcompute.VirtualMachineScaleSetIPConfigurationProperties{
   336  					PrivateIPAddressVersion: ptr.To(armcompute.IPVersionIPv4),
   337  					Subnet: &armcompute.APIEntityReference{
   338  						ID: ptr.To(azure.SubnetID(s.SubscriptionID, s.VNetResourceGroup, s.VNetName, n.SubnetName)),
   339  					},
   340  				},
   341  			}
   342  
   343  			if j == 0 {
   344  				// Always use the first IPConfig as the Primary
   345  				ipconfig.Properties.Primary = ptr.To(true)
   346  			}
   347  			ipconfigs = append(ipconfigs, ipconfig)
   348  		}
   349  		if s.IPv6Enabled {
   350  			ipv6Config := armcompute.VirtualMachineScaleSetIPConfiguration{
   351  				Name: ptr.To("ipConfigv6"),
   352  				Properties: &armcompute.VirtualMachineScaleSetIPConfigurationProperties{
   353  					PrivateIPAddressVersion: ptr.To(armcompute.IPVersionIPv6),
   354  					Primary:                 ptr.To(false),
   355  					Subnet: &armcompute.APIEntityReference{
   356  						ID: ptr.To(azure.SubnetID(s.SubscriptionID, s.VNetResourceGroup, s.VNetName, n.SubnetName)),
   357  					},
   358  				},
   359  			}
   360  			ipconfigs = append(ipconfigs, ipv6Config)
   361  		}
   362  		if i == 0 {
   363  			ipconfigs[0].Properties.LoadBalancerBackendAddressPools = azure.PtrSlice(&backendAddressPools)
   364  			nicConfig.Properties.Primary = ptr.To(true)
   365  		}
   366  		nicConfig.Properties.IPConfigurations = azure.PtrSlice(&ipconfigs)
   367  		nicConfigs = append(nicConfigs, nicConfig)
   368  	}
   369  	return &nicConfigs
   370  }
   371  
   372  // generateStorageProfile generates a pointer to an armcompute.VirtualMachineScaleSetStorageProfile which can utilized for VM creation.
   373  func (s *ScaleSetSpec) generateStorageProfile(ctx context.Context) (*armcompute.VirtualMachineScaleSetStorageProfile, error) {
   374  	_, _, done := tele.StartSpanWithLogger(ctx, "scalesets.ScaleSetSpec.generateStorageProfile")
   375  	defer done()
   376  
   377  	storageProfile := &armcompute.VirtualMachineScaleSetStorageProfile{
   378  		OSDisk: &armcompute.VirtualMachineScaleSetOSDisk{
   379  			OSType:       ptr.To(armcompute.OperatingSystemTypes(s.OSDisk.OSType)),
   380  			CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage),
   381  			DiskSizeGB:   s.OSDisk.DiskSizeGB,
   382  		},
   383  	}
   384  
   385  	// enable ephemeral OS
   386  	if s.OSDisk.DiffDiskSettings != nil {
   387  		if !s.SKU.HasCapability(resourceskus.EphemeralOSDisk) {
   388  			return nil, fmt.Errorf("vm size %s does not support ephemeral os. select a different vm size or disable ephemeral os", s.Size)
   389  		}
   390  
   391  		storageProfile.OSDisk.DiffDiskSettings = &armcompute.DiffDiskSettings{
   392  			Option: ptr.To(armcompute.DiffDiskOptions(s.OSDisk.DiffDiskSettings.Option)),
   393  		}
   394  	}
   395  
   396  	if s.OSDisk.ManagedDisk != nil {
   397  		storageProfile.OSDisk.ManagedDisk = &armcompute.VirtualMachineScaleSetManagedDiskParameters{}
   398  		if s.OSDisk.ManagedDisk.StorageAccountType != "" {
   399  			storageProfile.OSDisk.ManagedDisk.StorageAccountType = ptr.To(armcompute.StorageAccountTypes(s.OSDisk.ManagedDisk.StorageAccountType))
   400  		}
   401  		if s.OSDisk.ManagedDisk.DiskEncryptionSet != nil {
   402  			storageProfile.OSDisk.ManagedDisk.DiskEncryptionSet = &armcompute.DiskEncryptionSetParameters{ID: ptr.To(s.OSDisk.ManagedDisk.DiskEncryptionSet.ID)}
   403  		}
   404  	}
   405  
   406  	if s.OSDisk.CachingType != "" {
   407  		storageProfile.OSDisk.Caching = ptr.To(armcompute.CachingTypes(s.OSDisk.CachingType))
   408  	}
   409  
   410  	dataDisks := make([]armcompute.VirtualMachineScaleSetDataDisk, len(s.DataDisks))
   411  	for i, disk := range s.DataDisks {
   412  		dataDisks[i] = armcompute.VirtualMachineScaleSetDataDisk{
   413  			CreateOption: ptr.To(armcompute.DiskCreateOptionTypesEmpty),
   414  			DiskSizeGB:   ptr.To[int32](disk.DiskSizeGB),
   415  			Lun:          disk.Lun,
   416  			Name:         ptr.To(azure.GenerateDataDiskName(s.Name, disk.NameSuffix)),
   417  		}
   418  
   419  		if disk.ManagedDisk != nil {
   420  			dataDisks[i].ManagedDisk = &armcompute.VirtualMachineScaleSetManagedDiskParameters{
   421  				StorageAccountType: ptr.To(armcompute.StorageAccountTypes(disk.ManagedDisk.StorageAccountType)),
   422  			}
   423  
   424  			if disk.ManagedDisk.DiskEncryptionSet != nil {
   425  				dataDisks[i].ManagedDisk.DiskEncryptionSet = &armcompute.DiskEncryptionSetParameters{ID: ptr.To(disk.ManagedDisk.DiskEncryptionSet.ID)}
   426  			}
   427  		}
   428  	}
   429  	storageProfile.DataDisks = azure.PtrSlice(&dataDisks)
   430  
   431  	if s.VMImage == nil {
   432  		return nil, errors.Errorf("vm image is nil")
   433  	}
   434  	imageRef, err := converters.ImageToSDK(s.VMImage)
   435  	if err != nil {
   436  		return nil, err
   437  	}
   438  
   439  	storageProfile.ImageReference = imageRef
   440  
   441  	return storageProfile, nil
   442  }
   443  
   444  func (s *ScaleSetSpec) generateOSProfile(_ context.Context) (*armcompute.VirtualMachineScaleSetOSProfile, error) {
   445  	sshKey, err := base64.StdEncoding.DecodeString(s.SSHKeyData)
   446  	if err != nil {
   447  		return nil, errors.Wrap(err, "failed to decode ssh public key")
   448  	}
   449  
   450  	osProfile := &armcompute.VirtualMachineScaleSetOSProfile{
   451  		ComputerNamePrefix: ptr.To(s.Name),
   452  		AdminUsername:      ptr.To(azure.DefaultUserName),
   453  		CustomData:         ptr.To(s.BootstrapData),
   454  	}
   455  
   456  	switch s.OSDisk.OSType {
   457  	case string(armcompute.OperatingSystemTypesWindows):
   458  		// Cloudbase-init is used to generate a password.
   459  		// https://cloudbase-init.readthedocs.io/en/latest/plugins.html#setting-password-main
   460  		//
   461  		// We generate a random password here in case of failure
   462  		// but the password on the VM will NOT be the same as created here.
   463  		// Access is provided via SSH public key that is set during deployment
   464  		// Azure also provides a way to reset user passwords in the case of need.
   465  		osProfile.AdminPassword = ptr.To(generators.SudoRandomPassword(123))
   466  		osProfile.WindowsConfiguration = &armcompute.WindowsConfiguration{
   467  			EnableAutomaticUpdates: ptr.To(false),
   468  		}
   469  	default:
   470  		osProfile.LinuxConfiguration = &armcompute.LinuxConfiguration{
   471  			DisablePasswordAuthentication: ptr.To(true),
   472  			SSH: &armcompute.SSHConfiguration{
   473  				PublicKeys: []*armcompute.SSHPublicKey{
   474  					{
   475  						Path:    ptr.To(fmt.Sprintf("/home/%s/.ssh/authorized_keys", azure.DefaultUserName)),
   476  						KeyData: ptr.To(string(sshKey)),
   477  					},
   478  				},
   479  			},
   480  		}
   481  	}
   482  
   483  	return osProfile, nil
   484  }
   485  
   486  func (s *ScaleSetSpec) generateImagePlan(ctx context.Context) *armcompute.Plan {
   487  	_, log, done := tele.StartSpanWithLogger(ctx, "scalesets.ScaleSetSpec.generateImagePlan")
   488  	defer done()
   489  
   490  	if s.VMImage == nil {
   491  		log.V(2).Info("no vm image found, disabling plan")
   492  		return nil
   493  	}
   494  
   495  	if s.VMImage.SharedGallery != nil && s.VMImage.SharedGallery.Publisher != nil && s.VMImage.SharedGallery.SKU != nil && s.VMImage.SharedGallery.Offer != nil {
   496  		return &armcompute.Plan{
   497  			Publisher: s.VMImage.SharedGallery.Publisher,
   498  			Name:      s.VMImage.SharedGallery.SKU,
   499  			Product:   s.VMImage.SharedGallery.Offer,
   500  		}
   501  	}
   502  
   503  	if s.VMImage.Marketplace == nil || !s.VMImage.Marketplace.ThirdPartyImage {
   504  		return nil
   505  	}
   506  
   507  	if s.VMImage.Marketplace.Publisher == "" || s.VMImage.Marketplace.SKU == "" || s.VMImage.Marketplace.Offer == "" {
   508  		return nil
   509  	}
   510  
   511  	return &armcompute.Plan{
   512  		Publisher: ptr.To(s.VMImage.Marketplace.Publisher),
   513  		Name:      ptr.To(s.VMImage.Marketplace.SKU),
   514  		Product:   ptr.To(s.VMImage.Marketplace.Offer),
   515  	}
   516  }
   517  
   518  func (s *ScaleSetSpec) getSecurityProfile() (*armcompute.SecurityProfile, error) {
   519  	if s.SecurityProfile == nil {
   520  		return nil, nil
   521  	}
   522  
   523  	if !s.SKU.HasCapability(resourceskus.EncryptionAtHost) {
   524  		return nil, azure.WithTerminalError(errors.Errorf("encryption at host is not supported for VM type %s", s.Size))
   525  	}
   526  
   527  	return &armcompute.SecurityProfile{
   528  		EncryptionAtHost: ptr.To(*s.SecurityProfile.EncryptionAtHost),
   529  	}, nil
   530  }