Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions data/data/install.openshift.io_installconfigs.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ require (
github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0
github.com/nutanix-cloud-native/prism-go-client v0.5.0
github.com/onsi/gomega v1.38.2
github.com/openshift/api v0.0.0-20251120220512-cb382c9eaf42
github.com/openshift/api v0.0.0-20260105114749-aae5635a71a7
github.com/openshift/assisted-image-service v0.0.0-20240607085136-02df2e56dde6
github.com/openshift/assisted-service/api v0.0.0
github.com/openshift/assisted-service/client v0.0.0
github.com/openshift/assisted-service/models v0.0.0
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235
github.com/openshift/client-go v0.0.0-20260105124352-f93a4291f9ae
github.com/openshift/cloud-credential-operator v0.0.0-20240404165937-5e8812d64187
github.com/openshift/cluster-api-provider-baremetal v0.0.0-20220408122422-7a548effc26e
github.com/openshift/cluster-api-provider-libvirt v0.2.1-0.20230308152226-83c0473d4429
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -888,8 +888,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/openshift/api v0.0.0-20251120220512-cb382c9eaf42 h1:Mo2FlDdoCZ+BE2W4C0lNcxEDeIIhfsYFP6vj4Sggp8w=
github.com/openshift/api v0.0.0-20251120220512-cb382c9eaf42/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/api v0.0.0-20260105114749-aae5635a71a7 h1:DeKd90ff6ieG02cFroiRTh7oKguGVaEYyTDkXHLIn5A=
github.com/openshift/api v0.0.0-20260105114749-aae5635a71a7/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/assisted-image-service v0.0.0-20240607085136-02df2e56dde6 h1:U6ve+dnHlHhAELoxX+rdFOHVhoaYl0l9qtxwYtsO6C0=
github.com/openshift/assisted-image-service v0.0.0-20240607085136-02df2e56dde6/go.mod h1:o2H5VwQhUD8P6XsK6dRmKpCCJqVvv12KJQZBXmcCXCU=
github.com/openshift/assisted-service/api v0.0.0-20250922204150-a52b83145bea h1:YhJ9iHKKT5ooAdVr8qq3BdudhTxP/WF0XYDT5gzi1ak=
Expand All @@ -902,8 +902,8 @@ github.com/openshift/baremetal-operator/apis v0.0.0-20231128154154-6736c9b9c6c8
github.com/openshift/baremetal-operator/apis v0.0.0-20231128154154-6736c9b9c6c8/go.mod h1:CvKrrnAcvvtrZIc9y9WaqWmJhK0AJ9sWnh+VP4d7jcM=
github.com/openshift/baremetal-operator/pkg/hardwareutils v0.0.0-20231128154154-6736c9b9c6c8 h1:38vY9w7dXqB7tI9g1GCUnpahNDyBbp9Yylq+BQ154YE=
github.com/openshift/baremetal-operator/pkg/hardwareutils v0.0.0-20231128154154-6736c9b9c6c8/go.mod h1:399nvdaqoU9rTI25UdFw2EWcVjmJPpeZPIhfDAIx/XU=
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY=
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM=
github.com/openshift/client-go v0.0.0-20260105124352-f93a4291f9ae h1:veyDeAOBVJun1KoOsTIRlD7Q5LwRR32kfS2IPjPXJKE=
github.com/openshift/client-go v0.0.0-20260105124352-f93a4291f9ae/go.mod h1:leoeMrUnO40DwByGl7we2l+h6HQq3Y6bHUa+DnmRl+8=
github.com/openshift/cloud-credential-operator v0.0.0-20240404165937-5e8812d64187 h1:v2D/+SWsOPsl4Syz1SVjo7m3L0ethuRGR++ubsb89oA=
github.com/openshift/cloud-credential-operator v0.0.0-20240404165937-5e8812d64187/go.mod h1:eyA6FG71366St6Q1TW+jXdQbald0rUwtEPhAREMlyhA=
github.com/openshift/cloud-provider-vsphere v1.19.1-0.20240626105621-6464d0bb4928 h1:gX0HAKR0f40xmMWlUSn8DBMCjip8Iuzg5XToWAv6Uzw=
Expand Down
47 changes: 47 additions & 0 deletions pkg/asset/installconfig/aws/dedicatedhosts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package aws

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/sirupsen/logrus"
)

