From 0f0778cc7ad7a71f3c83bc89f93a898a3ceecf72 Mon Sep 17 00:00:00 2001 From: Chin2691 Date: Tue, 19 May 2026 17:18:17 +0530 Subject: [PATCH 1/3] fix: validate duplicate vSphere failureDomain topology Adds a check in validateFailureDomains that detects when two or more failure domains have identical topology (same server, datacenter, computeCluster, datastore, networks, and resourcePool). Copy-pasted failure domains that differ only in name/region/zone labels provide no additional fault tolerance and can cause subtle scheduling issues. Inventory paths (computeCluster, datastore, resourcePool) are canonicalized with filepath.Clean before comparison so that syntactically different but semantically equivalent paths (e.g. trailing slashes, double separators) are normalized. Bug: https://redhat.atlassian.net/browse/OCPBUGS-86074 Co-authored-by: Cursor --- pkg/types/vsphere/validation/platform.go | 34 +++++++++++++++++++ pkg/types/vsphere/validation/platform_test.go | 12 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pkg/types/vsphere/validation/platform.go b/pkg/types/vsphere/validation/platform.go index 75209d8bc0..86178eb338 100644 --- a/pkg/types/vsphere/validation/platform.go +++ b/pkg/types/vsphere/validation/platform.go @@ -5,6 +5,7 @@ import ( "net" "path/filepath" "regexp" + "sort" "strings" "github.com/sirupsen/logrus" @@ -163,6 +164,7 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl var associatedVCenter *vsphere.VCenter zoneNames := make(map[string]string) + fdTopologies := make(map[string]string) for index, failureDomain := range p.FailureDomains { if regionName, ok := zoneNames[failureDomain.Zone]; !ok { @@ -171,6 +173,14 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl allErrs = append(allErrs, field.Invalid(fldPath.Child("zone"), failureDomain.Zone, fmt.Sprintf("cannot be used more than once for the failure domain region %q", failureDomain.Region))) } + topoKey := vsphereFailureDomainTopologyKey(failureDomain) + if prevName, exists := fdTopologies[topoKey]; exists { + allErrs = append(allErrs, field.Invalid(fldPath.Index(index), failureDomain.Name, + fmt.Sprintf("failure domain %q has identical topology (same server, datacenter, computeCluster, datastore, networks, resourcePool) as %q; this provides no additional fault tolerance", failureDomain.Name, prevName))) + } else { + fdTopologies[topoKey] = failureDomain.Name + } + if failureDomain.ZoneType == "" && failureDomain.RegionType == "" { logrus.Debug("using the defaults regionType is Datacenter and zoneType is ComputeCluster") } @@ -340,6 +350,30 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl return allErrs } +// vsphereFailureDomainTopologyKey builds a comparable key from the infrastructure-defining +// fields of a failure domain topology. Inventory paths are canonicalized with filepath.Clean +// so that syntactically different but equivalent paths (e.g. trailing slashes) are normalized. +func vsphereFailureDomainTopologyKey(fd vsphere.FailureDomain) string { + networks := make([]string, len(fd.Topology.Networks)) + copy(networks, fd.Topology.Networks) + sort.Strings(networks) + + normalizePath := func(p string) string { + if p == "" { + return "" + } + return filepath.Clean(p) + } + + return fmt.Sprintf("server=%s;dc=%s;cluster=%s;ds=%s;nets=%s;rp=%s", + fd.Server, + fd.Topology.Datacenter, + normalizePath(fd.Topology.ComputeCluster), + normalizePath(fd.Topology.Datastore), + strings.Join(networks, ","), + normalizePath(fd.Topology.ResourcePool)) +} + // validateDiskType checks that the specified diskType is valid. func validateDiskType(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/types/vsphere/validation/platform_test.go b/pkg/types/vsphere/validation/platform_test.go index 099188dd99..767977d59c 100644 --- a/pkg/types/vsphere/validation/platform_test.go +++ b/pkg/types/vsphere/validation/platform_test.go @@ -378,7 +378,7 @@ func TestValidatePlatform(t *testing.T) { for i := range p.FailureDomains { p.FailureDomains[i].Topology.ComputeCluster = "/test-datacenter/host/HostClusterFolder/test-cluster" - p.FailureDomains[i].Topology.ResourcePool = "/test-datacenter/host/HostClusterFolder/test-cluster/Resources/test-resourcepool" + p.FailureDomains[i].Topology.ResourcePool = fmt.Sprintf("/test-datacenter/host/HostClusterFolder/test-cluster/Resources/test-resourcepool-%d", i) p.FailureDomains[i].Topology.Datastore = "/test-datacenter/datastore/StorageFolder/test-datastore" p.FailureDomains[i].Topology.Folder = "/test-datacenter/vm/VMFolder/test-folder" } @@ -552,6 +552,16 @@ func TestValidatePlatform(t *testing.T) { }(), expectedError: `test-path\.failureDomains\.name\[1\]: Duplicate value: "test-east-1a"`, }, + { + name: "Multi-zone platform failureDomain duplicate topology", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Server = p.FailureDomains[0].Server + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, + }, { name: "Multi-zone platform failureDomain zone missing tag category", platform: func() *vsphere.Platform { From 7da08ace337b136c31bab894f464e396483330d8 Mon Sep 17 00:00:00 2001 From: Chin2691 Date: Wed, 20 May 2026 08:27:39 +0530 Subject: [PATCH 2/3] fix: address reviewer feedback on topology duplicate detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor from fmt.Sprintf string key to comparable struct map key (type-safe, no delimiter injection risk, compiler-checked) - Include Topology.HostGroup in comparison so vm-host zonal failure domains with different HostGroups are not falsely rejected - Switch from filepath.Clean to path.Clean for vSphere inventory paths (URL-style, always forward slashes — not OS paths) - Use \x00 as network separator to eliminate join ambiguity - Add positive test: HostGroup FDs with same topology but different HostGroups must pass validation - Update error message to include hostGroup in the list of compared fields Bug: https://redhat.atlassian.net/browse/OCPBUGS-86073 Co-authored-by: Cursor --- pkg/types/vsphere/validation/platform.go | 53 +++++++++++++------ pkg/types/vsphere/validation/platform_test.go | 17 ++++++ 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/pkg/types/vsphere/validation/platform.go b/pkg/types/vsphere/validation/platform.go index 86178eb338..94cf4c6093 100644 --- a/pkg/types/vsphere/validation/platform.go +++ b/pkg/types/vsphere/validation/platform.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "net" + "path" "path/filepath" "regexp" "sort" @@ -164,7 +165,7 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl var associatedVCenter *vsphere.VCenter zoneNames := make(map[string]string) - fdTopologies := make(map[string]string) + fdTopologies := make(map[vsphereTopologyKey]string) for index, failureDomain := range p.FailureDomains { if regionName, ok := zoneNames[failureDomain.Zone]; !ok { @@ -173,10 +174,10 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl allErrs = append(allErrs, field.Invalid(fldPath.Child("zone"), failureDomain.Zone, fmt.Sprintf("cannot be used more than once for the failure domain region %q", failureDomain.Region))) } - topoKey := vsphereFailureDomainTopologyKey(failureDomain) + topoKey := normalizedTopologyKey(failureDomain) if prevName, exists := fdTopologies[topoKey]; exists { allErrs = append(allErrs, field.Invalid(fldPath.Index(index), failureDomain.Name, - fmt.Sprintf("failure domain %q has identical topology (same server, datacenter, computeCluster, datastore, networks, resourcePool) as %q; this provides no additional fault tolerance", failureDomain.Name, prevName))) + fmt.Sprintf("failure domain %q has identical topology (same server, datacenter, computeCluster, datastore, networks, resourcePool, hostGroup) as %q; this provides no additional fault tolerance", failureDomain.Name, prevName))) } else { fdTopologies[topoKey] = failureDomain.Name } @@ -350,10 +351,30 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl return allErrs } -// vsphereFailureDomainTopologyKey builds a comparable key from the infrastructure-defining -// fields of a failure domain topology. Inventory paths are canonicalized with filepath.Clean -// so that syntactically different but equivalent paths (e.g. trailing slashes) are normalized. -func vsphereFailureDomainTopologyKey(fd vsphere.FailureDomain) string { +// vsphereTopologyKey is a comparable struct representing the physical infrastructure +// fields of a failure domain. Used as a map key to detect duplicate topologies. +// +// Included fields (define physical placement): +// - server, datacenter, computeCluster, datastore, networks, resourcePool, hostGroup +// +// Excluded fields (identity/metadata, not infrastructure): +// - Name, Region, Zone, RegionType, ZoneType (scheduling labels) +// - Folder, Template, TagIDs (VM metadata, not fault-zone identity) +type vsphereTopologyKey struct { + server string + datacenter string + computeCluster string + datastore string + networks string + resourcePool string + hostGroup string +} + +// normalizedTopologyKey builds a comparable struct key from a failure domain. +// Inventory paths are canonicalized with path.Clean (vSphere paths are URL-style, +// always use forward slashes). Networks are sorted and joined with \x00 to avoid +// ambiguity from names containing commas. +func normalizedTopologyKey(fd vsphere.FailureDomain) vsphereTopologyKey { networks := make([]string, len(fd.Topology.Networks)) copy(networks, fd.Topology.Networks) sort.Strings(networks) @@ -362,16 +383,18 @@ func vsphereFailureDomainTopologyKey(fd vsphere.FailureDomain) string { if p == "" { return "" } - return filepath.Clean(p) + return path.Clean(p) } - return fmt.Sprintf("server=%s;dc=%s;cluster=%s;ds=%s;nets=%s;rp=%s", - fd.Server, - fd.Topology.Datacenter, - normalizePath(fd.Topology.ComputeCluster), - normalizePath(fd.Topology.Datastore), - strings.Join(networks, ","), - normalizePath(fd.Topology.ResourcePool)) + return vsphereTopologyKey{ + server: fd.Server, + datacenter: fd.Topology.Datacenter, + computeCluster: normalizePath(fd.Topology.ComputeCluster), + datastore: normalizePath(fd.Topology.Datastore), + networks: strings.Join(networks, "\x00"), + resourcePool: normalizePath(fd.Topology.ResourcePool), + hostGroup: fd.Topology.HostGroup, + } } // validateDiskType checks that the specified diskType is valid. diff --git a/pkg/types/vsphere/validation/platform_test.go b/pkg/types/vsphere/validation/platform_test.go index 767977d59c..9957716bfd 100644 --- a/pkg/types/vsphere/validation/platform_test.go +++ b/pkg/types/vsphere/validation/platform_test.go @@ -562,6 +562,23 @@ func TestValidatePlatform(t *testing.T) { }(), expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, }, + { + name: "Valid HostGroup failure domains with same topology but different HostGroups", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].RegionType = vsphere.ComputeClusterFailureDomain + p.FailureDomains[0].ZoneType = vsphere.HostGroupFailureDomain + p.FailureDomains[0].Topology.HostGroup = "host-group-a" + p.FailureDomains[0].Topology.ResourcePool = "/test-datacenter/host/test-cluster/Resources/test-resourcepool" + + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].RegionType = vsphere.ComputeClusterFailureDomain + p.FailureDomains[1].ZoneType = vsphere.HostGroupFailureDomain + p.FailureDomains[1].Topology.HostGroup = "host-group-b" + return p + }(), + }, { name: "Multi-zone platform failureDomain zone missing tag category", platform: func() *vsphere.Platform { From 8657c8c15b4667d13907def6d6ab63b8e9cd5a87 Mon Sep 17 00:00:00 2001 From: Chin2691 Date: Wed, 3 Jun 2026 17:33:35 +0530 Subject: [PATCH 3/3] fix: use order-sensitive slice comparison for network dedup Addresses reviewer feedback from @vr4manta and @jcpowermac: - Networks are now compared positionally (order matters per +listType=atomic) instead of sorting and joining into a string key. Two failure domains with the same networks in different order are now correctly treated as distinct topologies. - Replaced filepath.Clean with path.Clean for vSphere inventory paths, which use forward slashes regardless of OS (govmomi convention). - The topology key struct no longer contains a networks field; instead a separate networksEqual() helper performs the slice comparison after the scalar key matches. Test matrix covers: multi-net same/different order, subset lengths, path normalization (trailing slash, double slash), HostGroup zonal same/different hostGroup, and differentiation by all scalar fields. Co-authored-by: Cursor --- pkg/types/vsphere/validation/platform.go | 50 ++++---- pkg/types/vsphere/validation/platform_test.go | 112 ++++++++++++++++++ 2 files changed, 139 insertions(+), 23 deletions(-) diff --git a/pkg/types/vsphere/validation/platform.go b/pkg/types/vsphere/validation/platform.go index 94cf4c6093..786559cbe9 100644 --- a/pkg/types/vsphere/validation/platform.go +++ b/pkg/types/vsphere/validation/platform.go @@ -4,9 +4,7 @@ import ( "fmt" "net" "path" - "path/filepath" "regexp" - "sort" "strings" "github.com/sirupsen/logrus" @@ -165,7 +163,11 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl var associatedVCenter *vsphere.VCenter zoneNames := make(map[string]string) - fdTopologies := make(map[vsphereTopologyKey]string) + type topologyEntry struct { + name string + networks []string + } + fdTopologies := make(map[vsphereTopologyKey][]topologyEntry) for index, failureDomain := range p.FailureDomains { if regionName, ok := zoneNames[failureDomain.Zone]; !ok { @@ -175,11 +177,17 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl } topoKey := normalizedTopologyKey(failureDomain) - if prevName, exists := fdTopologies[topoKey]; exists { - allErrs = append(allErrs, field.Invalid(fldPath.Index(index), failureDomain.Name, - fmt.Sprintf("failure domain %q has identical topology (same server, datacenter, computeCluster, datastore, networks, resourcePool, hostGroup) as %q; this provides no additional fault tolerance", failureDomain.Name, prevName))) - } else { - fdTopologies[topoKey] = failureDomain.Name + duplicate := false + for _, entry := range fdTopologies[topoKey] { + if slices.Equal(failureDomain.Topology.Networks, entry.networks) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(index), failureDomain.Name, + fmt.Sprintf("failure domain %q has identical topology (same server, datacenter, computeCluster, datastore, networks, resourcePool, hostGroup) as %q; this provides no additional fault tolerance", failureDomain.Name, entry.name))) + duplicate = true + break + } + } + if !duplicate { + fdTopologies[topoKey] = append(fdTopologies[topoKey], topologyEntry{name: failureDomain.Name, networks: failureDomain.Topology.Networks}) } if failureDomain.ZoneType == "" && failureDomain.RegionType == "" { @@ -260,7 +268,7 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl if !strings.Contains(failureDomain.Topology.Datastore, failureDomain.Topology.Datacenter) { return append(allErrs, field.Invalid(topologyFld.Child("datastore"), failureDomain.Topology.Datastore, "the datastore defined does not exist in the correct datacenter")) } - p.FailureDomains[index].Topology.Datastore = filepath.Clean(p.FailureDomains[index].Topology.Datastore) + p.FailureDomains[index].Topology.Datastore = path.Clean(p.FailureDomains[index].Topology.Datastore) } if len(failureDomain.Topology.TagIDs) > 10 { @@ -315,7 +323,7 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl if len(failureDomain.Topology.Datacenter) != 0 && !strings.Contains(failureDomain.Topology.Datacenter, datacenterName) { return append(allErrs, field.Invalid(topologyFld.Child("computeCluster"), computeCluster, fmt.Sprintf("compute cluster must be in datacenter %s", failureDomain.Topology.Datacenter))) } - p.FailureDomains[index].Topology.ComputeCluster = filepath.Clean(p.FailureDomains[index].Topology.ComputeCluster) + p.FailureDomains[index].Topology.ComputeCluster = path.Clean(p.FailureDomains[index].Topology.ComputeCluster) } if len(failureDomain.Topology.ResourcePool) != 0 { @@ -334,7 +342,7 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl return append(allErrs, field.Invalid(topologyFld.Child("resourcePool"), resourcePool, fmt.Sprintf("resource pool must be in compute cluster %s", failureDomain.Topology.ComputeCluster))) } - p.FailureDomains[index].Topology.ResourcePool = filepath.Clean(p.FailureDomains[index].Topology.ResourcePool) + p.FailureDomains[index].Topology.ResourcePool = path.Clean(p.FailureDomains[index].Topology.ResourcePool) } // Validate that template and clusterOSImage are mutually exclusive @@ -344,41 +352,38 @@ func validateFailureDomains(p *vsphere.Platform, platformFldPath *field.Path, fl } if len(failureDomain.Topology.Template) > 0 { - p.FailureDomains[index].Topology.Template = filepath.Clean(p.FailureDomains[index].Topology.Template) + p.FailureDomains[index].Topology.Template = path.Clean(p.FailureDomains[index].Topology.Template) } } return allErrs } -// vsphereTopologyKey is a comparable struct representing the physical infrastructure -// fields of a failure domain. Used as a map key to detect duplicate topologies. +// vsphereTopologyKey is a comparable struct representing the scalar physical +// infrastructure fields of a failure domain. Used as a map key for first-pass +// dedup. Networks are compared separately (order-sensitive slice comparison) +// because Go map keys cannot contain slices. // // Included fields (define physical placement): -// - server, datacenter, computeCluster, datastore, networks, resourcePool, hostGroup +// - server, datacenter, computeCluster, datastore, resourcePool, hostGroup // // Excluded fields (identity/metadata, not infrastructure): // - Name, Region, Zone, RegionType, ZoneType (scheduling labels) // - Folder, Template, TagIDs (VM metadata, not fault-zone identity) +// - Networks (compared separately via slices.Equal, order matters) type vsphereTopologyKey struct { server string datacenter string computeCluster string datastore string - networks string resourcePool string hostGroup string } // normalizedTopologyKey builds a comparable struct key from a failure domain. // Inventory paths are canonicalized with path.Clean (vSphere paths are URL-style, -// always use forward slashes). Networks are sorted and joined with \x00 to avoid -// ambiguity from names containing commas. +// always use forward slashes). func normalizedTopologyKey(fd vsphere.FailureDomain) vsphereTopologyKey { - networks := make([]string, len(fd.Topology.Networks)) - copy(networks, fd.Topology.Networks) - sort.Strings(networks) - normalizePath := func(p string) string { if p == "" { return "" @@ -391,7 +396,6 @@ func normalizedTopologyKey(fd vsphere.FailureDomain) vsphereTopologyKey { datacenter: fd.Topology.Datacenter, computeCluster: normalizePath(fd.Topology.ComputeCluster), datastore: normalizePath(fd.Topology.Datastore), - networks: strings.Join(networks, "\x00"), resourcePool: normalizePath(fd.Topology.ResourcePool), hostGroup: fd.Topology.HostGroup, } diff --git a/pkg/types/vsphere/validation/platform_test.go b/pkg/types/vsphere/validation/platform_test.go index 9957716bfd..00fbcb4892 100644 --- a/pkg/types/vsphere/validation/platform_test.go +++ b/pkg/types/vsphere/validation/platform_test.go @@ -579,6 +579,118 @@ func TestValidatePlatform(t *testing.T) { return p }(), }, + { + name: "Duplicate topology with same networks same order (multi-net)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].Topology.Networks = []string{"net-a", "net-b"} + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Server = p.FailureDomains[0].Server + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, + }, + { + name: "Valid: same networks different order (multi-net, order matters)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].Topology.Networks = []string{"net-a", "net-b"} + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.Networks = []string{"net-b", "net-a"} + return p + }(), + }, + { + name: "Valid: different networks (multi-net)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].Topology.Networks = []string{"net-a", "net-b"} + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.Networks = []string{"net-a", "net-c"} + return p + }(), + }, + { + name: "Valid: subset networks (different length)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].Topology.Networks = []string{"net-a"} + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.Networks = []string{"net-a", "net-b"} + return p + }(), + }, + { + name: "Duplicate topology with path normalization (trailing slash)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.ComputeCluster = "/test-datacenter/host/test-cluster/" + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, + }, + { + name: "Duplicate topology with path normalization (double slash)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.Datastore = "/test-datacenter/datastore//test-datastore" + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, + }, + { + name: "Valid: different datastore (not duplicate)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.Datastore = "/test-datacenter/datastore/other-datastore" + return p + }(), + }, + { + name: "Valid: different server (not duplicate)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Server = "other-vcenter" + return p + }(), + }, + { + name: "Duplicate topology HostGroup zonal same hostGroup", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[0].RegionType = vsphere.ComputeClusterFailureDomain + p.FailureDomains[0].ZoneType = vsphere.HostGroupFailureDomain + p.FailureDomains[0].Topology.HostGroup = "host-group-a" + p.FailureDomains[0].Topology.ResourcePool = "/test-datacenter/host/test-cluster/Resources/test-resourcepool" + + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].RegionType = vsphere.ComputeClusterFailureDomain + p.FailureDomains[1].ZoneType = vsphere.HostGroupFailureDomain + return p + }(), + expectedError: `test-path\.failureDomains\[1\]: Invalid value: "test-east-2a": failure domain "test-east-2a" has identical topology .* as "test-east-1a"; this provides no additional fault tolerance`, + }, + { + name: "Valid: different resourcePool (not duplicate)", + platform: func() *vsphere.Platform { + p := validPlatform() + p.FailureDomains[1].Server = p.FailureDomains[0].Server + p.FailureDomains[1].Topology = p.FailureDomains[0].Topology + p.FailureDomains[1].Topology.ResourcePool = "/test-datacenter/host/test-cluster/Resources/other-pool" + return p + }(), + }, { name: "Multi-zone platform failureDomain zone missing tag category", platform: func() *vsphere.Platform {