Skip to content

Commit fa514de

Browse files
Update tags support for Subnet (#81)
Issue #, if available: aws-controllers-k8s/community#1369 Description of changes: - Added custom code that supports updating tags for **Subnet** resource. - Added integration test to check whether tags are being updated as expected. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 9b10dc2 commit fa514de

File tree

9 files changed

+307
-4
lines changed

9 files changed

+307
-4
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2022-09-06T20:17:17Z"
2+
build_date: "2022-09-06T21:58:54Z"
33
build_hash: 87477ae8ca8ac6ddb8c565bbd910cc7e30f55ed0
4-
go_version: go1.18.1
4+
go_version: go1.18.3
55
version: v0.19.3
66
api_directory_checksum: 3960da50b98c36b05635d3abbfd129ae7bb48e32
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.42.0
99
generator_config_info:
10-
file_checksum: b17fda4c7e3ae9446dd568f3be1f08d70ce15588
10+
file_checksum: 6c21f6d996045b6c6c7c79074147f9bbcff5a73c
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ resources:
405405
template_path: hooks/subnet/sdk_create_post_set_output.go.tpl
406406
sdk_read_many_post_set_output:
407407
template_path: hooks/subnet/sdk_read_many_post_set_output.go.tpl
408+
sdk_file_end:
409+
template_path: hooks/subnet/sdk_file_end.go.tpl
408410
update_operation:
409411
custom_method_name: customUpdateSubnet
410412
TransitGateway:

generator.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ resources:
405405
template_path: hooks/subnet/sdk_create_post_set_output.go.tpl
406406
sdk_read_many_post_set_output:
407407
template_path: hooks/subnet/sdk_read_many_post_set_output.go.tpl
408+
sdk_file_end:
409+
template_path: hooks/subnet/sdk_file_end.go.tpl
408410
update_operation:
409411
custom_method_name: customUpdateSubnet
410412
TransitGateway:

pkg/resource/subnet/hooks.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package subnet
1616
import (
1717
"context"
1818

19+
svcapitypes "github.com/aws-controllers-k8s/ec2-controller/apis/v1alpha1"
1920
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
2021
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
2122
ackutils "github.com/aws-controllers-k8s/runtime/pkg/util"
@@ -43,6 +44,12 @@ func (rm *resourceManager) customUpdateSubnet(
4344
}
4445
}
4546

47+
if delta.DifferentAt("Spec.Tags") {
48+
if err = rm.syncTags(ctx, desired, latest); err != nil {
49+
return nil, err
50+
}
51+
}
52+
4653
return &resource{ko}, nil
4754
}
4855

@@ -243,6 +250,121 @@ func toStrPtr(str string) *string {
243250
return &str
244251
}
245252