// Host holds metadata for a dedicated host.
type Host struct {
ID string
Zone string
}

// dedicatedHosts retrieves a list of dedicated hosts for the given region and
// returns them in a map keyed by the host ID.
func dedicatedHosts(ctx context.Context, session *session.Session, region string) (map[string]Host, error) {
hostsByID := map[string]Host{}

client := ec2.New(session, aws.NewConfig().WithRegion(region))
input := &ec2.DescribeHostsInput{}

if err := client.DescribeHostsPagesWithContext(ctx, input, func(page *ec2.DescribeHostsOutput, lastPage bool) bool {
for _, h := range page.Hosts {
id := aws.StringValue(h.HostId)
if id == "" {
// Skip entries lacking an ID (should not happen)
continue
}

logrus.Debugf("Found dedicatd host: %s", id)
hostsByID[id] = Host{
ID: id,
Zone: aws.StringValue(h.AvailabilityZone),
}
}
return !lastPage
}); err != nil {
return nil, fmt.Errorf("fetching dedicated hosts: %w", err)
}

return hostsByID, nil
}
21 changes: 21 additions & 0 deletions pkg/asset/installconfig/aws/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Metadata struct {
vpc VPC
instanceTypes map[string]InstanceType

Hosts map[string]Host
Region string `json:"region,omitempty"`
ProvidedSubnets []typesaws.Subnet `json:"subnets,omitempty"`
Services []typesaws.ServiceEndpoint `json:"services,omitempty"`
Expand Down Expand Up @@ -390,3 +391,23 @@ func (m *Metadata) InstanceTypes(ctx context.Context) (map[string]InstanceType,

return m.instanceTypes, nil
}

// DedicatedHosts retrieves all hosts available for use to verify against this installation for configured region.
func (m *Metadata) DedicatedHosts(ctx context.Context) (map[string]Host, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

if len(m.Hosts) == 0 {
awsSession, err := m.unlockedSession(ctx)
if err != nil {
return nil, err
}

m.Hosts, err = dedicatedHosts(ctx, awsSession, m.Region)
if err != nil {
return nil, fmt.Errorf("error listing dedicated hosts: %w", err)
}
}

return m.Hosts, nil
}
51 changes: 51 additions & 0 deletions pkg/asset/installconfig/aws/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/url"
"slices"
"sort"

ec2v2 "github.com/aws/aws-sdk-go-v2/service/ec2"
Expand Down Expand Up @@ -474,6 +475,8 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
}
}

allErrs = append(allErrs, validateHostPlacement(ctx, meta, fldPath, pool)...)

return allErrs
}

Expand All @@ -492,6 +495,54 @@ func translateEC2Arches(arches []string) sets.Set[string] {
return res
}

func validateHostPlacement(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
allErrs := field.ErrorList{}

if pool.HostPlacement == nil {
return allErrs
}

if pool.HostPlacement.Affinity != nil && *pool.HostPlacement.Affinity == awstypes.HostAffinityDedicatedHost {
placementPath := fldPath.Child("hostPlacement")
if pool.HostPlacement.DedicatedHost != nil {
configuredHosts := pool.HostPlacement.DedicatedHost

// Check to see if all configured hosts exist
foundHosts, err := meta.DedicatedHosts(ctx)
if err != nil {
allErrs = append(allErrs, field.InternalError(placementPath.Child("dedicatedHost"), err))
} else {
// Check the returned configured hosts to see if the dedicated hosts defined in install-config exists.
for idx, host := range configuredHosts {
dhPath := placementPath.Child("dedicatedHost").Index(idx)

// Is host in AWS?
foundHost, ok := foundHosts[host.ID]
if !ok {
errMsg := fmt.Sprintf("dedicated host %s not found", host.ID)
allErrs = append(allErrs, field.Invalid(dhPath, host, errMsg))
continue
}

// Is host valid for pools region and zone config?
if !slices.Contains(pool.Zones, foundHost.Zone) {
errMsg := fmt.Sprintf("dedicated host %s is not available in pool's zone list", host.ID)
allErrs = append(allErrs, field.Invalid(dhPath, host, errMsg))
}

// If user configured the zone for the dedicated host, validate that it matches the actual zone in AWS
if host.Zone != "" && host.Zone != foundHost.Zone {
errMsg := fmt.Sprintf("dedicated host %s is configured with zone %s but actually belongs to zone %s", host.ID, host.Zone, foundHost.Zone)
allErrs = append(allErrs, field.Invalid(dhPath.Child("zone"), host.Zone, errMsg))
}
}
}
}
}

return allErrs
}

