github.com/openshift/installer@v1.4.17/pkg/asset/agent/installconfig.go (about)

     1  package agent
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  
     8  	"github.com/pkg/errors"
     9  	"github.com/sirupsen/logrus"
    10  	"k8s.io/apimachinery/pkg/util/validation/field"
    11  
    12  	configv1 "github.com/openshift/api/config/v1"
    13  	"github.com/openshift/assisted-service/models"
    14  	"github.com/openshift/installer/pkg/asset"
    15  	"github.com/openshift/installer/pkg/asset/installconfig"
    16  	"github.com/openshift/installer/pkg/asset/releaseimage"
    17  	"github.com/openshift/installer/pkg/types"
    18  	"github.com/openshift/installer/pkg/types/baremetal"
    19  	baremetaldefaults "github.com/openshift/installer/pkg/types/baremetal/defaults"
    20  	baremetalvalidation "github.com/openshift/installer/pkg/types/baremetal/validation"
    21  	"github.com/openshift/installer/pkg/types/external"
    22  	"github.com/openshift/installer/pkg/types/none"
    23  	"github.com/openshift/installer/pkg/types/validation"
    24  	"github.com/openshift/installer/pkg/types/vsphere"
    25  )
    26  
    27  const (
    28  	// InstallConfigFilename is the file containing the install-config.
    29  	InstallConfigFilename = "install-config.yaml"
    30  )
    31  
    32  // OptionalInstallConfig is an InstallConfig where the default is empty, rather
    33  // than generated from running the survey.
    34  type OptionalInstallConfig struct {
    35  	installconfig.AssetBase
    36  	Supplied bool
    37  }
    38  
    39  var _ asset.WritableAsset = (*OptionalInstallConfig)(nil)
    40  
    41  // Dependencies returns all of the dependencies directly needed by an
    42  // InstallConfig asset.
    43  func (a *OptionalInstallConfig) Dependencies() []asset.Asset {
    44  	// Return no dependencies for the Agent install config, because it is
    45  	// optional. We don't need to run the survey if it doesn't exist, since the
    46  	// user may have supplied cluster-manifests that fully define the cluster.
    47  	return []asset.Asset{}
    48  }
    49  
    50  // Generate generates the install-config.yaml file.
    51  func (a *OptionalInstallConfig) Generate(_ context.Context, parents asset.Parents) error {
    52  	// Just generate an empty install config, since we have no dependencies.
    53  	return nil
    54  }
    55  
    56  // Load returns the installconfig from disk.
    57  func (a *OptionalInstallConfig) Load(f asset.FileFetcher) (bool, error) {
    58  	ctx := context.TODO()
    59  	found, err := a.LoadFromFile(f)
    60  	if found && err == nil {
    61  		a.Supplied = true
    62  		if err := a.validateInstallConfig(ctx, a.Config).ToAggregate(); err != nil {
    63  			return false, errors.Wrapf(err, "invalid install-config configuration")
    64  		}
    65  		if err := a.RecordFile(); err != nil {
    66  			return false, err
    67  		}
    68  	}
    69  	return found, err
    70  }
    71  
    72  func (a *OptionalInstallConfig) validateInstallConfig(ctx context.Context, installConfig *types.InstallConfig) field.ErrorList {
    73  	var allErrs field.ErrorList
    74  	if err := validation.ValidateInstallConfig(a.Config, true); err != nil {
    75  		allErrs = append(allErrs, err...)
    76  	}
    77  
    78  	if err := a.validateSupportedPlatforms(installConfig); err != nil {
    79  		allErrs = append(allErrs, err...)
    80  	}
    81  
    82  	if err := a.validateSupportedArchs(installConfig); err != nil {
    83  		allErrs = append(allErrs, err...)
    84  	}
    85  	if err := a.validateReleaseArch(ctx, installConfig); err != nil {
    86  		allErrs = append(allErrs, err...)
    87  	}
    88  
    89  	if installConfig.FeatureSet != configv1.Default {
    90  		allErrs = append(allErrs, field.NotSupported(field.NewPath("FeatureSet"), installConfig.FeatureSet, []string{string(configv1.Default)}))
    91  	}
    92  
    93  	warnUnusedConfig(installConfig)
    94  
    95  	numMasters, numWorkers := GetReplicaCount(installConfig)
    96  	logrus.Infof(fmt.Sprintf("Configuration has %d master replicas and %d worker replicas", numMasters, numWorkers))
    97  
    98  	if err := a.validateControlPlaneConfiguration(installConfig); err != nil {
    99  		allErrs = append(allErrs, err...)
   100  	}
   101  
   102  	if err := a.validateSNOConfiguration(installConfig); err != nil {
   103  		allErrs = append(allErrs, err...)
   104  	}
   105  
   106  	return allErrs
   107  }
   108  
   109  func (a *OptionalInstallConfig) validateSupportedPlatforms(installConfig *types.InstallConfig) field.ErrorList {
   110  	allErrs := ValidateSupportedPlatforms(installConfig.Platform, string(installConfig.ControlPlane.Architecture))
   111  	return append(allErrs, a.validatePlatformsByName(installConfig)...)
   112  }
   113  
   114  // ValidateSupportedPlatforms verifies if the specified platform/arch is supported or not.
   115  func ValidateSupportedPlatforms(platform types.Platform, controlPlaneArch string) field.ErrorList {
   116  	var allErrs field.ErrorList
   117  
   118  	fieldPath := field.NewPath("Platform")
   119  
   120  	if platform.Name() != "" && !IsSupportedPlatform(HivePlatformType(platform)) {
   121  		allErrs = append(allErrs, field.NotSupported(fieldPath, platform.Name(), SupportedInstallerPlatforms()))
   122  	}
   123  	if platform.Name() != none.Name && controlPlaneArch == types.ArchitecturePPC64LE {
   124  		allErrs = append(allErrs, field.Invalid(fieldPath, platform.Name(), fmt.Sprintf("CPU architecture \"%s\" only supports platform \"%s\".", types.ArchitecturePPC64LE, none.Name)))
   125  	}
   126  	if platform.Name() != none.Name && controlPlaneArch == types.ArchitectureS390X {
   127  		allErrs = append(allErrs, field.Invalid(fieldPath, platform.Name(), fmt.Sprintf("CPU architecture \"%s\" only supports platform \"%s\".", types.ArchitectureS390X, none.Name)))
   128  	}
   129  	return allErrs
   130  }
   131  
   132  func (a *OptionalInstallConfig) validatePlatformsByName(installConfig *types.InstallConfig) field.ErrorList {
   133  	var allErrs field.ErrorList
   134  
   135  	if installConfig.Platform.Name() == external.Name {
   136  		if installConfig.Platform.External.PlatformName == string(models.PlatformTypeOci) &&
   137  			installConfig.Platform.External.CloudControllerManager != external.CloudControllerManagerTypeExternal {
   138  			fieldPath := field.NewPath("Platform", "External", "CloudControllerManager")
   139  			allErrs = append(allErrs, field.Invalid(fieldPath, installConfig.Platform.External.CloudControllerManager, fmt.Sprintf("When using external %s platform, %s must be set to %s", string(models.PlatformTypeOci), fieldPath, external.CloudControllerManagerTypeExternal)))
   140  		}
   141  	}
   142  
   143  	if installConfig.Platform.Name() == vsphere.Name {
   144  		allErrs = append(allErrs, a.validateVSpherePlatform(installConfig)...)
   145  	}
   146  
   147  	if installConfig.Platform.Name() == baremetal.Name {
   148  		allErrs = append(allErrs, a.validateBMCConfig(installConfig)...)
   149  	}
   150  
   151  	return allErrs
   152  }
   153  
   154  func (a *OptionalInstallConfig) validateReleaseArch(ctx context.Context, installConfig *types.InstallConfig) field.ErrorList {
   155  	var allErrs field.ErrorList
   156  
   157  	fieldPath := field.NewPath("ControlPlane", "Architecture")
   158  	releaseImage := &releaseimage.Image{}
   159  	asseterr := releaseImage.Generate(ctx, asset.Parents{})
   160  	if asseterr != nil {
   161  		allErrs = append(allErrs, field.InternalError(fieldPath, asseterr))
   162  	}
   163  	releaseArch, err := DetermineReleaseImageArch(installConfig.PullSecret, releaseImage.PullSpec)
   164  	if err != nil {
   165  		logrus.Warnf("Unable to validate the release image architecture, skipping validation")
   166  	} else {
   167  		// Validate that the release image supports the install-config architectures.
   168  		switch releaseArch {
   169  		// Check the release image to see if it is multi.
   170  		case "multi":
   171  			logrus.Debugf("multi architecture release image %s found, all archs supported", releaseImage.PullSpec)
   172  		// If the release image isn't multi, then its single arch, and it must match the cpu architecture.
   173  		case string(installConfig.ControlPlane.Architecture):
   174  			logrus.Debugf("Supported architecture %s found for the release image: %s", installConfig.ControlPlane.Architecture, releaseImage.PullSpec)
   175  		default:
   176  			allErrs = append(allErrs, field.Forbidden(fieldPath, fmt.Sprintf("unsupported release image architecture. ControlPlane Arch: %s doesn't match Release Image Arch: %s", installConfig.ControlPlane.Architecture, releaseArch)))
   177  		}
   178  	}
   179  	return allErrs
   180  }
   181  
   182  func (a *OptionalInstallConfig) validateSupportedArchs(installConfig *types.InstallConfig) field.ErrorList {
   183  	var allErrs field.ErrorList
   184  
   185  	fieldPath := field.NewPath("ControlPlane", "Architecture")
   186  
   187  	switch string(installConfig.ControlPlane.Architecture) {
   188  	case types.ArchitectureAMD64:
   189  	case types.ArchitectureARM64:
   190  	case types.ArchitecturePPC64LE:
   191  	case types.ArchitectureS390X:
   192  	default:
   193  		allErrs = append(allErrs, field.NotSupported(fieldPath, installConfig.ControlPlane.Architecture, []string{types.ArchitectureAMD64, types.ArchitectureARM64, types.ArchitecturePPC64LE, types.ArchitectureS390X}))
   194  	}
   195  
   196  	for i, compute := range installConfig.Compute {
   197  		fieldPath := field.NewPath(fmt.Sprintf("Compute[%d]", i), "Architecture")
   198  
   199  		switch string(compute.Architecture) {
   200  		case types.ArchitectureAMD64:
   201  		case types.ArchitectureARM64:
   202  		case types.ArchitecturePPC64LE:
   203  		case types.ArchitectureS390X:
   204  		default:
   205  			allErrs = append(allErrs, field.NotSupported(fieldPath, compute.Architecture, []string{types.ArchitectureAMD64, types.ArchitectureARM64, types.ArchitecturePPC64LE, types.ArchitectureS390X}))
   206  		}
   207  	}
   208  
   209  	return allErrs
   210  }
   211  
   212  func (a *OptionalInstallConfig) validateControlPlaneConfiguration(installConfig *types.InstallConfig) field.ErrorList {
   213  	var allErrs field.ErrorList
   214  	var fieldPath *field.Path
   215  
   216  	if installConfig.ControlPlane != nil {
   217  		if *installConfig.ControlPlane.Replicas != 1 && *installConfig.ControlPlane.Replicas != 3 {
   218  			fieldPath = field.NewPath("ControlPlane", "Replicas")
   219  			allErrs = append(allErrs, field.Invalid(fieldPath, installConfig.ControlPlane.Replicas, fmt.Sprintf("ControlPlane.Replicas can only be set to 3 or 1. Found %v", *installConfig.ControlPlane.Replicas)))
   220  		}
   221  	}
   222  	return allErrs
   223  }
   224  
   225  func (a *OptionalInstallConfig) validateSNOConfiguration(installConfig *types.InstallConfig) field.ErrorList {
   226  	var allErrs field.ErrorList
   227  	var fieldPath *field.Path
   228  
   229  	var workers int
   230  	for _, worker := range installConfig.Compute {
   231  		workers = workers + int(*worker.Replicas)
   232  	}
   233  
   234  	if installConfig.ControlPlane != nil && *installConfig.ControlPlane.Replicas == 1 {
   235  		if workers == 0 {
   236  			if (installConfig.Platform.Name() == none.Name || installConfig.Platform.Name() == external.Name) && installConfig.Networking.NetworkType != "OVNKubernetes" {
   237  				fieldPath = field.NewPath("Networking", "NetworkType")
   238  				allErrs = append(allErrs, field.Invalid(fieldPath, installConfig.Networking.NetworkType, "Only OVNKubernetes network type is allowed for Single Node OpenShift (SNO) cluster"))
   239  			}
   240  			if installConfig.Platform.Name() != none.Name && installConfig.Platform.Name() != external.Name {
   241  				fieldPath = field.NewPath("Platform")
   242  				allErrs = append(allErrs, field.Invalid(fieldPath, installConfig.Platform.Name(), fmt.Sprintf("Only platform %s and %s supports 1 ControlPlane and 0 Compute nodes", none.Name, external.Name)))
   243  			}
   244  		} else {
   245  			fieldPath = field.NewPath("Compute", "Replicas")
   246  			allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf("Total number of Compute.Replicas must be 0 when ControlPlane.Replicas is 1 for platform %s or %s. Found %v", none.Name, external.Name, workers)))
   247  		}
   248  	}
   249  	return allErrs
   250  }
   251  
   252  // VCenterCredentialsAreProvided returns true if server, username, password, or at least one datacenter
   253  // have been provided.
   254  func VCenterCredentialsAreProvided(vcenter vsphere.VCenter) bool {
   255  	if vcenter.Server != "" || vcenter.Username != "" || vcenter.Password != "" || len(vcenter.Datacenters) > 0 {
   256  		return true
   257  	}
   258  	return false
   259  }
   260  
   261  func (a *OptionalInstallConfig) validateVSpherePlatform(installConfig *types.InstallConfig) field.ErrorList {
   262  	var allErrs field.ErrorList
   263  	vspherePlatform := installConfig.Platform.VSphere
   264  	vcenterServers := map[string]bool{}
   265  	userProvidedCredentials := false
   266  	for _, vcenter := range vspherePlatform.VCenters {
   267  		vcenterServers[vcenter.Server] = true
   268  
   269  		// If any one of the required credential values is entered, then the user is choosing to enter credentials
   270  		if VCenterCredentialsAreProvided(vcenter) {
   271  			// Then check all required credential values are filled
   272  			userProvidedCredentials = true
   273  			message := "All credential fields are required if any one is specified"
   274  			if vcenter.Server == "" {
   275  				fieldPath := field.NewPath("Platform", "VSphere", "vcenter")
   276  				allErrs = append(allErrs, field.Required(fieldPath, message))
   277  			}
   278  			if vcenter.Username == "" {
   279  				fieldPath := field.NewPath("Platform", "VSphere", "user")
   280  				if vspherePlatform.DeprecatedVCenter != "" || vspherePlatform.DeprecatedPassword != "" || vspherePlatform.DeprecatedDatacenter != "" {
   281  					fieldPath = field.NewPath("Platform", "VSphere", "username")
   282  				}
   283  				allErrs = append(allErrs, field.Required(fieldPath, message))
   284  			}
   285  			if vcenter.Password == "" {
   286  				fieldPath := field.NewPath("Platform", "VSphere", "password")
   287  				allErrs = append(allErrs, field.Required(fieldPath, message))
   288  			}
   289  			if len(vcenter.Datacenters) == 0 {
   290  				fieldPath := field.NewPath("Platform", "VSphere", "datacenter")
   291  				allErrs = append(allErrs, field.Required(fieldPath, message))
   292  			}
   293  		}
   294  	}
   295  
   296  	for _, failureDomain := range vspherePlatform.FailureDomains {
   297  		// Although folder is optional in IPI/UPI, it must be set for agent-based installs.
   298  		// If it is not set, assisted-service will set a placeholder value for folder:
   299  		// "/datacenterplaceholder/vm/folderplaceholder"
   300  		//
   301  		// When assisted-service generates the install-config for the cluster, it will fail
   302  		// validation because the placeholder value's datacenter name may not match
   303  		// the datacenter set in the failureDomain in the install-config.yaml submitted
   304  		// to the agent-based create image command.
   305  		if failureDomain.Topology.Folder == "" && userProvidedCredentials {
   306  			fieldPath := field.NewPath("Platform", "VSphere", "failureDomains", "topology", "folder")
   307  			allErrs = append(allErrs, field.Required(fieldPath, "must specify a folder for agent-based installs"))
   308  		}
   309  	}
   310  
   311  	return allErrs
   312  }
   313  
   314  // ClusterName returns the name of the cluster, or a default name if no
   315  // InstallConfig is supplied.
   316  func (a *OptionalInstallConfig) ClusterName() string {
   317  	if a.Config != nil && a.Config.ObjectMeta.Name != "" {
   318  		return a.Config.ObjectMeta.Name
   319  	}
   320  	return "agent-cluster"
   321  }
   322  
   323  // ClusterNamespace returns the namespace of the cluster.
   324  func (a *OptionalInstallConfig) ClusterNamespace() string {
   325  	if a.Config != nil && a.Config.ObjectMeta.Namespace != "" {
   326  		return a.Config.ObjectMeta.Namespace
   327  	}
   328  	return ""
   329  }
   330  
   331  // GetBaremetalHosts gets the hosts defined for a baremetal platform.
   332  func (a *OptionalInstallConfig) GetBaremetalHosts() []*baremetal.Host {
   333  	if a.Config != nil && a.Config.Platform.Name() == baremetal.Name {
   334  		return a.Config.Platform.BareMetal.Hosts
   335  	}
   336  	return nil
   337  }
   338  
   339  func (a *OptionalInstallConfig) validateBMCConfig(installConfig *types.InstallConfig) field.ErrorList {
   340  	var allErrs field.ErrorList
   341  
   342  	bmcConfigured := false
   343  	for _, host := range installConfig.Platform.BareMetal.Hosts {
   344  		if host.BMC.Address == "" {
   345  			continue
   346  		}
   347  		bmcConfigured = true
   348  	}
   349  
   350  	if bmcConfigured {
   351  		fieldPath := field.NewPath("Platform", "BareMetal")
   352  		allErrs = append(allErrs, baremetalvalidation.ValidateProvisioningNetworking(installConfig.Platform.BareMetal, installConfig.Networking, fieldPath)...)
   353  	}
   354  
   355  	return allErrs
   356  }
   357  
   358  func warnUnusedConfig(installConfig *types.InstallConfig) {
   359  	// "Proxyonly" is the default set from generic install config code
   360  	if installConfig.AdditionalTrustBundlePolicy != "Proxyonly" {
   361  		fieldPath := field.NewPath("AdditionalTrustBundlePolicy")
   362  		logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, installConfig.AdditionalTrustBundlePolicy))
   363  	}
   364  
   365  	for i, compute := range installConfig.Compute {
   366  		if compute.Hyperthreading != "Enabled" {
   367  			fieldPath := field.NewPath(fmt.Sprintf("Compute[%d]", i), "Hyperthreading")
   368  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, compute.Hyperthreading))
   369  		}
   370  
   371  		if compute.Platform != (types.MachinePoolPlatform{}) {
   372  			fieldPath := field.NewPath(fmt.Sprintf("Compute[%d]", i), "Platform")
   373  			logrus.Warnf(fmt.Sprintf("%s is ignored", fieldPath))
   374  		}
   375  	}
   376  
   377  	if installConfig.ControlPlane.Hyperthreading != "Enabled" {
   378  		fieldPath := field.NewPath("ControlPlane", "Hyperthreading")
   379  		logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, installConfig.ControlPlane.Hyperthreading))
   380  	}
   381  
   382  	if installConfig.ControlPlane.Platform != (types.MachinePoolPlatform{}) {
   383  		fieldPath := field.NewPath("ControlPlane", "Platform")
   384  		logrus.Warnf(fmt.Sprintf("%s is ignored", fieldPath))
   385  	}
   386  
   387  	switch installConfig.Platform.Name() {
   388  
   389  	case baremetal.Name:
   390  		defaultIc := &types.InstallConfig{Platform: types.Platform{BareMetal: &baremetal.Platform{}}}
   391  		baremetaldefaults.SetPlatformDefaults(defaultIc.Platform.BareMetal, defaultIc)
   392  
   393  		baremetal := installConfig.Platform.BareMetal
   394  		defaultBM := defaultIc.Platform.BareMetal
   395  		// Compare values from generic installconfig code to check for changes
   396  		if baremetal.LibvirtURI != defaultBM.LibvirtURI {
   397  			fieldPath := field.NewPath("Platform", "Baremetal", "LibvirtURI")
   398  			logrus.Debugf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.LibvirtURI))
   399  		}
   400  		if baremetal.BootstrapProvisioningIP != defaultBM.BootstrapProvisioningIP {
   401  			fieldPath := field.NewPath("Platform", "Baremetal", "BootstrapProvisioningIP")
   402  			logrus.Debugf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.BootstrapProvisioningIP))
   403  		}
   404  		if baremetal.ExternalBridge != defaultBM.ExternalBridge {
   405  			fieldPath := field.NewPath("Platform", "Baremetal", "ExternalBridge")
   406  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.ExternalBridge))
   407  		}
   408  		if baremetal.ProvisioningBridge != defaultBM.ProvisioningBridge {
   409  			fieldPath := field.NewPath("Platform", "Baremetal", "ProvisioningBridge")
   410  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.ProvisioningBridge))
   411  		}
   412  
   413  		for i, host := range baremetal.Hosts {
   414  			// The default is UEFI. +kubebuilder:validation:Enum="";UEFI;UEFISecureBoot;legacy. Set from generic install config code
   415  			if host.BootMode != "UEFI" {
   416  				fieldPath := field.NewPath("Platform", "Baremetal", fmt.Sprintf("Hosts[%d]", i), "BootMode")
   417  				logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, host.BootMode))
   418  			}
   419  		}
   420  
   421  		if baremetal.DefaultMachinePlatform != nil {
   422  			fieldPath := field.NewPath("Platform", "Baremetal", "DefaultMachinePlatform")
   423  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.DefaultMachinePlatform))
   424  		}
   425  		if baremetal.BootstrapOSImage != "" {
   426  			fieldPath := field.NewPath("Platform", "Baremetal", "BootstrapOSImage")
   427  			logrus.Debugf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.BootstrapOSImage))
   428  		}
   429  		// ClusterOSImage is ignored even in IPI now, so we probably don't need to check it at all.
   430  
   431  		if baremetal.BootstrapExternalStaticIP != "" {
   432  			fieldPath := field.NewPath("Platform", "Baremetal", "BootstrapExternalStaticIP")
   433  			logrus.Debugf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.BootstrapExternalStaticIP))
   434  		}
   435  		if baremetal.BootstrapExternalStaticGateway != "" {
   436  			fieldPath := field.NewPath("Platform", "Baremetal", "BootstrapExternalStaticGateway")
   437  			logrus.Debugf(fmt.Sprintf("%s: %s is ignored", fieldPath, baremetal.BootstrapExternalStaticGateway))
   438  		}
   439  	case vsphere.Name:
   440  		vspherePlatform := installConfig.Platform.VSphere
   441  
   442  		if vspherePlatform.ClusterOSImage != "" {
   443  			fieldPath := field.NewPath("Platform", "VSphere", "ClusterOSImage")
   444  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.ClusterOSImage))
   445  		}
   446  		if vspherePlatform.DefaultMachinePlatform != nil && !reflect.DeepEqual(*vspherePlatform.DefaultMachinePlatform, vsphere.MachinePool{}) {
   447  			fieldPath := field.NewPath("Platform", "VSphere", "DefaultMachinePlatform")
   448  			logrus.Warnf(fmt.Sprintf("%s: %v is ignored", fieldPath, vspherePlatform.DefaultMachinePlatform))
   449  		}
   450  		if vspherePlatform.DiskType != "" {
   451  			fieldPath := field.NewPath("Platform", "VSphere", "DiskType")
   452  			logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DiskType))
   453  		}
   454  
   455  		if vspherePlatform.LoadBalancer != nil && !reflect.DeepEqual(*vspherePlatform.LoadBalancer, configv1.VSpherePlatformLoadBalancer{}) {
   456  			fieldPath := field.NewPath("Platform", "VSphere", "LoadBalancer")
   457  			logrus.Warnf(fmt.Sprintf("%s: %v is ignored", fieldPath, vspherePlatform.LoadBalancer))
   458  		}
   459  
   460  		if len(vspherePlatform.Hosts) > 1 {
   461  			fieldPath := field.NewPath("Platform", "VSphere", "Hosts")
   462  			logrus.Warnf(fmt.Sprintf("%s: %v is ignored", fieldPath, vspherePlatform.Hosts))
   463  		}
   464  	}
   465  	// "External" is the default set from generic install config code
   466  	if installConfig.Publish != "External" {
   467  		fieldPath := field.NewPath("Publish")
   468  		logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, installConfig.Publish))
   469  	}
   470  	if installConfig.CredentialsMode != "" {
   471  		fieldPath := field.NewPath("CredentialsMode")
   472  		logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, installConfig.CredentialsMode))
   473  	}
   474  	if installConfig.BootstrapInPlace != nil && installConfig.BootstrapInPlace.InstallationDisk != "" {
   475  		fieldPath := field.NewPath("BootstrapInPlace", "InstallationDisk")
   476  		logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, installConfig.BootstrapInPlace.InstallationDisk))
   477  	}
   478  }
   479  
   480  // GetReplicaCount gets the configured master and worker replicas.
   481  func GetReplicaCount(installConfig *types.InstallConfig) (numMasters, numWorkers int64) {
   482  	numRequiredMasters := int64(0)
   483  	if installConfig.ControlPlane != nil && installConfig.ControlPlane.Replicas != nil {
   484  		numRequiredMasters += *installConfig.ControlPlane.Replicas
   485  	}
   486  
   487  	numRequiredWorkers := int64(0)
   488  	for _, worker := range installConfig.Compute {
   489  		if worker.Replicas != nil {
   490  			numRequiredWorkers += *worker.Replicas
   491  		}
   492  	}
   493  
   494  	return numRequiredMasters, numRequiredWorkers
   495  }