Skip to content

Commit 098f153

Browse files
authoredDec 22, 2021
Support Table tags updates (#22)
Issue: aws-controllers-k8s/community#1029 This patch adds hooks and delta helpers to properly update Table tags. Description of changes: - Drop the generate updateTable function and use a custom one - Add hooks to properly compute the tags delta and make necessary API calls to update them - Add e2e tests for table tag updates By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent c66c402 commit 098f153

File tree

15 files changed

+524
-472
lines changed

15 files changed

+524
-472
lines changed
 

Diff for: ‎apis/v1alpha1/ack-generate-metadata.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2021-10-27T21:04:06Z"
3-
build_hash: d2b063806d25cfcae4f2d4eb44f8e3f713b23e8e
4-
go_version: go1.15
2+
build_date: "2021-12-22T13:40:06Z"
3+
build_hash: 6f17f51682dc0d16c36aa456fd22855ce9282fbc
4+
go_version: go1.16.4
55
version: v0.15.2
66
api_directory_checksum: c1d144a18336326f141e97e6800b47f64ed992cc
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.38.47
99
generator_config_info:
10-
file_checksum: 3d4ab94742ecf92212b94a5e47bdda8258589718
10+
file_checksum: 2c5f9585b1b0356e601e17efc7f5c47bdabdf4ab
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

Diff for: ‎apis/v1alpha1/generator.yaml

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ operations:
77
primary_identifier_field_name: BackupArn
88
resources:
99
Table:
10+
fields:
11+
Tags:
12+
compare:
13+
is_ignored: true
1014
exceptions:
1115
errors:
1216
404:
@@ -15,11 +19,13 @@ resources:
1519
- InternalServerError
1620
- LimitExceededException
1721
- ResourceInUseException
22+
update_operation:
23+
custom_method_name: customUpdateTable
1824
hooks:
25+
delta_pre_compare:
26+
code: customPreCompare(delta, a, b)
1927
sdk_read_one_post_set_output:
2028
template_path: hooks/table/sdk_read_one_post_set_output.go.tpl
21-
sdk_update_pre_build_request:
22-
template_path: hooks/table/sdk_update_pre_build_request.go.tpl
2329
sdk_delete_pre_build_request:
2430
template_path: hooks/table/sdk_delete_pre_build_request.go.tpl
2531
GlobalTable:

Diff for: ‎generator.yaml

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ operations:
77
primary_identifier_field_name: BackupArn
88
resources:
99
Table:
10+
fields:
11+
Tags:
12+
compare:
13+
is_ignored: true
1014
exceptions:
1115
errors:
1216
404:
@@ -15,11 +19,13 @@ resources:
1519
- InternalServerError
1620
- LimitExceededException
1721
- ResourceInUseException
22+
update_operation:
23+
custom_method_name: customUpdateTable
1824
hooks:
25+
delta_pre_compare:
26+
code: customPreCompare(delta, a, b)
1927
sdk_read_one_post_set_output:
2028
template_path: hooks/table/sdk_read_one_post_set_output.go.tpl
21-
sdk_update_pre_build_request:
22-
template_path: hooks/table/sdk_update_pre_build_request.go.tpl
2329
sdk_delete_pre_build_request:
2430
template_path: hooks/table/sdk_delete_pre_build_request.go.tpl
2531
GlobalTable:

Diff for: ‎helm/templates/deployment.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,12 @@ spec:
7575
value: {{ join "," .Values.resourceTags | quote }}
7676
terminationGracePeriodSeconds: 10
7777
nodeSelector: {{ toYaml .Values.deployment.nodeSelector | nindent 8 }}
78+
{{ if .Values.deployment.tolerations -}}
79+
tolerations: {{ toYaml .Values.deployment.tolerations | nindent 8 }}
80+
{{ end -}}
81+
{{ if .Values.deployment.affinity -}}
82+
affinity: {{ toYaml .Values.deployment.affinity | nindent 8 }}
83+
{{ end -}}
84+
{{ if .Values.deployment.priorityClassName -}}
85+
priorityClassName: {{ .Values.deployment.priorityClassName -}}
86+
{{ end -}}

Diff for: ‎helm/values.yaml

+12-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,20 @@ deployment:
1515
annotations: {}
1616
labels: {}
1717
containerPort: 8080
18+
# Which nodeSelector to set?
19+
# See: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector
1820
nodeSelector:
1921
kubernetes.io/os: linux
20-
22+
# Which tolerations to set?
23+
# See: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
24+
tolerations: {}
25+
# What affinity to set?
26+
# See: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
27+
affinity: {}
28+
# Which priorityClassName to set?
29+
# See: https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#pod-priority
30+
priorityClassName:
31+
2132
metrics:
2233
service:
2334
# Set to true to automatically create a Kubernetes Service resource for the

Diff for: ‎pkg/resource/backup/sdk.go

-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎pkg/resource/table/delta.go

+1-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎pkg/resource/table/hooks.go

+247-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@
1414
package table
1515

1616
import (
17+
"context"
1718
"errors"
1819
"time"
1920

20-
"github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1"
21+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
2122
ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue"
23+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
24+
ackutil "github.com/aws-controllers-k8s/runtime/pkg/util"
25+
svcsdk "github.com/aws/aws-sdk-go/service/dynamodb"
26+
corev1 "k8s.io/api/core/v1"
27+
28+
"github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1"
2229
)
2330

2431
var (
@@ -95,3 +102,242 @@ func isTableUpdating(r *resource) bool {
95102
dbis := *r.ko.Status.TableStatus
96103
return dbis == string(v1alpha1.TableStatus_SDK_UPDATING)
97104
}
105+
106+
func (rm *resourceManager) customUpdateTable(
107+
ctx context.Context,
108+
desired *resource,
109+
latest *resource,
110+
delta *ackcompare.Delta,
111+
) (updated *resource, err error) {
112+
rlog := ackrtlog.FromContext(ctx)
113+
exit := rlog.Trace("rm.customUpdateTable")
114+
defer exit(err)
115+
116+
if isTableDeleting(latest) {
117+
msg := "table is currently being deleted"
118+
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
119+
return desired, requeueWaitWhileDeleting
120+
}
121+
if isTableCreating(latest) {
122+
msg := "table is currently being created"
123+
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
124+
return desired, requeueWaitWhileCreating
125+
}
126+
if isTableUpdating(latest) {
127+
msg := "table is currently being updated"
128+
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
129+
return desired, requeueWaitWhileUpdating
130+
}
131+
if tableHasTerminalStatus(latest) {
132+
msg := "table is in '" + *latest.ko.Status.TableStatus + "' status"
133+
setTerminalCondition(desired, corev1.ConditionTrue, &msg, nil)
134+
setSyncedCondition(desired, corev1.ConditionTrue, nil, nil)
135+
return desired, nil
136+
}
137+
138+
// Merge in the information we read from the API call above to the copy of
139+
// the original Kubernetes object we passed to the function
140+
ko := desired.ko.DeepCopy()
141+
rm.setStatusDefaults(ko)
142+
143+
if delta.DifferentAt("Spec.Tags") {
144+
if err := rm.syncTableTags(ctx, latest, desired); err != nil {
145+
return nil, err
146+
}
147+
}
148+
149+
// TODO(hilalymh): support updating all table field
150+
return &resource{ko}, nil
151+
}
152+
153+
// syncTableTags updates a dynamodb table tags.
154+
//
155+
// TODO(hilalymh): move this function to a common utility file. This function can be reused
156+
// to tag GlobalTable resources.
157+
func (rm *resourceManager) syncTableTags(
158+
ctx context.Context,
159+
latest *resource,
160+
desired *resource,
161+
) (err error) {
162+
rlog := ackrtlog.FromContext(ctx)
163+
exit := rlog.Trace("rm.syncTableTags")
164+
defer exit(err)
165+
166+
added, updated, removed := computeTagsDelta(latest.ko.Spec.Tags, desired.ko.Spec.Tags)
167+
168+
// There are no API calls to update an existing tag. To update a tag we will have to first
169+
// delete it and then recreate it with the new value.
170+
171+
// Tags to remove
172+
for _, updatedTag := range updated {
173+
removed = append(removed, updatedTag.Key)
174+
}
175+
// Tags to create
176+
added = append(added, updated...)
177+
178+
if len(removed) > 0 {
179+
_, err = rm.sdkapi.UntagResourceWithContext(
180+
ctx,
181+
&svcsdk.UntagResourceInput{
182+
ResourceArn: (*string)(latest.ko.Status.ACKResourceMetadata.ARN),
183+
TagKeys: removed,
184+
},
185+
)
186+
rm.metrics.RecordAPICall("GET", "UntagResource", err)
187+
if err != nil {
188+
return err
189+
}
190+
}
191+
192+
if len(added) > 0 {
193+
_, err = rm.sdkapi.TagResourceWithContext(
194+
ctx,
195+
&svcsdk.TagResourceInput{
196+
ResourceArn: (*string)(latest.ko.Status.ACKResourceMetadata.ARN),
197+
Tags: sdkTagsFromResourceTags(added),
198+
},
199+
)
200+
rm.metrics.RecordAPICall("GET", "UntagResource", err)
201+
if err != nil {
202+
return err
203+
}
204+
}
205+
return nil
206+
}
207+
208+
// equalTags returns true if two Tag arrays are equal regardless of the order
209+
// of their elements.
210+
func equalTags(
211+
a []*v1alpha1.Tag,
212+
b []*v1alpha1.Tag,
213+
) bool {
214+
added, updated, removed := computeTagsDelta(a, b)
215+
return len(added) == 0 && len(updated) == 0 && len(removed) == 0
216+
}
217+
218+
// resourceTagsFromSDKTags transforms a *svcsdk.Tag array to a *v1alpha1.Tag array.
219+
func resourceTagsFromSDKTags(svcTags []*svcsdk.Tag) []*v1alpha1.Tag {
220+
tags := make([]*v1alpha1.Tag, len(svcTags))
221+
for i := range svcTags {
222+
tags[i] = &v1alpha1.Tag{
223+
Key: svcTags[i].Key,
224+
Value: svcTags[i].Value,
225+
}
226+
}
227+
return tags
228+
}
229+
230+
// svcTagsFromResourceTags transforms a *v1alpha1.Tag array to a *svcsdk.Tag array.
231+
func sdkTagsFromResourceTags(rTags []*v1alpha1.Tag) []*svcsdk.Tag {
232+
tags := make([]*svcsdk.Tag, len(rTags))
233+
for i := range rTags {
234+
tags[i] = &svcsdk.Tag{
235+
Key: rTags[i].Key,
236+
Value: rTags[i].Value,
237+
}
238+
}
239+
return tags
240+
}
241+
242+
// computeTagsDelta compares two Tag arrays and return three different list
243+
// containing the added, updated and removed tags.
244+
// The removed tags only contains the Key of tags
245+
func computeTagsDelta(
246+
a []*v1alpha1.Tag,
247+
b []*v1alpha1.Tag,
248+
) (added, updated []*v1alpha1.Tag, removed []*string) {
249+
var visitedIndexes []string
250+
mainLoop:
251+
for _, aElement := range a {
252+
visitedIndexes = append(visitedIndexes, *aElement.Key)
253+
for _, bElement := range b {
254+
if equalStrings(aElement.Key, bElement.Key) {
255+
if !equalStrings(aElement.Value, bElement.Value) {
256+
updated = append(updated, bElement)
257+
}
258+
continue mainLoop
259+
}
260+
}
261+
removed = append(removed, aElement.Key)
262+
}
263+
for _, bElement := range b {
264+
if !ackutil.InStrings(*bElement.Key, visitedIndexes) {
265+
added = append(added, bElement)
266+
}
267+
}
268+
return added, updated, removed
269+
}
270+
271+
func equalStrings(a, b *string) bool {
272+
if a == nil {
273+
return b == nil || *b == ""
274+
}
275+
return (*a == "" && b == nil) || *a == *b
276+
}
277+
278+
// setResourceAdditionalFields will describe the fields that are not return by
279+
// DescribeTable calls
280+
func (rm *resourceManager) setResourceAdditionalFields(
281+
ctx context.Context,
282+
ko *v1alpha1.Table,
283+
) (err error) {
284+
rlog := ackrtlog.FromContext(ctx)
285+
exit := rlog.Trace("rm.setResourceAdditionalFields")
286+
defer exit(err)
287+
288+
ko.Spec.Tags, err = rm.getResourceTagsPagesWithContext(ctx, string(*ko.Status.ACKResourceMetadata.ARN))
289+
if err != nil {
290+
return err
291+
}
292+
293+
return nil
294+
}
295+
296+
// getResourceTagsPagesWithContext queries the list of tags of a given resource.
297+
func (rm *resourceManager) getResourceTagsPagesWithContext(ctx context.Context, resourceARN string) ([]*v1alpha1.Tag, error) {
298+
var err error
299+
rlog := ackrtlog.FromContext(ctx)
300+
exit := rlog.Trace("rm.getResourceTagsPagesWithContext")
301+
defer exit(err)
302+
303+
tags := []*v1alpha1.Tag{}
304+
305+
var token *string = nil
306+
for {
307+
var listTagsOfResourceOutput *svcsdk.ListTagsOfResourceOutput
308+
listTagsOfResourceOutput, err = rm.sdkapi.ListTagsOfResourceWithContext(
309+
ctx,
310+
&svcsdk.ListTagsOfResourceInput{
311+
NextToken: token,
312+
ResourceArn: &resourceARN,
313+
},
314+
)
315+
rm.metrics.RecordAPICall("GET", "ListTagsOfResource", err)
316+
if err != nil {
317+
return nil, err
318+
}
319+
tags = append(tags, resourceTagsFromSDKTags(listTagsOfResourceOutput.Tags)...)
320+
if listTagsOfResourceOutput.NextToken == nil {
321+
break
322+
}
323+
token = listTagsOfResourceOutput.NextToken
324+
}
325+
return tags, nil
326+
}
327+
328+
func customPreCompare(
329+
delta *ackcompare.Delta,
330+
a *resource,
331+
b *resource,
332+
) {
333+
// TODO(hilalymh): customDeltaFunctions for AttributeDefintions
334+
// TODO(hilalymh): customDeltaFunctions for GlobalSecondaryIndexes
335+
336+
if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) {
337+
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
338+
} else if a.ko.Spec.Tags != nil && b.ko.Spec.Tags != nil {
339+
if !equalTags(a.ko.Spec.Tags, b.ko.Spec.Tags) {
340+
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
341+
}
342+
}
343+
}

Diff for: ‎pkg/resource/table/hooks_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 table
15+
16+
import (
17+
"reflect"
18+
"testing"
19+
20+
"github.com/aws/aws-sdk-go/aws"
21+
22+
"github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1"
23+
)
24+
25+
var (
26+
Tag1 = &v1alpha1.Tag{
27+
Key: aws.String("k1"),
28+
Value: aws.String("v1"),
29+
}
30+
Tag2 = &v1alpha1.Tag{
31+
Key: aws.String("k2"),
32+
Value: aws.String("v2"),
33+
}
34+
Tag2Updated = &v1alpha1.Tag{
35+
Key: aws.String("k2"),
36+
Value: aws.String("v2-updated"),
37+
}
38+
Tag3 = &v1alpha1.Tag{
39+
Key: aws.String("k3"),
40+
Value: aws.String("v3"),
41+
}
42+
)
43+
44+
func Test_computeTagsDelta(t *testing.T) {
45+
type args struct {
46+
a []*v1alpha1.Tag
47+
b []*v1alpha1.Tag
48+
}
49+
tests := []struct {
50+
name string
51+
args args
52+
wantAdded []*v1alpha1.Tag
53+
wantUpdated []*v1alpha1.Tag
54+
wantRemoved []*string
55+
}{
56+
{
57+
name: "nil arrays",
58+
args: args{
59+
a: nil,
60+
b: nil,
61+
},
62+
wantAdded: nil,
63+
wantRemoved: nil,
64+
wantUpdated: nil,
65+
},
66+
{
67+
name: "empty arrays",
68+
args: args{
69+
a: []*v1alpha1.Tag{},
70+
b: []*v1alpha1.Tag{},
71+
},
72+
wantAdded: nil,
73+
wantRemoved: nil,
74+
wantUpdated: nil,
75+
},
76+
{
77+
name: "added tags",
78+
args: args{
79+
a: []*v1alpha1.Tag{},
80+
b: []*v1alpha1.Tag{Tag1, Tag2},
81+
},
82+
wantAdded: []*v1alpha1.Tag{Tag1, Tag2},
83+
wantRemoved: nil,
84+
wantUpdated: nil,
85+
},
86+
{
87+
name: "removed tags",
88+
args: args{
89+
a: []*v1alpha1.Tag{Tag1, Tag2},
90+
b: nil,
91+
},
92+
wantAdded: nil,
93+
wantRemoved: []*string{aws.String("k1"), aws.String("k2")},
94+
wantUpdated: nil,
95+
},
96+
{
97+
name: "updated tags",
98+
args: args{
99+
a: []*v1alpha1.Tag{Tag1, Tag2},
100+
b: []*v1alpha1.Tag{Tag1, Tag2Updated},
101+
},
102+
wantAdded: nil,
103+
wantRemoved: nil,
104+
wantUpdated: []*v1alpha1.Tag{Tag2Updated},
105+
},
106+
{
107+
name: "added, updated and removed tags",
108+
args: args{
109+
a: []*v1alpha1.Tag{Tag1, Tag2},
110+
// remove Tag1, update Tag2 and add Tag3
111+
b: []*v1alpha1.Tag{Tag2Updated, Tag3},
112+
},
113+
wantAdded: []*v1alpha1.Tag{Tag3},
114+
wantRemoved: []*string{aws.String("k1")},
115+
wantUpdated: []*v1alpha1.Tag{Tag2Updated},
116+
},
117+
}
118+
for _, tt := range tests {
119+
t.Run(tt.name, func(t *testing.T) {
120+
gotAdded, gotUpdated, gotRemoved := computeTagsDelta(tt.args.a, tt.args.b)
121+
if !reflect.DeepEqual(gotAdded, tt.wantAdded) {
122+
t.Errorf("computeTagsDelta() gotAdded = %v, want %v", gotAdded, tt.wantAdded)
123+
}
124+
if !reflect.DeepEqual(gotUpdated, tt.wantUpdated) {
125+
t.Errorf("computeTagsDelta() gotUpdated = %v, want %v", gotUpdated, tt.wantUpdated)
126+
}
127+
if !reflect.DeepEqual(gotRemoved, tt.wantRemoved) {
128+
t.Errorf("computeTagsDelta() gotRemoved = %v, want %v", gotRemoved, tt.wantRemoved)
129+
}
130+
})
131+
}
132+
}

Diff for: ‎pkg/resource/table/sdk.go

+5-429
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎templates/hooks/table/sdk_read_one_post_set_output.go.tpl

+3
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
}
44
if isTableUpdating(&resource{ko}) {
55
return &resource{ko}, requeueWaitWhileUpdating
6+
}
7+
if err := rm.setResourceAdditionalFields(ctx, ko); err != nil {
8+
return nil, err
69
}

