Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 87 additions & 14 deletions pkg/types/validation/installconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -316,15 +326,15 @@ func inferIPVersionFromInstallConfig(n *types.Networking) (hasIPv4, hasIPv6 bool
if i == 0 {
has.Primary = corev1.IPv4Protocol
}
if k == "serviceNetwork" {
if k == networkTypeService {
hasIPv4 = true
}
} else {
has.IPv6 = true
if i == 0 {
has.Primary = corev1.IPv6Protocol
}
if k == "serviceNetwork" {
if k == networkTypeService {
hasIPv6 = true
}
}
Expand Down Expand Up @@ -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:
Expand All @@ -403,15 +425,32 @@ 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:
allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "IPv6", "single-stack IPv6 is not supported for this platform"))
}

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:
Expand All @@ -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{}

Expand Down
134 changes: 130 additions & 4 deletions pkg/types/validation/installconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -283,7 +284,7 @@ func validDualStackNetworkingConfig() *types.Networking {
},
}
}
func InvalidPrimaryV6DualStackNetworkingConfig() *types.Networking {
func validPrimaryV6DualStackNetworkingConfig() *types.Networking {
return &types.Networking{
NetworkType: "OVNKubernetes",
MachineNetwork: []types.MachineNetworkEntry{
Expand All @@ -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{
{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
}
Expand All @@ -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(),
}
Expand Down