From 02e75a5452b9c3f1c9afb9cbf90553af1e369740 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 31 Aug 2026 16:50:10 -0700 Subject: [PATCH 1/3] aws: provision dedicated IAM resources for edge compute pools The installer only creates IAM resources for the control-plane and worker pool while the edge (local/wavelength zone) pool has none and re-uses the those of worker nodes. This introduces a few problems: - If an IAM role is specified for edge pool, it is ignored. - If an IAM role is specified for worker pool, it's also applied to edge pool unexpectedly. - If an IAM instance profile is specified for worker pool, the edge pool references a non-existing default worker profile since the installer doesn't create one. --- pkg/infrastructure/aws/clusterapi/iam.go | 65 +++++++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/pkg/infrastructure/aws/clusterapi/iam.go b/pkg/infrastructure/aws/clusterapi/iam.go index c5ac6aaeaa0..e9d9911e2f6 100644 --- a/pkg/infrastructure/aws/clusterapi/iam.go +++ b/pkg/infrastructure/aws/clusterapi/iam.go @@ -18,12 +18,14 @@ import ( "github.com/openshift/installer/pkg/asset/installconfig" awsconfig "github.com/openshift/installer/pkg/asset/installconfig/aws" + "github.com/openshift/installer/pkg/types" awstypes "github.com/openshift/installer/pkg/types/aws" ) const ( - master = "master" - worker = "worker" + master = types.MachinePoolControlPlaneRoleName + worker = types.MachinePoolComputeRoleName + edge = types.MachinePoolEdgeRoleName ) var ( @@ -93,6 +95,19 @@ var ( }, }, }, + edge: { + Version: "2012-10-17", + Statement: []iamv1.StatementEntry{ + { + Effect: "Allow", + Action: iamv1.Actions{ + "ec2:DescribeInstances", + "ec2:DescribeRegions", + }, + Resource: iamv1.Resources{"*"}, + }, + }, + }, } ) @@ -155,7 +170,15 @@ func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.Insta defaultProfile = dmp.IAMProfile } - for _, role := range []string{master, worker} { + // Reconcile a role for the control plane plus every configured compute + // pool. The worker pool is always present; the edge pool is optional and + // only appears here when the user has configured it. + roles := []string{master} + for _, compute := range ic.Config.Compute { + roles = append(roles, compute.Name) + } + + for _, role := range roles { instanceProfile := defaultProfile switch role { case master: @@ -163,8 +186,12 @@ func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.Insta instanceProfile = cp.Platform.AWS.IAMProfile } case worker: - if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMProfile) > 0 { - instanceProfile = w[0].Platform.AWS.IAMProfile + if wp := getComputeMachinePoolByName(ic, worker); wp != nil && wp.Platform.AWS != nil && len(wp.Platform.AWS.IAMProfile) > 0 { + instanceProfile = wp.Platform.AWS.IAMProfile + } + case edge: + if ep := getComputeMachinePoolByName(ic, edge); ep != nil && ep.Platform.AWS != nil && len(ep.Platform.AWS.IAMProfile) > 0 { + instanceProfile = ep.Platform.AWS.IAMProfile } } @@ -228,8 +255,13 @@ func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy str } workerRole := defaultRole - if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMRole) > 0 { - workerRole = w[0].Platform.AWS.IAMRole + if wp := getComputeMachinePoolByName(&ic, worker); wp != nil && wp.Platform.AWS != nil && len(wp.Platform.AWS.IAMRole) > 0 { + workerRole = wp.Platform.AWS.IAMRole + } + + edgeRole := defaultRole + if ep := getComputeMachinePoolByName(&ic, edge); ep != nil && ep.Platform.AWS != nil && len(ep.Platform.AWS.IAMRole) > 0 { + edgeRole = ep.Platform.AWS.IAMRole } switch { @@ -237,6 +269,8 @@ func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy str return masterRole, nil case nodeRole == worker && len(workerRole) > 0: return workerRole, nil + case nodeRole == edge && len(edgeRole) > 0: + return edgeRole, nil } if _, err := svc.GetRole(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil { @@ -262,8 +296,12 @@ func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy str } // Put the policy inline. + policy, ok := policies[nodeRole] + if !ok { + return "", fmt.Errorf("no IAM policy defined for role %q", nodeRole) + } policyName := aws.String(fmt.Sprintf("%s-%s-policy", infraID, nodeRole)) - b, err := json.Marshal(policies[nodeRole]) + b, err := json.Marshal(policy) if err != nil { return "", fmt.Errorf("failed to marshal %s policy: %w", nodeRole, err) } @@ -306,3 +344,14 @@ func getEC2ServicePrincipal(region string) (string, error) { logrus.Debugf("Using domain name: %s for EC2 service principal ID", domain) return fmt.Sprintf("ec2.%s", domain), nil } + +// getComputeMachinePoolByName returns the machine pool for the compute pool with the +// given role name (worker or edge). +func getComputeMachinePoolByName(ic *installconfig.InstallConfig, name string) *types.MachinePool { + for _, compute := range ic.Config.Compute { + if compute.Name == name { + return &compute + } + } + return nil +} From 05a79869f530a10aef6dac6902c07fe153ea8e23 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 31 Aug 2026 16:50:20 -0700 Subject: [PATCH 2/3] aws: use default compute instance profiles by machine pool role Worker and edge machine sets both derived the default instance profile name from a hard-coded "-worker-profile" suffix. Use the pool role so edge machine sets reference the edge instance profile instead of pointing to the worker profile. --- .../machines/aws/clusterapi_machinesets.go | 2 +- .../aws/clusterapi_machinesets_test.go | 23 ++++++++++++++++++- pkg/asset/machines/aws/machinesets.go | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/asset/machines/aws/clusterapi_machinesets.go b/pkg/asset/machines/aws/clusterapi_machinesets.go index 5f46d05a004..610d5dc41f7 100644 --- a/pkg/asset/machines/aws/clusterapi_machinesets.go +++ b/pkg/asset/machines/aws/clusterapi_machinesets.go @@ -40,7 +40,7 @@ func ClusterAPIMachineSets(in *MachineSetInput) ([]capa.AWSMachineTemplate, []ca instanceProfile := mpool.IAMProfile if len(instanceProfile) == 0 { - instanceProfile = fmt.Sprintf("%s-worker-profile", in.ClusterID) + instanceProfile = fmt.Sprintf("%s-%s-profile", in.ClusterID, in.Role) } tags, err := CapaTagsFromUserTags(in.ClusterID, in.InstallConfigPlatformAWS.UserTags) diff --git a/pkg/asset/machines/aws/clusterapi_machinesets_test.go b/pkg/asset/machines/aws/clusterapi_machinesets_test.go index 893d6febf76..3c59f0bf8b6 100644 --- a/pkg/asset/machines/aws/clusterapi_machinesets_test.go +++ b/pkg/asset/machines/aws/clusterapi_machinesets_test.go @@ -181,7 +181,7 @@ func TestClusterAPIMachineSets(t *testing.T) { }, }, { - name: "default IAM profile uses cluster ID", + name: "default worker IAM profile uses the worker pool role", input: func() *MachineSetInput { in := defaultMachineSetInput() in.Pool.Platform.AWS.Zones = []string{"us-east-1a"} @@ -199,6 +199,26 @@ func TestClusterAPIMachineSets(t *testing.T) { } }, }, + { + name: "default edge IAM profile uses the edge pool role", + input: func() *MachineSetInput { + in := defaultMachineSetInput() + in.Role = types.MachinePoolEdgeRoleName + in.Pool.Platform.AWS.Zones = []string{"us-east-1a"} + in.Pool.Replicas = ptr.To(int64(1)) + return in + }(), + validate: func(t *testing.T, templates []capa.AWSMachineTemplate, _ []capi.MachineSet) { + t.Helper() + if len(templates) != 1 { + t.Fatalf("expected 1 template, got %d", len(templates)) + } + got := templates[0].Spec.Template.Spec.IAMInstanceProfile + if got != "test-cluster-edge-profile" { + t.Errorf("IAM profile = %q, want %q", got, "test-cluster-edge-profile") + } + }, + }, { name: "custom IAM profile", input: func() *MachineSetInput { @@ -307,6 +327,7 @@ func defaultMachineSetInput() *MachineSetInput { InstallConfigPlatformAWS: &awstypes.Platform{ Region: "us-east-1", }, + Role: types.MachinePoolComputeRoleName, Pool: &types.MachinePool{ Name: types.MachinePoolComputeRoleName, Replicas: ptr.To(int64(3)), diff --git a/pkg/asset/machines/aws/machinesets.go b/pkg/asset/machines/aws/machinesets.go index 65513856c42..478803b6279 100644 --- a/pkg/asset/machines/aws/machinesets.go +++ b/pkg/asset/machines/aws/machinesets.go @@ -87,7 +87,7 @@ func MachineSets(in *MachineSetInput) ([]*machineapi.MachineSet, error) { instanceProfile := mpool.IAMProfile if len(instanceProfile) == 0 { - instanceProfile = fmt.Sprintf("%s-worker-profile", in.ClusterID) + instanceProfile = fmt.Sprintf("%s-%s-profile", in.ClusterID, in.Role) } dedicatedHost := DedicatedHost(in.Hosts, mpool.HostPlacement, az) From 65963fa5d2104537fc58f5e9a1a555c919e7d857 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 15 Sep 2026 13:52:43 -0700 Subject: [PATCH 3/3] destroy: ensure edge instance profile is cleaned up --- pkg/destroy/aws/aws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/destroy/aws/aws.go b/pkg/destroy/aws/aws.go index 97d1fbc04c3..2a5e2f4a973 100644 --- a/pkg/destroy/aws/aws.go +++ b/pkg/destroy/aws/aws.go @@ -420,7 +420,7 @@ func (o *ClusterUninstaller) RunWithContext(ctx context.Context) ([]string, erro func (o *ClusterUninstaller) findUntaggableResources(ctx context.Context, deleted sets.Set[string]) (sets.Set[string], error) { resources := sets.New[string]() o.Logger.Debug("search for IAM instance profiles") - for _, profileType := range []string{"master", "worker", "bootstrap"} { + for _, profileType := range []string{"master", "worker", "bootstrap", "edge"} { profile := fmt.Sprintf("%s-%s-profile", o.ClusterID, profileType) response, err := o.IAMClient.GetInstanceProfile(ctx, &iamv2.GetInstanceProfileInput{InstanceProfileName: &profile}) if err != nil {