Diff for: ‎templates/hooks/table/sdk_update_pre_build_request.go.tpl

-21
This file was deleted.

Diff for: ‎test/e2e/__init__.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from acktest.k8s import resource as k8s
2222
from acktest.resources import load_resource_file
23+
from acktest.aws.identity import get_region
2324

2425
SERVICE_NAME = "dynamodb"
2526
CRD_GROUP = "dynamodb.services.k8s.aws"
@@ -60,4 +61,21 @@ def wait_for_cr_status(
6061
f"Wait for status: {desired_status} timed out. Actual status: {actual_status}"
6162
)
6263

63-
assert actual_status == desired_status
64+
assert actual_status == desired_status
65+
66+
def get_resource_tags(resource_arn: str):
67+
region = get_region()
68+
ddb_client = boto3.client('dynamodb', region_name=region)
69+
tags = []
70+
next_token = ""
71+
while True:
72+
resp = ddb_client.list_tags_of_resource(
73+
ResourceArn=resource_arn,
74+
NextToken=next_token,
75+
)
76+
tags += resp['Tags']
77+
if not 'NextToken' in resp.keys():
78+
break
79+
next_token = resp['NextToken']
80+
81+
return tags

Diff for: ‎test/e2e/resources/table_forums.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ spec:
77
attributeDefinitions:
88
- attributeName: ForumName
99
attributeType: S
10-
- attributeName: Subject
11-
attributeType: S
1210
- attributeName: LastPostDateTime
1311
attributeType: S
12+
- attributeName: Subject
13+
attributeType: S
1414
keySchema:
1515
- attributeName: ForumName
1616
keyType: HASH

