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

     1  package validation
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  	"k8s.io/apimachinery/pkg/util/validation/field"
    11  	"k8s.io/utils/pointer"
    12  	utilsslice "k8s.io/utils/strings/slices"
    13  
    14  	configv1 "github.com/openshift/api/config/v1"
    15  	operv1 "github.com/openshift/api/operator/v1"
    16  	"github.com/openshift/installer/pkg/ipnet"
    17  	"github.com/openshift/installer/pkg/types"
    18  	"github.com/openshift/installer/pkg/types/aws"
    19  	"github.com/openshift/installer/pkg/types/azure"
    20  	"github.com/openshift/installer/pkg/types/baremetal"
    21  	"github.com/openshift/installer/pkg/types/external"
    22  	"github.com/openshift/installer/pkg/types/gcp"
    23  	"github.com/openshift/installer/pkg/types/ibmcloud"
    24  	"github.com/openshift/installer/pkg/types/none"
    25  	"github.com/openshift/installer/pkg/types/nutanix"
    26  	"github.com/openshift/installer/pkg/types/openstack"
    27  	"github.com/openshift/installer/pkg/types/powervs"
    28  	"github.com/openshift/installer/pkg/types/vsphere"
    29  )
    30  
    31  const TechPreviewNoUpgrade = "TechPreviewNoUpgrade"
    32  
    33  func validInstallConfig() *types.InstallConfig {
    34  	return &types.InstallConfig{
    35  		TypeMeta: metav1.TypeMeta{
    36  			APIVersion: types.InstallConfigVersion,
    37  		},
    38  		ObjectMeta: metav1.ObjectMeta{
    39  			Name: "test-cluster",
    40  		},
    41  		BaseDomain:   "test-domain",
    42  		Networking:   validIPv4NetworkingConfig(),
    43  		ControlPlane: validMachinePool("master"),
    44  		Compute:      []types.MachinePool{*validMachinePool("worker")},
    45  		Platform: types.Platform{
    46  			AWS: validAWSPlatform(),
    47  		},
    48  		PullSecret: `{"auths":{"example.com":{"auth":"authorization value"}}}`,
    49  		Publish:    types.ExternalPublishingStrategy,
    50  		Proxy: &types.Proxy{
    51  			HTTPProxy:  "http://user:password@127.0.0.1:8080",
    52  			HTTPSProxy: "https://user:password@127.0.0.1:8080",
    53  			NoProxy:    "valid-proxy.com,172.30.0.0/16",
    54  		},
    55  	}
    56  }
    57  
    58  func validAWSPlatform() *aws.Platform {
    59  	return &aws.Platform{
    60  		Region: "us-east-1",
    61  	}
    62  }
    63  
    64  func validAzureStackPlatform() *azure.Platform {
    65  	return &azure.Platform{
    66  		Region:                      "test-region",
    67  		ARMEndpoint:                 "http://test-endpoint.com",
    68  		BaseDomainResourceGroupName: "test-basedomain-rg",
    69  		CloudName:                   azure.StackCloud,
    70  		OutboundType:                "Loadbalancer",
    71  	}
    72  }
    73  
    74  func validGCPPlatform() *gcp.Platform {
    75  	return &gcp.Platform{
    76  		ProjectID: "myProject",
    77  		Region:    "us-east1",
    78  	}
    79  }
    80  
    81  func validIBMCloudPlatform() *ibmcloud.Platform {
    82  	return &ibmcloud.Platform{
    83  		Region: "us-south",
    84  	}
    85  }
    86  
    87  func validSSHKey() string {
    88  	return "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD1+D0ns3LYRPeFK2nqOtVKBGueBQGdBLre5A+afvjaIj/QgtJuwv3rb6Uso8GMPbFlj693/b9BcV0TGxa5lC8cAGKrpxKUPvZ0WLRFLMP5HKBFf6+N4SQR9NKi7Liw8Km1GW9l+s/gMFz/ypANTg8PqvR4yglW+6jJEuKdCy/q14s9kEn4czifBzqiBw60gUiDdWbawl8yF+TxiqeKTCfw4HTeY6j1vui0ROuN2XAWgdH999rNAr1QY8BPMTjQJ5X7jeFgagq7u+snXgWycoDsn4fZP1XL91nQXLdZZgJ3T/qtjUbQt4wUuiqCu4cyN8KRoFQBtX9X7TKU8aH/Kkf+t67zS/SE0ZgvCkNr+iaqYVyHpmBoLh3AaWUYJ2bQ7fx9FvEGLcDYNkwqBED6VwuqB7nw+zGYVouGLs+2UKjfc+A1BOP0Q/2ACEkt1u5iLA+dfEC5nMMThIMNgXpjpsYLsGDKV+e9fEzrTphYtYs/XKaYlG634kGMk7wdgsHoTL0= localhost"
    89  }
    90  
    91  func validPowerVSPlatform() *powervs.Platform {
    92  	return &powervs.Platform{
    93  		Zone: "dal10",
    94  	}
    95  }
    96  
    97  func validVSpherePlatform() *vsphere.Platform {
    98  	return &vsphere.Platform{
    99  		VCenters: []vsphere.VCenter{
   100  			{
   101  				Server:   "test-vcenter",
   102  				Port:     443,
   103  				Username: "test-username",
   104  				Password: "test-password",
   105  				Datacenters: []string{
   106  					"test-datacenter",
   107  				},
   108  			},
   109  		},
   110  		FailureDomains: []vsphere.FailureDomain{
   111  			{
   112  				Name:   "test-east-1a",
   113  				Region: "test-east",
   114  				Zone:   "test-east-1a",
   115  				Server: "test-vcenter",
   116  				Topology: vsphere.Topology{
   117  					Datacenter:     "test-datacenter",
   118  					ComputeCluster: "/test-datacenter/host/test-cluster",
   119  					Datastore:      "/test-datacenter/datastore/test-datastore",
   120  					Networks:       []string{"test-portgroup"},
   121  					ResourcePool:   "/test-datacenter/host/test-cluster/Resources/test-resourcepool",
   122  					Folder:         "/test-datacenter/vm/test-folder",
   123  				},
   124  			},
   125  			{
   126  				Name:   "test-east-2a",
   127  				Region: "test-east",
   128  				Zone:   "test-east-2a",
   129  				Server: "test-vcenter",
   130  				Topology: vsphere.Topology{
   131  					Datacenter:     "test-datacenter",
   132  					ComputeCluster: "/test-datacenter/host/test-cluster",
   133  					Datastore:      "/test-datacenter/datastore/test-datastore",
   134  					Networks:       []string{"test-portgroup"},
   135  					Folder:         "/test-datacenter/vm/test-folder",
   136  				},
   137  			},
   138  		},
   139  	}
   140  }
   141  
   142  func validBareMetalPlatform() *baremetal.Platform {
   143  	iface, _ := net.Interfaces()
   144  	return &baremetal.Platform{
   145  		LibvirtURI:                   "qemu+tcp://192.168.122.1/system",
   146  		ProvisioningNetworkInterface: "ens3",
   147  		ProvisioningNetworkCIDR:      ipnet.MustParseCIDR("192.168.111.0/24"),
   148  		BootstrapProvisioningIP:      "192.168.111.1",
   149  		ClusterProvisioningIP:        "192.168.111.2",
   150  		ProvisioningNetwork:          baremetal.ManagedProvisioningNetwork,
   151  		Hosts: []*baremetal.Host{
   152  			{
   153  				Name:           "host1",
   154  				Role:           "master",
   155  				BootMACAddress: "CA:FE:CA:FE:00:00",
   156  				BMC: baremetal.BMC{
   157  					Username: "root",
   158  					Password: "password",
   159  					Address:  "ipmi://192.168.111.1",
   160  				},
   161  			},
   162  			{
   163  				Name:           "host2",
   164  				Role:           "worker",
   165  				BootMACAddress: "CA:FE:CA:FE:00:01",
   166  				BMC: baremetal.BMC{
   167  					Username: "root",
   168  					Password: "password",
   169  					Address:  "ipmi://192.168.111.2",
   170  				},
   171  			},
   172  		},
   173  		ExternalBridge:         iface[0].Name,
   174  		ProvisioningBridge:     iface[0].Name,
   175  		DefaultMachinePlatform: &baremetal.MachinePool{},
   176  		APIVIPs:                []string{"10.0.0.5"},
   177  		IngressVIPs:            []string{"10.0.0.4"},
   178  	}
   179  }
   180  
   181  func validOpenStackPlatform() *openstack.Platform {
   182  	return &openstack.Platform{
   183  		Cloud:           "test-cloud",
   184  		ExternalNetwork: "test-network",
   185  		DefaultMachinePlatform: &openstack.MachinePool{
   186  			FlavorName: "test-flavor",
   187  		},
   188  		APIVIPs:     []string{"10.0.0.5"},
   189  		IngressVIPs: []string{"10.0.0.4"},
   190  	}
   191  }
   192  
   193  func validNutanixPlatform() *nutanix.Platform {
   194  	return &nutanix.Platform{
   195  		PrismCentral: nutanix.PrismCentral{
   196  			Endpoint: nutanix.PrismEndpoint{Address: "test-pc", Port: 8080},
   197  			Username: "test-username-pc",
   198  			Password: "test-password-pc",
   199  		},
   200  		PrismElements: []nutanix.PrismElement{{
   201  			UUID:     "test-pe-uuid",
   202  			Endpoint: nutanix.PrismEndpoint{Address: "test-pe", Port: 8081},
   203  		}},
   204  		SubnetUUIDs: []string{"test-subnet"},
   205  	}
   206  }
   207  
   208  func validIPv4NetworkingConfig() *types.Networking {
   209  	return &types.Networking{
   210  		NetworkType: "OVNKubernetes",
   211  		MachineNetwork: []types.MachineNetworkEntry{
   212  			{
   213  				CIDR: *ipnet.MustParseCIDR("10.0.0.0/16"),
   214  			},
   215  		},
   216  		ServiceNetwork: []ipnet.IPNet{
   217  			*ipnet.MustParseCIDR("172.30.0.0/16"),
   218  		},
   219  		ClusterNetwork: []types.ClusterNetworkEntry{
   220  			{
   221  				CIDR:       *ipnet.MustParseCIDR("192.168.1.0/24"),
   222  				HostPrefix: 28,
   223  			},
   224  		},
   225  		ClusterNetworkMTU: 0,
   226  	}
   227  }
   228  
   229  func validIPv6NetworkingConfig() *types.Networking {
   230  	return &types.Networking{
   231  		NetworkType: "OVNKubernetes",
   232  		MachineNetwork: []types.MachineNetworkEntry{
   233  			{
   234  				CIDR: *ipnet.MustParseCIDR("ffd0::/48"),
   235  			},
   236  		},
   237  		ServiceNetwork: []ipnet.IPNet{
   238  			*ipnet.MustParseCIDR("ffd1::/112"),
   239  		},
   240  		ClusterNetwork: []types.ClusterNetworkEntry{
   241  			{
   242  				CIDR:       *ipnet.MustParseCIDR("ffd2::/48"),
   243  				HostPrefix: 64,
   244  			},
   245  		},
   246  	}
   247  }
   248  
   249  func validDualStackNetworkingConfig() *types.Networking {
   250  	return &types.Networking{
   251  		NetworkType: "OVNKubernetes",
   252  		MachineNetwork: []types.MachineNetworkEntry{
   253  			{
   254  				CIDR: *ipnet.MustParseCIDR("10.0.0.0/16"),
   255  			},
   256  			{
   257  				CIDR: *ipnet.MustParseCIDR("ffd0::/48"),
   258  			},
   259  		},
   260  		ServiceNetwork: []ipnet.IPNet{
   261  			*ipnet.MustParseCIDR("172.30.0.0/16"),
   262  			*ipnet.MustParseCIDR("ffd1::/112"),
   263  		},
   264  		ClusterNetwork: []types.ClusterNetworkEntry{
   265  			{
   266  				CIDR:       *ipnet.MustParseCIDR("192.168.1.0/24"),
   267  				HostPrefix: 28,
   268  			},
   269  			{
   270  				CIDR:       *ipnet.MustParseCIDR("ffd2::/48"),
   271  				HostPrefix: 64,
   272  			},
   273  		},
   274  	}
   275  }
   276  
   277  func TestValidateInstallConfig(t *testing.T) {
   278  	cases := []struct {
   279  		name          string
   280  		installConfig *types.InstallConfig
   281  		expectedError string
   282  	}{
   283  		{
   284  			name:          "minimal",
   285  			installConfig: validInstallConfig(),
   286  		},
   287  		{
   288  			name: "invalid version",
   289  			installConfig: func() *types.InstallConfig {
   290  				c := validInstallConfig()
   291  				c.APIVersion = "bad-version"
   292  				return c
   293  			}(),
   294  			expectedError: fmt.Sprintf(`^apiVersion: Invalid value: "bad-version": install-config version must be %q`, types.InstallConfigVersion),
   295  		},
   296  		{
   297  			name: "invalid name",
   298  			installConfig: func() *types.InstallConfig {
   299  				c := validInstallConfig()
   300  				c.ObjectMeta.Name = "bad-name-"
   301  				return c
   302  			}(),
   303  			expectedError: `^metadata.name: Invalid value: "bad-name-": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '\.', and must start and end with an alphanumeric character \(e\.g\. 'example\.com', regex used for validation is '\[a-z0-9]\(\[-a-z0-9]\*\[a-z0-9]\)\?\(\\\.\[a-z0-9]\(\[-a-z0-9]\*\[a-z0-9]\)\?\)\*'\)$`,
   304  		},
   305  		{
   306  			name: "invalid ssh key",
   307  			installConfig: func() *types.InstallConfig {
   308  				c := validInstallConfig()
   309  				c.SSHKey = "bad-ssh-key"
   310  				return c
   311  			}(),
   312  			expectedError: `^sshKey: Invalid value: "bad-ssh-key": ssh: no key found$`,
   313  		},
   314  		{
   315  			name: "invalid base domain",
   316  			installConfig: func() *types.InstallConfig {
   317  				c := validInstallConfig()
   318  				c.BaseDomain = ".bad-domain."
   319  				return c
   320  			}(),
   321  			expectedError: `^baseDomain: Invalid value: "\.bad-domain\.": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '\.', and must start and end with an alphanumeric character \(e\.g\. 'example\.com', regex used for validation is '\[a-z0-9]\(\[-a-z0-9]\*\[a-z0-9]\)\?\(\\\.\[a-z0-9]\(\[-a-z0-9]\*\[a-z0-9]\)\?\)\*'\)$`,
   322  		},
   323  		{
   324  			name: "overly long cluster domain",
   325  			installConfig: func() *types.InstallConfig {
   326  				c := validInstallConfig()
   327  				c.ObjectMeta.Name = fmt.Sprintf("test-cluster%042d", 0)
   328  				c.BaseDomain = fmt.Sprintf("test-domain%056d.a%060d.b%060d.c%060d", 0, 0, 0, 0)
   329  				return c
   330  			}(),
   331  			expectedError: `^baseDomain: Invalid value: "` + fmt.Sprintf("test-cluster%042d.test-domain%056d.a%060d.b%060d.c%060d", 0, 0, 0, 0, 0) + `": must be no more than 253 characters$`,
   332  		},
   333  		{
   334  			name: "missing networking",
   335  			installConfig: func() *types.InstallConfig {
   336  				c := validInstallConfig()
   337  				c.Networking = nil
   338  				return c
   339  			}(),
   340  			expectedError: `^networking: Required value: networking is required$`,
   341  		},
   342  		{
   343  			name: "invalid network type",
   344  			installConfig: func() *types.InstallConfig {
   345  				c := validInstallConfig()
   346  				c.Networking.NetworkType = ""
   347  				return c
   348  			}(),
   349  			expectedError: `^networking.networkType: Required value: network provider type required$`,
   350  		},
   351  		{
   352  			name: "missing service network",
   353  			installConfig: func() *types.InstallConfig {
   354  				c := validInstallConfig()
   355  				c.Networking.ServiceNetwork = nil
   356  				return c
   357  			}(),
   358  			expectedError: `^networking\.serviceNetwork: Required value: a service network is required$`,
   359  		},
   360  		{
   361  			name: "invalid service network",
   362  			installConfig: func() *types.InstallConfig {
   363  				c := validInstallConfig()
   364  				c.Networking.ServiceNetwork[0] = *ipnet.MustParseCIDR("13.0.128.0/16")
   365  				return c
   366  			}(),
   367  			expectedError: `^networking\.serviceNetwork\[0\]: Invalid value: "13\.0\.128\.0/16": invalid network address. got 13\.0\.128\.0/16, expecting 13\.0\.0\.0/16$`,
   368  		},
   369  		{
   370  			name: "overlapping service network and machine cidr",
   371  			installConfig: func() *types.InstallConfig {
   372  				c := validInstallConfig()
   373  				c.Networking.ServiceNetwork[0] = *ipnet.MustParseCIDR("10.0.2.0/24")
   374  				return c
   375  			}(),
   376  			expectedError: `^networking\.serviceNetwork\[0\]: Invalid value: "10\.0\.2\.0/24": service network must not overlap with any of the machine networks$`,
   377  		},
   378  		{
   379  			name: "overlapping machine network and machine network",
   380  			installConfig: func() *types.InstallConfig {
   381  				c := validInstallConfig()
   382  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
   383  					{CIDR: *ipnet.MustParseCIDR("13.0.0.0/16")},
   384  					{CIDR: *ipnet.MustParseCIDR("13.0.2.0/24")},
   385  				}
   386  
   387  				return c
   388  			}(),
   389  			// also triggers the only-one-machine-network validation
   390  			expectedError: `^networking\.machineNetwork\[1\]: Invalid value: "13\.0\.2\.0/24": machine network must not overlap with machine network 0$`,
   391  		},
   392  		{
   393  			name: "overlapping service network and service network",
   394  			installConfig: func() *types.InstallConfig {
   395  				c := validInstallConfig()
   396  				c.Networking.ServiceNetwork = []ipnet.IPNet{
   397  					*ipnet.MustParseCIDR("13.0.0.0/16"),
   398  					*ipnet.MustParseCIDR("13.0.2.0/24"),
   399  				}
   400  
   401  				return c
   402  			}(),
   403  			// also triggers the only-one-service-network validation
   404  			expectedError: `^\[networking\.serviceNetwork\[1\]: Invalid value: "13\.0\.2\.0/24": service network must not overlap with service network 0, networking\.serviceNetwork: Invalid value: "13\.0\.0\.0/16, 13\.0\.2\.0/24": only one service network can be specified]$`,
   405  		},
   406  		{
   407  			name: "missing machine networks",
   408  			installConfig: func() *types.InstallConfig {
   409  				c := validInstallConfig()
   410  				c.Networking.MachineNetwork = nil
   411  				return c
   412  			}(),
   413  			expectedError: `^networking\.machineNetwork: Required value: at least one machine network is required$`,
   414  		},
   415  		{
   416  			name: "invalid machine cidr",
   417  			installConfig: func() *types.InstallConfig {
   418  				c := validInstallConfig()
   419  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{{CIDR: *ipnet.MustParseCIDR("11.0.128.0/16")}}
   420  				return c
   421  			}(),
   422  			expectedError: `^networking\.machineNetwork\[0\]: Invalid value: "11\.0\.128\.0/16": invalid network address. got 11\.0\.128\.0/16, expecting 11\.0\.0\.0/16$`,
   423  		},
   424  		{
   425  			name: "invalid cluster network",
   426  			installConfig: func() *types.InstallConfig {
   427  				c := validInstallConfig()
   428  				c.Networking.ClusterNetwork = []types.ClusterNetworkEntry{{CIDR: *ipnet.MustParseCIDR("12.0.128.0/16"), HostPrefix: 23}}
   429  				return c
   430  			}(),
   431  			expectedError: `^networking\.clusterNetwork\[0]\.cidr: Invalid value: "12\.0\.128\.0/16": invalid network address. got 12\.0\.128\.0/16, expecting 12\.0\.0\.0/16$`,
   432  		},
   433  		{
   434  			name: "overlapping cluster network and machine cidr",
   435  			installConfig: func() *types.InstallConfig {
   436  				c := validInstallConfig()
   437  				c.Networking.ClusterNetwork[0].CIDR = *ipnet.MustParseCIDR("10.0.3.0/24")
   438  				return c
   439  			}(),
   440  			expectedError: `^networking\.clusterNetwork\[0]\.cidr: Invalid value: "10\.0\.3\.0/24": cluster network must not overlap with any of the machine networks$`,
   441  		},
   442  		{
   443  			name: "overlapping cluster network and service network",
   444  			installConfig: func() *types.InstallConfig {
   445  				c := validInstallConfig()
   446  				c.Networking.ClusterNetwork[0].CIDR = *ipnet.MustParseCIDR("172.30.2.0/24")
   447  				return c
   448  			}(),
   449  			expectedError: `^networking\.clusterNetwork\[0]\.cidr: Invalid value: "172\.30\.2\.0/24": cluster network must not overlap with service network 0$`,
   450  		},
   451  		{
   452  			name: "overlapping cluster network and cluster network",
   453  			installConfig: func() *types.InstallConfig {
   454  				c := validInstallConfig()
   455  				c.Networking.ClusterNetwork = []types.ClusterNetworkEntry{
   456  					{CIDR: *ipnet.MustParseCIDR("12.0.0.0/16"), HostPrefix: 23},
   457  					{CIDR: *ipnet.MustParseCIDR("12.0.3.0/24"), HostPrefix: 25},
   458  				}
   459  				return c
   460  			}(),
   461  			expectedError: `^networking\.clusterNetwork\[1]\.cidr: Invalid value: "12\.0\.3\.0/24": cluster network must not overlap with cluster network 0$`,
   462  		},
   463  		{
   464  			name: "cluster network host prefix too large",
   465  			installConfig: func() *types.InstallConfig {
   466  				c := validInstallConfig()
   467  				c.Networking.ClusterNetwork[0].CIDR = *ipnet.MustParseCIDR("192.168.1.0/24")
   468  				c.Networking.ClusterNetwork[0].HostPrefix = 23
   469  				return c
   470  			}(),
   471  			expectedError: `^networking\.clusterNetwork\[0]\.hostPrefix: Invalid value: 23: cluster network host subnetwork prefix must not be larger size than CIDR 192.168.1.0/24$`,
   472  		},
   473  		{
   474  			name: "cluster network host prefix unset",
   475  			installConfig: func() *types.InstallConfig {
   476  				c := validInstallConfig()
   477  				c.Networking.NetworkType = "OVNKubernetes"
   478  				c.Networking.ClusterNetwork[0].CIDR = *ipnet.MustParseCIDR("192.168.1.0/24")
   479  				c.Networking.ClusterNetwork[0].HostPrefix = 0
   480  				return c
   481  			}(),
   482  			expectedError: `^networking\.clusterNetwork\[0]\.hostPrefix: Invalid value: 0: cluster network host subnetwork prefix must not be larger size than CIDR 192.168.1.0/24$`,
   483  		},
   484  		{
   485  			name: "cluster network host prefix unset ignored",
   486  			installConfig: func() *types.InstallConfig {
   487  				c := validInstallConfig()
   488  				c.Networking.NetworkType = "HostPrefixNotRequiredPlugin"
   489  				c.Networking.ClusterNetwork[0].CIDR = *ipnet.MustParseCIDR("192.168.1.0/24")
   490  				return c
   491  			}(),
   492  			expectedError: ``,
   493  		},
   494  		{
   495  			name: "networking clusterNetworkMTU - valid high limit ovn",
   496  			installConfig: func() *types.InstallConfig {
   497  				c := validInstallConfig()
   498  				c.Networking.NetworkType = string(operv1.NetworkTypeOVNKubernetes)
   499  				c.Networking.ClusterNetworkMTU = 8901
   500  				fmt.Println(c.Platform.Name())
   501  				return c
   502  			}(),
   503  		},
   504  		{
   505  			name: "networking clusterNetworkMTU - valid low limit",
   506  			installConfig: func() *types.InstallConfig {
   507  				c := validInstallConfig()
   508  				c.Networking.NetworkType = string(operv1.NetworkTypeOVNKubernetes)
   509  				c.Networking.ClusterNetworkMTU = 1000
   510  				return c
   511  			}(),
   512  		},
   513  		{
   514  			name: "networking clusterNetworkMTU - invalid value lower",
   515  			installConfig: func() *types.InstallConfig {
   516  				c := validInstallConfig()
   517  				c.Networking.ClusterNetworkMTU = 999
   518  				return c
   519  			}(),
   520  			expectedError: `^networking\.clusterNetworkMTU: Invalid value: 999: cluster network MTU is lower than the minimum value of 1000$`,
   521  		},
   522  		{
   523  			name: "networking clusterNetworkMTU - invalid value ovn",
   524  			installConfig: func() *types.InstallConfig {
   525  				c := validInstallConfig()
   526  				c.Networking.NetworkType = string(operv1.NetworkTypeOVNKubernetes)
   527  				c.Networking.ClusterNetworkMTU = 8951
   528  				return c
   529  			}(),
   530  			expectedError: `^networking\.clusterNetworkMTU: Invalid value: 8951: cluster network MTU exceeds the maximum value with the network plugin OVNKubernetes of 8901$`,
   531  		},
   532  		{
   533  			name: "networking clusterNetworkMTU - invalid jumbo value",
   534  			installConfig: func() *types.InstallConfig {
   535  				c := validInstallConfig()
   536  				c.Networking.ClusterNetworkMTU = 9002
   537  				return c
   538  			}(),
   539  			expectedError: `^networking\.clusterNetworkMTU: Invalid value: 9002: cluster network MTU exceeds the maximum value of 9001$`,
   540  		},
   541  		{
   542  			name: "networking clusterNetworkMTU - invalid for non-aws",
   543  			installConfig: func() *types.InstallConfig {
   544  				c := validInstallConfig()
   545  				c.Networking.NetworkType = string(operv1.NetworkTypeOVNKubernetes)
   546  				c.Networking.ClusterNetworkMTU = 8901
   547  				c.Platform = types.Platform{
   548  					None: &none.Platform{},
   549  				}
   550  				return c
   551  			}(),
   552  			expectedError: `^networking\.clusterNetworkMTU: Invalid value: 8901: cluster network MTU is allowed only in AWS deployments`,
   553  		},
   554  		{
   555  			name: "networking clusterNetworkMTU - unsupported network type",
   556  			installConfig: func() *types.InstallConfig {
   557  				c := validInstallConfig()
   558  				c.Networking.NetworkType = string(operv1.NetworkTypeOpenShiftSDN)
   559  				c.Networking.ClusterNetworkMTU = 8000
   560  				return c
   561  			}(),
   562  			expectedError: `networking.networkType: Invalid value: "OpenShiftSDN": networkType OpenShiftSDN is not supported, please use OVNKubernetes, networking.clusterNetworkMTU: Invalid value: 8000: cluster network MTU is not valid with network plugin OpenShiftSDN`,
   563  		},
   564  		{
   565  			name: "missing control plane",
   566  			installConfig: func() *types.InstallConfig {
   567  				c := validInstallConfig()
   568  				c.ControlPlane = nil
   569  				return c
   570  			}(),
   571  			expectedError: `^controlPlane: Required value: controlPlane is required$`,
   572  		},
   573  		{
   574  			name: "control plane with 0 replicas",
   575  			installConfig: func() *types.InstallConfig {
   576  				c := validInstallConfig()
   577  				c.ControlPlane.Replicas = pointer.Int64Ptr(0)
   578  				return c
   579  			}(),
   580  			expectedError: `^controlPlane.replicas: Invalid value: 0: number of control plane replicas must be positive$`,
   581  		},
   582  		{
   583  			name: "invalid control plane",
   584  			installConfig: func() *types.InstallConfig {
   585  				c := validInstallConfig()
   586  				c.ControlPlane.Replicas = nil
   587  				return c
   588  			}(),
   589  			expectedError: `^controlPlane.replicas: Required value: replicas is required$`,
   590  		},
   591  		{
   592  			name: "missing compute",
   593  			installConfig: func() *types.InstallConfig {
   594  				c := validInstallConfig()
   595  				c.Compute = nil
   596  				return c
   597  			}(),
   598  		},
   599  		{
   600  			name: "empty compute",
   601  			installConfig: func() *types.InstallConfig {
   602  				c := validInstallConfig()
   603  				c.Compute = []types.MachinePool{}
   604  				return c
   605  			}(),
   606  		},
   607  		{
   608  			name: "duplicate compute",
   609  			installConfig: func() *types.InstallConfig {
   610  				c := validInstallConfig()
   611  				c.Compute = []types.MachinePool{
   612  					*validMachinePool("worker"),
   613  					*validMachinePool("worker"),
   614  				}
   615  				return c
   616  			}(),
   617  			expectedError: `^compute\[1\]\.name: Duplicate value: "worker"$`,
   618  		},
   619  		{
   620  			name: "no compute replicas",
   621  			installConfig: func() *types.InstallConfig {
   622  				c := validInstallConfig()
   623  				c.Compute = []types.MachinePool{
   624  					func() types.MachinePool {
   625  						p := *validMachinePool("worker")
   626  						p.Replicas = pointer.Int64Ptr(0)
   627  						return p
   628  					}(),
   629  				}
   630  				return c
   631  			}(),
   632  		},
   633  		{
   634  			name: "missing platform",
   635  			installConfig: func() *types.InstallConfig {
   636  				c := validInstallConfig()
   637  				c.Platform = types.Platform{}
   638  				return c
   639  			}(),
   640  			expectedError: `^platform: Invalid value: "": must specify one of the platforms \(aws, azure, baremetal, external, gcp, ibmcloud, none, nutanix, openstack, powervs, vsphere\)$`,
   641  		},
   642  		{
   643  			name: "multiple platforms",
   644  			installConfig: func() *types.InstallConfig {
   645  				c := validInstallConfig()
   646  				c.Platform.IBMCloud = validIBMCloudPlatform()
   647  				return c
   648  			}(),
   649  			expectedError: `^platform: Invalid value: "aws": must only specify a single type of platform; cannot use both "aws" and "ibmcloud"$`,
   650  		},
   651  		{
   652  			name: "invalid aws platform",
   653  			installConfig: func() *types.InstallConfig {
   654  				c := validInstallConfig()
   655  				c.Platform = types.Platform{
   656  					AWS: &aws.Platform{},
   657  				}
   658  				return c
   659  			}(),
   660  			expectedError: `^platform\.aws\.region: Required value: region must be specified$`,
   661  		},
   662  		{
   663  			name: "valid none platform",
   664  			installConfig: func() *types.InstallConfig {
   665  				c := validInstallConfig()
   666  				c.Platform = types.Platform{
   667  					None: &none.Platform{},
   668  				}
   669  				return c
   670  			}(),
   671  		},
   672  		{
   673  			name: "valid openstack platform",
   674  			installConfig: func() *types.InstallConfig {
   675  				c := validInstallConfig()
   676  				c.Platform = types.Platform{
   677  					OpenStack: validOpenStackPlatform(),
   678  				}
   679  				return c
   680  			}(),
   681  		},
   682  		{
   683  			name: "valid baremetal platform",
   684  			installConfig: func() *types.InstallConfig {
   685  				c := validInstallConfig()
   686  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "v4.11"}
   687  				c.Capabilities.AdditionalEnabledCapabilities = append(c.Capabilities.AdditionalEnabledCapabilities, configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityOperatorLifecycleManager)
   688  				c.Platform = types.Platform{
   689  					BareMetal: validBareMetalPlatform(),
   690  				}
   691  				return c
   692  			}(),
   693  		},
   694  		{
   695  			name: "valid vsphere platform",
   696  			installConfig: func() *types.InstallConfig {
   697  				c := validInstallConfig()
   698  				c.Platform = types.Platform{
   699  					VSphere: validVSpherePlatform(),
   700  				}
   701  				return c
   702  			}(),
   703  		},
   704  		{
   705  			name: "invalid vsphere platform",
   706  			installConfig: func() *types.InstallConfig {
   707  				c := validInstallConfig()
   708  				c.Platform = types.Platform{
   709  					VSphere: validVSpherePlatform(),
   710  				}
   711  				c.Platform.VSphere.VCenters[0].Server = ""
   712  				return c
   713  			}(),
   714  			expectedError: `platform\.vsphere\.vcenters\[0]\.server: Required value: must be the domain name or IP address of the vCenter(.*)`,
   715  		},
   716  		{
   717  			name: "invalid vsphere folder",
   718  			installConfig: func() *types.InstallConfig {
   719  				c := validInstallConfig()
   720  				c.Platform = types.Platform{
   721  					VSphere: validVSpherePlatform(),
   722  				}
   723  				c.Platform.VSphere.FailureDomains[0].Topology.Folder = "my-folder"
   724  				return c
   725  			}(),
   726  			expectedError: `^platform\.vsphere\.failureDomains\.topology.folder: Invalid value: "my-folder": full path of folder must be provided in format /<datacenter>/vm/<folder>$`,
   727  		},
   728  		{
   729  			name: "invalid vsphere resource pool",
   730  			installConfig: func() *types.InstallConfig {
   731  				c := validInstallConfig()
   732  				c.Platform = types.Platform{
   733  					VSphere: validVSpherePlatform(),
   734  				}
   735  				c.Platform.VSphere.FailureDomains[0].Topology.ResourcePool = "my-resource-pool"
   736  				return c
   737  			}(),
   738  			expectedError: `^platform\.vsphere\.failureDomains\.topology\.resourcePool: Invalid value: "my-resource-pool": full path of resource pool must be provided in format /<datacenter>/host/<cluster>/\.\.\.$`,
   739  		},
   740  		{
   741  			name: "empty proxy settings",
   742  			installConfig: func() *types.InstallConfig {
   743  				c := validInstallConfig()
   744  				c.Proxy.HTTPProxy = ""
   745  				c.Proxy.HTTPSProxy = ""
   746  				c.Proxy.NoProxy = ""
   747  				return c
   748  			}(),
   749  			expectedError: `^proxy: Required value: must include httpProxy or httpsProxy$`,
   750  		},
   751  		{
   752  			name: "invalid HTTPProxy",
   753  			installConfig: func() *types.InstallConfig {
   754  				c := validInstallConfig()
   755  				c.Proxy.HTTPProxy = "http://bad%20uri"
   756  				return c
   757  			}(),
   758  			expectedError: `^proxy.httpProxy: Invalid value: "http://bad%20uri": parse "http://bad%20uri": invalid URL escape "%20"$`,
   759  		},
   760  		{
   761  			name: "invalid HTTPProxy Schema missing",
   762  			installConfig: func() *types.InstallConfig {
   763  				c := validInstallConfig()
   764  				c.Proxy.HTTPProxy = "http//baduri"
   765  				return c
   766  			}(),
   767  			expectedError: `^proxy.httpProxy: Invalid value: "http//baduri": parse "http//baduri": invalid URI for request$`,
   768  		},
   769  		{
   770  			name: "HTTPProxy with port overlapping with Cluster Networks",
   771  			installConfig: func() *types.InstallConfig {
   772  				c := validInstallConfig()
   773  				c.Proxy.HTTPProxy = "http://192.168.1.25:3030"
   774  				c.Networking = validIPv4NetworkingConfig()
   775  				return c
   776  			}(),
   777  			expectedError: `^proxy.httpProxy: Invalid value: "http://192.168.1.25:3030": proxy value is part of the cluster networks$`,
   778  		},
   779  		{
   780  			name: "overlapping HTTPProxy and Cluster Networks",
   781  			installConfig: func() *types.InstallConfig {
   782  				c := validInstallConfig()
   783  				c.Proxy.HTTPProxy = "http://192.168.1.25"
   784  				c.Networking = validIPv4NetworkingConfig()
   785  				return c
   786  			}(),
   787  			expectedError: `^proxy.httpProxy: Invalid value: "http://192.168.1.25": proxy value is part of the cluster networks$`,
   788  		},
   789  		{
   790  			name: "non-overlapping HTTPProxy and Cluster Networks",
   791  			installConfig: func() *types.InstallConfig {
   792  				c := validInstallConfig()
   793  				c.Proxy.HTTPProxy = "http://192.169.1.25"
   794  				c.Networking = validIPv4NetworkingConfig()
   795  				return c
   796  			}(),
   797  		},
   798  		{
   799  			name: "overlapping HTTPProxy and more than one Cluster Networks",
   800  			installConfig: func() *types.InstallConfig {
   801  				c := validInstallConfig()
   802  				c.Proxy.HTTPProxy = "http://192.168.1.25"
   803  				c.Networking = validIPv4NetworkingConfig()
   804  				c.ClusterNetwork = append(c.ClusterNetwork, []types.ClusterNetworkEntry{
   805  					{
   806  						CIDR:       *ipnet.MustParseCIDR("192.168.0.0/16"),
   807  						HostPrefix: 28,
   808  					},
   809  				}...,
   810  				)
   811  				return c
   812  			}(),
   813  			expectedError: `^\Q[networking.clusterNetwork[1].cidr: Invalid value: "192.168.0.0/16": cluster network must not overlap with cluster network 0, proxy.httpProxy: Invalid value: "http://192.168.1.25": proxy value is part of the cluster networks]\E$`,
   814  		},
   815  		{
   816  			name: "non-overlapping HTTPProxy and Service Networks",
   817  			installConfig: func() *types.InstallConfig {
   818  				c := validInstallConfig()
   819  				c.Proxy.HTTPProxy = "http://172.31.0.25"
   820  				c.Networking = validIPv4NetworkingConfig()
   821  				return c
   822  			}(),
   823  		},
   824  		{
   825  			name: "HTTPProxy with port overlapping with Service Networks",
   826  			installConfig: func() *types.InstallConfig {
   827  				c := validInstallConfig()
   828  				c.Proxy.HTTPProxy = "http://172.30.0.25:3030"
   829  				c.Networking = validIPv4NetworkingConfig()
   830  				return c
   831  			}(),
   832  			expectedError: `^proxy.httpProxy: Invalid value: "http://172.30.0.25:3030": proxy value is part of the service networks$`,
   833  		},
   834  		{
   835  			name: "overlapping HTTPProxy and Service Networks",
   836  			installConfig: func() *types.InstallConfig {
   837  				c := validInstallConfig()
   838  				c.Proxy.HTTPProxy = "http://172.30.0.25"
   839  				c.Networking = validIPv4NetworkingConfig()
   840  				return c
   841  			}(),
   842  			expectedError: `^proxy.httpProxy: Invalid value: "http://172.30.0.25": proxy value is part of the service networks$`,
   843  		},
   844  		{
   845  			name: "overlapping HTTPProxy and more than one Service Networks",
   846  			installConfig: func() *types.InstallConfig {
   847  				c := validInstallConfig()
   848  				c.Proxy.HTTPProxy = "http://172.30.0.25"
   849  				c.Networking = validIPv4NetworkingConfig()
   850  				c.ServiceNetwork = append(c.ServiceNetwork, []ipnet.IPNet{
   851  					*ipnet.MustParseCIDR("172.30.1.0/24"),
   852  				}...,
   853  				)
   854  				return c
   855  			}(),
   856  			expectedError: `^\Q[networking.serviceNetwork[1]: Invalid value: "172.30.1.0/24": service network must not overlap with service network 0, networking.serviceNetwork: Invalid value: "172.30.0.0/16, 172.30.1.0/24": only one service network can be specified, proxy.httpProxy: Invalid value: "http://172.30.0.25": proxy value is part of the service networks]\E$`,
   857  		},
   858  		{
   859  			name: "non-overlapping HTTPSProxy and Cluster Networks",
   860  			installConfig: func() *types.InstallConfig {
   861  				c := validInstallConfig()
   862  				c.Proxy.HTTPSProxy = "http://192.168.2.25"
   863  				c.Networking = validIPv4NetworkingConfig()
   864  				return c
   865  			}(),
   866  		},
   867  		{
   868  			name: "HTTPSProxy with port overlapping with Cluster Networks",
   869  			installConfig: func() *types.InstallConfig {
   870  				c := validInstallConfig()
   871  				c.Proxy.HTTPSProxy = "http://192.168.1.25:3030"
   872  				c.Networking = validIPv4NetworkingConfig()
   873  				return c
   874  			}(),
   875  			expectedError: `^proxy.httpsProxy: Invalid value: "http://192.168.1.25:3030": proxy value is part of the cluster networks$`,
   876  		},
   877  		{
   878  			name: "overlapping HTTPSProxy and Cluster Networks",
   879  			installConfig: func() *types.InstallConfig {
   880  				c := validInstallConfig()
   881  				c.Proxy.HTTPSProxy = "http://192.168.1.25"
   882  				c.Networking = validIPv4NetworkingConfig()
   883  				return c
   884  			}(),
   885  			expectedError: `^proxy.httpsProxy: Invalid value: "http://192.168.1.25": proxy value is part of the cluster networks$`,
   886  		},
   887  		{
   888  			name: "overlapping HTTPSProxy and more than one Cluster Networks",
   889  			installConfig: func() *types.InstallConfig {
   890  				c := validInstallConfig()
   891  				c.Proxy.HTTPSProxy = "http://192.168.1.25"
   892  				c.Networking = validIPv4NetworkingConfig()
   893  				c.ClusterNetwork = append(c.ClusterNetwork, []types.ClusterNetworkEntry{
   894  					{
   895  						CIDR:       *ipnet.MustParseCIDR("192.168.0.0/16"),
   896  						HostPrefix: 28,
   897  					},
   898  				}...,
   899  				)
   900  				return c
   901  			}(),
   902  			expectedError: `^\Q[networking.clusterNetwork[1].cidr: Invalid value: "192.168.0.0/16": cluster network must not overlap with cluster network 0, proxy.httpsProxy: Invalid value: "http://192.168.1.25": proxy value is part of the cluster networks]\E$`,
   903  		},
   904  		{
   905  			name: "overlapping HTTPSProxy and Service Networks",
   906  			installConfig: func() *types.InstallConfig {
   907  				c := validInstallConfig()
   908  				c.Proxy.HTTPSProxy = "http://172.30.0.25"
   909  				c.Networking = validIPv4NetworkingConfig()
   910  				return c
   911  			}(),
   912  			expectedError: `^proxy.httpsProxy: Invalid value: "http://172.30.0.25": proxy value is part of the service networks$`,
   913  		},
   914  		{
   915  			name: "HTTPSProxy with port overlapping with Service Networks",
   916  			installConfig: func() *types.InstallConfig {
   917  				c := validInstallConfig()
   918  				c.Proxy.HTTPSProxy = "http://172.30.0.25:3030"
   919  				c.Networking = validIPv4NetworkingConfig()
   920  				return c
   921  			}(),
   922  			expectedError: `^proxy.httpsProxy: Invalid value: "http://172.30.0.25:3030": proxy value is part of the service networks$`,
   923  		},
   924  		{
   925  			name: "overlapping HTTPSProxy and more than one Service Networks",
   926  			installConfig: func() *types.InstallConfig {
   927  				c := validInstallConfig()
   928  				c.Proxy.HTTPSProxy = "http://172.30.0.25"
   929  				c.Networking = validIPv4NetworkingConfig()
   930  				c.ServiceNetwork = append(c.ServiceNetwork, []ipnet.IPNet{
   931  					*ipnet.MustParseCIDR("172.30.1.0/24"),
   932  				}...,
   933  				)
   934  				return c
   935  			}(),
   936  			expectedError: `^\Q[networking.serviceNetwork[1]: Invalid value: "172.30.1.0/24": service network must not overlap with service network 0, networking.serviceNetwork: Invalid value: "172.30.0.0/16, 172.30.1.0/24": only one service network can be specified, proxy.httpsProxy: Invalid value: "http://172.30.0.25": proxy value is part of the service networks]\E$`,
   937  		},
   938  		{
   939  			name: "invalid HTTPProxy Schema different schema",
   940  			installConfig: func() *types.InstallConfig {
   941  				c := validInstallConfig()
   942  				c.Proxy.HTTPProxy = "ftp://baduri"
   943  				return c
   944  			}(),
   945  			expectedError: `^proxy.httpProxy: Unsupported value: "ftp": supported values: "http"$`,
   946  		},
   947  		{
   948  			name: "invalid HTTPSProxy",
   949  			installConfig: func() *types.InstallConfig {
   950  				c := validInstallConfig()
   951  				c.Proxy.HTTPSProxy = "https://bad%20uri"
   952  				return c
   953  			}(),
   954  			expectedError: `^proxy.httpsProxy: Invalid value: "https://bad%20uri": parse "https://bad%20uri": invalid URL escape "%20"$`,
   955  		},
   956  		{
   957  			name: "invalid HTTPSProxy Schema missing",
   958  			installConfig: func() *types.InstallConfig {
   959  				c := validInstallConfig()
   960  				c.Proxy.HTTPSProxy = "http//baduri"
   961  				return c
   962  			}(),
   963  			expectedError: `^proxy.httpsProxy: Invalid value: "http//baduri": parse "http//baduri": invalid URI for request$`,
   964  		},
   965  		{
   966  			name: "invalid HTTPSProxy Schema different schema",
   967  			installConfig: func() *types.InstallConfig {
   968  				c := validInstallConfig()
   969  				c.Proxy.HTTPSProxy = "ftp://baduri"
   970  				return c
   971  			}(),
   972  			expectedError: `^proxy.httpsProxy: Unsupported value: "ftp": supported values: "http", "https"$`,
   973  		},
   974  		{
   975  			name: "invalid NoProxy domain",
   976  			installConfig: func() *types.InstallConfig {
   977  				c := validInstallConfig()
   978  				c.Proxy.NoProxy = "good-no-proxy.com,*.bad-proxy"
   979  				return c
   980  			}(),
   981  			expectedError: `^\Qproxy.noProxy: Invalid value: "good-no-proxy.com,*.bad-proxy": each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element 1 "*.bad-proxy"\E$`,
   982  		},
   983  		{
   984  			name: "invalid NoProxy spaces",
   985  			installConfig: func() *types.InstallConfig {
   986  				c := validInstallConfig()
   987  				c.Proxy.NoProxy = "good-no-proxy.com, *.bad-proxy"
   988  				return c
   989  			}(),
   990  			expectedError: `^\Q[proxy.noProxy: Invalid value: "good-no-proxy.com, *.bad-proxy": noProxy must not have spaces, proxy.noProxy: Invalid value: "good-no-proxy.com, *.bad-proxy": each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element 1 "*.bad-proxy"]\E$`,
   991  		},
   992  		{
   993  			name: "invalid NoProxy CIDR",
   994  			installConfig: func() *types.InstallConfig {
   995  				c := validInstallConfig()
   996  				c.Proxy.NoProxy = "good-no-proxy.com,172.bad.CIDR.0/16"
   997  				return c
   998  			}(),
   999  			expectedError: `^\Qproxy.noProxy: Invalid value: "good-no-proxy.com,172.bad.CIDR.0/16": each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element 1 "172.bad.CIDR.0/16"\E$`,
  1000  		},
  1001  		{
  1002  			name: "invalid NoProxy domain & CIDR",
  1003  			installConfig: func() *types.InstallConfig {
  1004  				c := validInstallConfig()
  1005  				c.Proxy.NoProxy = "good-no-proxy.com,a-good-one,*.bad-proxy.,another,172.bad.CIDR.0/16,good-end"
  1006  				return c
  1007  			}(),
  1008  			expectedError: `^\Q[proxy.noProxy: Invalid value: "good-no-proxy.com,a-good-one,*.bad-proxy.,another,172.bad.CIDR.0/16,good-end": each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element 2 "*.bad-proxy.", proxy.noProxy: Invalid value: "good-no-proxy.com,a-good-one,*.bad-proxy.,another,172.bad.CIDR.0/16,good-end": each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element 4 "172.bad.CIDR.0/16"]\E$`,
  1009  		},
  1010  		{
  1011  			name: "valid * NoProxy",
  1012  			installConfig: func() *types.InstallConfig {
  1013  				c := validInstallConfig()
  1014  				c.Proxy.NoProxy = "*"
  1015  				return c
  1016  			}(),
  1017  		},
  1018  		{
  1019  			name: "valid GCP platform",
  1020  			installConfig: func() *types.InstallConfig {
  1021  				c := validInstallConfig()
  1022  				c.Platform = types.Platform{
  1023  					GCP: validGCPPlatform(),
  1024  				}
  1025  				return c
  1026  			}(),
  1027  		},
  1028  		{
  1029  			name: "invalid GCP cluster name",
  1030  			installConfig: func() *types.InstallConfig {
  1031  				c := validInstallConfig()
  1032  				c.Platform = types.Platform{
  1033  					GCP: validGCPPlatform(),
  1034  				}
  1035  				c.ObjectMeta.Name = "1-invalid-cluster"
  1036  				return c
  1037  			}(),
  1038  			expectedError: `^metadata\.name: Invalid value: "1-invalid-cluster": cluster name must begin with a lower-case letter$`,
  1039  		},
  1040  		{
  1041  			name: "valid ibmcloud platform",
  1042  			installConfig: func() *types.InstallConfig {
  1043  				c := validInstallConfig()
  1044  				c.Platform = types.Platform{
  1045  					IBMCloud: validIBMCloudPlatform(),
  1046  				}
  1047  				return c
  1048  			}(),
  1049  		},
  1050  		{
  1051  			name: "invalid ibmcloud platform",
  1052  			installConfig: func() *types.InstallConfig {
  1053  				c := validInstallConfig()
  1054  				c.Platform = types.Platform{
  1055  					IBMCloud: &ibmcloud.Platform{},
  1056  				}
  1057  				return c
  1058  			}(),
  1059  			expectedError: `^\Qplatform.ibmcloud.region: Required value: region must be specified\E$`,
  1060  		},
  1061  		{
  1062  			name: "valid powervs platform",
  1063  			installConfig: func() *types.InstallConfig {
  1064  				c := validInstallConfig()
  1065  				c.SSHKey = validSSHKey()
  1066  				c.Platform = types.Platform{
  1067  					PowerVS: validPowerVSPlatform(),
  1068  				}
  1069  				return c
  1070  			}(),
  1071  		},
  1072  		{
  1073  			name: "valid powervs platform manual credential mod",
  1074  			installConfig: func() *types.InstallConfig {
  1075  				c := validInstallConfig()
  1076  				c.SSHKey = validSSHKey()
  1077  				c.Platform = types.Platform{
  1078  					PowerVS: validPowerVSPlatform(),
  1079  				}
  1080  				c.CredentialsMode = types.ManualCredentialsMode
  1081  				return c
  1082  			}(),
  1083  		},
  1084  		{
  1085  			name: "invalid powervs platform mint credential mod",
  1086  			installConfig: func() *types.InstallConfig {
  1087  				c := validInstallConfig()
  1088  				c.SSHKey = validSSHKey()
  1089  				c.Platform = types.Platform{
  1090  					PowerVS: validPowerVSPlatform(),
  1091  				}
  1092  				c.CredentialsMode = types.MintCredentialsMode
  1093  				return c
  1094  			}(),
  1095  			expectedError: `^credentialsMode: Unsupported value: "Mint": supported values: "Manual"$`,
  1096  		},
  1097  		{
  1098  			name: "invalid powervs platform",
  1099  			installConfig: func() *types.InstallConfig {
  1100  				c := validInstallConfig()
  1101  				c.SSHKey = validSSHKey()
  1102  				c.Platform = types.Platform{
  1103  					PowerVS: &powervs.Platform{},
  1104  				}
  1105  				return c
  1106  			}(),
  1107  			expectedError: `^\Qplatform.powervs.zone: Required value: zone must be specified\E$`,
  1108  		},
  1109  		{
  1110  			name: "valid azurestack platform",
  1111  			installConfig: func() *types.InstallConfig {
  1112  				c := validInstallConfig()
  1113  				c.Platform = types.Platform{
  1114  					Azure: validAzureStackPlatform(),
  1115  				}
  1116  				return c
  1117  			}(),
  1118  		},
  1119  		{
  1120  			name: "invalid azurestack platform mint credentials mod",
  1121  			installConfig: func() *types.InstallConfig {
  1122  				c := validInstallConfig()
  1123  				c.Platform = types.Platform{
  1124  					Azure: validAzureStackPlatform(),
  1125  				}
  1126  				c.CredentialsMode = types.MintCredentialsMode
  1127  				return c
  1128  			}(),
  1129  			expectedError: `^credentialsMode: Unsupported value: "Mint": supported values: "Manual"$`,
  1130  		},
  1131  		{
  1132  			name: "release image source is not valid",
  1133  			installConfig: func() *types.InstallConfig {
  1134  				c := validInstallConfig()
  1135  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1136  					Source: "ocp/release-x.y",
  1137  				}}
  1138  				return c
  1139  			}(),
  1140  			expectedError: `^imageContentSources\[0\]\.source: Invalid value: "ocp/release-x\.y": the repository provided is invalid: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, \'\-\' or \'\.\', and must start and end with an alphanumeric character \(e.g. \'example\.com\', regex used for validation is \'\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\(\\\.\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\)\*\'\)`,
  1141  		},
  1142  		{
  1143  			name: "release image source's mirror is not valid",
  1144  			installConfig: func() *types.InstallConfig {
  1145  				c := validInstallConfig()
  1146  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1147  					Source:  "q.io/ocp/release-x.y",
  1148  					Mirrors: []string{"ocp/openshift-x.y"},
  1149  				}}
  1150  				return c
  1151  			}(),
  1152  			expectedError: `^imageContentSources\[0\]\.mirrors\[0\]: Invalid value: "ocp/openshift-x\.y": the repository provided is invalid: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, \'\-\' or \'\.\', and must start and end with an alphanumeric character \(e.g. \'example\.com\', regex used for validation is \'\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\(\\\.\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\)\*\'\)`,
  1153  		},
  1154  		{
  1155  			name: "release image source's mirror is valid",
  1156  			installConfig: func() *types.InstallConfig {
  1157  				c := validInstallConfig()
  1158  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1159  					Source:  "q.io/ocp/release-x.y",
  1160  					Mirrors: []string{"mirror.example.com:5000"},
  1161  				}}
  1162  				return c
  1163  			}(),
  1164  		},
  1165  		{
  1166  			name: "release image source is not repository but reference by digest",
  1167  			installConfig: func() *types.InstallConfig {
  1168  				c := validInstallConfig()
  1169  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1170  					Source: "quay.io/ocp/release-x.y@sha256:397c867cc10bcc90cf05ae9b71dd3de6000535e27cb6c704d9f503879202582c",
  1171  				}}
  1172  				return c
  1173  			}(),
  1174  			expectedError: `^imageContentSources\[0\]\.source: Invalid value: "quay\.io/ocp/release-x\.y@sha256:397c867cc10bcc90cf05ae9b71dd3de6000535e27cb6c704d9f503879202582c": must be repository--not reference$`,
  1175  		},
  1176  		{
  1177  			name: "release image source is not repository but reference by tag",
  1178  			installConfig: func() *types.InstallConfig {
  1179  				c := validInstallConfig()
  1180  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1181  					Source: "quay.io/ocp/release-x.y:latest",
  1182  				}}
  1183  				return c
  1184  			}(),
  1185  			expectedError: `^imageContentSources\[0\]\.source: Invalid value: "quay\.io/ocp/release-x\.y:latest": must be repository--not reference$`,
  1186  		},
  1187  		{
  1188  			name: "valid release image source",
  1189  			installConfig: func() *types.InstallConfig {
  1190  				c := validInstallConfig()
  1191  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1192  					Source: "quay.io/ocp/release-x.y",
  1193  				}}
  1194  				return c
  1195  			}(),
  1196  		},
  1197  		{
  1198  			name: "release image source is not valid ImageDigestSource",
  1199  			installConfig: func() *types.InstallConfig {
  1200  				c := validInstallConfig()
  1201  				c.ImageDigestSources = []types.ImageDigestSource{{
  1202  					Source: "ocp/release-x.y",
  1203  				}}
  1204  				return c
  1205  			}(),
  1206  			expectedError: `^imageDigestSources\[0\]\.source: Invalid value: "ocp/release-x\.y": the repository provided is invalid: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, \'\-\' or \'\.\', and must start and end with an alphanumeric character \(e.g. \'example\.com\', regex used for validation is \'\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\(\\\.\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\)\*\'\)`,
  1207  		},
  1208  		{
  1209  			name: "release image source's mirror is not valid ImageDigestSource",
  1210  			installConfig: func() *types.InstallConfig {
  1211  				c := validInstallConfig()
  1212  				c.ImageDigestSources = []types.ImageDigestSource{{
  1213  					Source:  "q.io/ocp/release-x.y",
  1214  					Mirrors: []string{"ocp/openshift-x.y"},
  1215  				}}
  1216  				return c
  1217  			}(),
  1218  			expectedError: `^imageDigestSources\[0\]\.mirrors\[0\]: Invalid value: "ocp/openshift-x\.y": the repository provided is invalid: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, \'\-\' or \'\.\', and must start and end with an alphanumeric character \(e.g. \'example\.com\', regex used for validation is \'\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\(\\\.\[a\-z0\-9\]\(\[\-a\-z0\-9\]\*\[a\-z0\-9\]\)\?\)\*\'\)`,
  1219  		},
  1220  		{
  1221  			name: "release image source's mirror is valid ImageDigestSource",
  1222  			installConfig: func() *types.InstallConfig {
  1223  				c := validInstallConfig()
  1224  				c.ImageDigestSources = []types.ImageDigestSource{{
  1225  					Source:  "q.io/ocp/release-x.y",
  1226  					Mirrors: []string{"mirror.example.com:5000"},
  1227  				}}
  1228  				return c
  1229  			}(),
  1230  		},
  1231  		{
  1232  			name: "release image source is not repository but reference by digest ImageDigestSource",
  1233  			installConfig: func() *types.InstallConfig {
  1234  				c := validInstallConfig()
  1235  				c.ImageDigestSources = []types.ImageDigestSource{{
  1236  					Source: "quay.io/ocp/release-x.y@sha256:397c867cc10bcc90cf05ae9b71dd3de6000535e27cb6c704d9f503879202582c",
  1237  				}}
  1238  				return c
  1239  			}(),
  1240  			expectedError: `^imageDigestSources\[0\]\.source: Invalid value: "quay\.io/ocp/release-x\.y@sha256:397c867cc10bcc90cf05ae9b71dd3de6000535e27cb6c704d9f503879202582c": must be repository--not reference$`,
  1241  		},
  1242  		{
  1243  			name: "release image source is not repository but reference by tag ImageDigestSource",
  1244  			installConfig: func() *types.InstallConfig {
  1245  				c := validInstallConfig()
  1246  				c.ImageDigestSources = []types.ImageDigestSource{{
  1247  					Source: "quay.io/ocp/release-x.y:latest",
  1248  				}}
  1249  				return c
  1250  			}(),
  1251  			expectedError: `^imageDigestSources\[0\]\.source: Invalid value: "quay\.io/ocp/release-x\.y:latest": must be repository--not reference$`,
  1252  		},
  1253  		{
  1254  			name: "valid release image source ImageDigstSourrce",
  1255  			installConfig: func() *types.InstallConfig {
  1256  				c := validInstallConfig()
  1257  				c.ImageDigestSources = []types.ImageDigestSource{{
  1258  					Source: "quay.io/ocp/release-x.y",
  1259  				}}
  1260  				return c
  1261  			}(),
  1262  		},
  1263  		{
  1264  			name: "error out ImageContentSources and ImageDigestSources and are set at the same time",
  1265  			installConfig: func() *types.InstallConfig {
  1266  				c := validInstallConfig()
  1267  				c.DeprecatedImageContentSources = []types.ImageContentSource{{
  1268  					Source:  "q.io/ocp/source",
  1269  					Mirrors: []string{"ocp/openshift/mirror"},
  1270  				}}
  1271  				c.ImageDigestSources = []types.ImageDigestSource{{
  1272  					Source:  "q.io/ocp/source",
  1273  					Mirrors: []string{"ocp-digest/openshift/mirror"}}}
  1274  				return c
  1275  			}(),
  1276  			expectedError: `cannot set imageContentSources and imageDigestSources at the same time`,
  1277  		},
  1278  		{
  1279  			name: "invalid publishing strategy",
  1280  			installConfig: func() *types.InstallConfig {
  1281  				c := validInstallConfig()
  1282  				c.Publish = types.PublishingStrategy("ExternalInternalDoNotCare")
  1283  				return c
  1284  			}(),
  1285  			expectedError: `^publish: Unsupported value: \"ExternalInternalDoNotCare\": supported values: \"External\", \"Internal\"`,
  1286  		},
  1287  
  1288  		{
  1289  			name: "valid dual-stack configuration",
  1290  			installConfig: func() *types.InstallConfig {
  1291  				c := validInstallConfig()
  1292  				c.Platform = types.Platform{None: &none.Platform{}}
  1293  				c.Networking = validDualStackNetworkingConfig()
  1294  				return c
  1295  			}(),
  1296  		},
  1297  		{
  1298  			name: "valid single-stack IPv6 configuration",
  1299  			installConfig: func() *types.InstallConfig {
  1300  				c := validInstallConfig()
  1301  				c.Platform = types.Platform{None: &none.Platform{}}
  1302  				c.Networking = validIPv6NetworkingConfig()
  1303  				return c
  1304  			}(),
  1305  		},
  1306  		{
  1307  			name: "invalid dual-stack configuration, bad platform",
  1308  			installConfig: func() *types.InstallConfig {
  1309  				c := validInstallConfig()
  1310  				c.Platform = types.Platform{GCP: validGCPPlatform()}
  1311  				c.Networking = validDualStackNetworkingConfig()
  1312  				return c
  1313  			}(),
  1314  			expectedError: `Invalid value: "DualStack": dual-stack IPv4/IPv6 is not supported for this platform, specify only one type of address`,
  1315  		},
  1316  		{
  1317  			name: "invalid single-stack IPv6 configuration, bad platform",
  1318  			installConfig: func() *types.InstallConfig {
  1319  				c := validInstallConfig()
  1320  				c.Platform = types.Platform{GCP: validGCPPlatform()}
  1321  				c.Networking = validIPv6NetworkingConfig()
  1322  				return c
  1323  			}(),
  1324  			expectedError: `Invalid value: "IPv6": single-stack IPv6 is not supported for this platform`,
  1325  		},
  1326  		{
  1327  			name: "invalid dual-stack configuration, machine has no IPv6",
  1328  			installConfig: func() *types.InstallConfig {
  1329  				c := validInstallConfig()
  1330  				c.Platform = types.Platform{None: &none.Platform{}}
  1331  				c.Networking = validDualStackNetworkingConfig()
  1332  				c.Networking.MachineNetwork = c.Networking.MachineNetwork[:1]
  1333  				return c
  1334  			}(),
  1335  			expectedError: `Invalid value: "10.0.0.0/16": dual-stack IPv4/IPv6 requires an IPv6 network in this list`,
  1336  		},
  1337  		{
  1338  			name: "invalid dual-stack configuration, IPv6-primary",
  1339  			installConfig: func() *types.InstallConfig {
  1340  				c := validInstallConfig()
  1341  				c.Platform = types.Platform{None: &none.Platform{}}
  1342  				c.Networking = validDualStackNetworkingConfig()
  1343  				c.Networking.ServiceNetwork = []ipnet.IPNet{
  1344  					c.Networking.ServiceNetwork[1],
  1345  					c.Networking.ServiceNetwork[0],
  1346  				}
  1347  				return c
  1348  			}(),
  1349  			expectedError: `Invalid value: "ffd1::/112, 172.30.0.0/16": IPv4 addresses must be listed before IPv6 addresses`,
  1350  		},
  1351  		{
  1352  			name: "valid dual-stack configuration with mixed-order clusterNetworks",
  1353  			installConfig: func() *types.InstallConfig {
  1354  				c := validInstallConfig()
  1355  				c.Platform = types.Platform{None: &none.Platform{}}
  1356  				c.Networking = validDualStackNetworkingConfig()
  1357  				c.Networking.ClusterNetwork = append(c.Networking.ClusterNetwork,
  1358  					types.ClusterNetworkEntry{
  1359  						CIDR:       *ipnet.MustParseCIDR("192.168.2.0/24"),
  1360  						HostPrefix: 28,
  1361  					},
  1362  				)
  1363  				// ClusterNetwork is now "IPv4, IPv6, IPv4", which is allowed
  1364  				return c
  1365  			}(),
  1366  		},
  1367  		{
  1368  			name: "invalid IPv6 hostprefix",
  1369  			installConfig: func() *types.InstallConfig {
  1370  				c := validInstallConfig()
  1371  				c.Platform = types.Platform{None: &none.Platform{}}
  1372  				c.Networking = validIPv6NetworkingConfig()
  1373  				c.Networking.ClusterNetwork[0].HostPrefix = 72
  1374  				return c
  1375  			}(),
  1376  			expectedError: `Invalid value: 72: cluster network host subnetwork prefix must be 64 for IPv6 networks`,
  1377  		},
  1378  		{
  1379  			name: "invalid IPv6 service network size",
  1380  			installConfig: func() *types.InstallConfig {
  1381  				c := validInstallConfig()
  1382  				c.Platform = types.Platform{None: &none.Platform{}}
  1383  				c.Networking = validIPv6NetworkingConfig()
  1384  				c.Networking.ServiceNetwork[0] = *ipnet.MustParseCIDR("ffd1::/48")
  1385  				return c
  1386  			}(),
  1387  			expectedError: `Invalid value: "ffd1::/48": subnet size for IPv6 service network should be /112`,
  1388  		},
  1389  		{
  1390  			name: "architecture is not supported",
  1391  			installConfig: func() *types.InstallConfig {
  1392  				c := validInstallConfig()
  1393  				c.Compute[0].Architecture = types.ArchitectureS390X
  1394  				c.ControlPlane.Architecture = types.ArchitectureS390X
  1395  				return c
  1396  			}(),
  1397  			expectedError: `[controlPlane.architecture: Unsupported value: "s390x": supported values: "amd64", "arm64", compute\[0\].architecture: Unsupported value: "s390x": supported values: "amd64", "arm64"]`,
  1398  		},
  1399  		{
  1400  			name: "architecture is not supported",
  1401  			installConfig: func() *types.InstallConfig {
  1402  				c := validInstallConfig()
  1403  				c.Compute[0].Architecture = types.ArchitecturePPC64LE
  1404  				c.ControlPlane.Architecture = types.ArchitecturePPC64LE
  1405  				return c
  1406  			}(),
  1407  			expectedError: `[controlPlane.architecture: Unsupported value: "ppc64le": supported values: "amd64", "arm64", compute\[0\].architecture: Unsupported value: "ppc64le": supported values: "amd64", "arm64"]`,
  1408  		},
  1409  		{
  1410  			name: "cluster is not heteregenous",
  1411  			installConfig: func() *types.InstallConfig {
  1412  				c := validInstallConfig()
  1413  				c.Platform = types.Platform{Azure: validAzureStackPlatform()}
  1414  				c.Compute[0].Architecture = types.ArchitectureARM64
  1415  				return c
  1416  			}(),
  1417  			expectedError: `^compute\[0\].architecture: Invalid value: "arm64": heteregeneous multi-arch is not supported; compute pool architecture must match control plane$`,
  1418  		},
  1419  		{
  1420  			name: "aws cluster is heteregeneous",
  1421  			installConfig: func() *types.InstallConfig {
  1422  				c := validInstallConfig()
  1423  				c.Compute[0].Architecture = types.ArchitectureARM64
  1424  				return c
  1425  			}(),
  1426  		},
  1427  		{
  1428  			name: "gcp cluster is heteregeneous",
  1429  			installConfig: func() *types.InstallConfig {
  1430  				c := validInstallConfig()
  1431  				c.Platform = types.Platform{GCP: validGCPPlatform()}
  1432  				c.Compute[0].Architecture = types.ArchitectureARM64
  1433  				return c
  1434  			}(),
  1435  		},
  1436  		{
  1437  			name: "valid cloud credentials mode",
  1438  			installConfig: func() *types.InstallConfig {
  1439  				c := validInstallConfig()
  1440  				c.CredentialsMode = types.PassthroughCredentialsMode
  1441  				return c
  1442  			}(),
  1443  		},
  1444  		{
  1445  			name: "invalidly set cloud credentials mode",
  1446  			installConfig: func() *types.InstallConfig {
  1447  				c := validInstallConfig()
  1448  				c.Platform = types.Platform{BareMetal: validBareMetalPlatform()}
  1449  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetCurrent}
  1450  				c.CredentialsMode = types.PassthroughCredentialsMode
  1451  				return c
  1452  			}(),
  1453  			expectedError: `^credentialsMode: Invalid value: "Passthrough": cannot be set when using the "baremetal" platform$`,
  1454  		},
  1455  		{
  1456  			name: "bad cloud credentials mode",
  1457  			installConfig: func() *types.InstallConfig {
  1458  				c := validInstallConfig()
  1459  				c.CredentialsMode = "bad-mode"
  1460  				return c
  1461  			}(),
  1462  			expectedError: `^credentialsMode: Unsupported value: "bad-mode": supported values: "Manual", "Mint", "Passthrough"$`,
  1463  		},
  1464  		{
  1465  			name: "allowed docker bridge with non-libvirt",
  1466  			installConfig: func() *types.InstallConfig {
  1467  				c := validInstallConfig()
  1468  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{{CIDR: *ipnet.MustParseCIDR("172.17.64.0/18")}}
  1469  				return c
  1470  			}(),
  1471  			expectedError: ``,
  1472  		},
  1473  		{
  1474  			name: "publish internal for non-cloud platform",
  1475  			installConfig: func() *types.InstallConfig {
  1476  				c := validInstallConfig()
  1477  				c.Platform = types.Platform{VSphere: validVSpherePlatform()}
  1478  				c.Publish = types.InternalPublishingStrategy
  1479  				return c
  1480  			}(),
  1481  			expectedError: `publish: Invalid value: "Internal": Internal publish strategy is not supported on "vsphere" platform`,
  1482  		},
  1483  		{
  1484  			name: "publish internal for cloud platform",
  1485  			installConfig: func() *types.InstallConfig {
  1486  				c := validInstallConfig()
  1487  				c.Platform = types.Platform{GCP: validGCPPlatform()}
  1488  				c.Publish = types.InternalPublishingStrategy
  1489  				return c
  1490  			}(),
  1491  		}, {
  1492  			name: "valid nutanix platform",
  1493  			installConfig: func() *types.InstallConfig {
  1494  				c := validInstallConfig()
  1495  				c.Platform = types.Platform{
  1496  					Nutanix: validNutanixPlatform(),
  1497  				}
  1498  				return c
  1499  			}(),
  1500  		}, {
  1501  			name: "invalid nutanix platform",
  1502  			installConfig: func() *types.InstallConfig {
  1503  				c := validInstallConfig()
  1504  				c.Platform = types.Platform{
  1505  					Nutanix: validNutanixPlatform(),
  1506  				}
  1507  				c.Platform.Nutanix.PrismCentral.Endpoint.Address = ""
  1508  				return c
  1509  			}(),
  1510  			expectedError: `^platform\.nutanix\.prismCentral\.endpoint\.address: Required value: must specify the Prism Central endpoint address$`,
  1511  		},
  1512  		{
  1513  			name: "invalid credentials mode for nutanix",
  1514  			installConfig: func() *types.InstallConfig {
  1515  				c := validInstallConfig()
  1516  				c.Platform = types.Platform{
  1517  					Nutanix: validNutanixPlatform(),
  1518  				}
  1519  				c.CredentialsMode = types.PassthroughCredentialsMode
  1520  				return c
  1521  			}(),
  1522  			expectedError: `credentialsMode: Unsupported value: "Passthrough": supported values: "Manual"$`,
  1523  		},
  1524  		{
  1525  			name: "valid credentials mode for nutanix",
  1526  			installConfig: func() *types.InstallConfig {
  1527  				c := validInstallConfig()
  1528  				c.Platform = types.Platform{
  1529  					Nutanix: validNutanixPlatform(),
  1530  				}
  1531  				c.CredentialsMode = types.ManualCredentialsMode
  1532  				return c
  1533  			}(),
  1534  		},
  1535  		{
  1536  			name: "valid baseline capability set",
  1537  			installConfig: func() *types.InstallConfig {
  1538  				c := validInstallConfig()
  1539  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "v4.11"}
  1540  				c.Capabilities.AdditionalEnabledCapabilities = append(c.Capabilities.AdditionalEnabledCapabilities, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityOperatorLifecycleManager)
  1541  				return c
  1542  			}(),
  1543  		},
  1544  		{
  1545  			name: "invalid empty string baseline capability set",
  1546  			installConfig: func() *types.InstallConfig {
  1547  				c := validInstallConfig()
  1548  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: ""}
  1549  				return c
  1550  			}(),
  1551  			expectedError: `capabilities.baselineCapabilitySet: Unsupported value: "": supported values: .*`,
  1552  		},
  1553  		{
  1554  			name: "invalid baseline capability set specified",
  1555  			installConfig: func() *types.InstallConfig {
  1556  				c := validInstallConfig()
  1557  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "vNotValid"}
  1558  				return c
  1559  			}(),
  1560  			expectedError: `capabilities.baselineCapabilitySet: Unsupported value: "vNotValid": supported values: .*`,
  1561  		},
  1562  		{
  1563  			name: "invalid capability marketplace specified without OperatorLifecycleManager",
  1564  			installConfig: func() *types.InstallConfig {
  1565  				c := validInstallConfig()
  1566  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "None",
  1567  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{"marketplace"}}
  1568  				return c
  1569  			}(),
  1570  			expectedError: `additionalEnabledCapabilities: Invalid value: \[\]v1.ClusterVersionCapability{"marketplace"}: the marketplace capability requires the OperatorLifecycleManager capability`,
  1571  		},
  1572  		{
  1573  			name: "valid additional enabled capability specified",
  1574  			installConfig: func() *types.InstallConfig {
  1575  				c := validInstallConfig()
  1576  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "v4.11"}
  1577  				c.Capabilities.AdditionalEnabledCapabilities = append(c.Capabilities.AdditionalEnabledCapabilities, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityOpenShiftSamples, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityOperatorLifecycleManager)
  1578  				return c
  1579  			}(),
  1580  		},
  1581  		{
  1582  			name: "invalid empty additional enabled capability specified",
  1583  			installConfig: func() *types.InstallConfig {
  1584  				c := validInstallConfig()
  1585  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "v4.11",
  1586  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{""}}
  1587  				return c
  1588  			}(),
  1589  			expectedError: `capabilities.additionalEnabledCapabilities\[0\]: Unsupported value: "": supported values: .*`,
  1590  		},
  1591  		{
  1592  			name: "invalid additional enabled capability specified",
  1593  			installConfig: func() *types.InstallConfig {
  1594  				c := validInstallConfig()
  1595  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "v4.11",
  1596  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{"not-valid"}}
  1597  				return c
  1598  			}(),
  1599  			expectedError: `capabilities.additionalEnabledCapabilities\[0\]: Unsupported value: "not-valid": supported values: .*`,
  1600  		},
  1601  		{
  1602  			name: "baremetal platform requires the baremetal capability",
  1603  			installConfig: func() *types.InstallConfig {
  1604  				c := validInstallConfig()
  1605  				c.Platform = types.Platform{
  1606  					BareMetal: validBareMetalPlatform(),
  1607  				}
  1608  				c.Capabilities = &types.Capabilities{BaselineCapabilitySet: "None", AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{"marketplace"}}
  1609  				return c
  1610  			}(),
  1611  			expectedError: `additionalEnabledCapabilities: Invalid value: \[\]v1.ClusterVersionCapability{"marketplace"}: platform baremetal requires the baremetal capability`,
  1612  		},
  1613  		//VIP tests
  1614  		{
  1615  			name: "apivip_v4_not_in_machinenetwork_cidr",
  1616  			installConfig: func() *types.InstallConfig {
  1617  				c := validInstallConfig()
  1618  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1619  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1620  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1621  				}
  1622  				c.Platform = types.Platform{
  1623  					BareMetal: validBareMetalPlatform(),
  1624  				}
  1625  				c.Platform.BareMetal.APIVIPs = []string{"192.168.222.1"}
  1626  
  1627  				return c
  1628  			}(),
  1629  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"192.168.222.1\": IP expected to be in one of the machine networks: 10.0.0.0/16,fe80::/10",
  1630  		},
  1631  		{
  1632  			name: "apivip_v4_not_in_machinenetwork_cidr_usermanaged_loadbalancer",
  1633  			installConfig: func() *types.InstallConfig {
  1634  				c := validInstallConfig()
  1635  				c.FeatureSet = configv1.TechPreviewNoUpgrade
  1636  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1637  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1638  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1639  				}
  1640  				c.Platform = types.Platform{
  1641  					BareMetal: validBareMetalPlatform(),
  1642  				}
  1643  				c.Platform.BareMetal.LoadBalancer = &configv1.BareMetalPlatformLoadBalancer{Type: configv1.LoadBalancerTypeUserManaged}
  1644  				c.Platform.BareMetal.APIVIPs = []string{"192.168.222.1"}
  1645  
  1646  				return c
  1647  			}(),
  1648  		},
  1649  		{
  1650  			name: "apivip_v6_not_in_machinenetwork_cidr",
  1651  			installConfig: func() *types.InstallConfig {
  1652  				c := validInstallConfig()
  1653  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1654  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1655  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1656  				}
  1657  				c.Platform = types.Platform{
  1658  					BareMetal: validBareMetalPlatform(),
  1659  				}
  1660  				c.Platform.BareMetal.APIVIPs = []string{"2001::1"}
  1661  
  1662  				return c
  1663  			}(),
  1664  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"2001::1\": IP expected to be in one of the machine networks: 10.0.0.0/16,fe80::/10",
  1665  		},
  1666  		{
  1667  			name: "apivips_v6_on_openshiftsdn",
  1668  			installConfig: func() *types.InstallConfig {
  1669  				c := validInstallConfig()
  1670  				c.Networking = validIPv6NetworkingConfig()
  1671  				c.Networking.NetworkType = string(operv1.NetworkTypeOpenShiftSDN)
  1672  
  1673  				c.Platform = types.Platform{
  1674  					BareMetal: validBareMetalPlatform(),
  1675  				}
  1676  				c.Platform.BareMetal.APIVIPs = []string{"ffd0::1"}
  1677  
  1678  				return c
  1679  			}(),
  1680  			expectedError: "[networking.networkType: Invalid value: \"OpenShiftSDN\": networkType OpenShiftSDN is not supported, please use OVNKubernetes, platform.baremetal.ingressVIPs: Invalid value: \"10.0.0.4\": IP expected to be in one of the machine networks: ffd0::/48]",
  1681  		},
  1682  		{
  1683  			name: "ingressvips_v6_on_openshiftsdn",
  1684  			installConfig: func() *types.InstallConfig {
  1685  				c := validInstallConfig()
  1686  				c.Networking = validIPv6NetworkingConfig()
  1687  				c.Networking.NetworkType = string(operv1.NetworkTypeOpenShiftSDN)
  1688  
  1689  				c.Platform = types.Platform{
  1690  					BareMetal: validBareMetalPlatform(),
  1691  				}
  1692  				c.Platform.BareMetal.IngressVIPs = []string{"ffd0::1"}
  1693  
  1694  				return c
  1695  			}(),
  1696  			expectedError: "[networking.networkType: Invalid value: \"OpenShiftSDN\": networkType OpenShiftSDN is not supported, please use OVNKubernetes, platform.baremetal.apiVIPs: Invalid value: \"10.0.0.5\": IP expected to be in one of the machine networks: ffd0::/48]",
  1697  		},
  1698  		{
  1699  			name: "too_many_apivips",
  1700  			installConfig: func() *types.InstallConfig {
  1701  				c := validInstallConfig()
  1702  				c.Platform = types.Platform{
  1703  					BareMetal: validBareMetalPlatform(),
  1704  				}
  1705  				c.Platform.BareMetal.APIVIPs = []string{"fe80::1", "fe80::2", "fe80::3"}
  1706  
  1707  				return c
  1708  			}(),
  1709  			expectedError: "platform.baremetal.apiVIPs: Too many: 3: must have at most 2 items",
  1710  		},
  1711  		{
  1712  			name: "invalid_apivip",
  1713  			installConfig: func() *types.InstallConfig {
  1714  				c := validInstallConfig()
  1715  				c.Platform = types.Platform{
  1716  					BareMetal: validBareMetalPlatform(),
  1717  				}
  1718  				c.Platform.BareMetal.APIVIPs = []string{""}
  1719  
  1720  				return c
  1721  			}(),
  1722  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"\": \"\" is not a valid IP",
  1723  		},
  1724  		{
  1725  			name: "invalid_apivip_2",
  1726  			installConfig: func() *types.InstallConfig {
  1727  				c := validInstallConfig()
  1728  				c.Platform = types.Platform{
  1729  					BareMetal: validBareMetalPlatform(),
  1730  				}
  1731  				c.Platform.BareMetal.APIVIPs = []string{"123.456.789.000"}
  1732  
  1733  				return c
  1734  			}(),
  1735  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"123.456.789.000\": \"123.456.789.000\" is not a valid IP",
  1736  		},
  1737  		{
  1738  			name: "invalid_apivip_format",
  1739  			installConfig: func() *types.InstallConfig {
  1740  				c := validInstallConfig()
  1741  				c.Platform = types.Platform{
  1742  					BareMetal: validBareMetalPlatform(),
  1743  				}
  1744  				c.Platform.BareMetal.APIVIPs = []string{"foobar"}
  1745  
  1746  				return c
  1747  			}(),
  1748  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  1749  		},
  1750  		{
  1751  			name: "invalid_apivip_format_one_of_many",
  1752  			installConfig: func() *types.InstallConfig {
  1753  				c := validInstallConfig()
  1754  				c.Platform = types.Platform{
  1755  					BareMetal: validBareMetalPlatform(),
  1756  				}
  1757  				c.Platform.BareMetal.APIVIPs = []string{"192.168.1.0", "foobar"}
  1758  
  1759  				return c
  1760  			}(),
  1761  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  1762  		},
  1763  		{
  1764  			name: "invalid_apivips_both_ipv4",
  1765  			installConfig: func() *types.InstallConfig {
  1766  				c := validInstallConfig()
  1767  				c.Platform = types.Platform{
  1768  					BareMetal: validBareMetalPlatform(),
  1769  				}
  1770  				c.Platform.BareMetal.APIVIPs = []string{"192.168.111.1", "192.168.111.2"}
  1771  
  1772  				return c
  1773  			}(),
  1774  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \\[\\]string\\{\"192.168.111.1\", \"192.168.111.2\"\\}: If two API VIPs are given, one must be an IPv4 address, the other an IPv6",
  1775  		},
  1776  		{
  1777  			name: "invalid_apis_both_ipv6",
  1778  			installConfig: func() *types.InstallConfig {
  1779  				c := validInstallConfig()
  1780  				c.Platform = types.Platform{
  1781  					BareMetal: validBareMetalPlatform(),
  1782  				}
  1783  				c.Platform.BareMetal.APIVIPs = []string{"fe80::1", "fe80::2"}
  1784  
  1785  				return c
  1786  			}(),
  1787  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \\[\\]string\\{\"fe80::1\", \"fe80::2\"\\}: If two API VIPs are given, one must be an IPv4 address, the other an IPv6",
  1788  		},
  1789  		{
  1790  			name: "ingressvip_v4_not_in_machinenetwork_cidr",
  1791  			installConfig: func() *types.InstallConfig {
  1792  				c := validInstallConfig()
  1793  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1794  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1795  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1796  				}
  1797  				c.Platform = types.Platform{
  1798  					BareMetal: validBareMetalPlatform(),
  1799  				}
  1800  				c.Platform.BareMetal.IngressVIPs = []string{"192.168.222.4"}
  1801  
  1802  				return c
  1803  			}(),
  1804  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \"192.168.222.4\": IP expected to be in one of the machine networks: 10.0.0.0/16,fe80::/10",
  1805  		},
  1806  		{
  1807  			name: "ingressvip_v6_not_in_machinenetwork_cidr",
  1808  			installConfig: func() *types.InstallConfig {
  1809  				c := validInstallConfig()
  1810  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1811  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1812  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1813  				}
  1814  				c.Platform = types.Platform{
  1815  					BareMetal: validBareMetalPlatform(),
  1816  				}
  1817  				c.Platform.BareMetal.IngressVIPs = []string{"2001::1"}
  1818  
  1819  				return c
  1820  			}(),
  1821  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \"2001::1\": IP expected to be in one of the machine networks: 10.0.0.0/16,fe80::/10",
  1822  		},
  1823  		{
  1824  			name: "vsphere_ingressvip_v4_not_in_machinenetwork_cidr",
  1825  			installConfig: func() *types.InstallConfig {
  1826  				c := validInstallConfig()
  1827  				c.Networking.MachineNetwork = []types.MachineNetworkEntry{
  1828  					{CIDR: *ipnet.MustParseCIDR("10.0.0.0/16")},
  1829  					{CIDR: *ipnet.MustParseCIDR("fe80::/10")},
  1830  				}
  1831  				c.Platform = types.Platform{
  1832  					VSphere: validVSpherePlatform(),
  1833  				}
  1834  				c.Platform.VSphere.IngressVIPs = []string{"192.168.222.4"}
  1835  				c.Platform.VSphere.APIVIPs = []string{"192.168.1.0"}
  1836  
  1837  				return c
  1838  			}(),
  1839  		},
  1840  		{
  1841  			name: "too_many_ingressvips",
  1842  			installConfig: func() *types.InstallConfig {
  1843  				c := validInstallConfig()
  1844  				c.Platform = types.Platform{
  1845  					BareMetal: validBareMetalPlatform(),
  1846  				}
  1847  				c.Platform.BareMetal.IngressVIPs = []string{"fe80::1", "fe80::2", "fe80::3"}
  1848  
  1849  				return c
  1850  			}(),
  1851  			expectedError: "platform.baremetal.ingressVIPs: Too many: 3: must have at most 2 items",
  1852  		},
  1853  		{
  1854  			name: "invalid_ingressvip",
  1855  			installConfig: func() *types.InstallConfig {
  1856  				c := validInstallConfig()
  1857  				c.Platform = types.Platform{
  1858  					BareMetal: validBareMetalPlatform(),
  1859  				}
  1860  				c.Platform.BareMetal.IngressVIPs = []string{""}
  1861  
  1862  				return c
  1863  			}(),
  1864  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \"\": \"\" is not a valid IP",
  1865  		},
  1866  		{
  1867  			name: "invalid_ingressvip_format",
  1868  			installConfig: func() *types.InstallConfig {
  1869  				c := validInstallConfig()
  1870  				c.Platform = types.Platform{
  1871  					BareMetal: validBareMetalPlatform(),
  1872  				}
  1873  				c.Platform.BareMetal.IngressVIPs = []string{"foobar"}
  1874  
  1875  				return c
  1876  			}(),
  1877  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  1878  		},
  1879  		{
  1880  			name: "invalid_ingressvip_format_one_of_many",
  1881  			installConfig: func() *types.InstallConfig {
  1882  				c := validInstallConfig()
  1883  				c.Platform = types.Platform{
  1884  					BareMetal: validBareMetalPlatform(),
  1885  				}
  1886  				c.Platform.BareMetal.IngressVIPs = []string{"192.1.1.1", "foobar"}
  1887  
  1888  				return c
  1889  			}(),
  1890  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  1891  		},
  1892  		{
  1893  			name: "invalid_ingressvips_both_ipv4",
  1894  			installConfig: func() *types.InstallConfig {
  1895  				c := validInstallConfig()
  1896  				c.Platform = types.Platform{
  1897  					BareMetal: validBareMetalPlatform(),
  1898  				}
  1899  				c.Platform.BareMetal.IngressVIPs = []string{"192.168.111.4", "192.168.111.5"}
  1900  
  1901  				return c
  1902  			}(),
  1903  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \\[\\]string\\{\"192.168.111.4\", \"192.168.111.5\"\\}: If two Ingress VIPs are given, one must be an IPv4 address, the other an IPv6",
  1904  		},
  1905  		{
  1906  			name: "invalid_ingressvips_both_ipv6",
  1907  			installConfig: func() *types.InstallConfig {
  1908  				c := validInstallConfig()
  1909  				c.Platform = types.Platform{
  1910  					BareMetal: validBareMetalPlatform(),
  1911  				}
  1912  				c.Platform.BareMetal.IngressVIPs = []string{"fe80::1", "fe80::2"}
  1913  
  1914  				return c
  1915  			}(),
  1916  			expectedError: "platform.baremetal.ingressVIPs: Invalid value: \\[\\]string\\{\"fe80::1\", \"fe80::2\"\\}: If two Ingress VIPs are given, one must be an IPv4 address, the other an IPv6",
  1917  		},
  1918  		{
  1919  			name: "identical_apivip_ingressvip",
  1920  			installConfig: func() *types.InstallConfig {
  1921  				c := validInstallConfig()
  1922  				c.Platform = types.Platform{
  1923  					BareMetal: validBareMetalPlatform(),
  1924  				}
  1925  				c.Platform.BareMetal.APIVIPs = []string{"fe80::1"}
  1926  				c.Platform.BareMetal.IngressVIPs = []string{"fe80::1"}
  1927  
  1928  				return c
  1929  			}(),
  1930  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"fe80::1\": VIP for API must not be one of the Ingress VIPs",
  1931  		},
  1932  		{
  1933  			name: "identical_apivip_ingressvip_usermanaged_loadbalancer",
  1934  			installConfig: func() *types.InstallConfig {
  1935  				c := validInstallConfig()
  1936  				c.FeatureSet = configv1.TechPreviewNoUpgrade
  1937  				c.Platform = types.Platform{
  1938  					BareMetal: validBareMetalPlatform(),
  1939  				}
  1940  				c.Platform.BareMetal.LoadBalancer = &configv1.BareMetalPlatformLoadBalancer{Type: configv1.LoadBalancerTypeUserManaged}
  1941  				c.Platform.BareMetal.APIVIPs = []string{"fe80::1"}
  1942  				c.Platform.BareMetal.IngressVIPs = []string{"fe80::1"}
  1943  
  1944  				return c
  1945  			}(),
  1946  		},
  1947  		{
  1948  			name: "identical_apivips_ingressvips_multiple_ips",
  1949  			installConfig: func() *types.InstallConfig {
  1950  				c := validInstallConfig()
  1951  				c.Platform = types.Platform{
  1952  					BareMetal: validBareMetalPlatform(),
  1953  				}
  1954  				c.Platform.BareMetal.APIVIPs = []string{"fe80::1", "192.1.2.3"}
  1955  				c.Platform.BareMetal.IngressVIPs = []string{"fe80::1", "192.1.2.4"}
  1956  
  1957  				return c
  1958  			}(),
  1959  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"fe80::1\": VIP for API must not be one of the Ingress VIPs",
  1960  		},
  1961  		{
  1962  			name: "apivip_ingressvip_are_synonyms",
  1963  			installConfig: func() *types.InstallConfig {
  1964  				c := validInstallConfig()
  1965  				c.Platform = types.Platform{
  1966  					BareMetal: validBareMetalPlatform(),
  1967  				}
  1968  				c.Platform.BareMetal.APIVIPs = []string{"2001:db8::5"}
  1969  				c.Platform.BareMetal.IngressVIPs = []string{"2001:db8:0::5"}
  1970  
  1971  				return c
  1972  			}(),
  1973  			expectedError: "platform.baremetal.apiVIPs: Invalid value: \"2001:db8::5\": VIP for API must not be one of the Ingress VIPs",
  1974  		},
  1975  		{
  1976  			name: "empty_api_vip_fields",
  1977  			installConfig: func() *types.InstallConfig {
  1978  				c := validInstallConfig()
  1979  				c.Platform = types.Platform{
  1980  					BareMetal: validBareMetalPlatform(),
  1981  				}
  1982  				c.Platform.BareMetal.DeprecatedAPIVIP = ""
  1983  				c.Platform.BareMetal.APIVIPs = []string{}
  1984  
  1985  				return c
  1986  			}(),
  1987  			expectedError: "platform.baremetal.apiVIPs: Required value: must specify at least one VIP for the API",
  1988  		},
  1989  		{
  1990  			name: "empty_ingress_vip_fields",
  1991  			installConfig: func() *types.InstallConfig {
  1992  				c := validInstallConfig()
  1993  				c.Platform = types.Platform{
  1994  					BareMetal: validBareMetalPlatform(),
  1995  				}
  1996  				c.Platform.BareMetal.DeprecatedIngressVIP = ""
  1997  				c.Platform.BareMetal.IngressVIPs = []string{}
  1998  
  1999  				return c
  2000  			}(),
  2001  			expectedError: "platform.baremetal.ingressVIPs: Required value: must specify at least one VIP for the Ingress",
  2002  		},
  2003  		{
  2004  			name: "baremetal API VIP set to an incorrect IP Family",
  2005  			installConfig: func() *types.InstallConfig {
  2006  				c := validInstallConfig()
  2007  				c.Networking = validDualStackNetworkingConfig()
  2008  				c.Platform = types.Platform{
  2009  					BareMetal: validBareMetalPlatform(),
  2010  				}
  2011  				c.Platform.BareMetal.APIVIPs = []string{"ffd0::"}
  2012  				return c
  2013  			}(),
  2014  			expectedError: `platform.baremetal.apiVIPs: Invalid value: "ffd0::": VIP for the API must be of the same IP family with machine network's primary IP Family for dual-stack IPv4/IPv6`,
  2015  		},
  2016  		{
  2017  			name: "baremetal Ingress VIP set to an incorrect IP Family",
  2018  			installConfig: func() *types.InstallConfig {
  2019  				c := validInstallConfig()
  2020  				c.Networking = validDualStackNetworkingConfig()
  2021  				c.Platform = types.Platform{
  2022  					BareMetal: validBareMetalPlatform(),
  2023  				}
  2024  				c.Platform.BareMetal.IngressVIPs = []string{"ffd0::"}
  2025  				return c
  2026  			}(),
  2027  			expectedError: `platform.baremetal.ingressVIPs: Invalid value: "ffd0::": VIP for the Ingress must be of the same IP family with machine network's primary IP Family for dual-stack IPv4/IPv6`,
  2028  		},
  2029  		{
  2030  			name: "should validate vips on baremetal (required)",
  2031  			installConfig: func() *types.InstallConfig {
  2032  				c := validInstallConfig()
  2033  				c.Platform = types.Platform{
  2034  					BareMetal: validBareMetalPlatform(),
  2035  				}
  2036  				c.Platform.BareMetal.DeprecatedAPIVIP = ""
  2037  				c.Platform.BareMetal.APIVIPs = []string{}
  2038  
  2039  				return c
  2040  			}(),
  2041  			expectedError: "platform.baremetal.apiVIPs: Required value: must specify at least one VIP for the API",
  2042  		},
  2043  		{
  2044  			name: "should validate vips on OpenStack (vips are required on openstack)",
  2045  			installConfig: func() *types.InstallConfig {
  2046  				c := validInstallConfig()
  2047  				c.Platform = types.Platform{
  2048  					OpenStack: validOpenStackPlatform(),
  2049  				}
  2050  				c.Platform.OpenStack.DeprecatedAPIVIP = ""
  2051  				c.Platform.OpenStack.APIVIPs = []string{}
  2052  
  2053  				return c
  2054  			}(),
  2055  			expectedError: "platform.openstack.apiVIPs: Required value: must specify at least one VIP for the API",
  2056  		},
  2057  		// {
  2058  		// 	name: "should not validate vips on OpenStack if not set (vips are not required on openstack)",
  2059  		// 	installConfig: func() *types.InstallConfig {
  2060  		// 		c := validInstallConfig()
  2061  		// 		c.Platform = types.Platform{
  2062  		// 			OpenStack: validOpenStackPlatform(),
  2063  		// 		}
  2064  		// 		c.Platform.OpenStack.DeprecatedAPIVIP = ""
  2065  		// 		c.Platform.OpenStack.APIVIPs = []string{}
  2066  
  2067  		// 		return c
  2068  		// 	}(),
  2069  		// },
  2070  		{
  2071  			name: "should validate vips on OpenStack if set (vips are not required on openstack)",
  2072  			installConfig: func() *types.InstallConfig {
  2073  				c := validInstallConfig()
  2074  				c.Platform = types.Platform{
  2075  					OpenStack: validOpenStackPlatform(),
  2076  				}
  2077  				c.Platform.OpenStack.APIVIPs = []string{"foobar"}
  2078  
  2079  				return c
  2080  			}(),
  2081  			expectedError: "platform.openstack.apiVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  2082  		},
  2083  		{
  2084  			name: "should not validate vips on VSphere if not set (vips are not required on VSphere)",
  2085  			installConfig: func() *types.InstallConfig {
  2086  				c := validInstallConfig()
  2087  				c.Platform = types.Platform{
  2088  					VSphere: validVSpherePlatform(),
  2089  				}
  2090  				c.Platform.VSphere.DeprecatedAPIVIP = ""
  2091  				c.Platform.VSphere.APIVIPs = []string{}
  2092  
  2093  				return c
  2094  			}(),
  2095  		},
  2096  		{
  2097  			name: "should validate vips on VSphere if set (vips are not required on VSphere)",
  2098  			installConfig: func() *types.InstallConfig {
  2099  				c := validInstallConfig()
  2100  				c.Platform = types.Platform{
  2101  					VSphere: validVSpherePlatform(),
  2102  				}
  2103  				c.Platform.VSphere.APIVIPs = []string{"foobar"}
  2104  
  2105  				return c
  2106  			}(),
  2107  			expectedError: "platform.vsphere.apiVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  2108  		},
  2109  		{
  2110  			name: "should not validate vips on Nutanix if not set (vips are not required on Nutanix)",
  2111  			installConfig: func() *types.InstallConfig {
  2112  				c := validInstallConfig()
  2113  				c.Platform = types.Platform{
  2114  					Nutanix: validNutanixPlatform(),
  2115  				}
  2116  				c.Platform.Nutanix.DeprecatedAPIVIP = ""
  2117  				c.Platform.Nutanix.APIVIPs = []string{}
  2118  
  2119  				return c
  2120  			}(),
  2121  		},
  2122  		{
  2123  			name: "should validate vips on Nutanix if set (vips are not required on Nutanix)",
  2124  			installConfig: func() *types.InstallConfig {
  2125  				c := validInstallConfig()
  2126  				c.Platform = types.Platform{
  2127  					Nutanix: validNutanixPlatform(),
  2128  				}
  2129  				c.Platform.Nutanix.APIVIPs = []string{"foobar"}
  2130  
  2131  				return c
  2132  			}(),
  2133  			expectedError: "platform.nutanix.apiVIPs: Invalid value: \"foobar\": \"foobar\" is not a valid IP",
  2134  		},
  2135  		{
  2136  			name: "should return error if only API VIP is set",
  2137  			installConfig: func() *types.InstallConfig {
  2138  				c := validInstallConfig()
  2139  				c.Platform = types.Platform{
  2140  					VSphere: validVSpherePlatform(),
  2141  				}
  2142  				c.Platform.VSphere.APIVIPs = []string{"10.0.0.1"}
  2143  				c.Platform.VSphere.IngressVIPs = []string{}
  2144  
  2145  				return c
  2146  			}(),
  2147  			expectedError: "platform.vsphere.ingressVIPs: Required value: must specify VIP for ingress, when VIP for API is set",
  2148  		},
  2149  		{
  2150  			name: "should return error if only Ingress VIP is set",
  2151  			installConfig: func() *types.InstallConfig {
  2152  				c := validInstallConfig()
  2153  				c.Platform = types.Platform{
  2154  					VSphere: validVSpherePlatform(),
  2155  				}
  2156  				c.Platform.VSphere.APIVIPs = []string{}
  2157  				c.Platform.VSphere.IngressVIPs = []string{"10.0.0.1"}
  2158  
  2159  				return c
  2160  			}(),
  2161  			expectedError: "platform.vsphere.apiVIPs: Required value: must specify VIP for API, when VIP for ingress is set",
  2162  		},
  2163  		{
  2164  			name: "valid custom features",
  2165  			installConfig: func() *types.InstallConfig {
  2166  				c := validInstallConfig()
  2167  				c.FeatureSet = configv1.CustomNoUpgrade
  2168  				c.FeatureGates = []string{
  2169  					"CustomFeature1=True",
  2170  					"CustomFeature2=False",
  2171  				}
  2172  				return c
  2173  			}(),
  2174  		},
  2175  		{
  2176  			name: "invalid custom features",
  2177  			installConfig: func() *types.InstallConfig {
  2178  				c := validInstallConfig()
  2179  				c.FeatureSet = configv1.CustomNoUpgrade
  2180  				c.FeatureGates = []string{
  2181  					"CustomFeature1=True",
  2182  					"CustomFeature2",
  2183  				}
  2184  				return c
  2185  			}(),
  2186  			expectedError: `featureGates\[1\]: Invalid value: "CustomFeature2": must match the format <feature-name>=<bool>`,
  2187  		},
  2188  		{
  2189  			name: "invalid custom features bool",
  2190  			installConfig: func() *types.InstallConfig {
  2191  				c := validInstallConfig()
  2192  				c.FeatureSet = configv1.CustomNoUpgrade
  2193  				c.FeatureGates = []string{
  2194  					"CustomFeature1=foo",
  2195  					"CustomFeature2=False",
  2196  				}
  2197  				return c
  2198  			}(),
  2199  			expectedError: `featureGates\[0\]: Invalid value: "CustomFeature1=foo": must match the format <feature-name>=<bool>, could not parse boolean value`,
  2200  		},
  2201  		{
  2202  			name: "custom features supplied with non-custom featureset",
  2203  			installConfig: func() *types.InstallConfig {
  2204  				c := validInstallConfig()
  2205  				c.FeatureSet = configv1.TechPreviewNoUpgrade
  2206  				c.FeatureGates = []string{
  2207  					"CustomFeature1=True",
  2208  					"CustomFeature2=False",
  2209  				}
  2210  				return c
  2211  			}(),
  2212  			expectedError: "featureGates: Forbidden: featureGates can only be used with the CustomNoUpgrade feature set",
  2213  		},
  2214  		{
  2215  			name: "valid disabled MAPI with baseline none and baremetal enabled",
  2216  			installConfig: func() *types.InstallConfig {
  2217  				c := validInstallConfig()
  2218  				c.Capabilities = &types.Capabilities{
  2219  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2220  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityBaremetal, configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager},
  2221  				}
  2222  				return c
  2223  			}(),
  2224  		},
  2225  		{
  2226  			name: "valid disabled MAPI capability configuration",
  2227  			installConfig: func() *types.InstallConfig {
  2228  				c := validInstallConfig()
  2229  				c.Capabilities = &types.Capabilities{
  2230  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
  2231  				}
  2232  				c.Capabilities.AdditionalEnabledCapabilities = append(c.Capabilities.AdditionalEnabledCapabilities, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityIngress)
  2233  				return c
  2234  			}(),
  2235  		},
  2236  		{
  2237  			name: "valid enabled MAPI capability configuration",
  2238  			installConfig: func() *types.InstallConfig {
  2239  				c := validInstallConfig()
  2240  				c.Capabilities = &types.Capabilities{
  2241  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2242  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityBaremetal, configv1.ClusterVersionCapabilityMachineAPI, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityIngress},
  2243  				}
  2244  				return c
  2245  			}(),
  2246  		},
  2247  		{
  2248  			name: "valid enabled MAPI capability configuration 2",
  2249  			installConfig: func() *types.InstallConfig {
  2250  				c := validInstallConfig()
  2251  				c.Capabilities = &types.Capabilities{
  2252  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2253  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityMachineAPI, configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityCloudControllerManager, configv1.ClusterVersionCapabilityIngress},
  2254  				}
  2255  				return c
  2256  			}(),
  2257  		},
  2258  		{
  2259  			name: "CloudCredential is enabled in cloud",
  2260  			installConfig: func() *types.InstallConfig {
  2261  				c := validInstallConfig()
  2262  				c.Capabilities = &types.Capabilities{
  2263  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetCurrent,
  2264  				}
  2265  				return c
  2266  			}(),
  2267  		},
  2268  		{
  2269  			name: "CloudCredential is disabled in cloud aws",
  2270  			installConfig: func() *types.InstallConfig {
  2271  				c := validInstallConfig()
  2272  				c.Capabilities = &types.Capabilities{
  2273  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
  2274  				}
  2275  				return c
  2276  			}(),
  2277  			expectedError: "disabling CloudCredential capability available only for baremetal platforms",
  2278  		},
  2279  		{
  2280  			name: "CloudCredential is disabled in cloud gcp",
  2281  			installConfig: func() *types.InstallConfig {
  2282  				c := validInstallConfig()
  2283  				c.GCP = validGCPPlatform()
  2284  				c.AWS = nil
  2285  				c.Capabilities = &types.Capabilities{
  2286  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
  2287  				}
  2288  				return c
  2289  			}(),
  2290  			expectedError: "disabling CloudCredential capability available only for baremetal platforms",
  2291  		},
  2292  		{
  2293  			name: "CloudCredential is enabled in baremetal",
  2294  			installConfig: func() *types.InstallConfig {
  2295  				c := validInstallConfig()
  2296  				c.BareMetal = validBareMetalPlatform()
  2297  				c.AWS = nil
  2298  				c.Capabilities = &types.Capabilities{
  2299  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetCurrent,
  2300  				}
  2301  				return c
  2302  			}(),
  2303  		},
  2304  		{
  2305  			name: "CloudCredential is disabled in baremetal",
  2306  			installConfig: func() *types.InstallConfig {
  2307  				c := validInstallConfig()
  2308  				c.BareMetal = validBareMetalPlatform()
  2309  				c.AWS = nil
  2310  				c.Capabilities = &types.Capabilities{
  2311  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2312  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityBaremetal, configv1.ClusterVersionCapabilityMachineAPI, configv1.ClusterVersionCapabilityIngress},
  2313  				}
  2314  				return c
  2315  			}(),
  2316  		},
  2317  		{
  2318  			name: "CloudController can't be disabled on cloud",
  2319  			installConfig: func() *types.InstallConfig {
  2320  				c := validInstallConfig()
  2321  				c.Capabilities = &types.Capabilities{
  2322  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
  2323  				}
  2324  				return c
  2325  			}(),
  2326  			expectedError: "disabling CloudControllerManager is only supported on the Baremetal, None, or External platform with cloudControllerManager value none",
  2327  		},
  2328  		{
  2329  			name: "valid disabled CloudController configuration none platform",
  2330  			installConfig: func() *types.InstallConfig {
  2331  				c := validInstallConfig()
  2332  				c.Platform.AWS = nil
  2333  				c.Platform.None = &none.Platform{}
  2334  				c.Capabilities = &types.Capabilities{
  2335  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2336  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityIngress},
  2337  				}
  2338  				return c
  2339  			}(),
  2340  		},
  2341  		{
  2342  			name: "valid disabled CloudController configuration platform baremetal",
  2343  			installConfig: func() *types.InstallConfig {
  2344  				c := validInstallConfig()
  2345  				c.Platform.AWS = nil
  2346  				c.Platform.BareMetal = validBareMetalPlatform()
  2347  				c.Capabilities = &types.Capabilities{
  2348  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2349  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityBaremetal, configv1.ClusterVersionCapabilityMachineAPI, configv1.ClusterVersionCapabilityIngress},
  2350  				}
  2351  				return c
  2352  			}(),
  2353  		},
  2354  		{
  2355  			name: "valid disabled CloudController configuration platform External",
  2356  			installConfig: func() *types.InstallConfig {
  2357  				c := validInstallConfig()
  2358  				c.Platform.AWS = nil
  2359  				c.Platform.External = &external.Platform{}
  2360  				c.Capabilities = &types.Capabilities{
  2361  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2362  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityIngress},
  2363  				}
  2364  				return c
  2365  			}(),
  2366  		},
  2367  		{
  2368  			name: "valid disabled CloudController configuration platform External 2",
  2369  			installConfig: func() *types.InstallConfig {
  2370  				c := validInstallConfig()
  2371  				c.Platform.AWS = nil
  2372  				c.Platform.External = &external.Platform{
  2373  					CloudControllerManager: external.CloudControllerManagerTypeNone,
  2374  				}
  2375  				c.Capabilities = &types.Capabilities{
  2376  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2377  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityCloudCredential, configv1.ClusterVersionCapabilityIngress},
  2378  				}
  2379  				return c
  2380  			}(),
  2381  		},
  2382  		{
  2383  			name: "invalid disabled CloudController configuration platform External 2",
  2384  			installConfig: func() *types.InstallConfig {
  2385  				c := validInstallConfig()
  2386  				c.Platform.AWS = nil
  2387  				c.Platform.External = &external.Platform{
  2388  					CloudControllerManager: external.CloudControllerManagerTypeExternal,
  2389  				}
  2390  				c.Capabilities = &types.Capabilities{
  2391  					BaselineCapabilitySet:         configv1.ClusterVersionCapabilitySetNone,
  2392  					AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityCloudCredential},
  2393  				}
  2394  				return c
  2395  			}(),
  2396  			expectedError: "disabling CloudControllerManager on External platform supported only with cloudControllerManager value none",
  2397  		},
  2398  		{
  2399  			name: "Ingress can't be disabled",
  2400  			installConfig: func() *types.InstallConfig {
  2401  				c := validInstallConfig()
  2402  				c.Capabilities = &types.Capabilities{
  2403  					BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
  2404  				}
  2405  				return c
  2406  			}(),
  2407  			expectedError: "the Ingress capability is required",
  2408  		},
  2409  	}
  2410  	for _, tc := range cases {
  2411  		t.Run(tc.name, func(t *testing.T) {
  2412  			err := ValidateInstallConfig(tc.installConfig, false).ToAggregate()
  2413  			if tc.expectedError == "" {
  2414  				assert.NoError(t, err)
  2415  			} else {
  2416  				assert.Regexp(t, tc.expectedError, err)
  2417  			}
  2418  		})
  2419  	}
  2420  }
  2421  
  2422  func Test_ensureIPv4IsFirstInDualStackSlice(t *testing.T) {
  2423  	tests := []struct {
  2424  		name    string
  2425  		vips    []string
  2426  		want    []string
  2427  		wantErr bool
  2428  	}{
  2429  		{
  2430  			name:    "should switch VIPs",
  2431  			vips:    []string{"fe80::0", "192.168.1.1"},
  2432  			want:    []string{"192.168.1.1", "fe80::0"},
  2433  			wantErr: false,
  2434  		},
  2435  		{
  2436  			name:    "should do nothing on single stack",
  2437  			vips:    []string{"192.168.1.1"},
  2438  			want:    []string{"192.168.1.1"},
  2439  			wantErr: false,
  2440  		},
  2441  		{
  2442  			name:    "should do nothing on correct order",
  2443  			vips:    []string{"192.168.1.1", "fe80::0"},
  2444  			want:    []string{"192.168.1.1", "fe80::0"},
  2445  			wantErr: false,
  2446  		},
  2447  		{
  2448  			name:    "return error on invalid number of vips",
  2449  			vips:    []string{"192.168.1.1", "fe80::0", "192.168.1.1"},
  2450  			want:    nil,
  2451  			wantErr: true,
  2452  		},
  2453  	}
  2454  	for _, tt := range tests {
  2455  		t.Run(tt.name, func(t *testing.T) {
  2456  			if err := ensureIPv4IsFirstInDualStackSlice(&tt.vips, field.NewPath("test")); (len(err) > 0) != tt.wantErr {
  2457  				t.Errorf("ensureIPv4IsFirstInDualStackSlice() error = %v, wantErr %v", err, tt.wantErr)
  2458  			}
  2459  			if !tt.wantErr && !utilsslice.Equal(tt.vips, tt.want) && len(tt.vips) == 2 {
  2460  				t.Errorf("ensureIPv4IsFirstInDualStackSlice() changed to %v, expected %v", tt.vips, tt.want)
  2461  			}
  2462  		})
  2463  	}
  2464  }