253+
// syncTags used to keep tags in sync using createTags and deleteTags functions
254+
func (rm *resourceManager) syncTags(
255+
ctx context.Context,
256+
desired *resource,
257+
latest *resource,
258+
) (err error) {
259+
rlog := ackrtlog.FromContext(ctx)
260+
exit := rlog.Trace("rm.syncTags")
261+
defer func(err error) {
262+
exit(err)
263+
}(err)
264+
265+
toAdd, toDelete := computeTagsDelta(
266+
desired.ko.Spec.Tags, latest.ko.Spec.Tags,
267+
)
268+
269+
if err = rm.deleteTags(ctx, latest, toDelete); err != nil {
270+
return err
271+
}
272+
273+
if err = rm.createTags(ctx, desired, toAdd); err != nil {
274+
return err
275+
}
276+
277+
return nil
278+
}
279+
280+
// createTags function creates tags for subnet resource using CreateTags API calls
281+
func (rm *resourceManager) createTags(
282+
ctx context.Context,
283+
r *resource,
284+
tags []*svcapitypes.Tag,
285+
) (err error) {
286+
rlog := ackrtlog.FromContext(ctx)
287+
exit := rlog.Trace("rm.createTags")
288+
defer exit(err)
289+
290+
resourceId := []*string{r.ko.Status.SubnetID}
291+
292+
for _, i := range tags {
293+
toAdd := rm.newTag(*i)
294+
req := &svcsdk.CreateTagsInput{
295+
Resources: resourceId,
296+
Tags: []*svcsdk.Tag{toAdd},
297+
}
298+
_, err := rm.sdkapi.CreateTagsWithContext(ctx, req)
299+
rm.metrics.RecordAPICall("CREATE", "CreateTags", err)
300+
if err != nil {
301+
return err
302+
}
303+
}
304+
return err
305+
}
306+
307+
// deleteTags function deletes tags from subnet resource using DeleteTags API calls
308+
func (rm *resourceManager) deleteTags(
309+
ctx context.Context,
310+
r *resource,
311+
tags []*svcapitypes.Tag,
312+
) (err error) {
313+
rlog := ackrtlog.FromContext(ctx)
314+
exit := rlog.Trace("rm.deleteTags")
315+
defer exit(err)
316+
317+
resourceId := []*string{r.ko.Status.SubnetID}
318+
319+
for _, i := range tags {
320+
toDelete := rm.newTag(*i)
321+
req := &svcsdk.DeleteTagsInput{
322+
Resources: resourceId,
323+
Tags: []*svcsdk.Tag{toDelete},
324+
}
325+
_, err = rm.sdkapi.DeleteTagsWithContext(ctx, req)
326+
rm.metrics.RecordAPICall("DELETE", "DeleteTags", err)
327+
if err != nil {
328+
return err
329+
}
330+
}
331+
return err
332+
}
333+
334+
// computeTagsDelta returns tags to be added and removed from the resource
335+
func computeTagsDelta(
336+
desired []*svcapitypes.Tag,
337+
latest []*svcapitypes.Tag,
338+
) (toAdd []*svcapitypes.Tag, toDelete []*svcapitypes.Tag) {
339+
340+
desiredTags := map[string]string{}
341+
for _, tag := range desired {
342+
desiredTags[*tag.Key] = *tag.Value
343+
}
344+
345+
latestTags := map[string]string{}
346+
for _, tag := range latest {
347+
latestTags[*tag.Key] = *tag.Value
348+
}
349+
350+
for _, tag := range desired {
351+
val, ok := latestTags[*tag.Key]
352+
if !ok || val != *tag.Value {
353+
toAdd = append(toAdd, tag)
354+
}
355+
}
356+
357+
for _, tag := range latest {
358+
_, ok := desiredTags[*tag.Key]
359+
if !ok {
360+
toDelete = append(toDelete, tag)
361+
}
362+
}
363+
364+
return toAdd, toDelete
365+
366+
}
367+
246368
// updateTagSpecificationsInCreateRequest adds
247369
// Tags defined in the Spec to CreateSubnetInput.TagSpecification
248370
// and ensures the ResourceType is always set to 'subnet'

pkg/resource/subnet/sdk.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
compareTags(delta, a, b)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{{ $CRD := .CRD }}
2+
{{ $SDKAPI := .SDKAPI }}
3+
4+
{{/* Generate helper methods for Subnet */}}
5+
{{- range $specFieldName, $specField := $CRD.Config.Resources.Subnet.Fields }}
6+
{{- if $specField.From }}
7+
{{- $operationName := $specField.From.Operation }}
8+
{{- $operation := (index $SDKAPI.API.Operations $operationName) -}}
9+
{{- range $subnetRefName, $subnetMemberRefs := $operation.InputRef.Shape.MemberRefs -}}
10+
{{- if eq $subnetRefName "Tags" }}
11+
{{- $subnetRef := $subnetMemberRefs.Shape.MemberRef }}
12+
{{- $subnetRefName = "Tag" }}
13+
func compare{{ $subnetRefName }}(
14+
a *svcapitypes.{{ $subnetRefName }},
15+
b *svcapitypes.{{ $subnetRefName }},
16+
) *ackcompare.Delta {
17+
delta := ackcompare.NewDelta()
18+
{{ GoCodeCompareStruct $CRD $subnetRef.Shape "delta" "a" "b" $subnetRefName 1 }}
19+
return delta
20+
}
21+
22+
func (rm *resourceManager) new{{ $subnetRefName }}(
23+
c svcapitypes.{{ $subnetRefName }},
24+
) *svcsdk.{{ $subnetRefName }} {
25+
res := &svcsdk.{{ $subnetRefName }}{}
26+
{{ GoCodeSetSDKForStruct $CRD "" "res" $subnetRef "" "c" 1 }}
27+
return res
28+
}
29+
{{- end }}
30+
{{- end }}
31+
{{- end }}
32+
{{- end }}

test/e2e/resources/subnet.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ metadata:
44
name: $SUBNET_NAME
55
spec:
66
cidrBlock: $CIDR_BLOCK
7-
vpcID: $VPC_ID
7+
vpcID: $VPC_ID
8+
tags:
9+
- key: $KEY
10+
value: $VALUE

test/e2e/tests/test_subnet.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def test_create_delete(self, ec2_client):
116116
assert cr is not None
117117
assert k8s.get_resource_exists(ref)
118118

119+
# Check resource synced successfully
120+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
121+
119122
resource = k8s.get_resource(ref)
120123
resource_id = resource["status"]["subnetID"]
121124

