From db146f42a9a7265681f9aef13cd6b994406fbd3e Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 19 Jan 2026 22:11:19 -0800 Subject: [PATCH 1/2] CORS-4244: adjust validations to allow dual-stack CIDRs on AWS Adjust the static validation to allow defining both IPv4 and IPv6 CIDR entries for clusterNetwork, serviceNetwork and machineNetwork. Note: for AWS, the IPv6 machineNetwork CIDR may not be available in advance. For example, if the installer creates the VPC, the IPv6 CIDR by default is allocated automatically by AWS. --- pkg/types/validation/installconfig.go | 101 ++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/pkg/types/validation/installconfig.go b/pkg/types/validation/installconfig.go index 1d9af71289..54678380ef 100644 --- a/pkg/types/validation/installconfig.go +++ b/pkg/types/validation/installconfig.go @@ -39,6 +39,7 @@ import ( gcpvalidation "github.com/openshift/installer/pkg/types/gcp/validation" "github.com/openshift/installer/pkg/types/ibmcloud" ibmcloudvalidation "github.com/openshift/installer/pkg/types/ibmcloud/validation" + "github.com/openshift/installer/pkg/types/network" "github.com/openshift/installer/pkg/types/nutanix" nutanixvalidation "github.com/openshift/installer/pkg/types/nutanix/validation" "github.com/openshift/installer/pkg/types/openstack" @@ -277,6 +278,15 @@ func ValidateInstallConfig(c *types.InstallConfig, usingAgentMethod bool) field. return allErrs } +const ( + // machine represents the machineNetwork (IP address pools for machines). + networkTypeMachine = "machineNetwork" + // service represents the serviceNetwork (IP address pools for services). + networkTypeService = "serviceNetwork" + // cluster represents the clusterNetwork (IP address pools for pods). + networkTypeCluster = "clusterNetwork" +) + // ipAddressType indicates the address types provided for a given field type ipAddressType struct { IPv4 bool @@ -299,13 +309,13 @@ func inferIPVersionFromInstallConfig(n *types.Networking) (hasIPv4, hasIPv6 bool } addresses = make(ipNetByField) for _, network := range n.MachineNetwork { - addresses["machineNetwork"] = append(addresses["machineNetwork"], network.CIDR) + addresses[networkTypeMachine] = append(addresses[networkTypeMachine], network.CIDR) } for _, network := range n.ServiceNetwork { - addresses["serviceNetwork"] = append(addresses["serviceNetwork"], network) + addresses[networkTypeService] = append(addresses[networkTypeService], network) } for _, network := range n.ClusterNetwork { - addresses["clusterNetwork"] = append(addresses["clusterNetwork"], network.CIDR) + addresses[networkTypeCluster] = append(addresses[networkTypeCluster], network.CIDR) } presence = make(ipAddressTypeByField) for k, ipnets := range addresses { @@ -316,7 +326,7 @@ func inferIPVersionFromInstallConfig(n *types.Networking) (hasIPv4, hasIPv6 bool if i == 0 { has.Primary = corev1.IPv4Protocol } - if k == "serviceNetwork" { + if k == networkTypeService { hasIPv4 = true } } else { @@ -324,7 +334,7 @@ func inferIPVersionFromInstallConfig(n *types.Networking) (hasIPv4, hasIPv6 bool if i == 0 { has.Primary = corev1.IPv6Protocol } - if k == "serviceNetwork" { + if k == networkTypeService { hasIPv6 = true } } @@ -375,23 +385,35 @@ func validateNetworkingIPVersion(n *types.Networking, p *types.Platform) field.E allowV6Primary = true case p.External != nil: allowV6Primary = true + case p.AWS != nil: + // Dualstack is only allowed if platform.aws.ipFamily is set to dual-stack variants + if ipFamily := p.AWS.IPFamily; ipFamily.DualStackEnabled() { + if ipFamily == network.DualStackIPv6Primary { + allowV6Primary = true + } + break + } + allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "DualStack", fmt.Sprintf("dual-stack IPv4/IPv6 can only be specified when platform.aws.ipFamily is %s or %s", network.DualStackIPv4Primary, network.DualStackIPv6Primary))) default: allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "DualStack", "dual-stack IPv4/IPv6 is not supported for this platform, specify only one type of address")) } - for k, v := range presence { + + for _, k := range sortedPresenceKeys(presence) { + v := presence[k] + // Validate that each network type (machineNetwork, serviceNetwork, clusterNetwork) has both IPv4 and IPv6 CIDRs switch { case v.IPv4 && !v.IPv6: + // On AWS, users may not be able to specify an IPv6 machineNetwork in advance. + // If the installer creates the VPC, IPv6 CIDR by default is automatically assigned by AWS. + if k == networkTypeMachine && p.AWS != nil { + break + } allErrs = append(allErrs, field.Invalid(field.NewPath("networking", k), strings.Join(ipnetworksToStrings(addresses[k]), ", "), "dual-stack IPv4/IPv6 requires an IPv6 network in this list")) case !v.IPv4 && v.IPv6: allErrs = append(allErrs, field.Invalid(field.NewPath("networking", k), strings.Join(ipnetworksToStrings(addresses[k]), ", "), "dual-stack IPv4/IPv6 requires an IPv4 network in this list")) } - // FIXME: we should allow either all-networks-IPv4Primary or - // all-networks-IPv6Primary, but the latter currently causes - // confusing install failures, so block it. - if !allowV6Primary && v.IPv4 && v.IPv6 && v.Primary != corev1.IPv4Protocol { - allErrs = append(allErrs, field.Invalid(field.NewPath("networking", k), strings.Join(ipnetworksToStrings(addresses[k]), ", "), "IPv4 addresses must be listed before IPv6 addresses")) - } + allErrs = append(allErrs, validateNetworkEntryOrder(p, v, addresses[k], allowV6Primary, field.NewPath("networking", k))...) } case hasIPv6: @@ -403,6 +425,13 @@ func validateNetworkingIPVersion(n *types.Networking, p *types.Platform) field.E case p.Nutanix != nil: case p.None != nil: case p.External != nil: + case p.AWS != nil: + // If dual-stack is enabled, there must be both IPv4 and IPv6 service CIDRs + if p.AWS.IPFamily.DualStackEnabled() { + allErrs = append(allErrs, field.Invalid(field.NewPath("networking", "serviceNetwork"), strings.Join(ipnetworksToStrings(n.ServiceNetwork), ", "), "when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type")) + break + } + fallthrough case p.Azure != nil && p.Azure.CloudName == azure.StackCloud: allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "IPv6", "Azure Stack does not support IPv6")) default: @@ -410,8 +439,18 @@ func validateNetworkingIPVersion(n *types.Networking, p *types.Platform) field.E } case hasIPv4: - if len(n.ServiceNetwork) > 1 { - allErrs = append(allErrs, field.Invalid(field.NewPath("networking", "serviceNetwork"), strings.Join(ipnetworksToStrings(n.ServiceNetwork), ", "), "only one service network can be specified")) + switch { + case p.AWS != nil: + // If dual-stack is enabled, there must be both IPv4 and IPv6 service CIDRs + if p.AWS.IPFamily.DualStackEnabled() { + allErrs = append(allErrs, field.Invalid(field.NewPath("networking", "serviceNetwork"), strings.Join(ipnetworksToStrings(n.ServiceNetwork), ", "), "when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type")) + break + } + fallthrough + default: + if len(n.ServiceNetwork) > 1 { + allErrs = append(allErrs, field.Invalid(field.NewPath("networking", "serviceNetwork"), strings.Join(ipnetworksToStrings(n.ServiceNetwork), ", "), "only one service network can be specified")) + } } default: @@ -421,6 +460,40 @@ func validateNetworkingIPVersion(n *types.Networking, p *types.Platform) field.E return allErrs } +// validateNetworkEntryOrder ensures the order of CIDR entries is correct in networking configurations. +// - IPv4 primary dual-stack: IPv4 CIDR first in list +// - IPv6 primary dual-stack: IPv6 CIDR first in list +// Some platforms have an explicit field to define the dual-stack variant, for example, platform.aws.ipFamily on AWS. +func validateNetworkEntryOrder(p *types.Platform, ipAddressType ipAddressType, networks []ipnet.IPNet, allowV6Primary bool, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // If missing either IPv4 or IPv6 CIDR, order validation is not applicable + // There is an existing validation to ensure both IPv4 and IPv6 CIDRs are available in dual-stack + if !ipAddressType.IPv4 || !ipAddressType.IPv6 { + return allErrs + } + + switch { + case p.AWS != nil: + ipFamily := p.AWS.IPFamily + + if ipFamily == network.DualStackIPv4Primary && ipAddressType.Primary == corev1.IPv6Protocol { + allErrs = append(allErrs, field.Invalid(fldPath, strings.Join(ipnetworksToStrings(networks), ", "), "DualStackIPv4Primary requires an IPv4 network first in this list")) + } + + if ipFamily == network.DualStackIPv6Primary && ipAddressType.Primary == corev1.IPv4Protocol { + allErrs = append(allErrs, field.Invalid(fldPath, strings.Join(ipnetworksToStrings(networks), ", "), "DualStackIPv6Primary requires an IPv6 network first in this list")) + } + default: + // For platforms that don't support IPv6-primary dual-stack, reject configurations with IPv6 CIDRs listed first. + if !allowV6Primary && ipAddressType.Primary != corev1.IPv4Protocol { + allErrs = append(allErrs, field.Invalid(fldPath, strings.Join(ipnetworksToStrings(networks), ", "), "IPv4 addresses must be listed before IPv6 addresses")) + } + } + + return allErrs +} + func validateNetworking(n *types.Networking, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} From 6f91c00a38323dde25daebb6b2f25e3ba171af50 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 19 Jan 2026 22:36:01 -0800 Subject: [PATCH 2/2] tests: add validation unit tests for dual-stack CIDR entries on AWS --- pkg/types/validation/installconfig_test.go | 134 ++++++++++++++++++++- 1 file changed, 130 insertions(+), 4 deletions(-) diff --git a/pkg/types/validation/installconfig_test.go b/pkg/types/validation/installconfig_test.go index 8bcca22985..87e31e2196 100644 --- a/pkg/types/validation/installconfig_test.go +++ b/pkg/types/validation/installconfig_test.go @@ -22,6 +22,7 @@ import ( "github.com/openshift/installer/pkg/types/external" "github.com/openshift/installer/pkg/types/gcp" "github.com/openshift/installer/pkg/types/ibmcloud" + "github.com/openshift/installer/pkg/types/network" "github.com/openshift/installer/pkg/types/none" "github.com/openshift/installer/pkg/types/nutanix" "github.com/openshift/installer/pkg/types/openstack" @@ -283,7 +284,7 @@ func validDualStackNetworkingConfig() *types.Networking { }, } } -func InvalidPrimaryV6DualStackNetworkingConfig() *types.Networking { +func validPrimaryV6DualStackNetworkingConfig() *types.Networking { return &types.Networking{ NetworkType: "OVNKubernetes", MachineNetwork: []types.MachineNetworkEntry{ @@ -295,8 +296,8 @@ func InvalidPrimaryV6DualStackNetworkingConfig() *types.Networking { }, }, ServiceNetwork: []ipnet.IPNet{ - *ipnet.MustParseCIDR("172.30.0.0/16"), *ipnet.MustParseCIDR("ffd1::/112"), + *ipnet.MustParseCIDR("172.30.0.0/16"), }, ClusterNetwork: []types.ClusterNetworkEntry{ { @@ -1730,6 +1731,121 @@ func TestValidateInstallConfig(t *testing.T) { return c }(), }, + { + name: "aws: valid dual-stack with DualStackIPv4Primary", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv4Primary + c.Networking = validDualStackNetworkingConfig() + return c + }(), + }, + { + name: "aws: valid AWS dual-stack with DualStackIPv6Primary", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv6Primary + c.Networking = validPrimaryV6DualStackNetworkingConfig() + return c + }(), + }, + { + name: "aws: valid dual-stack with DualStackIPv4Primary and only IPv4 machineNetwork (IPv6 auto-assigned)", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv4Primary + c.Networking = validDualStackNetworkingConfig() + c.Networking.MachineNetwork = c.Networking.MachineNetwork[:1] + return c + }(), + }, + { + name: "aws: invalid AWS dual-stack without ipFamily set", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + // IPFamily not set on AWS platform + c.Networking = validDualStackNetworkingConfig() + return c + }(), + expectedError: `networking: Invalid value: "DualStack": dual-stack IPv4/IPv6 can only be specified when platform.aws.ipFamily is DualStackIPv4Primary or DualStackIPv6Primary`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv4Primary but IPv6-primary networks", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = "DualStackIPv4Primary" + c.Networking = validPrimaryV6DualStackNetworkingConfig() + return c + }(), + expectedError: `^\Q[networking.clusterNetwork: Invalid value: "ffd2::/48, 192.168.1.0/24": DualStackIPv4Primary requires an IPv4 network first in this list, networking.machineNetwork: Invalid value: "ffd0::/48, 10.0.0.0/16": DualStackIPv4Primary requires an IPv4 network first in this list, networking.serviceNetwork: Invalid value: "ffd1::/112, 172.30.0.0/16": DualStackIPv4Primary requires an IPv4 network first in this list]\E$`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv6Primary but IPv4-primary networks", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = "DualStackIPv6Primary" + c.Networking = validDualStackNetworkingConfig() + return c + }(), + expectedError: `^\Q[networking.clusterNetwork: Invalid value: "192.168.1.0/24, ffd2::/48": DualStackIPv6Primary requires an IPv6 network first in this list, networking.machineNetwork: Invalid value: "10.0.0.0/16, ffd0::/48": DualStackIPv6Primary requires an IPv6 network first in this list, networking.serviceNetwork: Invalid value: "172.30.0.0/16, ffd1::/112": DualStackIPv6Primary requires an IPv6 network first in this list]\E$`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv4Primary but only IPv4 serviceNetwork", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv4Primary + c.Networking = validDualStackNetworkingConfig() + // Remove IPv6 service network, leaving only IPv4 + c.Networking.ServiceNetwork = c.Networking.ServiceNetwork[:1] + return c + }(), + expectedError: `networking.serviceNetwork: Invalid value: "172.30.0.0/16": when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv4Primary but only IPv6 serviceNetwork", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv4Primary + c.Networking = validDualStackNetworkingConfig() + // Remove IPv4 service network, leaving only IPv6 + c.Networking.ServiceNetwork = c.Networking.ServiceNetwork[1:] + return c + }(), + expectedError: `networking.serviceNetwork: Invalid value: "ffd1::/112": when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv6Primary but only IPv4 serviceNetwork", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv6Primary + c.Networking = validPrimaryV6DualStackNetworkingConfig() + // Remove IPv6 service network, leaving only IPv4 + c.Networking.ServiceNetwork = c.Networking.ServiceNetwork[1:] + return c + }(), + expectedError: `networking.serviceNetwork: Invalid value: "172.30.0.0/16": when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type`, + }, + { + name: "aws: invalid dual-stack with DualStackIPv6Primary but only IPv6 serviceNetwork", + installConfig: func() *types.InstallConfig { + c := validInstallConfig() + c.FeatureSet = configv1.TechPreviewNoUpgrade + c.Platform.AWS.IPFamily = network.DualStackIPv6Primary + c.Networking = validPrimaryV6DualStackNetworkingConfig() + // Remove IPv4 service network, leaving only IPv6 + c.Networking.ServiceNetwork = c.Networking.ServiceNetwork[:1] + return c + }(), + expectedError: `networking.serviceNetwork: Invalid value: "ffd1::/112": when installing dual-stack IPv4/IPv6 you must provide two service networks, one for each IP address type`, + }, { name: "invalid IPv6 hostprefix", installConfig: func() *types.InstallConfig { @@ -2383,7 +2499,12 @@ func TestValidateInstallConfig(t *testing.T) { name: "baremetal API VIP set to an incorrect IP Family with invalid primary IPv6 network", installConfig: func() *types.InstallConfig { c := validInstallConfig() - c.Networking = InvalidPrimaryV6DualStackNetworkingConfig() + c.Networking = validPrimaryV6DualStackNetworkingConfig() + // Make service network IPv6-primary (wrong order) + c.Networking.ServiceNetwork = []ipnet.IPNet{ + c.Networking.ServiceNetwork[1], + c.Networking.ServiceNetwork[0], + } c.Platform = types.Platform{ BareMetal: validBareMetalPlatform(), } @@ -2409,7 +2530,12 @@ func TestValidateInstallConfig(t *testing.T) { name: "baremetal Ingress VIP set to an incorrect IP Family with invalid primary IPv6 network", installConfig: func() *types.InstallConfig { c := validInstallConfig() - c.Networking = InvalidPrimaryV6DualStackNetworkingConfig() + c.Networking = validPrimaryV6DualStackNetworkingConfig() + // Make service network IPv6-primary (wrong order) + c.Networking.ServiceNetwork = []ipnet.IPNet{ + c.Networking.ServiceNetwork[1], + c.Networking.ServiceNetwork[0], + } c.Platform = types.Platform{ BareMetal: validBareMetalPlatform(), }