func validateSecurityGroupIDs(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool) field.ErrorList {
allErrs := field.ErrorList{}

Expand Down
100 changes: 87 additions & 13 deletions pkg/asset/installconfig/aws/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func TestValidate(t *testing.T) {
subnetsInVPC *SubnetGroups
vpcTags Tags
instanceTypes map[string]InstanceType
hosts map[string]Host
proxy string
publicOnly bool
expectErr string
Expand Down Expand Up @@ -1232,6 +1233,57 @@ func TestValidate(t *testing.T) {
},
expectErr: `^\Qplatform.aws.vpc.subnets: Forbidden: subnet subnet-valid-public-a1 is owned by other clusters [another-cluster] and cannot be used for new installations, another subnet must be created separately\E$`,
},
{
name: "valid dedicated host placement on compute",
installConfig: icBuild.build(
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
icBuild.withComputeHostPlacement([]string{"h-1234567890abcdef0"}, 0),
),
availRegions: validAvailRegions(),
availZones: validAvailZones(),
hosts: map[string]Host{
"h-1234567890abcdef0": {ID: "h-1234567890abcdef0", Zone: "a"},
},
},
{
name: "invalid dedicated host not found",
installConfig: icBuild.build(
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
icBuild.withComputeHostPlacement([]string{"h-aaaaaaaaaaaaaaaaa"}, 0),
),
availRegions: validAvailRegions(),
availZones: validAvailZones(),
hosts: map[string]Host{
"h-1234567890abcdef0": {ID: "h-1234567890abcdef0", Zone: "a"},
},
expectErr: "dedicated host h-aaaaaaaaaaaaaaaaa not found",
},
{
name: "invalid dedicated host zone not in pool zones",
installConfig: icBuild.build(
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
icBuild.withComputeHostPlacement([]string{"h-bbbbbbbbbbbbbbbbb"}, 0),
),
availRegions: validAvailRegions(),
availZones: validAvailZones(),
hosts: map[string]Host{
"h-bbbbbbbbbbbbbbbbb": {ID: "h-bbbbbbbbbbbbbbbbb", Zone: "b"},
},
expectErr: "is not available in pool's zone list",
},
{
name: "dedicated host placement on compute but for a zone that pool is not using",
installConfig: icBuild.build(
icBuild.withComputePlatformZones([]string{"b"}, true, 0),
icBuild.withComputeHostPlacementAndZone([]string{"h-1234567890abcdef0"}, "b", 0),
),
availRegions: validAvailRegions(),
availZones: validAvailZones(),
hosts: map[string]Host{
"h-1234567890abcdef0": {ID: "h-1234567890abcdef0", Zone: "a"},
},
expectErr: "dedicated host h-1234567890abcdef0 is configured with zone b but actually belongs to zone a",
},
}

// Register mock http(s) responses for tests.
Expand Down Expand Up @@ -1264,6 +1316,7 @@ func TestValidate(t *testing.T) {
Tags: test.vpcTags,
},
instanceTypes: test.instanceTypes,
Hosts: test.hosts,
ProvidedSubnets: test.installConfig.Platform.AWS.VPC.Subnets,
}