@@ -134,6 +137,101 @@ def test_create_delete(self, ec2_client):
134137
# Check Subnet no longer exists in AWS
135138
ec2_validator.assert_subnet(resource_id, exists=False)
136139

140+
def test_crud_tags(self, ec2_client):
141+
test_resource_values = REPLACEMENT_VALUES.copy()
142+
resource_name = random_suffix_name("subnet-test", 24)
143+
test_vpc = get_bootstrap_resources().SharedTestVPC
144+
vpc_id = test_vpc.vpc_id
145+
146+
test_resource_values["SUBNET_NAME"] = resource_name
147+
test_resource_values["VPC_ID"] = vpc_id
148+
# CIDR needs to be within SharedTestVPC range and not overlap other subnets
149+
test_resource_values["CIDR_BLOCK"] = "10.0.255.0/24"
150+
test_resource_values["KEY"] = "initialtagkey"
151+
test_resource_values["VALUE"] = "initialtagvalue"
152+
153+
# Load Subnet CR
154+
resource_data = load_ec2_resource(
155+
"subnet",
156+
additional_replacements=test_resource_values,
157+
)
158+
logging.debug(resource_data)
159+
160+
# Create k8s resource
161+
ref = k8s.CustomResourceReference(
162+
CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL,
163+
resource_name, namespace="default",
164+
)
165+
k8s.create_custom_resource(ref, resource_data)
166+
cr = k8s.wait_resource_consumed_by_controller(ref)
167+
168+
assert cr is not None
169+
assert k8s.get_resource_exists(ref)
170+
171+
# Check resource synced successfully
172+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
173+
174+
resource = k8s.get_resource(ref)
175+
resource_id = resource["status"]["subnetID"]
176+
time.sleep(CREATE_WAIT_AFTER_SECONDS)
177+
178+
# Check Subnet exists in AWS
179+
ec2_validator = EC2Validator(ec2_client)
180+
ec2_validator.assert_subnet(resource_id)
181+
182+
# Check tags exist for subnet resource
183+
assert resource["spec"]["tags"][0]["key"] == "initialtagkey"
184+
assert resource["spec"]["tags"][0]["value"] == "initialtagvalue"
185+
186+
# New pair of tags
187+
new_tags = [
188+
{
189+
"key": "updatedtagkey",
190+
"value": "updatedtagvalue",
191+
}
192+
193+
]
194+
195+
# Patch the subnet, updating the tags with new pair
196+
updates = {
197+
"spec": {"tags": new_tags},
198+
}
199+
200+
k8s.patch_custom_resource(ref, updates)
201+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
202+
203+
# Check resource synced successfully
204+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
205+
206+
# Assert tags are updated for subnet resource
207+
resource = k8s.get_resource(ref)
208+
assert resource["spec"]["tags"][0]["key"] == "updatedtagkey"
209+
assert resource["spec"]["tags"][0]["value"] == "updatedtagvalue"
210+
211+
# Patch the subnet resource, deleting the tags
212+
updates = {
213+
"spec": {"tags": []},
214+
}
215+
216+
k8s.patch_custom_resource(ref, updates)
217+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
218+
219+
# Check resource synced successfully
220+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
221+
222+
# Assert tags are deleted
223+
resource = k8s.get_resource(ref)
224+
assert len(resource['spec']['tags']) == 0
225+
226+
# Delete k8s resource
227+
_, deleted = k8s.delete_custom_resource(ref)
228+
assert deleted is True
229+
230+
time.sleep(DELETE_WAIT_AFTER_SECONDS)
231+
232+
# Check Subnet no longer exists in AWS
233+
ec2_validator.assert_subnet(resource_id, exists=False)
234+
137235
def test_route_table_assocations(self, ec2_client, default_route_tables):
138236
test_resource_values = REPLACEMENT_VALUES.copy()
139237
resource_name = random_suffix_name("subnet-test", 24)
@@ -165,6 +263,9 @@ def test_route_table_assocations(self, ec2_client, default_route_tables):
165263
assert cr is not None
166264
assert k8s.get_resource_exists(ref)
167265

266+
# Check resource synced successfully
267+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
268+
168269
resource = k8s.get_resource(ref)
169270
resource_id = resource["status"]["subnetID"]
170271

@@ -185,6 +286,9 @@ def test_route_table_assocations(self, ec2_client, default_route_tables):
185286
k8s.patch_custom_resource(ref, updates)
186287
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
187288

289+
# Check resource synced successfully
290+
assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
291+
188292
assert ec2_validator.get_route_table_association(initial_rt_cr["status"]["routeTableID"], resource_id) is None
189293
assert ec2_validator.get_route_table_association(default_route_tables[1][1]["status"]["routeTableID"], resource_id) is not None
190294

0 commit comments

Comments
 (0)