diff --git a/pkg/cloudprovider/cloudprovider.go b/pkg/cloudprovider/cloudprovider.go index a1be0a5ff..4e4d0444f 100644 --- a/pkg/cloudprovider/cloudprovider.go +++ b/pkg/cloudprovider/cloudprovider.go @@ -360,5 +360,8 @@ func GenerateNodeClaimName(vmName string) string { // makeZone returns the zone value in format of -. func makeZone(location string, zoneID string) string { + if zoneID == "" { + return "" + } return fmt.Sprintf("%s-%s", strings.ToLower(location), zoneID) } diff --git a/pkg/controllers/nodeclaim/garbagecollection/suite_test.go b/pkg/controllers/nodeclaim/garbagecollection/suite_test.go index a81c83b28..e72905e18 100644 --- a/pkg/controllers/nodeclaim/garbagecollection/suite_test.go +++ b/pkg/controllers/nodeclaim/garbagecollection/suite_test.go @@ -30,6 +30,7 @@ import ( "github.com/Azure/karpenter/pkg/cloudprovider" "github.com/Azure/karpenter/pkg/controllers/nodeclaim/garbagecollection" link "github.com/Azure/karpenter/pkg/controllers/nodeclaim/link" + "github.com/Azure/karpenter/pkg/fake" "github.com/Azure/karpenter/pkg/providers/instance" "github.com/Azure/karpenter/pkg/utils" . "github.com/onsi/ginkgo/v2" @@ -126,8 +127,9 @@ var _ = Describe("NodeClaimGarbageCollection", func() { BeforeEach(func() { id := utils.MkVMID(azureEnv.AzureResourceGraphAPI.ResourceGroup, "vm-a") vm = armcompute.VirtualMachine{ - ID: lo.ToPtr(id), - Name: lo.ToPtr("vm-a"), + ID: lo.ToPtr(id), + Name: lo.ToPtr("vm-a"), + Location: lo.ToPtr(fake.Region), Tags: map[string]*string{ instance.NodePoolTagKey: lo.ToPtr("default"), }, @@ -191,8 +193,9 @@ var _ = Describe("NodeClaimGarbageCollection", func() { azureEnv.VirtualMachinesAPI.Instances.Store( vmID, armcompute.VirtualMachine{ - ID: lo.ToPtr(utils.MkVMID(azureEnv.AzureResourceGraphAPI.ResourceGroup, vmName)), - Name: lo.ToPtr(vmName), + ID: lo.ToPtr(utils.MkVMID(azureEnv.AzureResourceGraphAPI.ResourceGroup, vmName)), + Name: lo.ToPtr(vmName), + Location: lo.ToPtr(fake.Region), Properties: &armcompute.VirtualMachineProperties{ TimeCreated: lo.ToPtr(time.Now().Add(-time.Minute * 10)), }, @@ -230,8 +233,9 @@ var _ = Describe("NodeClaimGarbageCollection", func() { azureEnv.VirtualMachinesAPI.Instances.Store( vmID, armcompute.VirtualMachine{ - ID: lo.ToPtr(utils.MkVMID(azureEnv.AzureResourceGraphAPI.ResourceGroup, vmName)), - Name: lo.ToPtr(vmName), + ID: lo.ToPtr(utils.MkVMID(azureEnv.AzureResourceGraphAPI.ResourceGroup, vmName)), + Name: lo.ToPtr(vmName), + Location: lo.ToPtr(fake.Region), Properties: &armcompute.VirtualMachineProperties{ TimeCreated: lo.ToPtr(time.Now().Add(-time.Minute * 10)), }, diff --git a/pkg/providers/instance/instance.go b/pkg/providers/instance/instance.go index 54ade243f..31b07f755 100644 --- a/pkg/providers/instance/instance.go +++ b/pkg/providers/instance/instance.go @@ -353,7 +353,7 @@ func newVMObject( CapacityTypeToPriority[capacityType]), ), }, - Zones: []*string{&zone}, + Zones: lo.Ternary(len(zone) > 0, []*string{&zone}, []*string{}), Tags: launchTemplate.Tags, } setVMPropertiesStorageProfile(vm.Properties, instanceType, nodeClass) @@ -560,9 +560,11 @@ func (p *Provider) pickSkuSizePriorityAndZone(ctx context.Context, nodeClaim *co }) zonesWithPriority := lo.Map(priorityOfferings, func(o corecloudprovider.Offering, _ int) string { return o.Zone }) if zone, ok := sets.New(zonesWithPriority...).PopAny(); ok { - // Zones in Offerings have - format; the zone returned from here will be used for VM instantiation, - // which expects just the zone number, without region - zone = string(zone[len(zone)-1]) + if len(zone) > 0 { + // Zones in zonal Offerings have - format; the zone returned from here will be used for VM instantiation, + // which expects just the zone number, without region + zone = string(zone[len(zone)-1]) + } return instanceType, priority, zone } return nil, "", "" @@ -637,6 +639,7 @@ func (p *Provider) getAKSIdentifyingExtension() *armcompute.VirtualMachineExtens return vmExtension } +// GetZoneID returns the zone ID for the given virtual machine, or an empty string if there is no zone specified func GetZoneID(vm *armcompute.VirtualMachine) (string, error) { if vm == nil { return "", fmt.Errorf("cannot pass in a nil virtual machine") @@ -645,7 +648,7 @@ func GetZoneID(vm *armcompute.VirtualMachine) (string, error) { return "", fmt.Errorf("virtual machine is missing name") } if vm.Zones == nil { - return "", fmt.Errorf("virtual machine %v zones are nil", *vm.Name) + return "", nil } if len(vm.Zones) == 1 { return *(vm.Zones)[0], nil @@ -653,7 +656,7 @@ func GetZoneID(vm *armcompute.VirtualMachine) (string, error) { if len(vm.Zones) > 1 { return "", fmt.Errorf("virtual machine %v has multiple zones", *vm.Name) } - return "", fmt.Errorf("virtual machine %v does not have any zones specified", *vm.Name) + return "", nil } func GetListQueryBuilder(rg string) *kql.Builder { diff --git a/pkg/providers/instancetype/instancetypes.go b/pkg/providers/instancetype/instancetypes.go index 3453058c5..f67a1f0dc 100644 --- a/pkg/providers/instancetype/instancetypes.go +++ b/pkg/providers/instancetype/instancetypes.go @@ -117,9 +117,13 @@ func instanceTypeZones(sku *skewer.SKU, region string) sets.Set[string] { // skewer returns numerical zones, like "1" (as keys in the map); // prefix each zone with "-", to have them match the labels placed on Node (e.g. "westus2-1") // Note this data comes from LocationInfo, then skewer is used to get the SKU info - return sets.New(lo.Map(lo.Keys(sku.AvailabilityZones(region)), func(zone string, _ int) string { - return fmt.Sprintf("%s-%s", region, zone) - })...) + if hasZonalSupport(region) { + return sets.New(lo.Map(lo.Keys(sku.AvailabilityZones(region)), func(zone string, _ int) string { + return fmt.Sprintf("%s-%s", region, zone) + })...) + } + + return sets.New("") // empty string means non-zonal offering } func (p *Provider) createOfferings(sku *skewer.SKU, zones sets.Set[string]) []cloudprovider.Offering { @@ -156,7 +160,7 @@ func (p *Provider) getInstanceTypes(ctx context.Context) (map[string]*skewer.SKU continue } - if p.isSupported(&skus[i], vmsize) { + if !skus[i].HasLocationRestriction(p.region) && p.isSupported(&skus[i], vmsize) { instanceTypes[skus[i].GetName()] = &skus[i] } } @@ -233,3 +237,49 @@ func MaxEphemeralOSDiskSizeGB(sku *skewer.SKU) float64 { // convert bytes to GB return maxDiskBytes / float64(units.Gigabyte) } + +var ( + // https://learn.microsoft.com/en-us/azure/reliability/availability-zones-service-support#azure-regions-with-availability-zone-support + // (could also be obtained programmatically) + zonalRegions = sets.New( + // Americas + "brazilsouth", + "canadacentral", + "centralus", + "eastus", + "eastus2", + "southcentralus", + "usgovvirginia", + "westus2", + "westus3", + // Europe + "francecentral", + "italynorth", + "germanywestcentral", + "norwayeast", + "northeurope", + "uksouth", + "westeurope", + "swedencentral", + "switzerlandnorth", + "polandcentral", + // Middle East + "qatarcentral", + "uaenorth", + "israelcentral", + // Africa + "southafricanorth", + // Asia Pacific + "australiaeast", + "centralindia", + "japaneast", + "koreacentral", + "southeastasia", + "eastasia", + "chinanorth3", + ) +) + +func hasZonalSupport(region string) bool { + return zonalRegions.Has(region) +}