Skip to content

Commit 31347ee

Browse files
committed
Add IPI installation on AWS dedicated hosts
1 parent ccad892 commit 31347ee

File tree

12 files changed

+675
-52
lines changed

12 files changed

+675
-52
lines changed

data/data/install.openshift.io_installconfigs.yaml

Lines changed: 216 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/aws/aws-sdk-go/aws"
8+
"github.com/aws/aws-sdk-go/aws/session"
9+
"github.com/aws/aws-sdk-go/service/ec2"
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
// Host holds metadata for a dedicated host.
14+
type Host struct {
15+
ID string
16+
Zone string
17+
}
18+
19+
// dedicatedHosts retrieves a list of dedicated hosts for the given region and
20+
// returns them in a map keyed by the host ID.
21+
func dedicatedHosts(ctx context.Context, session *session.Session, region string) (map[string]Host, error) {
22+
hostsByID := map[string]Host{}
23+
24+
client := ec2.New(session, aws.NewConfig().WithRegion(region))
25+
input := &ec2.DescribeHostsInput{}
26+
27+
if err := client.DescribeHostsPagesWithContext(ctx, input, func(page *ec2.DescribeHostsOutput, lastPage bool) bool {
28+
for _, h := range page.Hosts {
29+
id := aws.StringValue(h.HostId)
30+
if id == "" {
31+
// Skip entries lacking an ID (should not happen)
32+
continue
33+
}
34+
35+
logrus.Debugf("Found dedicatd host: %s", id)
36+
hostsByID[id] = Host{
37+
ID: id,
38+
Zone: aws.StringValue(h.AvailabilityZone),
39+
}
40+
}
41+
return !lastPage
42+
}); err != nil {
43+
return nil, fmt.Errorf("fetching dedicated hosts: %w", err)
44+
}
45+
46+
return hostsByID, nil
47+
}

pkg/asset/installconfig/aws/metadata.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Metadata struct {
2727
vpc VPC
2828
instanceTypes map[string]InstanceType
2929

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

391392
return m.instanceTypes, nil
392393
}
394+
395+
// DedicatedHosts retrieves all hosts available for use to verify against this installation for configured region.
396+
func (m *Metadata) DedicatedHosts(ctx context.Context) (map[string]Host, error) {
397+
m.mutex.Lock()
398+
defer m.mutex.Unlock()
399+
400+
if len(m.Hosts) == 0 {
401+
awsSession, err := m.unlockedSession(ctx)
402+
if err != nil {
403+
return nil, err
404+
}
405+
406+
m.Hosts, err = dedicatedHosts(ctx, awsSession, m.Region)
407+
if err != nil {
408+
return nil, fmt.Errorf("error listing dedicated hosts: %w", err)
409+
}
410+
}
411+
412+
return m.Hosts, nil
413+
}

pkg/asset/installconfig/aws/validation.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"net/http"
99
"net/url"
10+
"slices"
1011
"sort"
1112

1213
ec2v2 "github.com/aws/aws-sdk-go-v2/service/ec2"
@@ -466,6 +467,8 @@ func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Pat
466467
}
467468
}
468469

470+
allErrs = append(allErrs, validateHostPlacement(ctx, meta, fldPath, pool)...)
471+
469472
return allErrs
470473
}
471474

@@ -484,6 +487,46 @@ func translateEC2Arches(arches []string) sets.Set[string] {
484487
return res
485488
}
486489

