Skip to content

Commit 871897e

Browse files
Add support for Policy Tag updates (#15)
Fixes aws-controllers-k8s/community#1124 Description of changes: Adds custom hook code to support the `TagPolicy` and `UntagPolicy` SDK methods for adding, updating and deleting tags on the `Policy` custom resource. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent ba734fa commit 871897e

File tree

10 files changed

+297
-7
lines changed

10 files changed

+297
-7
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2022-01-24T21:17:47Z"
3-
build_hash: cccec82a27ddd880095383360df1fdc8f530842f
4-
go_version: go1.17.5
2+
build_date: "2022-02-02T19:49:37Z"
3+
build_hash: 4ebcd703a95a2fbd71bd07130f92aa6813c1398b
4+
go_version: go1.17.1
55
version: v0.16.3
66
api_directory_checksum: 5c586ade18ff0bb36fe5fcb6d3ffa78b36a2b2c6
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.40.2
99
generator_config_info:
10-
file_checksum: e1e788f094e9560f25c4aa9d3aad9f9b3628bd3d
10+
file_checksum: 72469db1ef2738db804a8c42687c20305eadc2c1
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@ resources:
2828
input_fields:
2929
PolicyName: Name
3030
hooks:
31+
sdk_read_one_post_set_output:
32+
template_path: hooks/policy/sdk_read_one_post_set_output.go.tpl
3133
sdk_create_post_set_output:
3234
template_path: hooks/policy/sdk_create_post_set_output.go.tpl
35+
update_operation:
36+
# There is no `UpdatePolicy` API operation. The only way to update a
37+
# policy is to update the properties individually (only a few properties
38+
# support this) or to delete the policy and recreate it entirely.
39+
#
40+
# This custom method will support updating the properties individually,
41+
# but there is currently no support for the delete/create option.
42+
custom_method_name: customUpdatePolicy
3343
exceptions:
3444
terminal_codes:
3545
- InvalidInput

generator.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@ resources:
2828
input_fields:
2929
PolicyName: Name
3030
hooks:
31+
sdk_read_one_post_set_output:
32+
template_path: hooks/policy/sdk_read_one_post_set_output.go.tpl
3133
sdk_create_post_set_output:
3234
template_path: hooks/policy/sdk_create_post_set_output.go.tpl
35+
update_operation:
36+
# There is no `UpdatePolicy` API operation. The only way to update a
37+
# policy is to update the properties individually (only a few properties
38+
# support this) or to delete the policy and recreate it entirely.
39+
#
40+
# This custom method will support updating the properties individually,
41+
# but there is currently no support for the delete/create option.
42+
custom_method_name: customUpdatePolicy
3343
exceptions:
3444
terminal_codes:
3545
- InvalidInput

pkg/resource/policy/hooks.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package policy
15+
16+
import (
17+
"context"
18+
19+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
20+
ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition"
21+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
22+
svcsdk "github.com/aws/aws-sdk-go/service/iam"
23+
corev1 "k8s.io/api/core/v1"
24+
25+
svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1"
26+
)
27+
28+
func (rm *resourceManager) customUpdatePolicy(
29+
ctx context.Context,
30+
desired *resource,
31+
latest *resource,
32+
delta *ackcompare.Delta,
33+
) (*resource, error) {
34+
ko := desired.ko.DeepCopy()
35+
36+
rm.setStatusDefaults(ko)
37+
38+
if err := rm.syncTags(ctx, &resource{ko}); err != nil {
39+
return nil, err
40+
}
41+
// There really isn't a status of a policy... it either exists or doesn't.
42+
// If we get here, that means the update was successful and the desired
43+
// state of the policy matches what we provided...
44+
ackcondition.SetSynced(&resource{ko}, corev1.ConditionTrue, nil, nil)
45+
46+
return &resource{ko}, nil
47+
}
48+
49+
// syncTags examines the Tags in the supplied Policy and calls the
50+
// ListPolicyTags, TagPolicy and UntagPolicy APIs to ensure that the set of
51+
// associated Tags stays in sync with the Policy.Spec.Tags
52+
func (rm *resourceManager) syncTags(
53+
ctx context.Context,
54+
r *resource,
55+
) (err error) {
56+
rlog := ackrtlog.FromContext(ctx)
57+
exit := rlog.Trace("rm.syncTags")
58+
defer exit(err)
59+
toAdd := []*svcapitypes.Tag{}
60+
toDelete := []*svcapitypes.Tag{}
61+
62+
existingTags, err := rm.getTags(ctx, r)
63+
if err != nil {
64+
return err
65+
}
66+
67+
for _, t := range r.ko.Spec.Tags {
68+
if !inTags(*t.Key, *t.Value, existingTags) {
69+
toAdd = append(toAdd, t)
70+
}
71+
}
72+
73+
for _, t := range existingTags {
74+
if !inTags(*t.Key, *t.Value, r.ko.Spec.Tags) {
75+
toDelete = append(toDelete, t)
76+
}
77+
}
78+
79+
if len(toDelete) > 0 {
80+
for _, t := range toDelete {
81+
rlog.Debug("removing tag from policy", "key", *t.Key, "value", *t.Value)
82+
}
83+
if err = rm.removeTags(ctx, r, toDelete); err != nil {
84+
return err
85+
}
86+
}
87+
88+
if len(toAdd) > 0 {
89+
for _, t := range toAdd {
90+
rlog.Debug("adding tag to policy", "key", *t.Key, "value", *t.Value)
91+
}
92+
if err = rm.addTags(ctx, r, toAdd); err != nil {
93+
return err
94+
}
95+
}
96+
97+
return nil
98+
}
99+
100+
// inTags returns true if the supplied key and value can be found in the
101+
// supplied list of Tag structs.
102+
//
103+
// TODO(jaypipes): When we finally standardize Tag handling in ACK, move this
104+
// to the ACK common runtime/ or pkg/ repos
105+
func inTags(
106+
key string,
107+
value string,
108+
tags []*svcapitypes.Tag,
109+
) bool {
110+
for _, t := range tags {
111+
if *t.Key == key && *t.Value == value {
112+
return true
113+
}
114+
}
115+
return false
116+
}
117+
118+
// getTags returns the list of tags attached to the Policy
119+
func (rm *resourceManager) getTags(
120+
ctx context.Context,
121+
r *resource,
122+
) ([]*svcapitypes.Tag, error) {
123+
var err error
124+
var resp *svcsdk.ListPolicyTagsOutput
125+
rlog := ackrtlog.FromContext(ctx)
126+
exit := rlog.Trace("rm.getTags")
127+
defer exit(err)
128+
129+
input := &svcsdk.ListPolicyTagsInput{}
130+
input.PolicyArn = (*string)(r.ko.Status.ACKResourceMetadata.ARN)
131+
res := []*svcapitypes.Tag{}
132+
133+
for {
134+
resp, err = rm.sdkapi.ListPolicyTagsWithContext(ctx, input)
135+
if err != nil || resp == nil {
136+
break
137+
}
138+
for _, t := range resp.Tags {
139+
res = append(res, &svcapitypes.Tag{Key: t.Key, Value: t.Value})
140+
}
141+
if resp.IsTruncated != nil && !*resp.IsTruncated {
142+
break
143+
}
144+
}
145+
rm.metrics.RecordAPICall("GET", "ListPolicyTags", err)
146+
return res, err
147+
}
148+
149+
// addTags adds the supplied Tags to the supplied Policy resource
150+
func (rm *resourceManager) addTags(
151+
ctx context.Context,
152+
r *resource,
153+
tags []*svcapitypes.Tag,
154+
) (err error) {
155+
rlog := ackrtlog.FromContext(ctx)
156+
exit := rlog.Trace("rm.addTags")
157+
defer exit(err)
158+
159+
input := &svcsdk.TagPolicyInput{}
160+
input.PolicyArn = (*string)(r.ko.Status.ACKResourceMetadata.ARN)
161+
inTags := []*svcsdk.Tag{}
162+
for _, t := range tags {
163+
inTags = append(inTags, &svcsdk.Tag{Key: t.Key, Value: t.Value})
164+
}
165+
input.Tags = inTags
166+
167+
_, err = rm.sdkapi.TagPolicyWithContext(ctx, input)
168+
rm.metrics.RecordAPICall("CREATE", "TagPolicy", err)
169+
return err
170+
}
171+
172+
// removeTags removes the supplied Tags from the supplied Policy resource
173+
func (rm *resourceManager) removeTags(
174+
ctx context.Context,
175+
r *resource,
176+
tags []*svcapitypes.Tag,
177+
) (err error) {
178+
rlog := ackrtlog.FromContext(ctx)
179+
exit := rlog.Trace("rm.removeTags")
180+
defer exit(err)
181+
182+
input := &svcsdk.UntagPolicyInput{}
183+
input.PolicyArn = (*string)(r.ko.Status.ACKResourceMetadata.ARN)
184+
inTagKeys := []*string{}
185+
for _, t := range tags {
186+
inTagKeys = append(inTagKeys, t.Key)
187+
}
188+
input.TagKeys = inTagKeys
189+
190+
_, err = rm.sdkapi.UntagPolicyWithContext(ctx, input)
191+
rm.metrics.RecordAPICall("DELETE", "UntagPolicy", err)
192+
return err
193+
}

pkg/resource/policy/sdk.go

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

pkg/resource/role/hooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func inTags(
199199
return false
200200
}
201201

202-
// getTags returns the list of Policy ARNs currently attached to the Role
202+
// getTags returns the list of tags to the Role
203203
func (rm *resourceManager) getTags(
204204
ctx context.Context,
205205
r *resource,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
if tags, err := rm.getTags(ctx, &resource{ko}); err != nil {
2+
return nil, err
3+
} else {
4+
ko.Spec.Tags = tags
5+
}

test/e2e/policy.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,16 @@ def get(policy_arn):
100100
return resp['Policy']
101101
except c.exceptions.NoSuchEntityException:
102102
return None
103+
104+
def get_tags(policy_arn):
105+
"""Returns a list containing the tags that have been associated to the
106+
supplied Policy.
107+
108+
If no such Policy exists, returns None.
109+
"""
110+
c = boto3.client('iam')
111+
try:
112+
resp = c.list_policy_tags(PolicyArn=policy_arn)
113+
return resp['Tags']
114+
except c.exceptions.NoSuchEntityException:
115+
return None

test/e2e/resources/policy_simple.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ spec:
66
name: $POLICY_NAME
77
description: $POLICY_DESCRIPTION
88
policyDocument: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListAllMyBuckets","Resource":"arn:aws:s3:::*"},{"Effect":"Allow","Action":["s3:List*"],"Resource":["*"]}]}'
9+
tags:
10+
- key: tag1
11+
value: val1

test/e2e/tests/test_policy.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
DELETE_WAIT_AFTER_SECONDS = 10
2929
CHECK_WAIT_AFTER_SECONDS = 10
30+
MODIFY_WAIT_AFTER_SECONDS = 10
3031

3132

3233
@service_marker
@@ -65,6 +66,56 @@ def test_crud(self):
6566

6667
policy.wait_until_exists(policy_arn)
6768

69+
# Same update code path check for tags...
70+
latest_tags = policy.get_tags(policy_arn)
71+
before_update_expected_tags = [
72+
{
73+
"Key": "tag1",
74+
"Value": "val1"
75+
}
76+
]
77+
assert latest_tags == before_update_expected_tags
78+
new_tags = [
79+
{
80+
"key": "tag2",
81+
"value": "val2",
82+
}
83+
]
84+
updates = {
85+
"spec": {"tags": new_tags},
86+
}
87+
k8s.patch_custom_resource(ref, updates)
88+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
89+
90+
latest_tags = policy.get_tags(policy_arn)
91+
after_update_expected_tags = [
92+
{
93+
"Key": "tag2",
94+
"Value": "val2",
95+
}
96+
]
97+
assert latest_tags == after_update_expected_tags
98+
new_tags = [
99+
{
100+
"key": "tag2",
101+
"value": "val3", # Update the value
102+
}
103+
]
104+
updates = {
105+
"spec": {"tags": new_tags},
106+
}
107+
k8s.patch_custom_resource(ref, updates)
108+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
109+
110+
latest_tags = policy.get_tags(policy_arn)
111+
after_update_expected_tags = [
112+
{
113+
"Key": "tag2",
114+
"Value": "val3",
115+
}
116+
]
117+
assert latest_tags == after_update_expected_tags
118+
68119
k8s.delete_custom_resource(ref)
69120

70121
time.sleep(DELETE_WAIT_AFTER_SECONDS)

0 commit comments

Comments
 (0)