Skip to content

Commit 8330444

Browse files
authored
support updating Tags on Role resources (#10)
Adds support for updating (adding/removing) Tags from a Role resource in the IAM controller. Unfortunately, the "standard" Tagris ListTagsForResource/TagResource/UntagResource AWS Resource Group Tagging API calls aren't supported for IAM. You need to use the IAM-specific TagRole/UntagRole/ListRoleTags IAM API calls to work with tags on Role resources. Following patches will add support for the same tags on Policy resources. Issue: aws-controllers-k8s/community#1119 Signed-off-by: Jay Pipes <[email protected]> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 468ea75 commit 8330444

File tree

8 files changed

+220
-11
lines changed

8 files changed

+220
-11
lines changed

config/iam/recommended-inline-policy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"iam:DeletePolicy",
1515
"iam:ListAttachedRolePolicies",
1616
"iam:AttachRolePolicy",
17-
"iam:DetachRolePolicy"
17+
"iam:DetachRolePolicy",
18+
"iam:ListRoleTags",
19+
"iam:TagRole",
20+
"iam:UntagRole"
1821
],
1922
"Resource": "*"
2023
}

pkg/resource/role/hooks.go

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
2020
ackutil "github.com/aws-controllers-k8s/runtime/pkg/util"
2121
svcsdk "github.com/aws/aws-sdk-go/service/iam"
22+
23+
svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1"
2224
)
2325

2426
// syncPolicies examines the PolicyARNs in the supplied Role and calls the
@@ -53,14 +55,14 @@ func (rm *resourceManager) syncPolicies(
5355
}
5456