Diff for: ‎test/e2e/tests/test_table.py

+74-2
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@
2222

2323
from acktest.resources import random_suffix_name
2424
from acktest.k8s import resource as k8s
25-
from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_dynamodb_resource, wait_for_cr_status
25+
from e2e import (
26+
service_marker, CRD_GROUP, CRD_VERSION,
27+
load_dynamodb_resource, wait_for_cr_status,
28+
get_resource_tags,
29+
)
2630
from e2e.replacement_values import REPLACEMENT_VALUES
2731

2832
RESOURCE_PLURAL = "tables"
2933

3034
DELETE_WAIT_AFTER_SECONDS = 15
35+
UPDATE_TAGS_WAIT_AFTER_SECONDS = 5
3136

3237
@pytest.fixture(scope="module")
3338
def dynamodb_client():
@@ -50,7 +55,7 @@ def get_table(self, dynamodb_client, table_name: str) -> dict:
5055
def table_exists(self, dynamodb_client, table_name: str) -> bool:
5156
return self.get_table(dynamodb_client, table_name) is not None
5257

53-
def test_smoke(self, dynamodb_client):
58+
def test_create_delete(self, dynamodb_client):
5459
resource_name = random_suffix_name("table", 32)
5560

5661
replacements = REPLACEMENT_VALUES.copy()
@@ -92,6 +97,73 @@ def test_smoke(self, dynamodb_client):
9297

