1
0
mirror of https://github.com/openshift/installer.git synced 2026-02-05 15:47:14 +01:00

Merge pull request #9521 from mshitrit/fencing-config-platform-none

OCPEDGE-1505: Enhance Platform none with Fencing Credentials
This commit is contained in:
openshift-merge-bot[bot]
2025-03-31 21:22:27 +00:00
committed by GitHub
11 changed files with 525 additions and 52 deletions

View File

@@ -57,6 +57,33 @@ spec:
- ""
- amd64
type: string
fencing:
description: |-
Fencing stores the information about a baremetal host's management controller.
Fencing may only be set for control plane nodes.
properties:
credentials:
description: Credentials stores the information about a baremetal
host's management controller.
items:
description: Credential stores the information about a baremetal
host's management controller.
properties:
address:
type: string
hostName:
type: string
password:
type: string
username:
type: string
required:
- address
- password
- username
type: object
type: array
type: object
hyperthreading:
default: Enabled
description: |-
@@ -1293,6 +1320,33 @@ spec:
- ""
- amd64
type: string
fencing:
description: |-
Fencing stores the information about a baremetal host's management controller.
Fencing may only be set for control plane nodes.
properties:
credentials:
description: Credentials stores the information about a baremetal
host's management controller.
items:
description: Credential stores the information about a baremetal
host's management controller.
properties:
address:
type: string
hostName:
type: string
password:
type: string
username:
type: string
required:
- address
- password
- username
type: object
type: array
type: object
hyperthreading:
default: Enabled
description: |-
@@ -2468,6 +2522,33 @@ spec:
- ""
- amd64
type: string
fencing:
description: |-
Fencing stores the information about a baremetal host's management controller.
Fencing may only be set for control plane nodes.
properties:
credentials:
description: Credentials stores the information about a baremetal
host's management controller.
items:
description: Credential stores the information about a baremetal
host's management controller.
properties:
address:
type: string
hostName:
type: string
password:
type: string
username:
type: string
required:
- address
- password
- username
type: object
type: array
type: object
hyperthreading:
default: Enabled
description: |-

2
go.mod
View File

@@ -44,6 +44,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6
github.com/aws/smithy-go v1.22.3
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e
github.com/clarketm/json v1.14.1
github.com/containers/image/v5 v5.31.0
@@ -181,7 +182,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect

View File

