diff --git a/pkg/types/nutanix/validation/platform.go b/pkg/types/nutanix/validation/platform.go index b981d3555a..6009d0939c 100644 --- a/pkg/types/nutanix/validation/platform.go +++ b/pkg/types/nutanix/validation/platform.go @@ -3,6 +3,8 @@ package validation import ( "fmt" "regexp" + "sort" + "strings" "k8s.io/apimachinery/pkg/util/validation/field" @@ -103,11 +105,20 @@ func ValidatePlatform(p *nutanix.Platform, fldPath *field.Path, c *types.Install if err != nil { allErrs = append(allErrs, field.InternalError(fldPath.Child("failureDomain", "name"), fmt.Errorf("fail to compile the pattern %q: %w", pattern, err))) } else { - for _, fd := range p.FailureDomains { + fdNames := make(map[string]int) + fdTopologies := make(map[string]string) + + for idx, fd := range p.FailureDomains { if !rexp.MatchString(fd.Name) { allErrs = append(allErrs, field.Invalid(fldPath.Child("failureDomain", "name"), fd.Name, fmt.Sprintf("failureDomain name should match the pattern %q.", pattern))) } + if prevIdx, exists := fdNames[fd.Name]; exists { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("failureDomains").Index(idx).Child("name"), fmt.Sprintf("failure domain name %q is already used by failureDomains[%d]", fd.Name, prevIdx))) + } else { + fdNames[fd.Name] = idx + } + if fd.PrismElement.UUID == "" { allErrs = append(allErrs, field.Required(fldPath.Child("failureDomain", "prismElement", "uuid"), "failureDomain prismElement uuid cannot be empty")) } @@ -117,6 +128,19 @@ func ValidatePlatform(p *nutanix.Platform, fldPath *field.Path, c *types.Install allErrs = append(allErrs, errs...) } + // Only check for duplicate topology after basic required-field validation, + // so users see actionable "required field" errors instead of misleading + // "identical topology" when fields are simply empty. + if fd.PrismElement.UUID != "" && len(fd.SubnetUUIDs) > 0 { + topoKey := nutanixFailureDomainTopologyKey(fd) + if prevName, exists := fdTopologies[topoKey]; exists { + allErrs = append(allErrs, field.Invalid(fldPath.Child("failureDomains").Index(idx), fd.Name, + fmt.Sprintf("failure domain %q has identical topology (same prismElement and subnets) as %q; this provides no additional fault tolerance", fd.Name, prevName))) + } else { + fdTopologies[topoKey] = fd.Name + } + } + for _, sc := range fd.StorageContainers { if sc.ReferenceName == "" { allErrs = append(allErrs, field.Required(fldPath.Child("failureDomain", "storageContainers", "referenceName"), fmt.Sprintf("failureDomain %q: missing storageContainer referenceName", fd.Name))) @@ -153,6 +177,15 @@ func validateLoadBalancer(lbType configv1.PlatformLoadBalancerType) bool { } } +// nutanixFailureDomainTopologyKey builds a comparable key from the infrastructure-defining +// fields of a failure domain: Prism Element UUID and sorted subnet UUIDs. +func nutanixFailureDomainTopologyKey(fd nutanix.FailureDomain) string { + subnets := make([]string, len(fd.SubnetUUIDs)) + copy(subnets, fd.SubnetUUIDs) + sort.Strings(subnets) + return fmt.Sprintf("pe=%s;subnets=%s", fd.PrismElement.UUID, strings.Join(subnets, "\x00")) +} + // validateSubnets validates the input subnetUUIDs meet the configuration requirements. func validateSubnets(fldPath *field.Path, subnetUUIDs []string) field.ErrorList { var errs field.ErrorList diff --git a/pkg/types/nutanix/validation/platform_test.go b/pkg/types/nutanix/validation/platform_test.go index 7ac9019697..27adc2f185 100644 --- a/pkg/types/nutanix/validation/platform_test.go +++ b/pkg/types/nutanix/validation/platform_test.go @@ -409,6 +409,85 @@ func TestValidatePlatform(t *testing.T) { return p }(), }, + { + name: "failureDomain with duplicate name", + platform: func() *nutanix.Platform { + p := validPlatform() + p.FailureDomains = []nutanix.FailureDomain{ + { + Name: "fd-1", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid-1", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe-1", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid-1"}, + }, + { + Name: "fd-1", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid-2", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe-2", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid-2"}, + }, + } + return p + }(), + expectedError: `test-path\.failureDomains\[1\]\.name: Duplicate value: "failure domain name \\"fd-1\\" is already used by failureDomains\[0\]"`, + }, + { + name: "failureDomain with duplicate topology same prismElement and subnet", + platform: func() *nutanix.Platform { + p := validPlatform() + p.FailureDomains = []nutanix.FailureDomain{ + { + Name: "fd-1", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid"}, + }, + { + Name: "fd-2", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid"}, + }, + } + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "fd-2": failure domain "fd-2" has identical topology \(same prismElement and subnets\) as "fd-1"; this provides no additional fault tolerance`, + }, + { + name: "valid failureDomain with different prismElements", + platform: func() *nutanix.Platform { + p := validPlatform() + p.FailureDomains = []nutanix.FailureDomain{ + { + Name: "fd-1", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid-1", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe-1", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid"}, + }, + { + Name: "fd-2", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid-2", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe-2", Port: 9440}}, + SubnetUUIDs: []string{"fd-subnet-uuid"}, + }, + } + return p + }(), + }, + { + name: "failureDomain duplicate topology with same subnets in different order", + platform: func() *nutanix.Platform { + p := validPlatform() + p.FailureDomains = []nutanix.FailureDomain{ + { + Name: "fd-1", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe", Port: 9440}}, + SubnetUUIDs: []string{"subnet-a", "subnet-b"}, + }, + { + Name: "fd-2", + PrismElement: nutanix.PrismElement{UUID: "fd-pe-uuid", Endpoint: nutanix.PrismEndpoint{Address: "fd-pe", Port: 9440}}, + SubnetUUIDs: []string{"subnet-b", "subnet-a"}, + }, + } + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "fd-2": failure domain "fd-2" has identical topology \(same prismElement and subnets\) as "fd-1"; this provides no additional fault tolerance`, + }, { name: "valid failureDomain with multiple subnets for multi-NIC", platform: func() *nutanix.Platform {