Skip to content

Commit 3f86cd2

Browse files
authored
support tags in DBCluster resources (#74)
Adds support for reading and updating tags for DBCluster resources. 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 a756781 commit 3f86cd2

File tree

12 files changed

+250
-2
lines changed

12 files changed

+250
-2
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2022-05-09T20:24:56Z"
2+
build_date: "2022-05-10T13:35:23Z"
33
build_hash: c6efa6ac643edb21219e0763541b2558718b5fe6
44
go_version: go1.18.1
55
version: v0.18.4-10-gc6efa6a
66
api_directory_checksum: e7bbd21f4f975f9cf1e1e804ebd450e8e310023d
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.42.0
99
generator_config_info:
10-
file_checksum: aca88fe4f1fa7b88393cb547b5352b26455f5d20
10+
file_checksum: abea9fcd7e75ec05fc8004dc944a49528ff3652f
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
@@ -67,6 +67,8 @@ resources:
6767
# resolved.
6868
custom_method_name: customUpdate
6969
hooks:
70+
delta_pre_compare:
71+
template_path: hooks/db_cluster/delta_pre_compare.go.tpl
7072
sdk_create_post_set_output:
7173
template_path: hooks/db_cluster/sdk_create_post_set_output.go.tpl
7274
sdk_read_many_post_set_output:

generator.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ resources:
6767
# resolved.
6868
custom_method_name: customUpdate
6969
hooks:
70+
delta_pre_compare:
71+
template_path: hooks/db_cluster/delta_pre_compare.go.tpl
7072
sdk_create_post_set_output:
7173
template_path: hooks/db_cluster/sdk_create_post_set_output.go.tpl
7274
sdk_read_many_post_set_output:

pkg/resource/db_cluster/custom_update.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ func (rm *resourceManager) customUpdate(
7575
if err != nil {
7676
return nil, err
7777
}
78+
if delta.DifferentAt("Spec.Tags") {
79+
if err = rm.syncTags(ctx, desired, latest); err != nil {
80+
return nil, err
81+
}
82+
}
7883
// Merge in the information we read from the API call above to the copy of
7984
// the original Kubernetes object we passed to the function
8085
ko := desired.ko.DeepCopy()

pkg/resource/db_cluster/delta.go

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

pkg/resource/db_cluster/hooks.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
package db_cluster
1515

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

21+
svcapitypes "github.com/aws-controllers-k8s/rds-controller/apis/v1alpha1"
22+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
2023
ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue"
24+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
25+
svcsdk "github.com/aws/aws-sdk-go/service/rds"
2126
)
2227

2328
// NOTE(jaypipes): The below list is derived from looking at the RDS control
@@ -145,3 +150,171 @@ func clusterDeleting(r *resource) bool {
145150
dbcs := *r.ko.Status.Status
146151
return dbcs == StatusDeleting
147152
}
153+
154+
// syncTags keeps the resource's tags in sync
155+
//
156+
// NOTE(jaypipes): RDS' Tagging APIs differ from other AWS APIs in the
157+
// following ways:
158+
//
159+
// 1. The names of the tagging API operations are different. Other APIs use the
160+
// Tagris `ListTagsForResource`, `TagResource` and `UntagResource` API
161+
// calls. RDS uses `ListTagsForResource`, `AddTagsToResource` and
162+
// `RemoveTagsFromResource`.
163+
//
164+
// 2. Even though the name of the `ListTagsForResource` API call is the same,
165+
// the structure of the input and the output are different from other APIs.
166+
// For the input, instead of a `ResourceArn` field, RDS names the field
167+
// `ResourceName`, but actually expects an ARN, not the cluster
168+
// name. This is the same for the `AddTagsToResource` and
169+
// `RemoveTagsFromResource` input shapes. For the output shape, the field is
170+
// called `TagList` instead of `Tags` but is otherwise the same struct with
171+
// a `Key` and `Value` member field.
172+
func (rm *resourceManager) syncTags(
173+
ctx context.Context,
174+
desired *resource,
175+
latest *resource,
176+
) (err error) {
177+
rlog := ackrtlog.FromContext(ctx)
178+
exit := rlog.Trace("rm.syncTags")
179+
defer func() { exit(err) }()
180+
181+
arn := (*string)(latest.ko.Status.ACKResourceMetadata.ARN)
182+
183+
toAdd, toDelete := computeTagsDelta(
184+
desired.ko.Spec.Tags, latest.ko.Spec.Tags,
185+
)
186+
187+
if len(toDelete) > 0 {
188+
rlog.Debug("removing tags from cluster", "tags", toDelete)
189+
_, err = rm.sdkapi.RemoveTagsFromResourceWithContext(
190+
ctx,
191+
&svcsdk.RemoveTagsFromResourceInput{
192+
ResourceName: arn,
193+
TagKeys: toDelete,
194+
},
195+
)
196+
rm.metrics.RecordAPICall("UPDATE", "RemoveTagsFromResource", err)
197+
if err != nil {
198+
return err
199+
}
200+
}
201+
202+
// NOTE(jaypipes): According to the RDS API documentation, adding a tag
203+
// with a new value overwrites any existing tag with the same key. So, we
204+
// don't need to do anything to "update" a Tag. Simply including it in the
205+
// AddTagsToResource call is enough.
206+
if len(toAdd) > 0 {
207+
rlog.Debug("adding tags to cluster", "tags", toAdd)
208+
_, err = rm.sdkapi.AddTagsToResourceWithContext(
209+
ctx,
210+
&svcsdk.AddTagsToResourceInput{
211+
ResourceName: arn,
212+
Tags: sdkTagsFromResourceTags(toAdd),
213+
},
214+
)
215+
rm.metrics.RecordAPICall("UPDATE", "AddTagsToResource", err)
216+
if err != nil {
217+
return err
218+
}
219+
}
220+
return nil
221+
}
222+
223+
// getTags retrieves the resource's associated tags
224+
func (rm *resourceManager) getTags(
225+
ctx context.Context,
226+
resourceARN string,
227+
) ([]*svcapitypes.Tag, error) {
228+
resp, err := rm.sdkapi.ListTagsForResourceWithContext(
229+
ctx,
230+
&svcsdk.ListTagsForResourceInput{
231+
ResourceName: &resourceARN,
232+
},
233+
)
234+
rm.metrics.RecordAPICall("GET", "ListTagsForResource", err)
235+
if err != nil {
236+
return nil, err
237+
}
238+
tags := make([]*svcapitypes.Tag, 0, len(resp.TagList))
239+
for _, tag := range resp.TagList {
240+
tags = append(tags, &svcapitypes.Tag{
241+
Key: tag.Key,
242+
Value: tag.Value,
243+
})
244+
}
245+
return tags, nil
246+
}
247+
248+
// compareTags adds a difference to the delta if the supplied resources have
249+
// different tag collections
250+
func compareTags(
251+
delta *ackcompare.Delta,
252+
a *resource,
253+
b *resource,
254+
) {
255+
if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) {
256+
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
257+
} else if len(a.ko.Spec.Tags) > 0 {
258+
if !equalTags(a.ko.Spec.Tags, b.ko.Spec.Tags) {
259+
delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
260+
}
261+
}
262+
}
263+
264+
// equalTags returns true if two Tag arrays are equal regardless of the order
265+
// of their elements.
266+
func equalTags(
267+
a []*svcapitypes.Tag,
268+
b []*svcapitypes.Tag,
269+
) bool {
270+
added, removed := computeTagsDelta(a, b)
271+
return len(added) == 0 && len(removed) == 0
272+
}
273+
274+
// computeTagsDelta compares two Tag arrays and returns the tags to add and the
275+
// tag keys to delete
276+
func computeTagsDelta(
277+
desired []*svcapitypes.Tag,
278+
latest []*svcapitypes.Tag,
279+
) (added []*svcapitypes.Tag, removed []*string) {
280+
toDelete := []*string{}
281+
toAdd := []*svcapitypes.Tag{}
282+
283+
desiredTags := map[string]string{}
284+
for _, tag := range desired {
285+
desiredTags[*tag.Key] = *tag.Value
286+
}
287+
288+
for _, tag := range desired {
289+
toAdd = append(toAdd, tag)
290+
}
291+
for _, tag := range latest {
292+
_, ok := desiredTags[*tag.Key]
293+
if !ok {
294+
toDelete = append(toDelete, tag.Key)
295+
}
296+
}
297+
return toAdd, toDelete
298+
}
299+
300+
// sdkTagsFromResourceTags transforms a *svcapitypes.Tag array to a *svcsdk.Tag
301+
// array.
302+
func sdkTagsFromResourceTags(
303+
rTags []*svcapitypes.Tag,
304+
) []*svcsdk.Tag {
305+
tags := make([]*svcsdk.Tag, len(rTags))
306+
for i := range rTags {
307+
tags[i] = &svcsdk.Tag{
308+
Key: rTags[i].Key,
309+
Value: rTags[i].Value,
310+
}
311+
}
312+
return tags
313+
}
314+
315+
func equalStrings(a, b *string) bool {
316+
if a == nil {
317+
return b == nil || *b == ""
318+
}
319+
return (*a == "" && b == nil) || *a == *b
320+
}