@@ -474,7 +474,7 @@ platform:
pullSecret: "{\"auths\":{\"example.com\":{\"auth\":\"authorization value\"}}}"
`,
expectedFound: false,
expectedError: "invalid install-config configuration: ControlPlane.Replicas: Invalid value: 2: ControlPlane.Replicas can only be set to 5, 4, 3, or 1. Found 2",
expectedError: "invalid install-config configuration: [controlPlane.fencing.credentials: Forbidden: there should be exactly two fencing credentials to support the two node cluster, instead 0 credentials were found, ControlPlane.Replicas: Invalid value: 2: ControlPlane.Replicas can only be set to 5, 4, 3, or 1. Found 2]",
},
{
name: "invalid platform for SNO cluster",

View File

@@ -101,7 +101,7 @@ platform:
pullSecret: "{\"auths\":{\"example.com\":{\"auth\":\"authorization value\"}}}"
`,
expectedFound: false,
expectedError: "invalid install-config configuration: ControlPlane.Replicas: Required value: Only Single Node OpenShift (SNO) is supported, total number of ControlPlane.Replicas must be 1. Found 2",
expectedError: "invalid install-config configuration: [controlPlane.fencing.credentials: Forbidden: there should be exactly two fencing credentials to support the two node cluster, instead 0 credentials were found, ControlPlane.Replicas: Required value: Only Single Node OpenShift (SNO) is supported, total number of ControlPlane.Replicas must be 1. Found 2]",
},
{
name: "invalid number of MachineNetworks",

View File

@@ -23,6 +23,7 @@ import (
"github.com/openshift/installer/pkg/ipnet"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/baremetal"
"github.com/openshift/installer/pkg/types/common"
"github.com/openshift/installer/pkg/validate"
)
@@ -180,53 +181,7 @@ func validateDHCPRange(p *baremetal.Platform, fldPath *field.Path) (allErrs fiel
// validateHostsBase validates the hosts based on a filtering function
func validateHostsBase(hosts []*baremetal.Host, fldPath *field.Path, filter validator.FilterFunc) field.ErrorList {
hostErrs := field.ErrorList{}
values := make(map[string]map[interface{}]struct{})
//Initialize a new validator and register a custom validation rule for the tag `uniqueField`
validate := validator.New()
validate.RegisterValidation("uniqueField", func(fl validator.FieldLevel) bool {
valueFound := false
fieldName := fl.Parent().Type().Name() + "." + fl.FieldName()
fieldValue := fl.Field().Interface()
if fl.Field().Type().Comparable() {
if _, present := values[fieldName]; !present {
values[fieldName] = make(map[interface{}]struct{})
}
fieldValues := values[fieldName]
if _, valueFound = fieldValues[fieldValue]; !valueFound {
fieldValues[fieldValue] = struct{}{}
}
} else {
panic(fmt.Sprintf("Cannot apply validation rule 'uniqueField' on field %s", fl.FieldName()))
}
return !valueFound
})
//Apply validations and translate errors
fldPath = fldPath.Child("hosts")
for idx, host := range hosts {
err := validate.StructFiltered(host, filter)
if err != nil {
hostType := reflect.TypeOf(hosts).Elem().Elem().Name()
for _, err := range err.(validator.ValidationErrors) {
childName := fldPath.Index(idx).Child(err.Namespace()[len(hostType)+1:])
switch err.Tag() {
case "required":
hostErrs = append(hostErrs, field.Required(childName, "missing "+err.Field()))
case "uniqueField":
hostErrs = append(hostErrs, field.Duplicate(childName, err.Value()))
}
}
}
}
return hostErrs
return common.ValidateUniqueAndRequiredFields(hosts, fldPath, filter, "hosts")
}
// filterHostsBMC is a function to control whether to filter BMC details of Hosts

View File

@@ -0,0 +1,67 @@
package common
import (
"errors"
"fmt"
"reflect"
"github.com/go-playground/validator/v10"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ValidateUniqueAndRequiredFields validated unique fields are indeed unique and that required fields exist on a generic element.
func ValidateUniqueAndRequiredFields[T any](elements []T, fldPath *field.Path, filter validator.FilterFunc, fieldName string) field.ErrorList {
errs := field.ErrorList{}
values := make(map[string]map[interface{}]struct{})
// Initialize a new validator and register a custom validation rule for the tag `uniqueField`.
validate := validator.New()
if err := validate.RegisterValidation("uniqueField", func(fl validator.FieldLevel) bool {
valueFound := false
fieldName := fl.Parent().Type().Name() + "." + fl.FieldName()
fieldValue := fl.Field().Interface()
if fl.Field().Type().Comparable() {
if _, present := values[fieldName]; !present {
values[fieldName] = make(map[interface{}]struct{})
}
fieldValues := values[fieldName]
if _, valueFound = fieldValues[fieldValue]; !valueFound {
fieldValues[fieldValue] = struct{}{}
}
} else {
panic(fmt.Sprintf("Cannot apply validation rule 'uniqueField' on field %s", fl.FieldName()))
}
return !valueFound
}); err != nil {
logrus.Error("Unexpected error registering validation", err)
}
// Apply validations and translate errors.
fldPath = fldPath.Child(fieldName)
for idx, element := range elements {
err := validate.StructFiltered(element, filter)
if err != nil {
elementType := reflect.TypeOf(elements).Elem().Elem().Name()
var validationErrs validator.ValidationErrors
if errors.As(err, &validationErrs) {
for _, fieldErr := range validationErrs {
childName := fldPath.Index(idx).Child(fieldErr.Namespace()[len(elementType)+1:])
switch fieldErr.Tag() {
case "required":
errs = append(errs, field.Required(childName, "missing "+fieldErr.Field()))
case "uniqueField":
errs = append(errs, field.Duplicate(childName, fieldErr.Value()))
}
}
}
}
}
return errs
}

View File

@@ -0,0 +1,21 @@
package validation
import (
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/openshift/api/features"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/featuregates"
)
// GatedFeatures determines all of the install config fields that should
// be validated to ensure that the proper featuregate is enabled when the field is used.
func GatedFeatures(c *types.InstallConfig) []featuregates.GatedInstallConfigFeature {
return []featuregates.GatedInstallConfigFeature{
{
FeatureGateName: features.FeatureGateDualReplica,
Condition: c.ControlPlane != nil && c.ControlPlane.Fencing != nil,
Field: field.NewPath("platform", "none", "fencingCredentials"),
},
}
}

View File

@@ -78,6 +78,11 @@ type MachinePool struct {
// +kubebuilder:default=amd64
// +optional
Architecture Architecture `json:"architecture,omitempty"`
// Fencing stores the information about a baremetal host's management controller.
// Fencing may only be set for control plane nodes.
// +optional
Fencing *Fencing `json:"fencing,omitempty"`
}
// MachinePoolPlatform is the platform-specific configuration for a machine
@@ -145,3 +150,18 @@ func (p *MachinePoolPlatform) Name() string {
return ""
}
}
// Fencing stores the information about a baremetal host's management controller.
type Fencing struct {
// Credentials stores the information about a baremetal host's management controller.
// +optional
Credentials []*Credential `json:"credentials,omitempty"`
}
// Credential stores the information about a baremetal host's management controller.
type Credential struct {
HostName string `json:"hostName,omitempty" validate:"required,uniqueField"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Address string `json:"address" validate:"required,uniqueField"`
}

View File

@@ -223,6 +223,33 @@ func TestFeatureGates(t *testing.T) {
return c
}(),
},
{
name: "FencingCredentials is not allowed with Feature Gates disabled",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.ControlPlane.Fencing = &types.Fencing{Credentials: []*types.Credential{{HostName: "host1"}, {HostName: "host2"}}}
return c
}(),
expected: `^platform.none.fencingCredentials: Forbidden: this field is protected by the DualReplica feature gate which must be enabled through either the TechPreviewNoUpgrade or CustomNoUpgrade feature set$`,
},
{
name: "FencingCredentials is allowed with TechPreviewNoUpgrade Feature Set",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.FeatureSet = v1.TechPreviewNoUpgrade
c.ControlPlane.Fencing = &types.Fencing{Credentials: []*types.Credential{{HostName: "host1"}, {HostName: "host2"}}}
return c
}(),
},
{
name: "FencingCredentials is allowed with DevPreviewNoUpgrade Feature Set",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.FeatureSet = v1.DevPreviewNoUpgrade
c.ControlPlane.Fencing = &types.Fencing{Credentials: []*types.Credential{{HostName: "host1"}, {HostName: "host2"}}}
return c
}(),
},
}
for _, tc := range cases {

View File

@@ -31,6 +31,8 @@ import (
azurevalidation "github.com/openshift/installer/pkg/types/azure/validation"
"github.com/openshift/installer/pkg/types/baremetal"
baremetalvalidation "github.com/openshift/installer/pkg/types/baremetal/validation"
"github.com/openshift/installer/pkg/types/common"
defaultsvalidation "github.com/openshift/installer/pkg/types/defaults/validation"
"github.com/openshift/installer/pkg/types/external"
"github.com/openshift/installer/pkg/types/featuregates"
"github.com/openshift/installer/pkg/types/gcp"
@@ -121,7 +123,7 @@ func ValidateInstallConfig(c *types.InstallConfig, usingAgentMethod bool) field.
}
allErrs = append(allErrs, validatePlatform(&c.Platform, usingAgentMethod, field.NewPath("platform"), c.Networking, c)...)
if c.ControlPlane != nil {
allErrs = append(allErrs, validateControlPlane(&c.Platform, c.ControlPlane, field.NewPath("controlPlane"))...)
allErrs = append(allErrs, validateControlPlane(c, field.NewPath("controlPlane"))...)
} else {
allErrs = append(allErrs, field.Required(field.NewPath("controlPlane"), "controlPlane is required"))
}
@@ -744,8 +746,10 @@ func validateOVNIPv4InternalJoinSubnet(n *types.Networking, fldPath *field.Path)
return allErrs
}
func validateControlPlane(platform *types.Platform, pool *types.MachinePool, fldPath *field.Path) field.ErrorList {
func validateControlPlane(installConfig *types.InstallConfig, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
platform := &installConfig.Platform
pool := installConfig.ControlPlane
if pool.Name != types.MachinePoolControlPlaneRoleName {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("name"), pool.Name, []string{types.MachinePoolControlPlaneRoleName}))
}
@@ -753,6 +757,7 @@ func validateControlPlane(platform *types.Platform, pool *types.MachinePool, fld
allErrs = append(allErrs, field.Invalid(fldPath.Child("replicas"), pool.Replicas, "number of control plane replicas must be positive"))
}
allErrs = append(allErrs, ValidateMachinePool(platform, pool, fldPath)...)
allErrs = append(allErrs, validateFencingCredentials(installConfig)...)
return allErrs
}
@@ -804,6 +809,10 @@ func validateCompute(platform *types.Platform, control *types.MachinePool, pools
allErrs = append(allErrs, field.Invalid(poolFldPath.Child("architecture"), p.Architecture, "heteregeneous multi-arch is not supported; compute pool architecture must match control plane"))
}
allErrs = append(allErrs, ValidateMachinePool(platform, &p, poolFldPath)...)
if p.Fencing != nil {
allErrs = append(allErrs, field.Invalid(poolFldPath.Child("fencing"), p.Fencing, "fencing is only valid for control plane"))
}
}
return allErrs
}
@@ -1466,6 +1475,10 @@ func validateGatedFeatures(c *types.InstallConfig) field.ErrorList {
gatedFeatures = append(gatedFeatures, azurevalidation.GatedFeatures(c)...)
}
if c.ControlPlane != nil {
gatedFeatures = append(gatedFeatures, defaultsvalidation.GatedFeatures(c)...)
}
fg := c.EnabledFeatureGates()
errMsgTemplate := "this field is protected by the %s feature gate which must be enabled through either the TechPreviewNoUpgrade or CustomNoUpgrade feature set"
@@ -1544,3 +1557,39 @@ func isV4NodeSubnetLargeEnough(cn []types.ClusterNetworkEntry, nodeSubnet *ipnet
// reserve one IP for the gw, one IP for network and one for broadcasting
return maxNodesNum < (1<<(addrLen-intSubnetMask) - 3), nil
}
// validateCredentialsNumber in case fencing credentials exists validates there are exactly 2.
func validateCredentialsNumber(controlPlane *types.MachinePool, fencing *types.Fencing, fldPath *field.Path) field.ErrorList {
errs := field.ErrorList{}
if controlPlane == nil || controlPlane.Replicas == nil {
// invalid use case covered by a different validation.
return errs
}
numOfCpReplicas := *controlPlane.Replicas
var numOfCredentials int
if fencing != nil {
numOfCredentials = len(fencing.Credentials)
}
if numOfCpReplicas == 2 {
if numOfCredentials != 2 {
errs = append(errs, field.Forbidden(fldPath, fmt.Sprintf("there should be exactly two fencing credentials to support the two node cluster, instead %d credentials were found", numOfCredentials)))
}
} else {
if numOfCredentials != 0 {
errs = append(errs, field.Forbidden(fldPath, fmt.Sprintf("there should not be any fencing credentials configured for a non dual replica control plane (Two Nodes Fencing) cluster, instead %d credentials were found", numOfCredentials)))
}
}
return errs
}
func validateFencingCredentials(installConfig *types.InstallConfig) (errors field.ErrorList) {
fldPath := field.NewPath("controlPlane", "fencing")
fencingCredentials := installConfig.ControlPlane.Fencing
allErrs := field.ErrorList{}
if fencingCredentials != nil {
allErrs = append(allErrs, common.ValidateUniqueAndRequiredFields(fencingCredentials.Credentials, fldPath, func([]byte) bool { return false }, "credentials")...)
}
allErrs = append(allErrs, validateCredentialsNumber(installConfig.ControlPlane, fencingCredentials, fldPath.Child("credentials"))...)
return allErrs
}

View File

@@ -5,6 +5,7 @@ import (
"net"
"testing"
"github.com/aws/smithy-go/ptr"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -2797,3 +2798,255 @@ func TestValidateReleaseArchitecture(t *testing.T) {
})
})
}
func TestValidateTNF(t *testing.T) {
cases := []struct {
name string
config *types.InstallConfig
machinePool *types.MachinePool
checkCompute bool
expected string
}{
{
config: installConfig().CpReplicas(3).build(),
name: "valid_empty_credentials",
expected: "",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1(), c2())).
CpReplicas(2).
build(),
name: "valid_two_credentials",
expected: "",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1(), c2(), c3())).
CpReplicas(2).build(),
name: "invalid_number_of_credentials_for_dual_replica",
expected: "controlPlane.fencing.credentials: Forbidden: there should be exactly two fencing credentials to support the two node cluster, instead 3 credentials were found",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1(), c2())).
CpReplicas(3).build(),
name: "invalid_number_of_credentials_for_non_dual_replica",
expected: "controlPlane.fencing.credentials: Forbidden: there should not be any fencing credentials configured for a non dual replica control plane \\(Two Nodes Fencing\\) cluster, instead 2 credentials were found",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(
c1().BMCAddress("ipmi://192.168.111.1"),
c2().BMCAddress("ipmi://192.168.111.1"))).
CpReplicas(2).build(),
name: "duplicate_bmc_address",
expected: "controlPlane.fencing.credentials\\[1\\].Address: Duplicate value: \"ipmi://192.168.111.1\"",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1().BMCAddress(""), c2())).
CpReplicas(2).build(),
name: "bmc_address_required",
expected: "controlPlane.fencing.credentials\\[0\\].Address: Required value: missing Address",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1(), c2().BMCUsername(""))).
CpReplicas(2).build(),
name: "bmc_username_required",
expected: "controlPlane.fencing.credentials\\[1\\].Username: Required value: missing Username",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1().BMCPassword(""), c2())).
CpReplicas(2).build(),
name: "bmc_password_required",
expected: "controlPlane.fencing.credentials\\[0\\].Password: Required value: missing Password",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Credential(c1().HostName(""), c2())).
CpReplicas(2).build(),
name: "host_name_required",
expected: "controlPlane.fencing.credentials\\[0\\].HostName: Required value: missing HostName",
},
{
config: installConfig().
MachinePoolCP(machinePool().
Architecture(types.ArchitectureAMD64).
Credential(c1(), c2())).
MachinePoolCompute(
machinePool().Name("worker").
Hyperthreading(types.HyperthreadingDisabled).
Architecture(types.ArchitectureAMD64).
Replicas(ptr.Int64(3)).
Credential(c1())).
CpReplicas(2).build(),
name: "host_name_required",
checkCompute: true,
expected: `compute\[\d+\]\.fencing: Invalid value: types\.Fencing\{Credentials:\[\]\*types\.Credential\{\(\*types\.Credential\)\(\S+\)\}\}: fencing is only valid for control plane`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Build default wrapping installConfig
var err error
if tc.checkCompute {
err = validateCompute(&tc.config.Platform, tc.config.ControlPlane, tc.config.Compute, field.NewPath("compute"), false).ToAggregate()
} else {
err = validateFencingCredentials(tc.config).ToAggregate()
}
if tc.expected == "" {
assert.NoError(t, err)
} else {
assert.Regexp(t, tc.expected, err)
}
})
}
}
type credentialBuilder struct {
types.Credential
}
func (hb *credentialBuilder) build() *types.Credential {
return &hb.Credential
}
func c1() *credentialBuilder {
return &credentialBuilder{
types.Credential{
HostName: "host1",
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
}
}
func c2() *credentialBuilder {
return &credentialBuilder{
types.Credential{
HostName: "host2",
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.2",
},
}
}
func c3() *credentialBuilder {
return &credentialBuilder{
types.Credential{
HostName: "host3",
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.3",
},
}
}
func (hb *credentialBuilder) HostName(value string) *credentialBuilder {
hb.Credential.HostName = value
return hb
}
func (hb *credentialBuilder) BMCAddress(value string) *credentialBuilder {
hb.Credential.Address = value
return hb
}
func (hb *credentialBuilder) BMCUsername(value string) *credentialBuilder {
hb.Credential.Username = value
return hb
}
func (hb *credentialBuilder) BMCPassword(value string) *credentialBuilder {
hb.Credential.Password = value
return hb
}
type machinePoolBuilder struct {
types.MachinePool
}
func (pb *machinePoolBuilder) build() *types.MachinePool {
return &pb.MachinePool
}
func machinePool() *machinePoolBuilder {
return &machinePoolBuilder{
types.MachinePool{}}
}
func (pb *machinePoolBuilder) Name(name string) *machinePoolBuilder {
pb.MachinePool.Name = name
return pb
}
func (pb *machinePoolBuilder) Replicas(replicas *int64) *machinePoolBuilder {
pb.MachinePool.Replicas = replicas
return pb
}
func (pb *machinePoolBuilder) Architecture(architecture types.Architecture) *machinePoolBuilder {
pb.MachinePool.Architecture = architecture
return pb
}
func (pb *machinePoolBuilder) Hyperthreading(hyperthreading types.HyperthreadingMode) *machinePoolBuilder {
pb.MachinePool.Hyperthreading = hyperthreading
return pb
}
func (pb *machinePoolBuilder) Credential(builders ...*credentialBuilder) *machinePoolBuilder {
pb.MachinePool.Fencing = &types.Fencing{}
for _, builder := range builders {
pb.MachinePool.Fencing.Credentials = append(pb.MachinePool.Fencing.Credentials, builder.build())
}
return pb
}
type installConfigBuilder struct {
types.InstallConfig
}
func installConfig() *installConfigBuilder {
return &installConfigBuilder{
InstallConfig: types.InstallConfig{},
}
}
func (icb *installConfigBuilder) CpReplicas(numOfCpReplicas int64) *installConfigBuilder {
if icb.InstallConfig.ControlPlane == nil {
icb.InstallConfig.ControlPlane = &types.MachinePool{}
}
icb.InstallConfig.ControlPlane.Replicas = &numOfCpReplicas
return icb
}
func (icb *installConfigBuilder) MachinePoolCP(builder *machinePoolBuilder) *installConfigBuilder {
icb.InstallConfig.ControlPlane = builder.build()
return icb
}
func (icb *installConfigBuilder) MachinePoolCompute(builders ...*machinePoolBuilder) *installConfigBuilder {
for _, builder := range builders {
icb.InstallConfig.Compute = append(icb.InstallConfig.Compute, *builder.build())
}
return icb
}
func (icb *installConfigBuilder) build() *types.InstallConfig {
return &icb.InstallConfig
}