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 }