490+
func validateHostPlacement(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) field.ErrorList {
491+
allErrs := field.ErrorList{}
492+
493+
if pool.HostPlacement == nil {
494+
return allErrs
495+
}
496+
497+
if pool.HostPlacement.Affinity != nil && *pool.HostPlacement.Affinity == awstypes.HostAffinityDedicatedHost {
498+
placementPath := fldPath.Child("hostPlacement")
499+
if pool.HostPlacement.DedicatedHost != nil {
500+
configuredHosts := pool.HostPlacement.DedicatedHost
501+
502+
// Check to see if all configured hosts exist
503+
foundHosts, err := meta.DedicatedHosts(ctx)
504+
if err != nil {
505+
allErrs = append(allErrs, field.InternalError(placementPath.Child("dedicatedHost"), err))
506+
} else {
507+
// Check the returned configured hosts to see if the dedicated hosts defined in install-config exists.
508+
for _, host := range configuredHosts {
509+
// Is host in AWS?
510+
foundHost, ok := foundHosts[host.ID]
511+
if !ok {
512+
errMsg := fmt.Sprintf("dedicated host %s not found", host.ID)
513+
allErrs = append(allErrs, field.Invalid(placementPath.Child("dedicatedHost"), host, errMsg))
514+
continue
515+
}
516+
517+
// Is host valid for pools region and zone config?
518+
if !slices.Contains(pool.Zones, foundHost.Zone) {
519+
errMsg := fmt.Sprintf("dedicated host %s is not available in pool's zone list", host.ID)
520+
allErrs = append(allErrs, field.Invalid(placementPath.Child("dedicatedHost"), host, errMsg))
521+
}
522+
}
523+
}
524+
}
525+
}
526+
527+
return allErrs
528+
}
529+
487530
func validateSecurityGroupIDs(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool) field.ErrorList {
488531
allErrs := field.ErrorList{}
489532

pkg/asset/installconfig/aws/validation_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func TestValidate(t *testing.T) {
7474
subnetsInVPC *SubnetGroups
7575
vpcTags Tags
7676
instanceTypes map[string]InstanceType
77+
hosts map[string]Host
7778
proxy string
7879
publicOnly bool
7980
expectErr string
@@ -1200,6 +1201,44 @@ func TestValidate(t *testing.T) {
12001201
},
12011202
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$`,
12021203
},
1204+
{
1205+
name: "valid dedicated host placement on compute",
1206+
installConfig: icBuild.build(
1207+
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
1208+
icBuild.withComputeHostPlacement([]string{"h-1234567890abcdef0"}, 0),
1209+
),
1210+
availRegions: validAvailRegions(),
1211+
availZones: validAvailZones(),
1212+
hosts: map[string]Host{
1213+
"h-1234567890abcdef0": {ID: "h-1234567890abcdef0", Zone: "a"},
1214+
},
1215+
},
1216+
{
1217+
name: "invalid dedicated host not found",
1218+
installConfig: icBuild.build(
1219+
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
1220+
icBuild.withComputeHostPlacement([]string{"h-aaaaaaaaaaaaaaaaa"}, 0),
1221+
),
1222+
availRegions: validAvailRegions(),
1223+
availZones: validAvailZones(),
1224+
hosts: map[string]Host{
1225+
"h-1234567890abcdef0": {ID: "h-1234567890abcdef0", Zone: "a"},
1226+
},
1227+
expectErr: "dedicated host h-aaaaaaaaaaaaaaaaa not found",
1228+
},
1229+
{
1230+
name: "invalid dedicated host zone not in pool zones",
1231+
installConfig: icBuild.build(
1232+
icBuild.withComputePlatformZones([]string{"a"}, true, 0),
1233+
icBuild.withComputeHostPlacement([]string{"h-bbbbbbbbbbbbbbbbb"}, 0),
1234+
),
1235+
availRegions: validAvailRegions(),
1236+
availZones: validAvailZones(),
1237+
hosts: map[string]Host{
1238+
"h-bbbbbbbbbbbbbbbbb": {ID: "h-bbbbbbbbbbbbbbbbb", Zone: "b"},
1239+
},
1240+
expectErr: "is not available in pool's zone list",
1241+
},
12031242
}
12041243

12051244
// Register mock http(s) responses for tests.
@@ -1232,6 +1271,7 @@ func TestValidate(t *testing.T) {
12321271
Tags: test.vpcTags,
12331272
},
12341273
instanceTypes: test.instanceTypes,
1274+
Hosts: test.hosts,
12351275
ProvidedSubnets: test.installConfig.Platform.AWS.VPC.Subnets,
12361276
}
12371277

@@ -1952,6 +1992,20 @@ func (icBuild icBuildForAWS) withComputePlatformZones(zones []string, overwrite
19521992
}
19531993
}
19541994

1995+
func (icBuild icBuildForAWS) withComputeHostPlacement(hostIDs []string, index int) icOption {
1996+
return func(ic *types.InstallConfig) {
1997+
aff := aws.HostAffinityDedicatedHost
1998+
dhs := make([]aws.DedicatedHost, 0, len(hostIDs))
1999+
for _, id := range hostIDs {
2000+
dhs = append(dhs, aws.DedicatedHost{ID: id})
2001+
}
2002+
ic.Compute[index].Platform.AWS.HostPlacement = &aws.HostPlacement{
2003+
Affinity: &aff,
2004+
DedicatedHost: dhs,
2005+
}
2006+
}
2007+
}
2008+
19552009
func (icBuild icBuildForAWS) withControlPlanePlatformAMI(amiID string) icOption {
19562010
return func(ic *types.InstallConfig) {
19572011
ic.ControlPlane.Platform.AWS.AMIID = amiID

pkg/asset/machines/aws/machines.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/apimachinery/pkg/util/sets"
1313
"k8s.io/utils/pointer"
14+
"k8s.io/utils/ptr"
1415

1516
v1 "github.com/openshift/api/config/v1"
1617
machinev1 "github.com/openshift/api/machine/v1"
@@ -35,6 +36,7 @@ type machineProviderInput struct {
3536
userTags map[string]string
3637
publicSubnet bool
3738
securityGroupIDs []string
39+
dedicatedHost string
3840
}
3941

4042
// Machines returns a list of machines for a machinepool.
@@ -291,6 +293,15 @@ func provider(in *machineProviderInput) (*machineapi.AWSMachineProviderConfig, e
291293
config.MetadataServiceOptions.Authentication = machineapi.MetadataServiceAuthentication(in.imds.Authentication)
292294
}
293295

296+
if in.dedicatedHost != "" {
297+
config.HostPlacement = &machineapi.HostPlacement{
298+
Affinity: ptr.To(machineapi.HostAffinityDedicatedHost),
299+
DedicatedHost: &machineapi.DedicatedHost{
300+
ID: in.dedicatedHost,
301+
},
302+
}
303+
}
304+
294305
return config, nil
295306
}
296307

@@ -340,3 +351,18 @@ func ConfigMasters(machines []machineapi.Machine, controlPlane *machinev1.Contro
340351
providerSpec := controlPlane.Spec.Template.OpenShiftMachineV1Beta1Machine.Spec.ProviderSpec.Value.Object.(*machineapi.AWSMachineProviderConfig)
341352
providerSpec.LoadBalancers = lbrefs
342353
}
354+
355+
// DedicatedHost sets dedicated hosts for the specified zone.
356+
func DedicatedHost(hosts map[string]aws.Host, placement *awstypes.HostPlacement, zone string) string {
357+
// If install-config has HostPlacements configured, lets check the DedicatedHosts to see if one matches our region & zone.
358+
if placement != nil {
359+
// 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.
360+
for _, host := range placement.DedicatedHost {
361+
hostDetails, found := hosts[host.ID]
362+
if found && hostDetails.Zone == zone {
363+
return hostDetails.ID
364+
}
365+
}
366+
}
367+
return ""
368+
}

pkg/asset/machines/aws/machinesets.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type MachineSetInput struct {
2525
Pool *types.MachinePool
2626
Role string
2727
UserDataSecret string
28+
Hosts map[string]icaws.Host
2829
}
2930

3031
// MachineSets returns a list of machinesets for a machinepool.
@@ -87,6 +88,8 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
8788
instanceProfile = fmt.Sprintf("%s-worker-profile", in.ClusterID)
8889
}
8990

91+
dedicatedHost := DedicatedHost(in.Hosts, mpool.HostPlacement, az)
92+
9093
provider, err := provider(&machineProviderInput{
9194
clusterID: in.ClusterID,
9295
region: in.InstallConfigPlatformAWS.Region,
@@ -102,12 +105,21 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
102105
userTags: in.InstallConfigPlatformAWS.UserTags,
103106
publicSubnet: publicSubnet,
104107
securityGroupIDs: in.Pool.Platform.AWS.AdditionalSecurityGroupIDs,
108+
dedicatedHost: dedicatedHost,
105109
})
106110
if err != nil {
107111
return nil, errors.Wrap(err, "failed to create provider")
108112
}
113+
114+
// If we are using any feature that is only available via CAPI, we must set the authoritativeAPI = ClusterAPI
115+
authoritativeAPI := machineapi.MachineAuthorityMachineAPI
116+
if isAuthoritativeClusterAPIRequired(provider) {
117+
authoritativeAPI = machineapi.MachineAuthorityClusterAPI
118+
}
119+
109120
name := fmt.Sprintf("%s-%s-%s", in.ClusterID, in.Pool.Name, az)
110121
spec := machineapi.MachineSpec{
122+
AuthoritativeAPI: authoritativeAPI,
111123
ProviderSpec: machineapi.ProviderSpec{
112124
Value: &runtime.RawExtension{Object: provider},
113125
},
@@ -130,7 +142,8 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
130142
},
131143
},
132144
Spec: machineapi.MachineSetSpec{
133-
Replicas: &replicas,
145+
AuthoritativeAPI: authoritativeAPI,
146+
Replicas: &replicas,
134147
Selector: metav1.LabelSelector{
135148
MatchLabels: map[string]string{
136149
"machine.openshift.io/cluster-api-machineset": name,
@@ -151,8 +164,17 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) {
151164
},
152165
},
153166
}
167+
154168
machinesets = append(machinesets, mset)
155169
}
156170

157171
return machinesets, nil
158172
}
173+
174+
// isAuthoritativeClusterAPIRequired is called to determine if the machine spec should have the AuthoritativeAPI set to ClusterAPI.
175+
func isAuthoritativeClusterAPIRequired(provider *machineapi.AWSMachineProviderConfig) bool {
176+
if provider.HostPlacement != nil && *provider.HostPlacement.Affinity != machineapi.HostAffinityAnyAvailable {
177+
return true
178+
}
179+
return false
180+
}

pkg/asset/machines/worker.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
533533
}
534534
}
535535

536+
dHosts := map[string]icaws.Host{}
537+
if pool.Platform.AWS.HostPlacement != nil {
538+
dHosts, err = installConfig.AWS.DedicatedHosts(ctx)
539+
if err != nil {
540+
return fmt.Errorf("failed to retrieve dedicated hosts for compute pool: %w", err)
541+
}
542+
}
543+
536544
pool.Platform.AWS = &mpool
537545
sets, err := aws.MachineSets(&aws.MachineSetInput{
538546
ClusterID: clusterID.InfraID,
@@ -543,6 +551,7 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error
543551
Pool: &pool,
544552
Role: pool.Name,
545553
UserDataSecret: workerUserDataSecretName,
554+
Hosts: dHosts,
546555
})
547556
if err != nil {
548557
return errors.Wrap(err, "failed to create worker machine objects")

0 commit comments

Comments
 (0)