Expand All @@ -1284,10 +1337,8 @@ func TestValidate(t *testing.T) {
err := Validate(context.TODO(), meta, test.installConfig)
if test.expectErr == "" {
assert.NoError(t, err)
} else {
if assert.Error(t, err) {
assert.Regexp(t, test.expectErr, err.Error())
}
} else if assert.Error(t, err) {
assert.Regexp(t, test.expectErr, err.Error())
}
})
}
Expand Down Expand Up @@ -1415,10 +1466,8 @@ func TestValidateForProvisioning(t *testing.T) {
err := ValidateForProvisioning(route53Client, ic, meta)
if test.expectedErr == "" {
assert.NoError(t, err)
} else {
if assert.Error(t, err) {
assert.Regexp(t, test.expectedErr, err.Error())
}
} else if assert.Error(t, err) {
assert.Regexp(t, test.expectedErr, err.Error())
}
})
}
Expand Down Expand Up @@ -1458,7 +1507,6 @@ func TestGetSubDomainDNSRecords(t *testing.T) {
route53Client := mock.NewMockAPI(mockCtrl)

for _, test := range cases {

t.Run(test.name, func(t *testing.T) {
ic := icBuild.build(icBuild.withBaseDomain(test.baseDomain))
if test.expectedErr != "" {
Expand All @@ -1483,10 +1531,8 @@ func TestGetSubDomainDNSRecords(t *testing.T) {
_, err := route53Client.GetSubDomainDNSRecords(&validDomainOutput, ic, nil)
if test.expectedErr == "" {
assert.NoError(t, err)
} else {
if assert.Error(t, err) {
assert.Regexp(t, test.expectedErr, err.Error())
}
} else if assert.Error(t, err) {
assert.Regexp(t, test.expectedErr, err.Error())
}
})
}
Expand Down Expand Up @@ -2004,6 +2050,34 @@ func (icBuild icBuildForAWS) withComputePlatformZones(zones []string, overwrite
}
}

func (icBuild icBuildForAWS) withComputeHostPlacement(hostIDs []string, index int) icOption {
return func(ic *types.InstallConfig) {
aff := aws.HostAffinityDedicatedHost
dhs := make([]aws.DedicatedHost, 0, len(hostIDs))
for _, id := range hostIDs {
dhs = append(dhs, aws.DedicatedHost{ID: id})
}
ic.Compute[index].Platform.AWS.HostPlacement = &aws.HostPlacement{
Affinity: &aff,
DedicatedHost: dhs,
}
}
}

func (icBuild icBuildForAWS) withComputeHostPlacementAndZone(hostIDs []string, zone string, index int) icOption {
return func(ic *types.InstallConfig) {
aff := aws.HostAffinityDedicatedHost
dhs := make([]aws.DedicatedHost, 0, len(hostIDs))
for _, id := range hostIDs {
dhs = append(dhs, aws.DedicatedHost{ID: id, Zone: zone})
}
ic.Compute[index].Platform.AWS.HostPlacement = &aws.HostPlacement{
Affinity: &aff,
DedicatedHost: dhs,
}
}
}

func (icBuild icBuildForAWS) withControlPlanePlatformAMI(amiID string) icOption {
return func(ic *types.InstallConfig) {
ic.ControlPlane.Platform.AWS.AMIID = amiID
Expand Down
25 changes: 25 additions & 0 deletions pkg/asset/machines/aws/machines.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type machineProviderInput struct {
publicSubnet bool
securityGroupIDs []string
cpuOptions *awstypes.CPUOptions
dedicatedHost string
}

// Machines returns a list of machines for a machinepool.
Expand Down Expand Up @@ -305,6 +306,15 @@ func provider(in *machineProviderInput) (*machineapi.AWSMachineProviderConfig, e
config.CPUOptions = &cpuOptions
}

if in.dedicatedHost != "" {
config.Placement.Host = &machineapi.HostPlacement{
Affinity: ptr.To(machineapi.HostAffinityDedicatedHost),
DedicatedHost: &machineapi.DedicatedHost{
ID: in.dedicatedHost,
},
}
}

return config, nil
}

Expand Down Expand Up @@ -354,3 +364,18 @@ func ConfigMasters(machines []machineapi.Machine, controlPlane *machinev1.Contro
providerSpec := controlPlane.Spec.Template.OpenShiftMachineV1Beta1Machine.Spec.ProviderSpec.Value.Object.(*machineapi.AWSMachineProviderConfig)
providerSpec.LoadBalancers = lbrefs
}

// DedicatedHost sets dedicated hosts for the specified zone.
func DedicatedHost(hosts map[string]aws.Host, placement *awstypes.HostPlacement, zone string) string {
// If install-config has HostPlacements configured, lets check the DedicatedHosts to see if one matches our region & zone.
if placement != nil {
// We only support one host ID currently for an instance. Need to also get host that matches the zone the machines will be put into.
for _, host := range placement.DedicatedHost {
hostDetails, found := hosts[host.ID]
if found && hostDetails.Zone == zone {
return hostDetails.ID
}
}
}
return ""
}
Loading