5557
for _, p := range toAdd {
56-
rlog.Debug("attaching policy to role", "policy_arn", *p)
57-
if err = rm.attachPolicy(ctx, r, p); err != nil {
58+
rlog.Debug("adding policy to role", "policy_arn", *p)
59+
if err = rm.addPolicy(ctx, r, p); err != nil {
5860
return err
5961
}
6062
}
6163
for _, p := range toDelete {
62-
rlog.Debug("detaching policy from role", "policy_arn", *p)
63-
if err = rm.detachPolicy(ctx, r, p); err != nil {
64+
rlog.Debug("removing policy from role", "policy_arn", *p)
65+
if err = rm.removePolicy(ctx, r, p); err != nil {
6466
return err
6567
}
6668
}
@@ -97,14 +99,14 @@ func (rm *resourceManager) getPolicies(
9799
return res, err
98100
}
99101

100-
// attachPolicy attaches the supplied Policy to the supplied Role resource
101-
func (rm *resourceManager) attachPolicy(
102+
// addPolicy adds the supplied Policy to the supplied Role resource
103+
func (rm *resourceManager) addPolicy(
102104
ctx context.Context,
103105
r *resource,
104106
policyARN *string,
105107
) (err error) {
106108
rlog := ackrtlog.FromContext(ctx)
107-
exit := rlog.Trace("rm.attachPolicy")
109+
exit := rlog.Trace("rm.addPolicy")
108110
defer exit(err)
109111

110112
input := &svcsdk.AttachRolePolicyInput{}
@@ -115,14 +117,14 @@ func (rm *resourceManager) attachPolicy(
115117
return err
116118
}
117119

118-
// detachPolicy detaches the supplied Policy from the supplied Role resource
119-
func (rm *resourceManager) detachPolicy(
120+
// removePolicy removes the supplied Policy from the supplied Role resource
121+
func (rm *resourceManager) removePolicy(
120122
ctx context.Context,
121123
r *resource,
122124
policyARN *string,
123125
) (err error) {
124126
rlog := ackrtlog.FromContext(ctx)
125-
exit := rlog.Trace("rm.detachPolicy")
127+
exit := rlog.Trace("rm.removePolicy")
126128
defer exit(err)
127129

128130
input := &svcsdk.DetachRolePolicyInput{}
@@ -132,3 +134,144 @@ func (rm *resourceManager) detachPolicy(
132134
rm.metrics.RecordAPICall("DELETE", "DetachRolePolicy", err)
133135
return err
134136
}
137+
138+
// syncTags examines the Tags in the supplied Role and calls the ListRoleTags,
139+
// TagRole and UntagRole APIs to ensure that the set of associated Tags stays
140+
// in sync with the Role.Spec.Tags
141+
func (rm *resourceManager) syncTags(
142+
ctx context.Context,
143+
r *resource,
144+
) (err error) {
145+
rlog := ackrtlog.FromContext(ctx)
146+
exit := rlog.Trace("rm.syncTags")
147+
defer exit(err)
148+
toAdd := []*svcapitypes.Tag{}
149+
toDelete := []*svcapitypes.Tag{}
150+
151+
existingTags, err := rm.getTags(ctx, r)
152+
if err != nil {
153+
return err
154+
}
155+
156+
for _, t := range r.ko.Spec.Tags {
157+
if !inTags(*t.Key, *t.Value, existingTags) {
158+
toAdd = append(toAdd, t)
159+
}
160+
}
161+
162+
for _, t := range existingTags {
163+
if !inTags(*t.Key, *t.Value, r.ko.Spec.Tags) {
164+
toDelete = append(toDelete, t)
165+
}
166+
}
167+
168+
for _, t := range toAdd {
169+
rlog.Debug("adding tag to role", "key", *t.Key, "value", *t.Value)
170+
}
171+
if err = rm.addTags(ctx, r, toAdd); err != nil {
172+
return err
173+
}
174+
for _, t := range toDelete {
175+
rlog.Debug("removing tag from role", "key", *t.Key, "value", *t.Value)
176+
}
177+
if err = rm.removeTags(ctx, r, toDelete); err != nil {
178+
return err
179+
}
180+
181+
return nil
182+
}
183+
184+
// inTags returns true if the supplied key and value can be found in the
185+
// supplied list of Tag structs.
186+
//
187+
// TODO(jaypipes): When we finally standardize Tag handling in ACK, move this
188+
// to the ACK common runtime/ or pkg/ repos
189+
func inTags(
190+
key string,
191+
value string,
192+
tags []*svcapitypes.Tag,
193+
) bool {
194+
for _, t := range tags {
195+
if *t.Key == key && *t.Value == value {
196+
return true
197+
}
198+
}
199+
return false
200+
}
201+
202+
// getTags returns the list of Policy ARNs currently attached to the Role
203+
func (rm *resourceManager) getTags(
204+
ctx context.Context,
205+
r *resource,
206+
) ([]*svcapitypes.Tag, error) {
207+
var err error
208+
var resp *svcsdk.ListRoleTagsOutput
209+
rlog := ackrtlog.FromContext(ctx)
210+
exit := rlog.Trace("rm.getTags")
211+
defer exit(err)
212+
213+
input := &svcsdk.ListRoleTagsInput{}
214+
input.RoleName = r.ko.Spec.Name
215+
res := []*svcapitypes.Tag{}
216+
217+
for {
218+
resp, err = rm.sdkapi.ListRoleTagsWithContext(ctx, input)
219+
if err != nil || resp == nil {
220+
break
221+
}
222+
for _, t := range resp.Tags {
223+
res = append(res, &svcapitypes.Tag{Key: t.Key, Value: t.Value})
224+
}
225+
if resp.IsTruncated != nil && !*resp.IsTruncated {
226+
break
227+
}
228+
}
229+
rm.metrics.RecordAPICall("GET", "ListRoleTags", err)
230+
return res, err
231+
}
232+
233+
// addTags adds the supplied Tags to the supplied Role resource
234+
func (rm *resourceManager) addTags(
235+
ctx context.Context,
236+
r *resource,
237+
tags []*svcapitypes.Tag,
238+
) (err error) {
239+
rlog := ackrtlog.FromContext(ctx)
240+
exit := rlog.Trace("rm.addTag")
241+
defer exit(err)
242+
243+
input := &svcsdk.TagRoleInput{}
244+
input.RoleName = r.ko.Spec.Name
245+
inTags := []*svcsdk.Tag{}
246+
for _, t := range tags {
247+
inTags = append(inTags, &svcsdk.Tag{Key: t.Key, Value: t.Value})
248+
}
249+
input.Tags = inTags
250+
251+
_, err = rm.sdkapi.TagRoleWithContext(ctx, input)
252+
rm.metrics.RecordAPICall("CREATE", "TagRole", err)
253+
return err
254+
}
255+
256+
// removeTags removes the supplied Tags from the supplied Role resource
257+
func (rm *resourceManager) removeTags(
258+
ctx context.Context,
259+
r *resource,
260+
tags []*svcapitypes.Tag,
261+
) (err error) {
262+
rlog := ackrtlog.FromContext(ctx)
263+
exit := rlog.Trace("rm.removeTag")
264+
defer exit(err)
265+
266+
input := &svcsdk.UntagRoleInput{}
267+
input.RoleName = r.ko.Spec.Name
268+
inTagKeys := []*string{}
269+
for _, t := range tags {
270+
inTagKeys = append(inTagKeys, t.Key)
271+
}
272+
input.TagKeys = inTagKeys
273+
274+
_, err = rm.sdkapi.UntagRoleWithContext(ctx, input)
275+
rm.metrics.RecordAPICall("DELETE", "UntagRole", err)
276+
return err
277+
}

pkg/resource/role/sdk.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/hooks/role/sdk_read_one_post_set_output.go.tpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
} else {
44
ko.Spec.Policies = policies
55
}
6+
if tags, err := rm.getTags(ctx, &resource{ko}); err != nil {
7+
return nil, err
8+
} else {
9+
ko.Spec.Tags = tags
10+
}

templates/hooks/role/sdk_update_post_set_output.go.tpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
if err := rm.syncPolicies(ctx, &resource{ko}); err != nil {
22
return nil, err
33
}
4+
if err := rm.syncTags(ctx, &resource{ko}); err != nil {
5+
return nil, err
6+
}
47
// There really isn't a status of a role... it either exists or doesn't. If
58
// we get here, that means the update was successful and the desired state
69
// of the role matches what we provided...

test/e2e/resources/role_simple.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ spec:
77
description: $ROLE_DESCRIPTION
88
maxSessionDuration: $MAX_SESSION_DURATION
99
assumeRolePolicyDocument: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]}'
10+
tags:
11+
- key: tag1
12+
value: val1

test/e2e/role.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,17 @@ def get_attached_policy_arns(role_name):
114114
return [p['PolicyArn'] for p in resp['AttachedPolicies']]
115115
except c.exceptions.NoSuchEntityException:
116116
return None
117+
118+
119+
def get_tags(role_name):
120+
"""Returns a list containing the tags that have been associated to the
121+
supplied Role.
122+
123+
If no such Role exists, returns None.
124+
"""
125+
c = boto3.client('iam')
126+
try:
127+
resp = c.list_role_tags(RoleName=role_name)
128+
return resp['Tags']
129+
except c.exceptions.NoSuchEntityException:
130+
return None

test/e2e/tests/test_role.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,36 @@ def test_crud(self):
103103
latest_policy_arns = role.get_attached_policy_arns(role_name)
104104
assert latest_policy_arns == policy_arns
105105

106+
# Same update code path check for tags...
107+
latest_tags = role.get_tags(role_name)
108+
before_update_expected_tags = [
109+
{
110+
"Key": "tag1",
111+
"Value": "val1"
112+
}
113+
]
114+
assert latest_tags == before_update_expected_tags
115+
new_tags = [
116+
{
117+
"key": "tag2",
118+
"value": "val2",
119+
}
120+
]
121+
updates = {
122+
"spec": {"tags": new_tags},
123+
}
124+
k8s.patch_custom_resource(ref, updates)
125+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
126+
127+
after_update_expected_tags = [
128+
{
129+
"Key": "tag2",
130+
"Value": "val2",
131+
}
132+
]
133+
latest_tags = role.get_tags(role_name)
134+
assert latest_tags == after_update_expected_tags
135+
106136
k8s.delete_custom_resource(ref)
107137

108138
time.sleep(DELETE_WAIT_AFTER_SECONDS)

0 commit comments

Comments
 (0)