9398
time.sleep(DELETE_WAIT_AFTER_SECONDS)
9499

100+
# Check DynamoDB Table doesn't exists
101+
exists = self.table_exists(dynamodb_client, resource_name)
102+
assert not exists
103+
104+
def test_table_update_tags(self, dynamodb_client):
105+
resource_name = random_suffix_name("table", 32)
106+
107+
replacements = REPLACEMENT_VALUES.copy()
108+
replacements["TABLE_NAME"] = resource_name
109+
110+
# Load Table CR
111+
resource_data = load_dynamodb_resource(
112+
"table_forums",
113+
additional_replacements=replacements,
114+
)
115+
logging.debug(resource_data)
116+
117+
# Create k8s resource
118+
ref = k8s.CustomResourceReference(
119+
CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL,
120+
resource_name, namespace="default",
121+
)
122+
k8s.create_custom_resource(ref, resource_data)
123+
cr = k8s.wait_resource_consumed_by_controller(ref)
124+
125+
assert cr is not None
126+
assert k8s.get_resource_exists(ref)
127+
128+
wait_for_cr_status(
129+
ref,
130+
"tableStatus",
131+
"ACTIVE",
132+
10,
133+
5,
134+
)
135+
136+
# Check DynamoDB Table exists
137+
exists = self.table_exists(dynamodb_client, resource_name)
138+
assert exists
139+
140+
# Get CR latest revision
141+
cr = k8s.wait_resource_consumed_by_controller(ref)
142+
143+
# Update table list of tags
144+
tags = [
145+
{
146+
"key": "key1",
147+
"value": "value1",
148+
},
149+
]
150+
cr["spec"]["tags"] = tags
151+
152+
# Patch k8s resource
153+
k8s.patch_custom_resource(ref, cr)
154+
time.sleep(UPDATE_TAGS_WAIT_AFTER_SECONDS)
155+
156+
table_tags = get_resource_tags(cr["status"]["ackResourceMetadata"]["arn"])
157+
assert len(table_tags) == len(tags)
158+
assert table_tags[0]['Key'] == tags[0]['key']
159+
assert table_tags[0]['Value'] == tags[0]['value']
160+
161+
# Delete k8s resource
162+
_, deleted = k8s.delete_custom_resource(ref)
163+
assert deleted is True
164+
165+
time.sleep(DELETE_WAIT_AFTER_SECONDS)
166+
95167
# Check DynamoDB Table doesn't exists
96168
exists = self.table_exists(dynamodb_client, resource_name)
97169
assert not exists

0 commit comments

Comments
 (0)
Please sign in to comment.