github.com/openshift/installer@v1.4.17/pkg/types/validation/installconfig.go (about)

     1  package validation
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/url"
     7  	"os"
     8  	"regexp"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  
    13  	dockerref "github.com/containers/image/v5/docker/reference"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  	"golang.org/x/crypto/ssh"
    17  	corev1 "k8s.io/api/core/v1"
    18  	"k8s.io/apimachinery/pkg/util/sets"
    19  	"k8s.io/apimachinery/pkg/util/validation/field"
    20  	utilsnet "k8s.io/utils/net"
    21  
    22  	configv1 "github.com/openshift/api/config/v1"
    23  	"github.com/openshift/api/features"
    24  	operv1 "github.com/openshift/api/operator/v1"
    25  	"github.com/openshift/installer/pkg/hostcrypt"
    26  	"github.com/openshift/installer/pkg/ipnet"
    27  	"github.com/openshift/installer/pkg/types"
    28  	"github.com/openshift/installer/pkg/types/aws"
    29  	awsvalidation "github.com/openshift/installer/pkg/types/aws/validation"
    30  	"github.com/openshift/installer/pkg/types/azure"
    31  	azurevalidation "github.com/openshift/installer/pkg/types/azure/validation"
    32  	"github.com/openshift/installer/pkg/types/baremetal"
    33  	baremetalvalidation "github.com/openshift/installer/pkg/types/baremetal/validation"
    34  	"github.com/openshift/installer/pkg/types/external"
    35  	"github.com/openshift/installer/pkg/types/featuregates"
    36  	"github.com/openshift/installer/pkg/types/gcp"
    37  	gcpvalidation "github.com/openshift/installer/pkg/types/gcp/validation"
    38  	"github.com/openshift/installer/pkg/types/ibmcloud"
    39  	ibmcloudvalidation "github.com/openshift/installer/pkg/types/ibmcloud/validation"
    40  	"github.com/openshift/installer/pkg/types/nutanix"
    41  	nutanixvalidation "github.com/openshift/installer/pkg/types/nutanix/validation"
    42  	"github.com/openshift/installer/pkg/types/openstack"
    43  	openstackvalidation "github.com/openshift/installer/pkg/types/openstack/validation"
    44  	"github.com/openshift/installer/pkg/types/ovirt"
    45  	ovirtvalidation "github.com/openshift/installer/pkg/types/ovirt/validation"
    46  	"github.com/openshift/installer/pkg/types/powervs"
    47  	powervsvalidation "github.com/openshift/installer/pkg/types/powervs/validation"
    48  	"github.com/openshift/installer/pkg/types/vsphere"
    49  	vspherevalidation "github.com/openshift/installer/pkg/types/vsphere/validation"
    50  	"github.com/openshift/installer/pkg/validate"
    51  	"github.com/openshift/installer/pkg/version"
    52  )
    53  
    54  // hostCryptBypassedAnnotation is set if the host crypt check was bypassed via environment variable.
    55  const hostCryptBypassedAnnotation = "install.openshift.io/hostcrypt-check-bypassed"
    56  
    57  // list of known plugins that require hostPrefix to be set
    58  var pluginsUsingHostPrefix = sets.NewString(string(operv1.NetworkTypeOVNKubernetes))
    59  
    60  // ValidateInstallConfig checks that the specified install config is valid.
    61  //
    62  //nolint:gocyclo
    63  func ValidateInstallConfig(c *types.InstallConfig, usingAgentMethod bool) field.ErrorList {
    64  	allErrs := field.ErrorList{}
    65  	if c.TypeMeta.APIVersion == "" {
    66  		return field.ErrorList{field.Required(field.NewPath("apiVersion"), "install-config version required")}
    67  	}
    68  	switch v := c.APIVersion; v {
    69  	case types.InstallConfigVersion:
    70  		// Current version
    71  	default:
    72  		return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), c.TypeMeta.APIVersion, fmt.Sprintf("install-config version must be %q", types.InstallConfigVersion))}
    73  	}
    74  
    75  	if c.FIPS {
    76  		allErrs = append(allErrs, validateFIPSconfig(c)...)
    77  	} else if c.SSHKey != "" {
    78  		if err := validate.SSHPublicKey(c.SSHKey); err != nil {
    79  			allErrs = append(allErrs, field.Invalid(field.NewPath("sshKey"), c.SSHKey, err.Error()))
    80  		}
    81  	}
    82  
    83  	if c.AdditionalTrustBundle != "" {
    84  		if err := validate.CABundle(c.AdditionalTrustBundle); err != nil {
    85  			allErrs = append(allErrs, field.Invalid(field.NewPath("additionalTrustBundle"), c.AdditionalTrustBundle, err.Error()))
    86  		}
    87  	}
    88  	if c.AdditionalTrustBundlePolicy != "" {
    89  		if err := validateAdditionalCABundlePolicy(c); err != nil {
    90  			allErrs = append(allErrs, field.Invalid(field.NewPath("additionalTrustBundlePolicy"), c.AdditionalTrustBundlePolicy, err.Error()))
    91  		}
    92  	}
    93  	nameErr := validate.ClusterName(c.ObjectMeta.Name)
    94  	if c.Platform.GCP != nil || c.Platform.Azure != nil {
    95  		nameErr = validate.ClusterName1035(c.ObjectMeta.Name)
    96  	}
    97  	if c.Platform.VSphere != nil || c.Platform.BareMetal != nil || c.Platform.OpenStack != nil || c.Platform.Nutanix != nil {
    98  		nameErr = validate.OnPremClusterName(c.ObjectMeta.Name)
    99  	}
   100  	if nameErr != nil {
   101  		allErrs = append(allErrs, field.Invalid(field.NewPath("metadata", "name"), c.ObjectMeta.Name, nameErr.Error()))
   102  	}
   103  	baseDomainErr := validate.DomainName(c.BaseDomain, true)
   104  	if baseDomainErr != nil {
   105  		allErrs = append(allErrs, field.Invalid(field.NewPath("baseDomain"), c.BaseDomain, baseDomainErr.Error()))
   106  	}
   107  	if nameErr == nil && baseDomainErr == nil {
   108  		clusterDomain := c.ClusterDomain()
   109  		if err := validate.DomainName(clusterDomain, true); err != nil {
   110  			allErrs = append(allErrs, field.Invalid(field.NewPath("baseDomain"), clusterDomain, err.Error()))
   111  		}
   112  	}
   113  	if c.Networking != nil {
   114  		allErrs = append(allErrs, validateNetworking(c.Networking, c.IsSingleNodeOpenShift(), field.NewPath("networking"))...)
   115  		allErrs = append(allErrs, validateNetworkingIPVersion(c.Networking, &c.Platform)...)
   116  		allErrs = append(allErrs, validateNetworkingForPlatform(c.Networking, &c.Platform, field.NewPath("networking"))...)
   117  		allErrs = append(allErrs, validateNetworkingClusterNetworkMTU(c, field.NewPath("networking", "clusterNetworkMTU"))...)
   118  		allErrs = append(allErrs, validateVIPsForPlatform(c.Networking, &c.Platform, field.NewPath("platform"))...)
   119  	} else {
   120  		allErrs = append(allErrs, field.Required(field.NewPath("networking"), "networking is required"))
   121  	}
   122  	allErrs = append(allErrs, validatePlatform(&c.Platform, usingAgentMethod, field.NewPath("platform"), c.Networking, c)...)
   123  	if c.ControlPlane != nil {
   124  		allErrs = append(allErrs, validateControlPlane(&c.Platform, c.ControlPlane, field.NewPath("controlPlane"))...)
   125  	} else {
   126  		allErrs = append(allErrs, field.Required(field.NewPath("controlPlane"), "controlPlane is required"))
   127  	}
   128  	multiArchEnabled := types.MultiArchFeatureGateEnabled(c.Platform.Name(), c.EnabledFeatureGates())
   129  	allErrs = append(allErrs, validateCompute(&c.Platform, c.ControlPlane, c.Compute, field.NewPath("compute"), multiArchEnabled)...)
   130  	if multiArchEnabled {
   131  		allErrs = append(allErrs, validateMultiReleasePayload(c.ControlPlane, c.Compute)...)
   132  	}
   133  	if err := validate.ImagePullSecret(c.PullSecret); err != nil {
   134  		allErrs = append(allErrs, field.Invalid(field.NewPath("pullSecret"), c.PullSecret, err.Error()))
   135  	}
   136  	if c.Proxy != nil {
   137  		allErrs = append(allErrs, validateProxy(c.Proxy, c, field.NewPath("proxy"))...)
   138  	}
   139  	allErrs = append(allErrs, validateImageContentSources(c.DeprecatedImageContentSources, field.NewPath("imageContentSources"))...)
   140  	if _, ok := validPublishingStrategies[c.Publish]; !ok {
   141  		allErrs = append(allErrs, field.NotSupported(field.NewPath("publish"), c.Publish, validPublishingStrategyValues))
   142  	}
   143  	allErrs = append(allErrs, validateImageDigestSources(c.ImageDigestSources, field.NewPath("imageDigestSources"))...)
   144  	if _, ok := validPublishingStrategies[c.Publish]; !ok {
   145  		allErrs = append(allErrs, field.NotSupported(field.NewPath("publish"), c.Publish, validPublishingStrategyValues))
   146  	}
   147  	if len(c.DeprecatedImageContentSources) != 0 && len(c.ImageDigestSources) != 0 {
   148  		allErrs = append(allErrs, field.Invalid(field.NewPath("imageContentSources"), c.Publish, "cannot set imageContentSources and imageDigestSources at the same time"))
   149  	}
   150  	if len(c.DeprecatedImageContentSources) != 0 {
   151  		logrus.Warningln("imageContentSources is deprecated, please use ImageDigestSources")
   152  	}
   153  	allErrs = append(allErrs, validateCloudCredentialsMode(c.CredentialsMode, field.NewPath("credentialsMode"), c.Platform)...)
   154  	if c.Capabilities != nil {
   155  		allErrs = append(allErrs, validateCapabilities(c.Capabilities, field.NewPath("capabilities"))...)
   156  	}
   157  
   158  	if c.Publish == types.InternalPublishingStrategy {
   159  		switch platformName := c.Platform.Name(); platformName {
   160  		case aws.Name, azure.Name, gcp.Name, ibmcloud.Name, powervs.Name:
   161  		default:
   162  			allErrs = append(allErrs, field.Invalid(field.NewPath("publish"), c.Publish, fmt.Sprintf("Internal publish strategy is not supported on %q platform", platformName)))
   163  		}
   164  	}
   165  
   166  	if c.Publish == types.MixedPublishingStrategy {
   167  		switch platformName := c.Platform.Name(); platformName {
   168  		case azure.Name:
   169  		default:
   170  			allErrs = append(allErrs, field.Invalid(field.NewPath("publish"), c.Publish, fmt.Sprintf("mixed publish strategy is not supported on %q platform", platformName)))
   171  		}
   172  		if c.OperatorPublishingStrategy == nil {
   173  			allErrs = append(allErrs, field.Invalid(field.NewPath("publish"), c.Publish, "please specify the operator publishing strategy for mixed publish strategy"))
   174  		}
   175  	} else if c.OperatorPublishingStrategy != nil {
   176  		allErrs = append(allErrs, field.Invalid(field.NewPath("operatorPublishingStrategy"), c.Publish, "operator publishing strategy is only allowed with mixed publishing strategy installs"))
   177  	}
   178  
   179  	if c.OperatorPublishingStrategy != nil {
   180  		acceptedValues := sets.New[string]("Internal", "External")
   181  		if c.OperatorPublishingStrategy.APIServer == "" {
   182  			c.OperatorPublishingStrategy.APIServer = "External"
   183  		}
   184  		if c.OperatorPublishingStrategy.Ingress == "" {
   185  			c.OperatorPublishingStrategy.Ingress = "External"
   186  		}
   187  		if !acceptedValues.Has(c.OperatorPublishingStrategy.APIServer) {
   188  			allErrs = append(allErrs, field.NotSupported(field.NewPath("apiserver"), c.OperatorPublishingStrategy.APIServer, sets.List(acceptedValues)))
   189  		}
   190  		if !acceptedValues.Has(c.OperatorPublishingStrategy.Ingress) {
   191  			allErrs = append(allErrs, field.NotSupported(field.NewPath("ingress"), c.OperatorPublishingStrategy.Ingress, sets.List(acceptedValues)))
   192  		}
   193  		if c.OperatorPublishingStrategy.APIServer == "Internal" && c.OperatorPublishingStrategy.Ingress == "Internal" {
   194  			allErrs = append(allErrs, field.Invalid(field.NewPath("publish"), c.OperatorPublishingStrategy.APIServer, "cannot set both fields to internal in a mixed cluster, use publish internal instead"))
   195  		}
   196  	}
   197  
   198  	if c.Capabilities != nil {
   199  		capSet := c.Capabilities.BaselineCapabilitySet
   200  		if capSet == "" {
   201  			capSet = configv1.ClusterVersionCapabilitySetCurrent
   202  		}
   203  		enabledCaps := sets.New[configv1.ClusterVersionCapability](configv1.ClusterVersionCapabilitySets[capSet]...)
   204  		enabledCaps.Insert(c.Capabilities.AdditionalEnabledCapabilities...)
   205  
   206  		if c.Capabilities.BaselineCapabilitySet == configv1.ClusterVersionCapabilitySetNone {
   207  			enabledCaps := sets.New[configv1.ClusterVersionCapability](c.Capabilities.AdditionalEnabledCapabilities...)
   208  			if enabledCaps.Has(configv1.ClusterVersionCapabilityMarketplace) && !enabledCaps.Has(configv1.ClusterVersionCapabilityOperatorLifecycleManager) {
   209  				allErrs = append(allErrs, field.Invalid(field.NewPath("additionalEnabledCapabilities"), c.Capabilities.AdditionalEnabledCapabilities,
   210  					"the marketplace capability requires the OperatorLifecycleManager capability"))
   211  			}
   212  			if c.Platform.BareMetal != nil && !enabledCaps.Has(configv1.ClusterVersionCapabilityBaremetal) {
   213  				allErrs = append(allErrs, field.Invalid(field.NewPath("additionalEnabledCapabilities"), c.Capabilities.AdditionalEnabledCapabilities,
   214  					"platform baremetal requires the baremetal capability"))
   215  			}
   216  		}
   217  
   218  		if enabledCaps.Has(configv1.ClusterVersionCapabilityMarketplace) && !enabledCaps.Has(configv1.ClusterVersionCapabilityOperatorLifecycleManager) {
   219  			allErrs = append(allErrs, field.Invalid(field.NewPath("additionalEnabledCapabilities"), c.Capabilities.AdditionalEnabledCapabilities,
   220  				"the marketplace capability requires the OperatorLifecycleManager capability"))
   221  		}
   222  
   223  		if !enabledCaps.Has(configv1.ClusterVersionCapabilityCloudCredential) {
   224  			// check if platform is cloud
   225  			if c.None == nil && c.BareMetal == nil {
   226  				allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities"), c.Capabilities,
   227  					"disabling CloudCredential capability available only for baremetal platforms"))
   228  			}
   229  		}
   230  
   231  		if !enabledCaps.Has(configv1.ClusterVersionCapabilityCloudControllerManager) {
   232  			if c.None == nil && c.BareMetal == nil && c.External == nil {
   233  				allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities"), c.Capabilities,
   234  					"disabling CloudControllerManager is only supported on the Baremetal, None, or External platform with cloudControllerManager value none"))
   235  			}
   236  			if c.External != nil && c.External.CloudControllerManager == external.CloudControllerManagerTypeExternal {
   237  				allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities"), c.Capabilities,
   238  					"disabling CloudControllerManager on External platform supported only with cloudControllerManager value none"))
   239  			}
   240  		}
   241  
   242  		if !enabledCaps.Has(configv1.ClusterVersionCapabilityIngress) {
   243  			allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities"), c.Capabilities,
   244  				"the Ingress capability is required"))
   245  		}
   246  	}
   247  
   248  	allErrs = append(allErrs, ValidateFeatureSet(c)...)
   249  
   250  	return allErrs
   251  }
   252  
   253  // ipAddressType indicates the address types provided for a given field
   254  type ipAddressType struct {
   255  	IPv4    bool
   256  	IPv6    bool
   257  	Primary corev1.IPFamily
   258  }
   259  
   260  // ipAddressTypeByField is a map of field path to ipAddressType
   261  type ipAddressTypeByField map[string]ipAddressType
   262  
   263  // ipNetByField is a map of field path to the IPNets
   264  type ipNetByField map[string][]ipnet.IPNet
   265  
   266  // inferIPVersionFromInstallConfig infers the user's desired ip version from the networking config.
   267  // Presence field names match the field path of the struct within the Networking type. This function
   268  // assumes a valid install config.
   269  func inferIPVersionFromInstallConfig(n *types.Networking) (hasIPv4, hasIPv6 bool, presence ipAddressTypeByField, addresses ipNetByField) {
   270  	if n == nil {
   271  		return
   272  	}
   273  	addresses = make(ipNetByField)
   274  	for _, network := range n.MachineNetwork {
   275  		addresses["machineNetwork"] = append(addresses["machineNetwork"], network.CIDR)
   276  	}
   277  	for _, network := range n.ServiceNetwork {
   278  		addresses["serviceNetwork"] = append(addresses["serviceNetwork"], network)
   279  	}
   280  	for _, network := range n.ClusterNetwork {
   281  		addresses["clusterNetwork"] = append(addresses["clusterNetwork"], network.CIDR)
   282  	}
   283  	presence = make(ipAddressTypeByField)
   284  	for k, ipnets := range addresses {
   285  		for i, ipnet := range ipnets {
   286  			has := presence[k]
   287  			if ipnet.IP.To4() != nil {
   288  				has.IPv4 = true
   289  				if i == 0 {
   290  					has.Primary = corev1.IPv4Protocol
   291  				}
   292  				if k == "serviceNetwork" {
   293  					hasIPv4 = true
   294  				}
   295  			} else {
   296  				has.IPv6 = true
   297  				if i == 0 {
   298  					has.Primary = corev1.IPv6Protocol
   299  				}
   300  				if k == "serviceNetwork" {
   301  					hasIPv6 = true
   302  				}
   303  			}
   304  			presence[k] = has
   305  		}
   306  	}
   307  	return
   308  }
   309  
   310  func ipnetworksToStrings(networks []ipnet.IPNet) []string {
   311  	var diag []string
   312  	for _, sn := range networks {
   313  		diag = append(diag, sn.String())
   314  	}
   315  	return diag
   316  }
   317  
   318  // validateNetworkingIPVersion checks parameters for consistency when the user
   319  // requests single-stack IPv6 or dual-stack modes.
   320  func validateNetworkingIPVersion(n *types.Networking, p *types.Platform) field.ErrorList {
   321  	var allErrs field.ErrorList
   322  
   323  	hasIPv4, hasIPv6, presence, addresses := inferIPVersionFromInstallConfig(n)
   324  
   325  	switch {
   326  	case hasIPv4 && hasIPv6:
   327  		if len(n.ServiceNetwork) != 2 {
   328  			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"))
   329  		}
   330  
   331  		allowV6Primary := false
   332  		experimentalDualStackEnabled, _ := strconv.ParseBool(os.Getenv("OPENSHIFT_INSTALL_EXPERIMENTAL_DUAL_STACK"))
   333  		switch {
   334  		case p.Azure != nil && experimentalDualStackEnabled:
   335  			logrus.Warnf("Using experimental Azure dual-stack support")
   336  		case p.BareMetal != nil:
   337  			// We now support ipv6-primary dual stack on baremetal
   338  			allowV6Primary = true
   339  		case p.VSphere != nil:
   340  			// as well as on vSphere
   341  			allowV6Primary = true
   342  		case p.OpenStack != nil:
   343  			allowV6Primary = true
   344  		case p.Ovirt != nil:
   345  		case p.Nutanix != nil:
   346  		case p.None != nil:
   347  		case p.External != nil:
   348  		default:
   349  			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"))
   350  		}
   351  		for k, v := range presence {
   352  			switch {
   353  			case v.IPv4 && !v.IPv6:
   354  				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"))
   355  			case !v.IPv4 && v.IPv6:
   356  				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"))
   357  			}
   358  
   359  			// FIXME: we should allow either all-networks-IPv4Primary or
   360  			// all-networks-IPv6Primary, but the latter currently causes
   361  			// confusing install failures, so block it.
   362  			if !allowV6Primary && v.IPv4 && v.IPv6 && v.Primary != corev1.IPv4Protocol {
   363  				allErrs = append(allErrs, field.Invalid(field.NewPath("networking", k), strings.Join(ipnetworksToStrings(addresses[k]), ", "), "IPv4 addresses must be listed before IPv6 addresses"))
   364  			}
   365  		}
   366  
   367  	case hasIPv6:
   368  		switch {
   369  		case p.BareMetal != nil:
   370  		case p.VSphere != nil:
   371  		case p.OpenStack != nil:
   372  		case p.Ovirt != nil:
   373  		case p.Nutanix != nil:
   374  		case p.None != nil:
   375  		case p.External != nil:
   376  		case p.Azure != nil && p.Azure.CloudName == azure.StackCloud:
   377  			allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "IPv6", "Azure Stack does not support IPv6"))
   378  		default:
   379  			allErrs = append(allErrs, field.Invalid(field.NewPath("networking"), "IPv6", "single-stack IPv6 is not supported for this platform"))
   380  		}
   381  
   382  	case hasIPv4:
   383  		if len(n.ServiceNetwork) > 1 {
   384  			allErrs = append(allErrs, field.Invalid(field.NewPath("networking", "serviceNetwork"), strings.Join(ipnetworksToStrings(n.ServiceNetwork), ", "), "only one service network can be specified"))
   385  		}
   386  
   387  	default:
   388  		// we should have a validation error for no specified machineNetwork, serviceNetwork, or clusterNetwork
   389  	}
   390  
   391  	return allErrs
   392  }
   393  
   394  func validateNetworking(n *types.Networking, singleNodeOpenShift bool, fldPath *field.Path) field.ErrorList {
   395  	allErrs := field.ErrorList{}
   396  	if n.NetworkType == "" {
   397  		allErrs = append(allErrs, field.Required(fldPath.Child("networkType"), "network provider type required"))
   398  	}
   399  
   400  	// NOTE(dulek): We're hardcoding "Kuryr" here as the plan is to remove it from the API very soon. We can remove
   401  	//              this check once some more general validation of the supported NetworkTypes is in place.
   402  	if n.NetworkType == "Kuryr" {
   403  		allErrs = append(allErrs, field.Invalid(fldPath.Child("networkType"), n.NetworkType, "networkType Kuryr is not supported on OpenShift later than 4.14"))
   404  	}
   405  
   406  	if n.NetworkType == string(operv1.NetworkTypeOpenShiftSDN) {
   407  		allErrs = append(allErrs, field.Invalid(fldPath.Child("networkType"), n.NetworkType, "networkType OpenShiftSDN is not supported, please use OVNKubernetes"))
   408  	}
   409  
   410  	if len(n.MachineNetwork) > 0 {
   411  		for i, network := range n.MachineNetwork {
   412  			if err := validate.SubnetCIDR(&network.CIDR.IPNet); err != nil {
   413  				allErrs = append(allErrs, field.Invalid(fldPath.Child("machineNetwork").Index(i), network.CIDR.String(), err.Error()))
   414  			}
   415  			for j, subNetwork := range n.MachineNetwork[0:i] {
   416  				if validate.DoCIDRsOverlap(&network.CIDR.IPNet, &subNetwork.CIDR.IPNet) {
   417  					allErrs = append(allErrs, field.Invalid(fldPath.Child("machineNetwork").Index(i), network.CIDR.String(), fmt.Sprintf("machine network must not overlap with machine network %d", j)))
   418  				}
   419  			}
   420  		}
   421  	} else {
   422  		allErrs = append(allErrs, field.Required(fldPath.Child("machineNetwork"), "at least one machine network is required"))
   423  	}
   424  
   425  	for i, sn := range n.ServiceNetwork {
   426  		if err := validate.ServiceSubnetCIDR(&sn.IPNet); err != nil {
   427  			allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceNetwork").Index(i), sn.String(), err.Error()))
   428  		}
   429  		for _, network := range n.MachineNetwork {
   430  			if validate.DoCIDRsOverlap(&sn.IPNet, &network.CIDR.IPNet) {
   431  				allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceNetwork").Index(i), sn.String(), "service network must not overlap with any of the machine networks"))
   432  			}
   433  		}
   434  		for j, snn := range n.ServiceNetwork[0:i] {
   435  			if validate.DoCIDRsOverlap(&sn.IPNet, &snn.IPNet) {
   436  				allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceNetwork").Index(i), sn.String(), fmt.Sprintf("service network must not overlap with service network %d", j)))
   437  			}
   438  		}
   439  	}
   440  	if len(n.ServiceNetwork) == 0 {
   441  		allErrs = append(allErrs, field.Required(fldPath.Child("serviceNetwork"), "a service network is required"))
   442  	}
   443  
   444  	for i, cn := range n.ClusterNetwork {
   445  		allErrs = append(allErrs, validateClusterNetwork(n, &cn, i, fldPath.Child("clusterNetwork").Index(i))...)
   446  	}
   447  	if len(n.ClusterNetwork) == 0 {
   448  		allErrs = append(allErrs, field.Required(fldPath.Child("clusterNetwork"), "cluster network required"))
   449  	}
   450  	return allErrs
   451  }
   452  
   453  func validateNetworkingForPlatform(n *types.Networking, platform *types.Platform, fldPath *field.Path) field.ErrorList {
   454  	allErrs := field.ErrorList{}
   455  	switch {
   456  	default:
   457  		warningMsgFmt := "%s: %s overlaps with default Docker Bridge subnet"
   458  		for idx, mn := range n.MachineNetwork {
   459  			if validate.DoCIDRsOverlap(&mn.CIDR.IPNet, validate.DockerBridgeCIDR) {
   460  				logrus.Warnf(warningMsgFmt, fldPath.Child("machineNetwork").Index(idx), mn.CIDR.String())
   461  			}
   462  		}
   463  		for idx, sn := range n.ServiceNetwork {
   464  			if validate.DoCIDRsOverlap(&sn.IPNet, validate.DockerBridgeCIDR) {
   465  				logrus.Warnf(warningMsgFmt, fldPath.Child("serviceNetwork").Index(idx), sn.String())
   466  			}
   467  		}
   468  		for idx, cn := range n.ClusterNetwork {
   469  			if validate.DoCIDRsOverlap(&cn.CIDR.IPNet, validate.DockerBridgeCIDR) {
   470  				logrus.Warnf(warningMsgFmt, fldPath.Child("clusterNetwork").Index(idx), cn.CIDR.String())
   471  			}
   472  		}
   473  	}
   474  	return allErrs
   475  }
   476  
   477  func validateClusterNetwork(n *types.Networking, cn *types.ClusterNetworkEntry, idx int, fldPath *field.Path) field.ErrorList {
   478  	allErrs := field.ErrorList{}
   479  	if err := validate.SubnetCIDR(&cn.CIDR.IPNet); err != nil {
   480  		allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), cn.CIDR.IPNet.String(), err.Error()))
   481  	}
   482  	for _, network := range n.MachineNetwork {
   483  		if validate.DoCIDRsOverlap(&cn.CIDR.IPNet, &network.CIDR.IPNet) {
   484  			allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), cn.CIDR.String(), "cluster network must not overlap with any of the machine networks"))
   485  		}
   486  	}
   487  	for i, sn := range n.ServiceNetwork {
   488  		if validate.DoCIDRsOverlap(&cn.CIDR.IPNet, &sn.IPNet) {
   489  			allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), cn.CIDR.String(), fmt.Sprintf("cluster network must not overlap with service network %d", i)))
   490  		}
   491  	}
   492  	for i, acn := range n.ClusterNetwork[0:idx] {
   493  		if validate.DoCIDRsOverlap(&cn.CIDR.IPNet, &acn.CIDR.IPNet) {
   494  			allErrs = append(allErrs, field.Invalid(fldPath.Child("cidr"), cn.CIDR.String(), fmt.Sprintf("cluster network must not overlap with cluster network %d", i)))
   495  		}
   496  	}
   497  	if cn.HostPrefix < 0 {
   498  		allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPrefix"), cn.HostPrefix, "hostPrefix must be positive"))
   499  	}
   500  	// ignore hostPrefix if the plugin does not use it and has it unset
   501  	if pluginsUsingHostPrefix.Has(n.NetworkType) || (cn.HostPrefix != 0) {
   502  		if ones, bits := cn.CIDR.Mask.Size(); cn.HostPrefix < int32(ones) {
   503  			allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPrefix"), cn.HostPrefix, "cluster network host subnetwork prefix must not be larger size than CIDR "+cn.CIDR.String()))
   504  		} else if bits == 128 && cn.HostPrefix != 64 {
   505  			allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPrefix"), cn.HostPrefix, "cluster network host subnetwork prefix must be 64 for IPv6 networks"))
   506  		}
   507  	}
   508  	return allErrs
   509  }
   510  
   511  func validateNetworkingClusterNetworkMTU(c *types.InstallConfig, fldPath *field.Path) field.ErrorList {
   512  	// higherLimitMTUVPC is the MTU limit for AWS VPC.
   513  	// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/network_mtu.html#jumbo_frame_instances
   514  	// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/network_mtu.html
   515  	const higherLimitMTUVPC uint32 = uint32(9001)
   516  
   517  	// lowerLimitMTUVPC is the lower limit to prevent users setting too low values impacting in the
   518  	// cluster network performance. Tested values with 1100 decreases 70% in the network performance
   519  	// in AWS deployments:
   520  	const lowerLimitMTUVPC uint32 = uint32(1000)
   521  
   522  	// higherLimitMTUEdge defines the maximium generally supported MTU in AWS Local and Wavelength Zones.
   523  	// Mostly AWS Local or Wavelength zones have limited MTU between those and in the Region.
   524  	// It is required to raise a warning message when the user-defined MTU is higher than general supported.
   525  	// https://docs.aws.amazon.com/local-zones/latest/ug/how-local-zones-work.html#considerations
   526  	// https://docs.aws.amazon.com/wavelength/latest/developerguide/how-wavelengths-work.html
   527  	const higherLimitMTUEdge uint32 = uint32(1300)
   528  
   529  	// MTU overhead for the network plugin OVNKubernetes.
   530  	// https://docs.openshift.com/container-platform/4.14/networking/changing-cluster-network-mtu.html#mtu-value-selection_changing-cluster-network-mtu
   531  	const minOverheadOVN uint32 = uint32(100)
   532  
   533  	allErrs := field.ErrorList{}
   534  
   535  	if c.Networking == nil {
   536  		return nil
   537  	}
   538  
   539  	if c.Networking.ClusterNetworkMTU == 0 {
   540  		return nil
   541  	}
   542  
   543  	if c.Platform.Name() != aws.Name {
   544  		return append(allErrs, field.Invalid(fldPath, int(c.Networking.ClusterNetworkMTU), "cluster network MTU is allowed only in AWS deployments"))
   545  	}
   546  
   547  	network := c.NetworkType
   548  	mtu := c.Networking.ClusterNetworkMTU
   549  
   550  	// Calculating the MTU limits considering the base overhead for each network plugin.
   551  	limitEdgeOVNKubernetes := higherLimitMTUEdge - minOverheadOVN
   552  	limitOVNKubernetes := higherLimitMTUVPC - minOverheadOVN
   553  
   554  	if mtu > higherLimitMTUVPC {
   555  		return append(allErrs, field.Invalid(fldPath, int(mtu), fmt.Sprintf("cluster network MTU exceeds the maximum value of %d", higherLimitMTUVPC)))
   556  	}
   557  
   558  	// Prevent too low MTU values.
   559  	// Tests in AWS Local Zones with MTU of 1100 decreased the network
   560  	// performance in 70%. The check protects the cluster stability from
   561  	// user defining too lower numbers.
   562  	// https://issues.redhat.com/browse/OCPBUGS-11098
   563  	if mtu < lowerLimitMTUVPC {
   564  		return append(allErrs, field.Invalid(fldPath, int(mtu), fmt.Sprintf("cluster network MTU is lower than the minimum value of %d", lowerLimitMTUVPC)))
   565  	}
   566  
   567  	hasEdgePool := false
   568  	warnEdgePool := false
   569  	for _, compute := range c.Compute {
   570  		if compute.Name == types.MachinePoolEdgeRoleName {
   571  			hasEdgePool = true
   572  			break
   573  		}
   574  	}
   575  
   576  	if network != string(operv1.NetworkTypeOVNKubernetes) {
   577  		return append(allErrs, field.Invalid(fldPath, int(mtu), fmt.Sprintf("cluster network MTU is not valid with network plugin %s", network)))
   578  	}
   579  
   580  	if mtu > limitOVNKubernetes {
   581  		return append(allErrs, field.Invalid(fldPath, int(mtu), fmt.Sprintf("cluster network MTU exceeds the maximum value with the network plugin %s of %d", network, limitOVNKubernetes)))
   582  	}
   583  	if hasEdgePool && (mtu > limitEdgeOVNKubernetes) {
   584  		warnEdgePool = true
   585  	}
   586  	if warnEdgePool {
   587  		logrus.Warnf("networking.ClusterNetworkMTU exceeds the maximum value generally supported by AWS Local or Wavelength zones. Please ensure all AWS Zones defined in the edge compute pool accepts the MTU %d bytes between nodes (EC2) in the zone and in the Region.", mtu)
   588  	}
   589  
   590  	return allErrs
   591  }
   592  
   593  func validateControlPlane(platform *types.Platform, pool *types.MachinePool, fldPath *field.Path) field.ErrorList {
   594  	allErrs := field.ErrorList{}
   595  	if pool.Name != types.MachinePoolControlPlaneRoleName {
   596  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("name"), pool.Name, []string{types.MachinePoolControlPlaneRoleName}))
   597  	}
   598  	if pool.Replicas != nil && *pool.Replicas == 0 {
   599  		allErrs = append(allErrs, field.Invalid(fldPath.Child("replicas"), pool.Replicas, "number of control plane replicas must be positive"))
   600  	}
   601  	allErrs = append(allErrs, ValidateMachinePool(platform, pool, fldPath)...)
   602  	return allErrs
   603  }
   604  
   605  func validateComputeEdge(platform *types.Platform, pName string, fldPath *field.Path, pfld *field.Path) field.ErrorList {
   606  	allErrs := field.ErrorList{}
   607  	if platform.Name() != aws.Name {
   608  		allErrs = append(allErrs, field.NotSupported(pfld.Child("name"), pName, []string{types.MachinePoolComputeRoleName}))
   609  	}
   610  
   611  	return allErrs
   612  }
   613  
   614  func validateCompute(platform *types.Platform, control *types.MachinePool, pools []types.MachinePool, fldPath *field.Path, isMultiArchEnabled bool) field.ErrorList {
   615  	allErrs := field.ErrorList{}
   616  	poolNames := map[string]bool{}
   617  	for i, p := range pools {
   618  		poolFldPath := fldPath.Index(i)
   619  		switch p.Name {
   620  		case types.MachinePoolComputeRoleName:
   621  		case types.MachinePoolEdgeRoleName:
   622  			allErrs = append(allErrs, validateComputeEdge(platform, p.Name, poolFldPath, poolFldPath)...)
   623  		default:
   624  			allErrs = append(allErrs, field.NotSupported(poolFldPath.Child("name"), p.Name, []string{types.MachinePoolComputeRoleName, types.MachinePoolEdgeRoleName}))
   625  		}
   626  
   627  		if poolNames[p.Name] {
   628  			allErrs = append(allErrs, field.Duplicate(poolFldPath.Child("name"), p.Name))
   629  		}
   630  		poolNames[p.Name] = true
   631  		if control != nil && control.Architecture != p.Architecture && !isMultiArchEnabled {
   632  			allErrs = append(allErrs, field.Invalid(poolFldPath.Child("architecture"), p.Architecture, "heteregeneous multi-arch is not supported; compute pool architecture must match control plane"))
   633  		}
   634  		allErrs = append(allErrs, ValidateMachinePool(platform, &p, poolFldPath)...)
   635  	}
   636  	return allErrs
   637  }
   638  
   639  // vips defines the VIPs to validate
   640  type vips struct {
   641  	API     []string
   642  	Ingress []string
   643  }
   644  
   645  // vipFields defines the field names to which validation errors for each VIP
   646  // type should be assigned to
   647  type vipFields struct {
   648  	APIVIPs     string
   649  	IngressVIPs string
   650  }
   651  
   652  // validateVIPsForPlatform validates the VIPs (for API and Ingress) for the
   653  // given platform
   654  func validateVIPsForPlatform(network *types.Networking, platform *types.Platform, fldPath *field.Path) field.ErrorList {
   655  	allErrs := field.ErrorList{}
   656  
   657  	virtualIPs := vips{}
   658  	newVIPsFields := vipFields{
   659  		APIVIPs:     "apiVIPs",
   660  		IngressVIPs: "ingressVIPs",
   661  	}
   662  
   663  	var lbType configv1.PlatformLoadBalancerType
   664  
   665  	switch {
   666  	case platform.BareMetal != nil:
   667  		virtualIPs = vips{
   668  			API:     platform.BareMetal.APIVIPs,
   669  			Ingress: platform.BareMetal.IngressVIPs,
   670  		}
   671  
   672  		if platform.BareMetal.LoadBalancer != nil {
   673  			lbType = platform.BareMetal.LoadBalancer.Type
   674  		}
   675  
   676  		allErrs = append(allErrs, validateAPIAndIngressVIPs(virtualIPs, newVIPsFields, true, true, lbType, network, fldPath.Child(baremetal.Name))...)
   677  	case platform.Nutanix != nil:
   678  		allErrs = append(allErrs, ensureIPv4IsFirstInDualStackSlice(&platform.Nutanix.APIVIPs, fldPath.Child(nutanix.Name, newVIPsFields.APIVIPs))...)
   679  		allErrs = append(allErrs, ensureIPv4IsFirstInDualStackSlice(&platform.Nutanix.IngressVIPs, fldPath.Child(nutanix.Name, newVIPsFields.IngressVIPs))...)
   680  
   681  		virtualIPs = vips{
   682  			API:     platform.Nutanix.APIVIPs,
   683  			Ingress: platform.Nutanix.IngressVIPs,
   684  		}
   685  
   686  		if platform.Nutanix.LoadBalancer != nil {
   687  			lbType = platform.Nutanix.LoadBalancer.Type
   688  		}
   689  
   690  		allErrs = append(allErrs, validateAPIAndIngressVIPs(virtualIPs, newVIPsFields, false, false, lbType, network, fldPath.Child(nutanix.Name))...)
   691  	case platform.OpenStack != nil:
   692  		virtualIPs = vips{
   693  			API:     platform.OpenStack.APIVIPs,
   694  			Ingress: platform.OpenStack.IngressVIPs,
   695  		}
   696  
   697  		if platform.OpenStack.LoadBalancer != nil {
   698  			lbType = platform.OpenStack.LoadBalancer.Type
   699  		}
   700  
   701  		allErrs = append(allErrs, validateAPIAndIngressVIPs(virtualIPs, newVIPsFields, true, true, lbType, network, fldPath.Child(openstack.Name))...)
   702  	case platform.VSphere != nil:
   703  		virtualIPs = vips{
   704  			API:     platform.VSphere.APIVIPs,
   705  			Ingress: platform.VSphere.IngressVIPs,
   706  		}
   707  
   708  		if platform.VSphere.LoadBalancer != nil {
   709  			lbType = platform.VSphere.LoadBalancer.Type
   710  		}
   711  
   712  		allErrs = append(allErrs, validateAPIAndIngressVIPs(virtualIPs, newVIPsFields, false, false, lbType, network, fldPath.Child(vsphere.Name))...)
   713  	case platform.Ovirt != nil:
   714  		allErrs = append(allErrs, ensureIPv4IsFirstInDualStackSlice(&platform.Ovirt.APIVIPs, fldPath.Child(ovirt.Name, newVIPsFields.APIVIPs))...)
   715  		allErrs = append(allErrs, ensureIPv4IsFirstInDualStackSlice(&platform.Ovirt.IngressVIPs, fldPath.Child(ovirt.Name, newVIPsFields.IngressVIPs))...)
   716  
   717  		newVIPsFields = vipFields{
   718  			APIVIPs:     "api_vips",
   719  			IngressVIPs: "ingress_vips",
   720  		}
   721  		virtualIPs = vips{
   722  			API:     platform.Ovirt.APIVIPs,
   723  			Ingress: platform.Ovirt.IngressVIPs,
   724  		}
   725  
   726  		if platform.Ovirt.LoadBalancer != nil {
   727  			lbType = platform.Ovirt.LoadBalancer.Type
   728  		}
   729  
   730  		allErrs = append(allErrs, validateAPIAndIngressVIPs(virtualIPs, newVIPsFields, true, true, lbType, network, fldPath.Child(ovirt.Name))...)
   731  	default:
   732  		//no vips to validate on this platform
   733  	}
   734  
   735  	return allErrs
   736  }
   737  
   738  func ensureIPv4IsFirstInDualStackSlice(vips *[]string, fldPath *field.Path) field.ErrorList {
   739  	errList := field.ErrorList{}
   740  	isDualStack, err := utilsnet.IsDualStackIPStrings(*vips)
   741  	if err != nil {
   742  		errList = append(errList, field.Invalid(fldPath, vips, err.Error()))
   743  		return errList
   744  	}
   745  
   746  	if isDualStack {
   747  		if len(*vips) == 2 {
   748  			if utilsnet.IsIPv4String((*vips)[1]) && utilsnet.IsIPv6String((*vips)[0]) {
   749  				(*vips)[0], (*vips)[1] = (*vips)[1], (*vips)[0]
   750  			}
   751  		} else {
   752  			errList = append(errList, field.Invalid(fldPath, vips, "wrong number of VIPs given. Expecting 2 VIPs for dual stack"))
   753  			return errList
   754  		}
   755  	}
   756  
   757  	return errList
   758  }
   759  
   760  // validateAPIAndIngressVIPs validates the API and Ingress VIPs
   761  //
   762  //nolint:gocyclo
   763  func validateAPIAndIngressVIPs(vips vips, fieldNames vipFields, vipIsRequired, reqVIPinMachineCIDR bool, lbType configv1.PlatformLoadBalancerType, n *types.Networking, fldPath *field.Path) field.ErrorList {
   764  	allErrs := field.ErrorList{}
   765  
   766  	if len(vips.API) == 0 {
   767  		if vipIsRequired {
   768  			allErrs = append(allErrs, field.Required(fldPath.Child(fieldNames.APIVIPs), "must specify at least one VIP for the API"))
   769  		}
   770  	} else if len(vips.API) <= 2 {
   771  		for _, vip := range vips.API {
   772  			if err := validate.IP(vip); err != nil {
   773  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.APIVIPs), vip, err.Error()))
   774  			}
   775  
   776  			// When using user-managed loadbalancer we do not require API and Ingress VIP to be different as well as
   777  			// we allow them to be from outside the machine network CIDR.
   778  			if lbType != configv1.LoadBalancerTypeUserManaged {
   779  				for _, ingressVIP := range vips.Ingress {
   780  					apiVIPNet := net.ParseIP(vip)
   781  					ingressVIPNet := net.ParseIP(ingressVIP)
   782  
   783  					if apiVIPNet.Equal(ingressVIPNet) {
   784  						allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.APIVIPs), vip, "VIP for API must not be one of the Ingress VIPs"))
   785  					}
   786  				}
   787  
   788  				if err := ValidateIPinMachineCIDR(vip, n); reqVIPinMachineCIDR && err != nil {
   789  					allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.APIVIPs), vip, err.Error()))
   790  				}
   791  			}
   792  		}
   793  
   794  		if len(vips.Ingress) == 0 {
   795  			allErrs = append(allErrs, field.Required(fldPath.Child(fieldNames.IngressVIPs), "must specify VIP for ingress, when VIP for API is set"))
   796  		}
   797  
   798  		if len(vips.API) == 1 {
   799  			hasIPv4, hasIPv6, presence, _ := inferIPVersionFromInstallConfig(n)
   800  
   801  			apiVIPIPFamily := corev1.IPv4Protocol
   802  			if utilsnet.IsIPv6String(vips.API[0]) {
   803  				apiVIPIPFamily = corev1.IPv6Protocol
   804  			}
   805  
   806  			if hasIPv4 && hasIPv6 && apiVIPIPFamily != presence["machineNetwork"].Primary {
   807  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.APIVIPs), vips.API[0], "VIP for the API must be of the same IP family with machine network's primary IP Family for dual-stack IPv4/IPv6"))
   808  			}
   809  		} else if len(vips.API) == 2 {
   810  			if isDualStack, _ := utilsnet.IsDualStackIPStrings(vips.API); !isDualStack {
   811  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.APIVIPs), vips.API, "If two API VIPs are given, one must be an IPv4 address, the other an IPv6"))
   812  			}
   813  		}
   814  	} else {
   815  		allErrs = append(allErrs, field.TooMany(fldPath.Child(fieldNames.APIVIPs), len(vips.API), 2))
   816  	}
   817  
   818  	if len(vips.Ingress) == 0 {
   819  		if vipIsRequired {
   820  			allErrs = append(allErrs, field.Required(fldPath.Child(fieldNames.IngressVIPs), "must specify at least one VIP for the Ingress"))
   821  		}
   822  	} else if len(vips.Ingress) <= 2 {
   823  		for _, vip := range vips.Ingress {
   824  			if err := validate.IP(vip); err != nil {
   825  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.IngressVIPs), vip, err.Error()))
   826  			}
   827  
   828  			// When using user-managed loadbalancer we do not require API and Ingress VIP to be different as well as
   829  			// we allow them to be from outside the machine network CIDR.
   830  			if lbType != configv1.LoadBalancerTypeUserManaged {
   831  				if err := ValidateIPinMachineCIDR(vip, n); reqVIPinMachineCIDR && err != nil {
   832  					allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.IngressVIPs), vip, err.Error()))
   833  				}
   834  			}
   835  		}
   836  
   837  		if len(vips.API) == 0 {
   838  			allErrs = append(allErrs, field.Required(fldPath.Child(fieldNames.APIVIPs), "must specify VIP for API, when VIP for ingress is set"))
   839  		}
   840  
   841  		if len(vips.Ingress) == 1 {
   842  			hasIPv4, hasIPv6, presence, _ := inferIPVersionFromInstallConfig(n)
   843  
   844  			ingressVIPIPFamily := corev1.IPv4Protocol
   845  			if utilsnet.IsIPv6String(vips.Ingress[0]) {
   846  				ingressVIPIPFamily = corev1.IPv6Protocol
   847  			}
   848  
   849  			if hasIPv4 && hasIPv6 && ingressVIPIPFamily != presence["machineNetwork"].Primary {
   850  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.IngressVIPs), vips.Ingress[0], "VIP for the Ingress must be of the same IP family with machine network's primary IP Family for dual-stack IPv4/IPv6"))
   851  			}
   852  		} else if len(vips.Ingress) == 2 {
   853  			if isDualStack, _ := utilsnet.IsDualStackIPStrings(vips.Ingress); !isDualStack {
   854  				allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldNames.IngressVIPs), vips.Ingress, "If two Ingress VIPs are given, one must be an IPv4 address, the other an IPv6"))
   855  			}
   856  		}
   857  	} else {
   858  		allErrs = append(allErrs, field.TooMany(fldPath.Child(fieldNames.IngressVIPs), len(vips.Ingress), 2))
   859  	}
   860  
   861  	return allErrs
   862  }
   863  
   864  // ValidateIPinMachineCIDR confirms if the specified VIP is in the machine CIDR.
   865  func ValidateIPinMachineCIDR(vip string, n *types.Networking) error {
   866  	var networks []string
   867  
   868  	for _, network := range n.MachineNetwork {
   869  		if network.CIDR.Contains(net.ParseIP(vip)) {
   870  			return nil
   871  		}
   872  		networks = append(networks, network.CIDR.String())
   873  	}
   874  
   875  	return fmt.Errorf("IP expected to be in one of the machine networks: %s", strings.Join(networks, ","))
   876  }
   877  
   878  func validatePlatform(platform *types.Platform, usingAgentMethod bool, fldPath *field.Path, network *types.Networking, c *types.InstallConfig) field.ErrorList {
   879  	allErrs := field.ErrorList{}
   880  	activePlatform := platform.Name()
   881  	platforms := make([]string, len(types.PlatformNames))
   882  	copy(platforms, types.PlatformNames)
   883  	platforms = append(platforms, types.HiddenPlatformNames...)
   884  	sort.Strings(platforms)
   885  	i := sort.SearchStrings(platforms, activePlatform)
   886  	if i == len(platforms) || platforms[i] != activePlatform {
   887  		allErrs = append(allErrs, field.Invalid(fldPath, activePlatform, fmt.Sprintf("must specify one of the platforms (%s)", strings.Join(platforms, ", "))))
   888  	}
   889  	validate := func(n string, value interface{}, validation func(*field.Path) field.ErrorList) {
   890  		if n != activePlatform {
   891  			allErrs = append(allErrs, field.Invalid(fldPath, activePlatform, fmt.Sprintf("must only specify a single type of platform; cannot use both %q and %q", activePlatform, n)))
   892  		}
   893  		allErrs = append(allErrs, validation(fldPath.Child(n))...)
   894  	}
   895  	if platform.AWS != nil {
   896  		validate(aws.Name, platform.AWS, func(f *field.Path) field.ErrorList {
   897  			return awsvalidation.ValidatePlatform(platform.AWS, c.CredentialsMode, f)
   898  		})
   899  	}
   900  	if platform.Azure != nil {
   901  		validate(azure.Name, platform.Azure, func(f *field.Path) field.ErrorList {
   902  			return azurevalidation.ValidatePlatform(platform.Azure, c.Publish, f, c)
   903  		})
   904  	}
   905  	if platform.GCP != nil {
   906  		validate(gcp.Name, platform.GCP, func(f *field.Path) field.ErrorList { return gcpvalidation.ValidatePlatform(platform.GCP, f, c) })
   907  	}
   908  	if platform.IBMCloud != nil {
   909  		validate(ibmcloud.Name, platform.IBMCloud, func(f *field.Path) field.ErrorList { return ibmcloudvalidation.ValidatePlatform(platform.IBMCloud, f) })
   910  	}
   911  	if platform.OpenStack != nil {
   912  		validate(openstack.Name, platform.OpenStack, func(f *field.Path) field.ErrorList {
   913  			return openstackvalidation.ValidatePlatform(platform.OpenStack, network, f, c)
   914  		})
   915  	}
   916  	if platform.PowerVS != nil {
   917  		if c.SSHKey == "" {
   918  			allErrs = append(allErrs, field.Required(field.NewPath("sshKey"), "sshKey is required"))
   919  		}
   920  		validate(powervs.Name, platform.PowerVS, func(f *field.Path) field.ErrorList {
   921  			return powervsvalidation.ValidatePlatform(platform.PowerVS, f)
   922  		})
   923  	}
   924  	if platform.VSphere != nil {
   925  		validate(vsphere.Name, platform.VSphere, func(f *field.Path) field.ErrorList {
   926  			return vspherevalidation.ValidatePlatform(platform.VSphere, usingAgentMethod, f, c)
   927  		})
   928  	}
   929  	if platform.BareMetal != nil {
   930  		validate(baremetal.Name, platform.BareMetal, func(f *field.Path) field.ErrorList {
   931  			return baremetalvalidation.ValidatePlatform(platform.BareMetal, usingAgentMethod, network, f, c)
   932  		})
   933  	}
   934  	if platform.Ovirt != nil {
   935  		validate(ovirt.Name, platform.Ovirt, func(f *field.Path) field.ErrorList {
   936  			return ovirtvalidation.ValidatePlatform(platform.Ovirt, f, c)
   937  		})
   938  	}
   939  	if platform.Nutanix != nil {
   940  		validate(nutanix.Name, platform.Nutanix, func(f *field.Path) field.ErrorList {
   941  			return nutanixvalidation.ValidatePlatform(platform.Nutanix, f, c)
   942  		})
   943  	}
   944  	return allErrs
   945  }
   946  
   947  func validateProxy(p *types.Proxy, c *types.InstallConfig, fldPath *field.Path) field.ErrorList {
   948  	allErrs := field.ErrorList{}
   949  
   950  	if p.HTTPProxy == "" && p.HTTPSProxy == "" {
   951  		allErrs = append(allErrs, field.Required(fldPath, "must include httpProxy or httpsProxy"))
   952  	}
   953  
   954  	if p.HTTPProxy != "" {
   955  		allErrs = append(allErrs, validateURI(p.HTTPProxy, fldPath.Child("httpProxy"), []string{"http"})...)
   956  		if c.Networking != nil {
   957  			allErrs = append(allErrs, validateIPProxy(p.HTTPProxy, c.Networking, fldPath.Child("httpProxy"))...)
   958  		}
   959  	}
   960  	if p.HTTPSProxy != "" {
   961  		allErrs = append(allErrs, validateURI(p.HTTPSProxy, fldPath.Child("httpsProxy"), []string{"http", "https"})...)
   962  		if c.Networking != nil {
   963  			allErrs = append(allErrs, validateIPProxy(p.HTTPSProxy, c.Networking, fldPath.Child("httpsProxy"))...)
   964  		}
   965  	}
   966  	if p.NoProxy != "" && p.NoProxy != "*" {
   967  		if strings.Contains(p.NoProxy, " ") {
   968  			allErrs = append(allErrs, field.Invalid(fldPath.Child("noProxy"), p.NoProxy, fmt.Sprintf("noProxy must not have spaces")))
   969  		}
   970  		for idx, v := range strings.Split(p.NoProxy, ",") {
   971  			v = strings.TrimSpace(v)
   972  			errDomain := validate.NoProxyDomainName(v)
   973  			_, _, errCIDR := net.ParseCIDR(v)
   974  			ip := net.ParseIP(v)
   975  			if errDomain != nil && errCIDR != nil && ip == nil {
   976  				allErrs = append(allErrs, field.Invalid(fldPath.Child("noProxy"), p.NoProxy, fmt.Sprintf(
   977  					"each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element %d %q", idx, v)))
   978  			}
   979  		}
   980  	}
   981  
   982  	return allErrs
   983  }
   984  
   985  func validateImageContentSources(groups []types.ImageContentSource, fldPath *field.Path) field.ErrorList {
   986  	allErrs := field.ErrorList{}
   987  	for gidx, group := range groups {
   988  		groupf := fldPath.Index(gidx)
   989  		if err := validateNamedRepository(group.Source); err != nil {
   990  			allErrs = append(allErrs, field.Invalid(groupf.Child("source"), group.Source, err.Error()))
   991  		}
   992  
   993  		for midx, mirror := range group.Mirrors {
   994  			if err := validateNamedRepository(mirror); err != nil {
   995  				allErrs = append(allErrs, field.Invalid(groupf.Child("mirrors").Index(midx), mirror, err.Error()))
   996  				continue
   997  			}
   998  		}
   999  	}
  1000  	return allErrs
  1001  }
  1002  
  1003  func validateImageDigestSources(groups []types.ImageDigestSource, fldPath *field.Path) field.ErrorList {
  1004  	allErrs := field.ErrorList{}
  1005  	for gidx, group := range groups {
  1006  		groupf := fldPath.Index(gidx)
  1007  		if err := validateNamedRepository(group.Source); err != nil {
  1008  			allErrs = append(allErrs, field.Invalid(groupf.Child("source"), group.Source, err.Error()))
  1009  		}
  1010  
  1011  		for midx, mirror := range group.Mirrors {
  1012  			if err := validateNamedRepository(mirror); err != nil {
  1013  				allErrs = append(allErrs, field.Invalid(groupf.Child("mirrors").Index(midx), mirror, err.Error()))
  1014  				continue
  1015  			}
  1016  		}
  1017  	}
  1018  	return allErrs
  1019  }
  1020  
  1021  func validateNamedRepository(r string) error {
  1022  	ref, err := dockerref.ParseNamed(r)
  1023  	if err != nil {
  1024  		// If a mirror name is provided without the named reference,
  1025  		// then the name is not considered canonical and will cause
  1026  		// an error. e.g. registry.lab.redhat.com:5000 will result
  1027  		// in an error. Instead we will check whether the input is
  1028  		// a valid hostname as a workaround.
  1029  		if err == dockerref.ErrNameNotCanonical {
  1030  			// If the hostname string contains a port, lets attempt
  1031  			// to split them
  1032  			host, _, err := net.SplitHostPort(r)
  1033  			if err != nil {
  1034  				host = r
  1035  			}
  1036  			if err = validate.Host(host); err != nil {
  1037  				return errors.Wrap(err, "the repository provided is invalid")
  1038  			}
  1039  			return nil
  1040  		}
  1041  		return errors.Wrap(err, "failed to parse")
  1042  	}
  1043  	if !dockerref.IsNameOnly(ref) {
  1044  		return errors.New("must be repository--not reference")
  1045  	}
  1046  	return nil
  1047  }
  1048  
  1049  var (
  1050  	validPublishingStrategies = map[types.PublishingStrategy]struct{}{
  1051  		types.ExternalPublishingStrategy: {},
  1052  		types.InternalPublishingStrategy: {},
  1053  		types.MixedPublishingStrategy:    {},
  1054  	}
  1055  
  1056  	validPublishingStrategyValues = func() []string {
  1057  		v := make([]string, 0, len(validPublishingStrategies))
  1058  		for m := range validPublishingStrategies {
  1059  			v = append(v, string(m))
  1060  		}
  1061  		sort.Strings(v)
  1062  		return v
  1063  	}()
  1064  )
  1065  
  1066  func validateCloudCredentialsMode(mode types.CredentialsMode, fldPath *field.Path, platform types.Platform) field.ErrorList {
  1067  	if mode == "" {
  1068  		return nil
  1069  	}
  1070  	allErrs := field.ErrorList{}
  1071  
  1072  	allowedAzureModes := []types.CredentialsMode{types.PassthroughCredentialsMode, types.ManualCredentialsMode}
  1073  	if platform.Azure != nil && platform.Azure.CloudName == azure.StackCloud {
  1074  		allowedAzureModes = []types.CredentialsMode{types.ManualCredentialsMode}
  1075  	}
  1076  
  1077  	// validPlatformCredentialsModes is a map from the platform name to a slice of credentials modes that are valid
  1078  	// for the platform. If a platform name is not in the map, then the credentials mode cannot be set for that platform.
  1079  	validPlatformCredentialsModes := map[string][]types.CredentialsMode{
  1080  		aws.Name:      {types.MintCredentialsMode, types.PassthroughCredentialsMode, types.ManualCredentialsMode},
  1081  		azure.Name:    allowedAzureModes,
  1082  		gcp.Name:      {types.MintCredentialsMode, types.PassthroughCredentialsMode, types.ManualCredentialsMode},
  1083  		ibmcloud.Name: {types.ManualCredentialsMode},
  1084  		powervs.Name:  {types.ManualCredentialsMode},
  1085  		nutanix.Name:  {types.ManualCredentialsMode},
  1086  	}
  1087  	if validModes, ok := validPlatformCredentialsModes[platform.Name()]; ok {
  1088  		validModesSet := sets.NewString()
  1089  		for _, m := range validModes {
  1090  			validModesSet.Insert(string(m))
  1091  		}
  1092  		if !validModesSet.Has(string(mode)) {
  1093  			allErrs = append(allErrs, field.NotSupported(fldPath, mode, validModesSet.List()))
  1094  		}
  1095  	} else {
  1096  		allErrs = append(allErrs, field.Invalid(fldPath, mode, fmt.Sprintf("cannot be set when using the %q platform", platform.Name())))
  1097  	}
  1098  	return allErrs
  1099  }
  1100  
  1101  // validateURI checks if the given url is of the right format. It also checks if the scheme of the uri
  1102  // provided is within the list of accepted schema provided as part of the input.
  1103  func validateURI(uri string, fldPath *field.Path, schemes []string) field.ErrorList {
  1104  	parsed, err := url.ParseRequestURI(uri)
  1105  	if err != nil {
  1106  		return field.ErrorList{field.Invalid(fldPath, uri, err.Error())}
  1107  	}
  1108  	for _, scheme := range schemes {
  1109  		if scheme == parsed.Scheme {
  1110  			return nil
  1111  		}
  1112  	}
  1113  	return field.ErrorList{field.NotSupported(fldPath, parsed.Scheme, schemes)}
  1114  }
  1115  
  1116  // validateIPProxy checks if the given proxy string is an IP and if so checks the service and
  1117  // cluster networks and returns error if the IP belongs in them. Returns nil if the proxy is
  1118  // not an IP address.
  1119  func validateIPProxy(proxy string, n *types.Networking, fldPath *field.Path) field.ErrorList {
  1120  	allErrs := field.ErrorList{}
  1121  
  1122  	parsed, err := url.ParseRequestURI(proxy)
  1123  	if err != nil {
  1124  		return allErrs
  1125  	}
  1126  
  1127  	proxyIP := net.ParseIP(parsed.Hostname())
  1128  	if proxyIP == nil {
  1129  		return nil
  1130  	}
  1131  
  1132  	for _, network := range n.ClusterNetwork {
  1133  		if network.CIDR.Contains(proxyIP) {
  1134  			allErrs = append(allErrs, field.Invalid(fldPath, proxy, "proxy value is part of the cluster networks"))
  1135  			break
  1136  		}
  1137  	}
  1138  
  1139  	for _, network := range n.ServiceNetwork {
  1140  		if network.Contains(proxyIP) {
  1141  			allErrs = append(allErrs, field.Invalid(fldPath, proxy, "proxy value is part of the service networks"))
  1142  			break
  1143  		}
  1144  	}
  1145  	return allErrs
  1146  }
  1147  
  1148  // validateFIPSconfig checks if the current install-config is compatible with FIPS standards
  1149  // and returns an error if it's not the case. As of this writing, only rsa or ecdsa algorithms are supported
  1150  // for ssh keys on FIPS.
  1151  func validateFIPSconfig(c *types.InstallConfig) field.ErrorList {
  1152  	allErrs := field.ErrorList{}
  1153  	if c.SSHKey != "" {
  1154  		sshParsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(c.SSHKey))
  1155  		if err != nil {
  1156  			allErrs = append(allErrs, field.Invalid(field.NewPath("sshKey"), c.SSHKey, fmt.Sprintf("Fatal error trying to parse configured public key: %s", err)))
  1157  		} else {
  1158  			sshKeyType := sshParsedKey.Type()
  1159  			re := regexp.MustCompile(`^ecdsa-sha2-nistp\d{3}$|^ssh-rsa$`)
  1160  			if !re.MatchString(sshKeyType) {
  1161  				allErrs = append(allErrs, field.Invalid(field.NewPath("sshKey"), c.SSHKey, fmt.Sprintf("SSH key type %s unavailable when FIPS is enabled. Please use rsa or ecdsa.", sshKeyType)))
  1162  			}
  1163  		}
  1164  	}
  1165  
  1166  	if err := hostcrypt.VerifyHostTargetState(c.FIPS); err != nil {
  1167  		if skip, ok := os.LookupEnv("OPENSHIFT_INSTALL_SKIP_HOSTCRYPT_VALIDATION"); ok && skip != "" {
  1168  			logrus.Warnf("%v", err)
  1169  			if c.Annotations == nil {
  1170  				c.Annotations = make(map[string]string)
  1171  			}
  1172  			c.Annotations[hostCryptBypassedAnnotation] = "true"
  1173  		} else {
  1174  			allErrs = append(allErrs, field.Forbidden(field.NewPath("fips"), err.Error()))
  1175  		}
  1176  	}
  1177  	return allErrs
  1178  }
  1179  
  1180  // validateCapabilities checks if additional, optional OpenShift components are specified in the
  1181  // install-config to be included in the installation.
  1182  func validateCapabilities(c *types.Capabilities, fldPath *field.Path) field.ErrorList {
  1183  	allErrs := field.ErrorList{}
  1184  
  1185  	allCapabilitySets := sets.NewString()
  1186  	allAvailableCapabilities := sets.NewString()
  1187  	// Create sets of all capability sets and *all* available capabilities across those capability sets
  1188  	for baselineSet, capabilities := range configv1.ClusterVersionCapabilitySets {
  1189  		allCapabilitySets.Insert(string(baselineSet))
  1190  		for _, capability := range capabilities {
  1191  			allAvailableCapabilities.Insert(string(capability))
  1192  		}
  1193  	}
  1194  
  1195  	if !allCapabilitySets.Has(string(c.BaselineCapabilitySet)) {
  1196  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("baselineCapabilitySet"), c.BaselineCapabilitySet, allCapabilitySets.List()))
  1197  	}
  1198  
  1199  	// Check to see the validity of additionalEnabledCapabilities specified by the user
  1200  	for i, capability := range c.AdditionalEnabledCapabilities {
  1201  		if !allAvailableCapabilities.Has(string(capability)) {
  1202  			allErrs = append(allErrs, field.NotSupported(fldPath.Child("additionalEnabledCapabilities").Index(i), capability, allAvailableCapabilities.List()))
  1203  		}
  1204  	}
  1205  	return allErrs
  1206  }
  1207  
  1208  func validateAdditionalCABundlePolicy(c *types.InstallConfig) error {
  1209  	switch c.AdditionalTrustBundlePolicy {
  1210  	case types.PolicyProxyOnly, types.PolicyAlways:
  1211  		return nil
  1212  	default:
  1213  		return fmt.Errorf("supported values \"Proxyonly\", \"Always\"")
  1214  	}
  1215  }
  1216  
  1217  // ValidateFeatureSet returns an error if a gated feature is used without opting into the feature set.
  1218  func ValidateFeatureSet(c *types.InstallConfig) field.ErrorList {
  1219  	allErrs := field.ErrorList{}
  1220  
  1221  	clusterProfile := types.GetClusterProfileName()
  1222  	featureSets, ok := features.AllFeatureSets()[clusterProfile]
  1223  	if !ok {
  1224  		logrus.Warnf("no feature sets for cluster profile %q", clusterProfile)
  1225  	}
  1226  	if _, ok := featureSets[c.FeatureSet]; c.FeatureSet != configv1.CustomNoUpgrade && !ok {
  1227  		sortedFeatureSets := func() []string {
  1228  			v := []string{}
  1229  			for n := range features.AllFeatureSets()[clusterProfile] {
  1230  				v = append(v, string(n))
  1231  			}
  1232  			// Add CustomNoUpgrade since it is not part of features sets for profiles
  1233  			v = append(v, string(configv1.CustomNoUpgrade))
  1234  			sort.Strings(v)
  1235  			return v
  1236  		}()
  1237  		allErrs = append(allErrs, field.NotSupported(field.NewPath("featureSet"), c.FeatureSet, sortedFeatureSets))
  1238  	}
  1239  
  1240  	if len(c.FeatureGates) > 0 {
  1241  		if c.FeatureSet != configv1.CustomNoUpgrade {
  1242  			allErrs = append(allErrs, field.Forbidden(field.NewPath("featureGates"), "featureGates can only be used with the CustomNoUpgrade feature set"))
  1243  		}
  1244  		allErrs = append(allErrs, validateCustomFeatureGates(c)...)
  1245  	}
  1246  
  1247  	// We can only accurately check gated features
  1248  	// if feature sets are correctly configured.
  1249  	if len(allErrs) == 0 {
  1250  		allErrs = append(allErrs, validateGatedFeatures(c)...)
  1251  	}
  1252  
  1253  	return allErrs
  1254  }
  1255  
  1256  // validateCustomFeatureGates checks that all provided custom features match the expected format.
  1257  // The expected format is <FeatureName>=<Enabled>.
  1258  func validateCustomFeatureGates(c *types.InstallConfig) field.ErrorList {
  1259  	allErrs := field.ErrorList{}
  1260  
  1261  	for i, rawFeature := range c.FeatureGates {
  1262  		featureParts := strings.Split(rawFeature, "=")
  1263  		if len(featureParts) != 2 {
  1264  			allErrs = append(allErrs, field.Invalid(field.NewPath("featureGates").Index(i), rawFeature, "must match the format <feature-name>=<bool>"))
  1265  			continue
  1266  		}
  1267  
  1268  		if _, err := strconv.ParseBool(featureParts[1]); err != nil {
  1269  			allErrs = append(allErrs, field.Invalid(field.NewPath("featureGates").Index(i), rawFeature, "must match the format <feature-name>=<bool>, could not parse boolean value"))
  1270  		}
  1271  	}
  1272  
  1273  	return allErrs
  1274  }
  1275  
  1276  // validateGatedFeatures ensures that any gated features used in
  1277  // the install config are enabled.
  1278  func validateGatedFeatures(c *types.InstallConfig) field.ErrorList {
  1279  	allErrs := field.ErrorList{}
  1280  
  1281  	gatedFeatures := []featuregates.GatedInstallConfigFeature{}
  1282  	switch {
  1283  	case c.GCP != nil:
  1284  		gatedFeatures = append(gatedFeatures, gcpvalidation.GatedFeatures(c)...)
  1285  	case c.VSphere != nil:
  1286  		gatedFeatures = append(gatedFeatures, vspherevalidation.GatedFeatures(c)...)
  1287  	}
  1288  
  1289  	fg := c.EnabledFeatureGates()
  1290  	errMsgTemplate := "this field is protected by the %s feature gate which must be enabled through either the TechPreviewNoUpgrade or CustomNoUpgrade feature set"
  1291  
  1292  	fgCheck := func(c featuregates.GatedInstallConfigFeature) {
  1293  		if !fg.Enabled(c.FeatureGateName) && c.Condition {
  1294  			errMsg := fmt.Sprintf(errMsgTemplate, c.FeatureGateName)
  1295  			allErrs = append(allErrs, field.Forbidden(c.Field, errMsg))
  1296  		}
  1297  	}
  1298  
  1299  	for _, gf := range gatedFeatures {
  1300  		fgCheck(gf)
  1301  	}
  1302  
  1303  	return allErrs
  1304  }
  1305  
  1306  // validateMultiReleasePayload ensures a multi payload is used when a multi-arch cluster config is enabled.
  1307  func validateMultiReleasePayload(controlPlanePool *types.MachinePool, computePool []types.MachinePool) field.ErrorList {
  1308  	allErrs := field.ErrorList{}
  1309  
  1310  	releaseArch, err := version.ReleaseArchitecture()
  1311  	if err != nil {
  1312  		return append(allErrs, field.InternalError(field.NewPath(""), err))
  1313  	}
  1314  
  1315  	switch releaseArch {
  1316  	case "multi":
  1317  		// All good
  1318  	case "unknown":
  1319  		for _, p := range computePool {
  1320  			if controlPlanePool != nil && controlPlanePool.Architecture != p.Architecture {
  1321  				// Overriding release architecture is a must during dev/CI so just log a warning instead of erroring out
  1322  				logrus.Warnln("Could not determine release architecture for multi arch cluster configuration. Make sure the release is a multi architecture payload.")
  1323  				break
  1324  			}
  1325  		}
  1326  	default:
  1327  		if controlPlanePool != nil && controlPlanePool.Architecture != types.Architecture(releaseArch) {
  1328  			errMsg := fmt.Sprintf("cannot create %s controlPlane node from a single architecture %s release payload", controlPlanePool.Architecture, releaseArch)
  1329  			allErrs = append(allErrs, field.Invalid(field.NewPath("controlPlane", "architecture"), controlPlanePool.Architecture, errMsg))
  1330  		}
  1331  		for i, p := range computePool {
  1332  			poolFldPath := field.NewPath("compute").Index(i)
  1333  			if p.Architecture != types.Architecture(releaseArch) {
  1334  				errMsg := fmt.Sprintf("cannot create %s compute node from a single architecture %s release payload", p.Architecture, releaseArch)
  1335  				allErrs = append(allErrs, field.Invalid(poolFldPath.Child("architecture"), p.Architecture, errMsg))
  1336  			}
  1337  		}
  1338  	}
  1339  
  1340  	return allErrs
  1341  }