pkg/resource/db_cluster/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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
compareTags(delta, a, b)

templates/hooks/db_cluster/sdk_read_many_post_set_output.go.tpl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil {
2+
resourceARN := (*string)(ko.Status.ACKResourceMetadata.ARN)
3+
tags, err := rm.getTags(ctx, *resourceARN)
4+
if err != nil {
5+
return nil, err
6+
}
7+
ko.Spec.Tags = tags
8+
}
19
if !clusterAvailable(&resource{ko}) {
210
// Setting resource synced condition to false will trigger a requeue of
311
// the resource. No need to return a requeue error here.

test/e2e/db_cluster.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,18 @@ def get(db_cluster_id):
122122
return resp['DBClusters'][0]
123123
except c.exceptions.DBClusterNotFoundFault:
124124
return None
125+
126+
127+
def get_tags(db_cluster_arn):
128+
"""Returns a dict containing the DB cluster's tag records from the RDS API.
129+
130+
If no such DB cluster exists, returns None.
131+
"""
132+
c = boto3.client('rds')
133+
try:
134+
resp = c.list_tags_for_resource(
135+
ResourceName=db_cluster_arn,
136+
)
137+
return resp['TagList']
138+
except c.exceptions.DBClusterNotFoundFault:
139+
return None

0 commit comments

Comments
 (0)