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 }