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 }