diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index 71d997af75..81249d7dfe 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -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: |- diff --git a/go.mod b/go.mod index 880e3e5140..d5bd90604d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/asset/agent/installconfig_test.go b/pkg/asset/agent/installconfig_test.go index c9b65f832b..c1e4abe16e 100644 --- a/pkg/asset/agent/installconfig_test.go +++ b/pkg/asset/agent/installconfig_test.go @@ -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", diff --git a/pkg/asset/imagebased/configimage/installconfig_test.go b/pkg/asset/imagebased/configimage/installconfig_test.go index e7b983e8b7..ad82675e23 100644 --- a/pkg/asset/imagebased/configimage/installconfig_test.go +++ b/pkg/asset/imagebased/configimage/installconfig_test.go @@ -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", diff --git a/pkg/types/baremetal/validation/platform.go b/pkg/types/baremetal/validation/platform.go index 0400608fc5..4444be5642 100644 --- a/pkg/types/baremetal/validation/platform.go +++ b/pkg/types/baremetal/validation/platform.go @@ -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 diff --git a/pkg/types/common/common.go b/pkg/types/common/common.go new file mode 100644 index 0000000000..64886a020a --- /dev/null +++ b/pkg/types/common/common.go @@ -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 +} diff --git a/pkg/types/defaults/validation/featuregates.go b/pkg/types/defaults/validation/featuregates.go new file mode 100644 index 0000000000..8476854fcb --- /dev/null +++ b/pkg/types/defaults/validation/featuregates.go @@ -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"), + }, + } +} diff --git a/pkg/types/machinepools.go b/pkg/types/machinepools.go index 2283ba0b1d..e818e2b3c1 100644 --- a/pkg/types/machinepools.go +++ b/pkg/types/machinepools.go @@ -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"` +} diff --git a/pkg/types/validation/featuregate_test.go b/pkg/types/validation/featuregate_test.go index f4ec2209b7..c19206dcfe 100644 --- a/pkg/types/validation/featuregate_test.go +++ b/pkg/types/validation/featuregate_test.go @@ -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 { diff --git a/pkg/types/validation/installconfig.go b/pkg/types/validation/installconfig.go index 2eb32da643..163472bacd 100644 --- a/pkg/types/validation/installconfig.go +++ b/pkg/types/validation/installconfig.go @@ -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 +} diff --git a/pkg/types/validation/installconfig_test.go b/pkg/types/validation/installconfig_test.go index b0d5cdbc80..d91ce6e420 100644 --- a/pkg/types/validation/installconfig_test.go +++ b/pkg/types/validation/installconfig_test.go @@ -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 +}