Skip to content

Commit cc61152

Browse files
Merge pull request #8031 from patrickdillon/capi-aws-iam
CORS-2900: CAPI AWS IAM
2 parents a6cd30f + d47a767 commit cc61152

File tree

6 files changed

+637
-4
lines changed

6 files changed

+637
-4
lines changed

pkg/asset/manifests/clusterapi/cluster.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@ func (c *Cluster) Generate(dependencies asset.Parents) error {
9696
var out *capiutils.GenerateClusterAssetsOutput
9797
switch platform := installConfig.Config.Platform.Name(); platform {
9898
case awstypes.Name:
99-
// Move this somewhere else.
100-
// if err := aws.PutIAMRoles(clusterID.InfraID, installConfig); err != nil {
101-
// return errors.Wrap(err, "failed to create IAM roles")
102-
// }
10399
var err error
104100
out, err = aws.GenerateClusterAssets(installConfig, clusterID)
105101
if err != nil {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
package clusterapi
22

33
import (
4+
"context"
5+
"fmt"
6+
47
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
58
awstypes "github.com/openshift/installer/pkg/types/aws"
69
)
710

811
var _ clusterapi.Provider = (*Provider)(nil)
12+
var _ clusterapi.PreProvider = (*Provider)(nil)
913

1014
// Provider implements AWS CAPI installation.
1115
type Provider struct{}
1216

1317
// Name gives the name of the provider, AWS.
1418
func (*Provider) Name() string { return awstypes.Name }
19+
20+
// PreProvision creates the IAM roles used by all nodes in the cluster.
21+
func (*Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionInput) error {
22+
if err := createIAMRoles(ctx, in.InfraID, in.InstallConfig); err != nil {
23+
return fmt.Errorf("failed to create IAM roles: %w", err)
24+
}
25+
return nil
26+
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package clusterapi
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/aws/aws-sdk-go/aws"
10+
"github.com/aws/aws-sdk-go/aws/awserr"
11+
"github.com/aws/aws-sdk-go/aws/endpoints"
12+
"github.com/aws/aws-sdk-go/service/iam"
13+
"github.com/sirupsen/logrus"
14+
iamv1 "sigs.k8s.io/cluster-api-provider-aws/v2/iam/api/v1beta1"
15+
16+
"github.com/openshift/installer/pkg/asset/installconfig"
17+
)
18+
19+
const (
20+
master = "master"
21+
worker = "worker"
22+
)
23+
24+
var (
25+
policies = map[string]*iamv1.PolicyDocument{
26+
master: {
27+
Version: "2012-10-17",
28+
Statement: []iamv1.StatementEntry{
29+
{
30+
Effect: "Allow",
31+
Action: []string{
32+
"ec2:AttachVolume",
33+
"ec2:AuthorizeSecurityGroupIngress",
34+
"ec2:CreateSecurityGroup",
35+
"ec2:CreateTags",
36+
"ec2:CreateVolume",
37+
"ec2:DeleteSecurityGroup",
38+
"ec2:DeleteVolume",
39+
"ec2:Describe*",
40+
"ec2:DetachVolume",
41+
"ec2:ModifyInstanceAttribute",
42+
"ec2:ModifyVolume",
43+
"ec2:RevokeSecurityGroupIngress",
44+
"elasticloadbalancing:AddTags",
45+
"elasticloadbalancing:AttachLoadBalancerToSubnets",
46+
"elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
47+
"elasticloadbalancing:CreateListener",
48+
"elasticloadbalancing:CreateLoadBalancer",
49+
"elasticloadbalancing:CreateLoadBalancerPolicy",
50+
"elasticloadbalancing:CreateLoadBalancerListeners",
51+
"elasticloadbalancing:CreateTargetGroup",
52+
"elasticloadbalancing:ConfigureHealthCheck",
53+
"elasticloadbalancing:DeleteListener",
54+
"elasticloadbalancing:DeleteLoadBalancer",
55+
"elasticloadbalancing:DeleteLoadBalancerListeners",
56+
"elasticloadbalancing:DeleteTargetGroup",
57+
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
58+
"elasticloadbalancing:DeregisterTargets",
59+
"elasticloadbalancing:Describe*",
60+
"elasticloadbalancing:DetachLoadBalancerFromSubnets",
61+
"elasticloadbalancing:ModifyListener",
62+
"elasticloadbalancing:ModifyLoadBalancerAttributes",
63+
"elasticloadbalancing:ModifyTargetGroup",
64+
"elasticloadbalancing:ModifyTargetGroupAttributes",
65+
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
66+
"elasticloadbalancing:RegisterTargets",
67+
"elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer",
68+
"elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
69+
"kms:DescribeKey",
70+
},
71+
Resource: iamv1.Resources{
72+
"*",
73+
},
74+
},
75+
},
76+
},
77+
worker: {
78+
Version: "2012-10-17",
79+
Statement: []iamv1.StatementEntry{
80+
{
81+
Effect: "Allow",
82+
Action: iamv1.Actions{
83+
"ec2:DescribeInstances",
84+
"ec2:DescribeRegions",
85+
},
86+
Resource: iamv1.Resources{"*"},
87+
},
88+
},
89+
},
90+
}
91+
)
92+
93+
// createIAMRoles creates the roles used by control-plane and compute nodes.
94+
func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.InstallConfig) error {
95+
logrus.Infoln("Creating IAM roles for control-plane and compute nodes")
96+
// Create the IAM Role with the aws sdk.
97+
// https://docs.aws.amazon.com/sdk-for-go/api/service/iam/#IAM.CreateRole
98+
session, err := ic.AWS.Session(ctx)
99+
if err != nil {
100+
return fmt.Errorf("failed to load AWS session: %w", err)
101+
}
102+
svc := iam.New(session)
103+
104+
// Create the IAM Roles for master and workers.
105+
tags := []*iam.Tag{
106+
{
107+
Key: aws.String(fmt.Sprintf("kubernetes.io/cluster/%s", infraID)),
108+
Value: aws.String("owned"),
109+
},
110+
}
111+
112+
for k, v := range ic.Config.AWS.UserTags {
113+
tags = append(tags, &iam.Tag{
114+
Key: aws.String(k),
115+
Value: aws.String(v),
116+
})
117+
}
118+
119+
assumePolicy := &iamv1.PolicyDocument{
120+
Version: "2012-10-17",
121+
Statement: iamv1.Statements{
122+
{
123+
Effect: "Allow",
124+
Principal: iamv1.Principals{
125+
iamv1.PrincipalService: []string{
126+
getPartitionService(ic.AWS.Region),
127+
},
128+
},
129+
Action: iamv1.Actions{
130+
"sts:AssumeRole",
131+
},
132+
},
133+
},
134+
}
135+
assumePolicyBytes, err := json.Marshal(assumePolicy)
136+
if err != nil {
137+
return fmt.Errorf("failed to marshal assume policy: %w", err)
138+
}
139+
140+
for _, role := range []string{master, worker} {
141+
roleName, err := getOrCreateIAMRole(ctx, role, infraID, string(assumePolicyBytes), *ic, tags, svc)
142+
if err != nil {
143+
return fmt.Errorf("failed to create IAM roles: %w", err)
144+
}
145+
146+
// Put the policy inline.
147+
policyName := aws.String(fmt.Sprintf("%s-%s-policy", infraID, role))
148+
b, err := json.Marshal(policies[role])
149+
if err != nil {
150+
return fmt.Errorf("failed to marshal %s policy: %w", role, err)
151+
}
152+
if _, err := svc.PutRolePolicyWithContext(ctx, &iam.PutRolePolicyInput{
153+
PolicyDocument: aws.String(string(b)),
154+
PolicyName: policyName,
155+
RoleName: aws.String(roleName),
156+
}); err != nil {
157+
return fmt.Errorf("failed to create inline policy for role %s: %w", role, err)
158+
}
159+
160+
profileName := aws.String(fmt.Sprintf("%s-%s-profile", infraID, role))
161+
if _, err := svc.GetInstanceProfileWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil {
162+
var awsErr awserr.Error
163+
if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException {
164+
return fmt.Errorf("failed to get %s instance profile: %w", role, err)
165+
}
166+
// If the profile does not exist, create it.
167+
if _, err := svc.CreateInstanceProfileWithContext(ctx, &iam.CreateInstanceProfileInput{
168+
InstanceProfileName: profileName,
169+
Tags: tags,
170+
}); err != nil {
171+
return fmt.Errorf("failed to create %s instance profile: %w", role, err)
172+
}
173+
if err := svc.WaitUntilInstanceProfileExistsWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil {
174+
return fmt.Errorf("failed to wait for %s instance profile to exist: %w", role, err)
175+
}
176+
177+
// Finally, attach the role to the profile.
178+
if _, err := svc.AddRoleToInstanceProfileWithContext(ctx, &iam.AddRoleToInstanceProfileInput{
179+
InstanceProfileName: profileName,
180+
RoleName: aws.String(roleName),
181+
}); err != nil {
182+
return fmt.Errorf("failed to add %s role to instance profile: %w", role, err)
183+
}
184+
}
185+
}
186+
187+
return nil
188+
}
189+
190+
// getOrCreateRole returns the name of the IAM role to be used,
191+
// creating it when not specified by the user in the install config.
192+
func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy string, ic installconfig.InstallConfig, tags []*iam.Tag, svc *iam.IAM) (string, error) {
193+
roleName := aws.String(fmt.Sprintf("%s-%s-role", infraID, nodeRole))
194+
195+
var defaultRole string
196+
if dmp := ic.Config.AWS.DefaultMachinePlatform; dmp != nil && len(dmp.IAMRole) > 0 {
197+
defaultRole = dmp.IAMRole
198+
}
199+
200+
masterRole := defaultRole
201+
if cp := ic.Config.ControlPlane; cp != nil && cp.Platform.AWS != nil && len(cp.Platform.AWS.IAMRole) > 0 {
202+
masterRole = cp.Platform.AWS.IAMRole
203+
}
204+
205+
workerRole := defaultRole
206+
if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMRole) > 0 {
207+
workerRole = w[0].Platform.AWS.IAMRole
208+
}
209+
210+
switch {
211+
case nodeRole == master && len(masterRole) > 0:
212+
return masterRole, nil
213+
case nodeRole == worker && len(workerRole) > 0:
214+
return workerRole, nil
215+
}
216+
217+
if _, err := svc.GetRoleWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil {
218+
var awsErr awserr.Error
219+
if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException {
220+
return "", fmt.Errorf("failed to get %s role: %w", nodeRole, err)
221+
}
222+
// If the role does not exist, create it.
223+
createRoleInput := &iam.CreateRoleInput{
224+
RoleName: roleName,
225+
AssumeRolePolicyDocument: aws.String(assumePolicy),
226+
Tags: tags,
227+
}
228+
if _, err := svc.CreateRoleWithContext(ctx, createRoleInput); err != nil {
229+
return "", fmt.Errorf("failed to create %s role: %w", nodeRole, err)
230+
}
231+
232+
if err := svc.WaitUntilRoleExistsWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil {
233+
return "", fmt.Errorf("failed to wait for %s role to exist: %w", nodeRole, err)
234+
}
235+
}
236+
return *roleName, nil
237+
}
238+
239+
func getPartitionService(region string) string {
240+
partitionDNSSuffix := "amazonaws.com"
241+
if ps, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); found {
242+
partitionDNSSuffix = ps.DNSSuffix()
243+
}
244+
return fmt.Sprintf("ec2.%s", partitionDNSSuffix)
245+
}

vendor/modules.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9881,6 +9881,7 @@ sigs.k8s.io/cluster-api/util/topology
98819881
sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1
98829882
sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2
98839883
sigs.k8s.io/cluster-api-provider-aws/v2/feature
9884+
sigs.k8s.io/cluster-api-provider-aws/v2/iam/api/v1beta1
98849885
# sigs.k8s.io/cluster-api-provider-azure v1.13.0 => sigs.k8s.io/cluster-api-provider-azure v1.11.1-0.20231026140308-a3f4914170d9
98859886
## explicit; go 1.20
98869887
sigs.k8s.io/cluster-api-provider-azure/api/v1beta1

0 commit comments

Comments
 (0)