diff --git a/Makefile b/Makefile index d671f12d..89b23fc4 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ MOCKGEN_VER := v0.4.0 MOCKGEN_BIN := mockgen MOCKGEN := $(BIN_DIR)/$(MOCKGEN_BIN)-$(MOCKGEN_VER) -GINKGO_VER := v2.22.0 +GINKGO_VER := v2.22.2 GINKGO_BIN := ginkgo GINKGO := $(BIN_DIR)/$(GINKGO_BIN)-$(GINKGO_VER) diff --git a/controller/aks-cluster-config-handler.go b/controller/aks-cluster-config-handler.go index b2e11279..a0954ddc 100644 --- a/controller/aks-cluster-config-handler.go +++ b/controller/aks-cluster-config-handler.go @@ -98,6 +98,7 @@ type azureClients struct { resourceGroupsClient services.ResourceGroupsClientInterface agentPoolsClient services.AgentPoolsClientInterface workplacesClient services.WorkplacesClientInterface + subscriptionsClient services.SubscriptionsClientInterface } func Register( diff --git a/go.mod b/go.mod index 3b0411d7..24e14a8c 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 github.com/Azure/go-autorest/autorest v0.11.29 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 github.com/onsi/ginkgo/v2 v2.22.2 diff --git a/go.sum b/go.sum index 98311d55..9698275a 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armope github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.2.0/go.mod h1:A4nzEXwVd5pAyneR6KOvUAo72svUc5rmCzRHhAbP6lA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/pkg/aks/check.go b/pkg/aks/check.go index 33ecd39c..d77f432c 100644 --- a/pkg/aks/check.go +++ b/pkg/aks/check.go @@ -4,9 +4,11 @@ import ( "context" "crypto/sha256" "fmt" + "log" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" "github.com/rancher/aks-operator/pkg/aks/services" "github.com/sirupsen/logrus" ) @@ -175,3 +177,63 @@ func generateUniqueLogWorkspace(workspaceName string) string { shaString := fmt.Sprintf("%x", hexHash) return fmt.Sprintf("%s-%s", s, shaString[0:16]) } + +var azRegionSupport = map[string]bool{ + "australiaeast": true, + "brazilsouth": true, + "canadacentral": true, + "centralindia": true, + "centralus": true, + "eastasia": true, + "eastus": true, + "eastus2": true, + "eastus2euap": true, + "francecentral": true, + "germanywestcentral": true, + "israelcentral": true, + "italynorth": true, + "japaneast": true, + "koreacentral": true, + "mexicocentral": true, + "newzealandnorth": true, + "northeurope": true, + "norwayeast": true, + "polandcentral": true, + "qatarcentral": true, + "southafricanorth": true, + "southcentralus": true, + "southeastasia": true, + "spaincentral": true, + "swedencentral": true, + "switzerlandnorth": true, + "uaenorth": true, + "uksouth": true, + "westeurope": true, + "westus2": true, + "westus3": true, +} + +func CheckAvailabilityZonesSupport(location string) bool { + return azRegionSupport[location] +} + +func CheckAvailabilityZonesSupportAPI(ctx context.Context, client services.SubscriptionsClientInterface, subscription string, location string) bool { + pager := client.NewListLocationsPager(subscription, &armsubscriptions.ClientListLocationsOptions{IncludeExtendedLocations: nil}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + log.Fatalf("failed to advance page: %v", err) + } + for _, v := range page.Value { + if v.Name != nil && *v.Name == location { + if len(v.AvailabilityZoneMappings) > 0 { + logrus.Debugf("Region %s supports availability zones : ", location) + return true + } + + } + } + } + logrus.Debugf("Region %s does not support availability zones", location) + return false +} diff --git a/pkg/aks/create.go b/pkg/aks/create.go index 03765052..25301f54 100644 --- a/pkg/aks/create.go +++ b/pkg/aks/create.go @@ -173,6 +173,9 @@ func createManagedCluster(ctx context.Context, cred *Credentials, workplacesClie agentProfile.OrchestratorVersion = np.OrchestratorVersion } if np.AvailabilityZones != nil && len(*np.AvailabilityZones) > 0 { + if !CheckAvailabilityZonesSupport(spec.ResourceLocation) { + return nil, fmt.Errorf("availability zones are not supported in region %s", spec.ResourceLocation) + } agentProfile.AvailabilityZones = utils.ConvertToSliceOfPointers(np.AvailabilityZones) } @@ -306,6 +309,10 @@ func createManagedCluster(ctx context.Context, cred *Credentials, workplacesClie // CreateOrUpdateAgentPool creates a new pool(s) in AKS. If one already exists it updates the upstream node pool with // any provided updates. func CreateOrUpdateAgentPool(ctx context.Context, agentPoolClient services.AgentPoolsClientInterface, spec *aksv1.AKSClusterConfigSpec, np *aksv1.AKSNodePool) error { + if np.AvailabilityZones != nil && len(*np.AvailabilityZones) > 0 && !CheckAvailabilityZonesSupport(spec.ResourceLocation) { + return fmt.Errorf("availability zones are not supported in region %s", spec.ResourceLocation) + } + agentProfile := &armcontainerservice.ManagedClusterAgentPoolProfileProperties{ Count: np.Count, MaxPods: np.MaxPods, diff --git a/pkg/aks/create_test.go b/pkg/aks/create_test.go index 2e246e4c..066b400c 100644 --- a/pkg/aks/create_test.go +++ b/pkg/aks/create_test.go @@ -336,6 +336,7 @@ var _ = Describe("newManagedCluster", func() { }, }, nil) clusterSpec.ResourceLocation = "chinaeast" + clusterSpec.NodePools[0].AvailabilityZones = nil managedCluster, err := createManagedCluster(ctx, cred, workplacesClientMock, clusterSpec, "test-phase") Expect(err).ToNot(HaveOccurred()) @@ -436,6 +437,13 @@ var _ = Describe("newManagedCluster", func() { Expect(managedCluster.Identity).To(BeNil()) }) + + It("should fail create managed cluster with az in region which doesn't support it", func() { + clusterSpec.ResourceLocation = "westus" + clusterSpec.NodePools[0].AvailabilityZones = to.Ptr([]string{"1", "2", "3"}) + _, err := createManagedCluster(ctx, cred, workplacesClientMock, clusterSpec, "test-phase") + Expect(err).To(HaveOccurred()) + }) }) var _ = Describe("CreateCluster", func() { diff --git a/pkg/aks/services/mock_services/doc.go b/pkg/aks/services/mock_services/doc.go index 9fb5cd35..b887e09a 100644 --- a/pkg/aks/services/mock_services/doc.go +++ b/pkg/aks/services/mock_services/doc.go @@ -5,4 +5,5 @@ package mock_services //go:generate ../../../../bin/mockgen -destination agentpools_mock.go -package mock_services -source ../agentpools.go AgentPoolsClientInterface //go:generate ../../../../bin/mockgen -destination groups_mock.go -package mock_services -source ../groups.go ResourceGroupsClientInterface //go:generate ../../../../bin/mockgen -destination managedclusters_mock.go -package mock_services -source ../managedclusters.go ManagedClustersClientInterface +//go:generate ../../../../bin/mockgen -destination subscriptions_mock.go -package mock_services -source ../subscriptions.go SubscriptionsClientInterface //go:generate ../../../../bin/mockgen -destination workplaces_mock.go -package mock_services -source ../workplaces.go WorkplacesClientInterface diff --git a/pkg/aks/services/mock_services/subscriptions_mock.go b/pkg/aks/services/mock_services/subscriptions_mock.go new file mode 100644 index 00000000..ba942608 --- /dev/null +++ b/pkg/aks/services/mock_services/subscriptions_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../subscriptions.go +// +// Generated by this command: +// +// mockgen -destination subscriptions_mock.go -package mock_services -source ../subscriptions.go SubscriptionsClientInterface +// + +// Package mock_services is a generated GoMock package. +package mock_services + +import ( + reflect "reflect" + + runtime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + armsubscriptions "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + gomock "go.uber.org/mock/gomock" +) + +// MockSubscriptionsClientInterface is a mock of SubscriptionsClientInterface interface. +type MockSubscriptionsClientInterface struct { + ctrl *gomock.Controller + recorder *MockSubscriptionsClientInterfaceMockRecorder +} + +// MockSubscriptionsClientInterfaceMockRecorder is the mock recorder for MockSubscriptionsClientInterface. +type MockSubscriptionsClientInterfaceMockRecorder struct { + mock *MockSubscriptionsClientInterface +} + +// NewMockSubscriptionsClientInterface creates a new mock instance. +func NewMockSubscriptionsClientInterface(ctrl *gomock.Controller) *MockSubscriptionsClientInterface { + mock := &MockSubscriptionsClientInterface{ctrl: ctrl} + mock.recorder = &MockSubscriptionsClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSubscriptionsClientInterface) EXPECT() *MockSubscriptionsClientInterfaceMockRecorder { + return m.recorder +} + +// NewListLocationsPager mocks base method. +func (m *MockSubscriptionsClientInterface) NewListLocationsPager(subscriptionID string, options *armsubscriptions.ClientListLocationsOptions) *runtime.Pager[armsubscriptions.ClientListLocationsResponse] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListLocationsPager", subscriptionID, options) + ret0, _ := ret[0].(*runtime.Pager[armsubscriptions.ClientListLocationsResponse]) + return ret0 +} + +// NewListLocationsPager indicates an expected call of NewListLocationsPager. +func (mr *MockSubscriptionsClientInterfaceMockRecorder) NewListLocationsPager(subscriptionID, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListLocationsPager", reflect.TypeOf((*MockSubscriptionsClientInterface)(nil).NewListLocationsPager), subscriptionID, options) +} diff --git a/pkg/aks/services/subscriptions.go b/pkg/aks/services/subscriptions.go new file mode 100644 index 00000000..ca1a4b54 --- /dev/null +++ b/pkg/aks/services/subscriptions.go @@ -0,0 +1,38 @@ +package services + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" +) + +type SubscriptionsClientInterface interface { + NewListLocationsPager(subscriptionID string, options *armsubscriptions.ClientListLocationsOptions) *runtime.Pager[armsubscriptions.ClientListLocationsResponse] +} + +type subscriptionClient struct { + subscriptionClient *armsubscriptions.Client +} + +func NewSubscriptionClient(credential *azidentity.ClientSecretCredential, cloud cloud.Configuration) (*subscriptionClient, error) { + options := arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Cloud: cloud, + }, + } + clientFactory, err := armsubscriptions.NewClientFactory(credential, &options) + if err != nil { + return nil, err + } + + return &subscriptionClient{ + subscriptionClient: clientFactory.NewClient(), + }, nil +} + +func (cl *subscriptionClient) NewListLocationsPager(subscriptionID string, options *armsubscriptions.ClientListLocationsOptions) *runtime.Pager[armsubscriptions.ClientListLocationsResponse] { + return cl.subscriptionClient.NewListLocationsPager(subscriptionID, options) +}