diff --git a/pkg/api/v1alpha1/nutanixdatacenterconfig_types.go b/pkg/api/v1alpha1/nutanixdatacenterconfig_types.go index 465ad17624cb..2ab1b77467f3 100644 --- a/pkg/api/v1alpha1/nutanixdatacenterconfig_types.go +++ b/pkg/api/v1alpha1/nutanixdatacenterconfig_types.go @@ -185,19 +185,19 @@ func (in *NutanixDatacenterConfig) Validate() error { return nil } -func createValidateNutanixResourceFunc(msgPrefix, entityName, mfstName string) (func (*NutanixResourceIdentifier) error) { - return func (ntnxRId *NutanixResourceIdentifier) error { +func createValidateNutanixResourceFunc(msgPrefix, entityName, mfstName string) func(*NutanixResourceIdentifier) error { + return func(ntnxRId *NutanixResourceIdentifier) error { if ntnxRId.Type != NutanixIdentifierName && ntnxRId.Type != NutanixIdentifierUUID { return fmt.Errorf("%s: invalid identifier type for %s: %s", msgPrefix, entityName, ntnxRId.Type) } - + if ntnxRId.Type == NutanixIdentifierName && (ntnxRId.Name == nil || *ntnxRId.Name == "") { return fmt.Errorf("%s: missing %s name: %s", msgPrefix, entityName, mfstName) } else if ntnxRId.Type == NutanixIdentifierUUID && (ntnxRId.UUID == nil || *ntnxRId.UUID == "") { return fmt.Errorf("%s: missing %s UUID: %s", msgPrefix, entityName, mfstName) } - - return nil + + return nil } } diff --git a/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-invalid-failuredomains.yaml b/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-invalid-failuredomains.yaml new file mode 100644 index 000000000000..b25f74bc958c --- /dev/null +++ b/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-invalid-failuredomains.yaml @@ -0,0 +1,30 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + name: eksa-unit-test + kind: Secret + failureDomains: + - name: "pe1" + cluster: + type: name + name: "prism-cluster-1" + subnets: + - name: "prism-subnet-1" + type: "name" + - uuid: "" + type: "uuid" + - name: "pe2" + cluster: + type: "uuid" + uuid: "468b7b36-d15b-406a-90f7-46d1560c4f4e" + subnets: + - name: "prism-subnet-1" + type: "name" + - uuid: "3e716c09-0613-46f3-b46a-beb89aa02295" + type: "uuid" diff --git a/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-valid-failuredomains.yaml b/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-valid-failuredomains.yaml new file mode 100644 index 000000000000..02f806ff343e --- /dev/null +++ b/pkg/api/v1alpha1/testdata/nutanix/datacenterconfig-valid-failuredomains.yaml @@ -0,0 +1,30 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + name: eksa-unit-test + kind: Secret + failureDomains: + - name: "pe1" + cluster: + type: name + name: "prism-cluster-1" + subnets: + - name: "prism-subnet-1" + type: "name" + - uuid: "3e716c09-0613-46f3-b46a-beb89aa02295" + type: "uuid" + - name: "pe2" + cluster: + type: "uuid" + uuid: "468b7b36-d15b-406a-90f7-46d1560c4f4e" + subnets: + - name: "prism-subnet-1" + type: "name" + - uuid: "3e716c09-0613-46f3-b46a-beb89aa02295" + type: "uuid" diff --git a/pkg/providers/nutanix/provider.go b/pkg/providers/nutanix/provider.go index e5a7de728682..d758362c95fd 100644 --- a/pkg/providers/nutanix/provider.go +++ b/pkg/providers/nutanix/provider.go @@ -226,7 +226,17 @@ func (p *Provider) SetupAndValidateDeleteCluster(ctx context.Context, cluster *t } // SetupAndValidateUpgradeCluster - Performs necessary setup and validations for upgrade cluster operation. -func (p *Provider) SetupAndValidateUpgradeCluster(ctx context.Context, _ *types.Cluster, clusterSpec *cluster.Spec, _ *cluster.Spec) error { +func (p *Provider) SetupAndValidateUpgradeCluster(ctx context.Context, cluster *types.Cluster, clusterSpec *cluster.Spec, _ *cluster.Spec) error { + // Get current Nutanix Datacenter Config + curDc, err := p.kubectlClient.GetEksaNutanixDatacenterConfig(ctx, p.datacenterConfig.Name, cluster.KubeconfigFile, clusterSpec.Cluster.Namespace) + if err != nil { + return fmt.Errorf("failed setup and validations: %v", err) + } + + if len(curDc.Spec.FailureDomains) != len(p.datacenterConfig.Spec.FailureDomains) { + return fmt.Errorf("failed setup and validations: failure domains upgrade doesn't supported in current release") + } + if err := p.SetupAndValidateUpgradeManagementComponents(ctx, clusterSpec); err != nil { return err } @@ -419,7 +429,6 @@ func needsNewEtcdTemplate(oldSpec, newSpec *cluster.Spec, oldNmc, newNmc *v1alph if oldSpec.Bundles.Spec.Number != newSpec.Bundles.Spec.Number { return true } - return AnyImmutableFieldChanged(oldNmc, newNmc) } diff --git a/pkg/providers/nutanix/provider_test.go b/pkg/providers/nutanix/provider_test.go index 119d683da6fa..5dd4bab83bbe 100644 --- a/pkg/providers/nutanix/provider_test.go +++ b/pkg/providers/nutanix/provider_test.go @@ -530,7 +530,20 @@ func TestNutanixProviderSetupAndValidateDeleteCluster(t *testing.T) { } func TestNutanixProviderSetupAndValidateUpgradeCluster(t *testing.T) { - provider := testDefaultNutanixProvider(t) + ctrl := gomock.NewController(t) + executable := mockexecutables.NewMockExecutable(ctrl) + executable.EXPECT().ExecuteWithStdin(gomock.Any(), gomock.Any(), gomock.Any()).Return(bytes.Buffer{}, nil).AnyTimes() + executable.EXPECT().Execute(gomock.Any(), "get", + "--ignore-not-found", "-o", "json", "--kubeconfig", "testdata/kubeconfig.yaml", "nutanixdatacenterconfigs.anywhere.eks.amazonaws.com", "--namespace", "default", "eksa-unit-test").Return(*bytes.NewBufferString(nutanixDatacenterConfigSpecJSON), nil).AnyTimes() + kubectl := executables.NewKubectl(executable) + mockClient := mocknutanix.NewMockClient(ctrl) + mockCertValidator := mockCrypto.NewMockTlsValidator(ctrl) + mockTransport := mocknutanix.NewMockRoundTripper(ctrl) + mockTransport.EXPECT().RoundTrip(gomock.Any()).Return(&http.Response{}, nil).AnyTimes() + mockHTTPClient := &http.Client{Transport: mockTransport} + mockWriter := filewritermocks.NewMockFileWriter(ctrl) + provider := testNutanixProvider(t, mockClient, kubectl, mockCertValidator, mockHTTPClient, mockWriter) + tests := []struct { name string clusterConfFile string @@ -558,7 +571,8 @@ func TestNutanixProviderSetupAndValidateUpgradeCluster(t *testing.T) { for _, tt := range tests { clusterSpec := test.NewFullClusterSpec(t, tt.clusterConfFile) - err := provider.SetupAndValidateUpgradeCluster(context.Background(), &types.Cluster{Name: "eksa-unit-test"}, clusterSpec, clusterSpec) + cluster := &types.Cluster{Name: "eksa-unit-test", KubeconfigFile: "testdata/kubeconfig.yaml"} + err := provider.SetupAndValidateUpgradeCluster(context.Background(), cluster, clusterSpec, clusterSpec) if tt.expectErr { assert.Error(t, err, tt.name) thenErrorExpected(t, tt.expectErrStr, err) diff --git a/pkg/providers/nutanix/template_test.go b/pkg/providers/nutanix/template_test.go index b13cfc9dc824..037081059f57 100644 --- a/pkg/providers/nutanix/template_test.go +++ b/pkg/providers/nutanix/template_test.go @@ -699,6 +699,9 @@ func TestTemplateBuilderFailureDomains(t *testing.T) { clusterSpec := test.NewFullClusterSpec(t, tc.Input) machineCfg := clusterSpec.NutanixMachineConfig(clusterSpec.Cluster.Spec.ControlPlaneConfiguration.MachineGroupRef.Name) + + t.Setenv(constants.EksaNutanixUsernameKey, "admin") + t.Setenv(constants.EksaNutanixPasswordKey, "password") creds := GetCredsFromEnv() bldr := NewNutanixTemplateBuilder(&clusterSpec.NutanixDatacenter.Spec, &machineCfg.Spec, nil, diff --git a/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains.yaml b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains.yaml new file mode 100644 index 000000000000..25f95fa4cf24 --- /dev/null +++ b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains.yaml @@ -0,0 +1,27 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + kind: Secret + name: "nutanix-credentials" + insecure: true + failureDomains: + - name: "pe1" + cluster: + type: name + name: "prism-cluster" + subnets: + - type: uuid + uuid: "2d166190-7759-4dc6-b835-923262d6b497" + - name: "pe2" + cluster: + type: uuid + uuid: "4d69ca7d-022f-49d1-a454-74535993bda4" + subnets: + - type: name + name: "prism-subnet" diff --git a/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_cluster.yaml b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_cluster.yaml new file mode 100644 index 000000000000..91a7f99954f3 --- /dev/null +++ b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_cluster.yaml @@ -0,0 +1,27 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + kind: Secret + name: "nutanix-credentials" + insecure: true + failureDomains: + - name: "pe1" + cluster: + type: name + name: "prism-cluster" + subnets: + - type: uuid + uuid: "2d166190-7759-4dc6-b835-923262d6b497" + - name: "pe2" + cluster: + type: uuid + uuid: "4d69ca7d-022f-49d1-a454-00005993bda4" + subnets: + - type: name + name: "prism-subnet" diff --git a/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_name.yaml b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_name.yaml new file mode 100644 index 000000000000..c4dda7d7650f --- /dev/null +++ b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_name.yaml @@ -0,0 +1,27 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + kind: Secret + name: "nutanix-credentials" + insecure: true + failureDomains: + - name: "FIZZBUZZ!!!!" + cluster: + type: name + name: "prism-cluster" + subnets: + - type: uuid + uuid: "2d166190-7759-4dc6-b835-923262d6b497" + - name: "pe2" + cluster: + type: uuid + uuid: "4d69ca7d-022f-49d1-a454-74535993bda4" + subnets: + - type: name + name: "prism-subnet" diff --git a/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_subnet.yaml b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_subnet.yaml new file mode 100644 index 000000000000..a35a86b484b2 --- /dev/null +++ b/pkg/providers/nutanix/testdata/datacenterConfig_with_failure_domains_invalid_subnet.yaml @@ -0,0 +1,27 @@ +apiVersion: anywhere.eks.amazonaws.com/v1alpha1 +kind: NutanixDatacenterConfig +metadata: + name: eksa-unit-test + namespace: default +spec: + endpoint: "prism.nutanix.com" + port: 9440 + credentialRef: + kind: Secret + name: "nutanix-credentials" + insecure: true + failureDomains: + - name: "pe1" + cluster: + type: name + name: "prism-cluster" + subnets: + - type: uuid + uuid: "2d166190-7759-4dc6-b835-000062d6b497" + - name: "pe2" + cluster: + type: uuid + uuid: "4d69ca7d-022f-49d1-a454-74535993bda4" + subnets: + - type: name + name: "prism-subnet" diff --git a/pkg/providers/nutanix/testdata/expected_results_failure_domains.yaml b/pkg/providers/nutanix/testdata/expected_results_failure_domains.yaml index 5e7832678e12..b3ff855aa819 100644 --- a/pkg/providers/nutanix/testdata/expected_results_failure_domains.yaml +++ b/pkg/providers/nutanix/testdata/expected_results_failure_domains.yaml @@ -601,3 +601,31 @@ spec: - kind: Secret name: test-nutanix-ccm-secret strategy: Reconcile +--- +apiVersion: v1 +kind: Secret +metadata: + name: "test-nutanix-ccm-secret" + namespace: "eksa-system" +stringData: + nutanix-ccm-secret.yaml: | + apiVersion: v1 + kind: Secret + metadata: + name: nutanix-creds + namespace: kube-system + stringData: + credentials: |- + [ + { + "type": "basic_auth", + "data": { + "prismCentral": { + "username": "admin", + "password": "password" + }, + "prismElements": null + } + } + ] +type: addons.cluster.x-k8s.io/resource-set diff --git a/pkg/providers/nutanix/validator.go b/pkg/providers/nutanix/validator.go index 2bce2a5c543c..d5cf8ad732ff 100644 --- a/pkg/providers/nutanix/validator.go +++ b/pkg/providers/nutanix/validator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "regexp" "strconv" "strings" @@ -126,6 +127,37 @@ func (v *Validator) ValidateDatacenterConfig(ctx context.Context, client Client, return err } + if err := v.validateFailureDomains(ctx, client, config); err != nil { + return err + } + + return nil +} + +func (v *Validator) validateFailureDomains(ctx context.Context, client Client, config *anywherev1.NutanixDatacenterConfig) error { + regexName, err := regexp.Compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + if err != nil { + return err + } + + for _, fd := range config.Spec.FailureDomains { + if res := regexName.MatchString(fd.Name); !res { + errorStr := `failure domain name should contains only small letters, digits, and hyphens. + It should start with small letter or digit` + return fmt.Errorf(errorStr) + } + + if err := v.validateClusterConfig(ctx, client, fd.Cluster); err != nil { + return err + } + + for _, subnet := range fd.Subnets { + if err := v.validateSubnetConfig(ctx, client, subnet); err != nil { + return err + } + } + } + return nil } diff --git a/pkg/providers/nutanix/validator_test.go b/pkg/providers/nutanix/validator_test.go index 802a365b3654..01fdb204aa17 100644 --- a/pkg/providers/nutanix/validator_test.go +++ b/pkg/providers/nutanix/validator_test.go @@ -5,7 +5,9 @@ import ( _ "embed" "encoding/json" "errors" + "fmt" "net/http" + "strings" "testing" "github.com/golang/mock/gomock" @@ -45,6 +47,18 @@ var nutanixDatacenterConfigSpecWithInvalidCredentialRefKind string //go:embed testdata/datacenterConfig_empty_credentialRef_name.yaml var nutanixDatacenterConfigSpecWithEmptyCredentialRefName string +//go:embed testdata/datacenterConfig_with_failure_domains.yaml +var nutanixDatacenterConfigSpecWithFailureDomain string + +//go:embed testdata/datacenterConfig_with_failure_domains_invalid_name.yaml +var nutanixDatacenterConfigSpecWithFailureDomainInvalidName string + +//go:embed testdata/datacenterConfig_with_failure_domains_invalid_cluster.yaml +var nutanixDatacenterConfigSpecWithFailureDomainInvalidCluster string + +//go:embed testdata/datacenterConfig_with_failure_domains_invalid_subnet.yaml +var nutanixDatacenterConfigSpecWithFailureDomainInvalidSubnet string + func fakeClusterList() *v3.ClusterListIntentResponse { return &v3.ClusterListIntentResponse{ Entities: []*v3.ClusterIntentResponse{ @@ -82,6 +96,96 @@ func fakeSubnetList() *v3.SubnetListIntentResponse { } } +func fakeClusterListForDCTest(filter *string) (*v3.ClusterListIntentResponse, error) { + data := &v3.ClusterListIntentResponse{ + Entities: []*v3.ClusterIntentResponse{ + { + Metadata: &v3.Metadata{ + UUID: utils.StringPtr("a15f6966-bfc7-4d1e-8575-224096fc1cdb"), + }, + Spec: &v3.Cluster{ + Name: utils.StringPtr("prism-cluster"), + }, + Status: &v3.ClusterDefStatus{ + Resources: &v3.ClusterObj{ + Config: &v3.ClusterConfig{ + ServiceList: []*string{utils.StringPtr("AOS")}, + }, + }, + }, + }, + { + Metadata: &v3.Metadata{ + UUID: utils.StringPtr("4d69ca7d-022f-49d1-a454-74535993bda4"), + }, + Spec: &v3.Cluster{ + Name: utils.StringPtr("prism-cluster-1"), + }, + Status: &v3.ClusterDefStatus{ + Resources: &v3.ClusterObj{ + Config: &v3.ClusterConfig{ + ServiceList: []*string{utils.StringPtr("AOS")}, + }, + }, + }, + }, + }, + } + + result := &v3.ClusterListIntentResponse{ + Entities: []*v3.ClusterIntentResponse{}, + } + + if filter != nil && *filter != "" { + str := strings.Replace(*filter, "name==", "", -1) + for _, cluster := range data.Entities { + if str == *cluster.Spec.Name { + result.Entities = append(result.Entities, cluster) + } + } + } + + return result, nil +} + +func fakeSubnetListForDCTest(filter *string) (*v3.SubnetListIntentResponse, error) { + data := &v3.SubnetListIntentResponse{ + Entities: []*v3.SubnetIntentResponse{ + { + Metadata: &v3.Metadata{ + UUID: utils.StringPtr("b15f6966-bfc7-4d1e-8575-224096fc1cdb"), + }, + Spec: &v3.Subnet{ + Name: utils.StringPtr("prism-subnet"), + }, + }, + { + Metadata: &v3.Metadata{ + UUID: utils.StringPtr("2d166190-7759-4dc6-b835-923262d6b497"), + }, + Spec: &v3.Subnet{ + Name: utils.StringPtr("prism-subnet-1"), + }, + }, + }, + } + + result := &v3.SubnetListIntentResponse{ + Entities: []*v3.SubnetIntentResponse{}, + } + + if filter != nil && *filter != "" { + str := strings.Replace(*filter, "name==", "", -1) + for _, subnet := range data.Entities { + if str == *subnet.Spec.Name { + result.Entities = append(result.Entities, subnet) + } + } + } + + return result, nil +} + func fakeImageList() *v3.ImageListIntentResponse { return &v3.ImageListIntentResponse{ Entities: []*v3.ImageIntentResponse{ @@ -596,11 +700,45 @@ func TestNutanixValidatorValidateDatacenterConfig(t *testing.T) { dcConfFile: nutanixDatacenterConfigSpecWithEmptyCredentialRefName, expectErr: true, }, + { + name: "valid failure domains", + dcConfFile: nutanixDatacenterConfigSpecWithFailureDomain, + expectErr: false, + }, + { + name: "failure domain with invalid name", + dcConfFile: nutanixDatacenterConfigSpecWithFailureDomainInvalidName, + expectErr: true, + }, + { + name: "failure domain with invalid cluster", + dcConfFile: nutanixDatacenterConfigSpecWithFailureDomainInvalidCluster, + expectErr: true, + }, + { + name: "failure domains with invalid subnet", + dcConfFile: nutanixDatacenterConfigSpecWithFailureDomainInvalidSubnet, + expectErr: true, + }, } ctrl := gomock.NewController(t) mockClient := mocknutanix.NewMockClient(ctrl) mockClient.EXPECT().GetCurrentLoggedInUser(gomock.Any()).Return(&v3.UserIntentResponse{}, nil).AnyTimes() + mockClient.EXPECT().ListCluster(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, filters *v3.DSMetadata) (*v3.ClusterListIntentResponse, error) { + return fakeClusterListForDCTest(filters.Filter) + }, + ).AnyTimes() + mockClient.EXPECT().ListSubnet(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, filters *v3.DSMetadata) (*v3.SubnetListIntentResponse, error) { + return fakeSubnetListForDCTest(filters.Filter) + }, + ).AnyTimes() + mockClient.EXPECT().GetSubnet(gomock.Any(), gomock.Eq("2d166190-7759-4dc6-b835-923262d6b497")).Return(nil, nil).AnyTimes() + mockClient.EXPECT().GetSubnet(gomock.Any(), gomock.Not("2d166190-7759-4dc6-b835-923262d6b497")).Return(nil, fmt.Errorf("")).AnyTimes() + mockClient.EXPECT().GetCluster(gomock.Any(), gomock.Eq("4d69ca7d-022f-49d1-a454-74535993bda4")).Return(nil, nil).AnyTimes() + mockClient.EXPECT().GetCluster(gomock.Any(), gomock.Not("4d69ca7d-022f-49d1-a454-74535993bda4")).Return(nil, fmt.Errorf("")).AnyTimes() mockTLSValidator := mockCrypto.NewMockTlsValidator(ctrl) mockTLSValidator.EXPECT().ValidateCert(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()