Skip to content

Commit fb55374

Browse files
authored
add new AWSResourceManager.EnsureControllerTags() method (#90)
Issue #, if available: aws-controllers-k8s/community#1261 Description of changes: * Adds new `AWSResourceManager.EnsureTags()` method * `EnsureTags` method will be called before invoking `reconcile` method to update the desired resource with the ACK controller tags * This change also introduces a new `Tags` shape, which will be used as mediator/hub to merge AWS tags represented using different go types. * code-generator will generate the logic of conversion to/from `Tags` By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent ae6e790 commit fb55374

File tree

6 files changed

+256
-0
lines changed

6 files changed

+256
-0
lines changed

mocks/pkg/types/aws_resource_manager.go

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

pkg/runtime/reconciler.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ func (r *resourceReconciler) Sync(
233233
}
234234
desired = resolvedRefDesired
235235

236+
rlog.Enter("rm.EnsureTags")
237+
err = rm.EnsureTags(ctx, desired)
238+
rlog.Exit("rm.EnsureTags", err)
239+
if err != nil {
240+
return desired, err
241+
}
242+
236243
rlog.Enter("rm.ReadOne")
237244
latest, err = rm.ReadOne(ctx, desired)
238245
rlog.Exit("rm.ReadOne", err)
@@ -373,6 +380,17 @@ func (r *resourceReconciler) createResource(
373380
return resolvedRefDesired, err
374381
}
375382
desired = resolvedRefDesired
383+
384+
// Ensure tags again after adding the finalizer and patching the
385+
// resource. Patching desired resource omits the controller tags
386+
// because they are not persisted in etcd. So we again ensure
387+
// that tags are present before performing the create operation.
388+
rlog.Enter("rm.EnsureTags")
389+
err = rm.EnsureTags(ctx, desired)
390+
rlog.Exit("rm.EnsureTags", err)
391+
if err != nil {
392+
return desired, err
393+
}
376394
}
377395

378396
rlog.Enter("rm.Create")

pkg/runtime/reconciler_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ func TestReconcilerCreate_BackoffRetries(t *testing.T) {
183183
rd.On("IsManaged", desired).Return(true)
184184
rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
185185
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
186+
rm.On("EnsureTags", ctx, desired).Return(nil)
186187

187188
r, kc := reconcilerMocks(rmf)
188189
kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
@@ -240,6 +241,7 @@ func TestReconcilerCreate_UnManagedResource_CheckReferencesResolveTwice(t *testi
240241
rd.On("IsManaged", desired).Return(true)
241242
rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
242243
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
244+
rm.On("EnsureTags", ctx, desired).Return(nil)
243245

244246
r, kc := reconcilerMocks(rmf)
245247

@@ -266,6 +268,8 @@ func TestReconcilerCreate_UnManagedResource_CheckReferencesResolveTwice(t *testi
266268
kc.AssertNotCalled(t, "Status")
267269
rm.AssertCalled(t, "LateInitialize", ctx, latest)
268270
rm.AssertCalled(t, "IsSynced", ctx, latest)
271+
rm.AssertNumberOfCalls(t, "EnsureTags", 2)
272+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
269273
}
270274

271275
func TestReconcilerCreate_ManagedResource_CheckReferencesResolveOnce(t *testing.T) {
@@ -315,6 +319,7 @@ func TestReconcilerCreate_ManagedResource_CheckReferencesResolveOnce(t *testing.
315319
rd.On("IsManaged", desired).Return(true)
316320
rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
317321
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
322+
rm.On("EnsureTags", ctx, desired).Return(nil)
318323

319324
r, kc := reconcilerMocks(rmf)
320325

@@ -341,6 +346,8 @@ func TestReconcilerCreate_ManagedResource_CheckReferencesResolveOnce(t *testing.
341346
kc.AssertNotCalled(t, "Status")
342347
rm.AssertCalled(t, "LateInitialize", ctx, latest)
343348
rm.AssertCalled(t, "IsSynced", ctx, latest)
349+
rm.AssertNumberOfCalls(t, "EnsureTags", 1)
350+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
344351
}
345352

346353
func TestReconcilerUpdate(t *testing.T) {
@@ -392,6 +399,7 @@ func TestReconcilerUpdate(t *testing.T) {
392399

393400
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
394401
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
402+
rm.On("EnsureTags", ctx, desired).Return(nil)
395403

396404
r, kc := reconcilerMocks(rmf)
397405

@@ -419,6 +427,7 @@ func TestReconcilerUpdate(t *testing.T) {
419427
kc.AssertNotCalled(t, "Status")
420428
rm.AssertCalled(t, "LateInitialize", ctx, latest)
421429
rm.AssertCalled(t, "IsSynced", ctx, latest)
430+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
422431
}
423432

424433
func TestReconcilerUpdate_ResourceNotSynced(t *testing.T) {
@@ -474,6 +483,7 @@ func TestReconcilerUpdate_ResourceNotSynced(t *testing.T) {
474483

475484
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
476485
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
486+
rm.On("EnsureTags", ctx, desired).Return(nil)
477487

478488
r, kc := reconcilerMocks(rmf)
479489

@@ -499,6 +509,7 @@ func TestReconcilerUpdate_ResourceNotSynced(t *testing.T) {
499509
kc.AssertNotCalled(t, "Status")
500510
rm.AssertCalled(t, "LateInitialize", ctx, latest)
501511
rm.AssertCalled(t, "IsSynced", ctx, latest)
512+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
502513
}
503514

504515
func TestReconcilerUpdate_NoDelta_ResourceNotSynced(t *testing.T) {
@@ -547,6 +558,7 @@ func TestReconcilerUpdate_NoDelta_ResourceNotSynced(t *testing.T) {
547558

548559
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
549560
rd.On("Delta", latest, latest).Return(delta)
561+
rm.On("EnsureTags", ctx, desired).Return(nil)
550562

551563
r, kc := reconcilerMocks(rmf)
552564

@@ -573,6 +585,7 @@ func TestReconcilerUpdate_NoDelta_ResourceNotSynced(t *testing.T) {
573585
kc.AssertNotCalled(t, "Status")
574586
rm.AssertCalled(t, "LateInitialize", ctx, latest)
575587
rm.AssertCalled(t, "IsSynced", ctx, latest)
588+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
576589
}
577590

578591
func TestReconcilerUpdate_NoDelta_ResourceSynced(t *testing.T) {
@@ -621,6 +634,7 @@ func TestReconcilerUpdate_NoDelta_ResourceSynced(t *testing.T) {
621634

622635
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
623636
rd.On("Delta", latest, latest).Return(delta)
637+
rm.On("EnsureTags", ctx, desired).Return(nil)
624638

625639
r, kc := reconcilerMocks(rmf)
626640

@@ -647,6 +661,7 @@ func TestReconcilerUpdate_NoDelta_ResourceSynced(t *testing.T) {
647661
kc.AssertNotCalled(t, "Status")
648662
rm.AssertCalled(t, "LateInitialize", ctx, latest)
649663
rm.AssertCalled(t, "IsSynced", ctx, latest)
664+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
650665
}
651666

652667
func TestReconcilerUpdate_IsSyncedError(t *testing.T) {
@@ -706,6 +721,7 @@ func TestReconcilerUpdate_IsSyncedError(t *testing.T) {
706721

707722
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
708723
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
724+
rm.On("EnsureTags", ctx, desired).Return(nil)
709725

710726
r, kc := reconcilerMocks(rmf)
711727

@@ -731,6 +747,7 @@ func TestReconcilerUpdate_IsSyncedError(t *testing.T) {
731747
kc.AssertNotCalled(t, "Status")
732748
rm.AssertCalled(t, "LateInitialize", ctx, latest)
733749
rm.AssertCalled(t, "IsSynced", ctx, latest)
750+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
734751
}
735752

736753
func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInMetadata(t *testing.T) {
@@ -778,6 +795,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInMetadata(t *testing.T) {
778795
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
779796
rm.On("IsSynced", ctx, latest).Return(true, nil)
780797
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
798+
rm.On("EnsureTags", ctx, desired).Return(nil)
781799

782800
r, kc := reconcilerMocks(rmf)
783801

@@ -795,6 +813,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInMetadata(t *testing.T) {
795813
rm.AssertCalled(t, "LateInitialize", ctx, latest)
796814
latest.AssertCalled(t, "DeepCopy")
797815
latest.AssertCalled(t, "SetStatus", latest)
816+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
798817
}
799818

800819
func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInSpec(t *testing.T) {
@@ -853,6 +872,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInSpec(t *testing.T) {
853872
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
854873
rm.On("IsSynced", ctx, latest).Return(true, nil)
855874
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
875+
rm.On("EnsureTags", ctx, desired).Return(nil)
856876

857877
r, kc := reconcilerMocks(rmf)
858878

@@ -868,6 +888,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInSpec(t *testing.T) {
868888
// Only the HandleReconcilerError wrapper function ever calls patchResourceStatus
869889
kc.AssertNotCalled(t, "Status")
870890
rm.AssertCalled(t, "LateInitialize", ctx, latest)
891+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
871892
}
872893

873894
func TestReconcilerHandleReconcilerError_PatchStatus_Latest(t *testing.T) {
@@ -998,6 +1019,7 @@ func TestReconcilerUpdate_ErrorInLateInitialization(t *testing.T) {
9981019
rm.On("LateInitialize", ctx, latest).Return(latest, requeueError)
9991020
rm.On("IsSynced", ctx, latest).Return(true, nil)
10001021
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
1022+
rm.On("EnsureTags", ctx, desired).Return(nil)
10011023

10021024
r, kc := reconcilerMocks(rmf)
10031025

@@ -1014,6 +1036,7 @@ func TestReconcilerUpdate_ErrorInLateInitialization(t *testing.T) {
10141036
// No difference in desired, latest metadata and spec
10151037
kc.AssertNotCalled(t, "Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch"))
10161038
rm.AssertCalled(t, "LateInitialize", ctx, latest)
1039+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
10171040
}
10181041

10191042
func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
@@ -1101,6 +1124,7 @@ func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
11011124
latest, nil,
11021125
)
11031126
rm.On("IsSynced", ctx, latest).Return(true, nil)
1127+
rm.On("EnsureTags", ctx, desired).Return(nil)
11041128

11051129
rmf, rd := managerFactoryMocks(desired, latest, false)
11061130

@@ -1114,6 +1138,7 @@ func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
11141138
rd.AssertNotCalled(t, "Delta", desired, latest)
11151139
rm.AssertNotCalled(t, "Update", ctx, desired, latest, delta)
11161140
rm.AssertNotCalled(t, "LateInitialize", ctx, latest)
1141+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
11171142
}
11181143

11191144
func TestReconcilerUpdate_ResolveReferencesError(t *testing.T) {
@@ -1176,6 +1201,7 @@ func TestReconcilerUpdate_ResolveReferencesError(t *testing.T) {
11761201
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
11771202
rm.On("IsSynced", ctx, latest).Return(true, nil)
11781203
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
1204+
rm.On("EnsureTags", ctx, desired).Return(nil)
11791205

11801206
r, kc := reconcilerMocks(rmf)
11811207

@@ -1197,4 +1223,90 @@ func TestReconcilerUpdate_ResolveReferencesError(t *testing.T) {
11971223
// Only the HandleReconcilerError wrapper function ever calls patchResourceStatus
11981224
kc.AssertNotCalled(t, "Status")
11991225
rm.AssertNotCalled(t, "LateInitialize", ctx, latest)
1226+
rm.AssertNotCalled(t, "EnsureTags", ctx, desired)
1227+
}
1228+
1229+
func TestReconcilerUpdate_EnsureControllerTagsError(t *testing.T) {
1230+
require := require.New(t)
1231+
1232+
ctx := context.TODO()
1233+
arn := ackv1alpha1.AWSResourceName("mybook-arn")
1234+
1235+
delta := ackcompare.NewDelta()
1236+
delta.Add("Spec.A", "val1", "val2")
1237+
1238+
desired, _, _ := resourceMocks()
1239+
desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
1240+
1241+
ids := &ackmocks.AWSResourceIdentifiers{}
1242+
ids.On("ARN").Return(&arn)
1243+
1244+
latest, latestRTObj, _ := resourceMocks()
1245+
latest.On("Identifiers").Return(ids)
1246+
1247+
ensureControllerTagsError := errors.New("failed to ensure controller tags")
1248+
1249+
// resourceReconciler.ensureConditions will ensure that if the resource
1250+
// manager has not set any Conditions on the resource, that at least an
1251+
// ACK.ResourceSynced condition with status Unknown will be set on the
1252+
// resource.
1253+
latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
1254+
latest.On(
1255+
"ReplaceConditions",
1256+
mock.AnythingOfType("[]*v1alpha1.Condition"),
1257+
).Return().Run(func(args mock.Arguments) {
1258+
conditions := args.Get(0).([]*ackv1alpha1.Condition)
1259+
assert.Equal(t, 1, len(conditions))
1260+
cond := conditions[0]
1261+
assert.Equal(t, ackv1alpha1.ConditionTypeResourceSynced, cond.Type)
1262+
// The non-terminal reconciler error causes the ResourceSynced
1263+
// condition to be False
1264+
assert.Equal(t, corev1.ConditionFalse, cond.Status)
1265+
assert.Equal(t, ackcondition.NotSyncedMessage, *cond.Message)
1266+
assert.Equal(t, ensureControllerTagsError.Error(), *cond.Reason)
1267+
})
1268+
1269+
rm := &ackmocks.AWSResourceManager{}
1270+
rm.On("ResolveReferences", ctx, nil, desired).Return(desired, nil)
1271+
rm.On("ReadOne", ctx, desired).Return(
1272+
latest, nil,
1273+
)
1274+
rm.On("Update", ctx, desired, latest, delta).Return(
1275+
latest, nil,
1276+
)
1277+
1278+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
1279+
rd.On("Delta", desired, latest).Return(
1280+
delta,
1281+
).Once()
1282+
rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
1283+
1284+
rm.On("LateInitialize", ctx, latest).Return(latest, nil)
1285+
rm.On("IsSynced", ctx, latest).Return(true, nil)
1286+
rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
1287+
rm.On("EnsureTags", ctx, desired).Return(
1288+
ensureControllerTagsError,
1289+
)
1290+
1291+
r, kc := reconcilerMocks(rmf)
1292+
1293+
kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
1294+
1295+
// With the above mocks and below assertions, we check that if we got a
1296+
// non-error return from `AWSResourceManager.ReadOne()` and the
1297+
// `AWSResourceDescriptor.Delta()` returned a non-empty Delta, that we end
1298+
// up calling the AWSResourceManager.Update() call in the Reconciler.Sync()
1299+
// method,
1300+
_, err := r.Sync(ctx, rm, desired)
1301+
require.NotNil(err)
1302+
rm.AssertCalled(t, "ResolveReferences", ctx, nil, desired)
1303+
rm.AssertNotCalled(t, "ReadOne", ctx, desired)
1304+
rd.AssertNotCalled(t, "Delta", desired, latest)
1305+
rm.AssertNotCalled(t, "Update", ctx, desired, latest, delta)
1306+
// No changes to metadata or spec so Patch on the object shouldn't be done
1307+
kc.AssertNotCalled(t, "Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch"))
1308+
// Only the HandleReconcilerError wrapper function ever calls patchResourceStatus
1309+
kc.AssertNotCalled(t, "Status")
1310+
rm.AssertNotCalled(t, "LateInitialize", ctx, latest)
1311+
rm.AssertCalled(t, "EnsureTags", ctx, desired)
12001312
}

pkg/tags/tags.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 tags
15+
16+
// Tags represents the AWS tags which will be added to the AWS resource.
17+
// Inside aws-sdk-go, Tags are represented using multiple types, Ex: map of
18+
// string, list of structs etc...
19+
// Tags type will be used as a hub/mediator to merge tags represented
20+
// using different types.
21+
type Tags map[string]string
22+
23+
// NewTags returns Tags with empty tags
24+
func NewTags() Tags {
25+
return map[string]string{}
26+
}
27+
28+
// Merge merges two set of Tags and returns the merge result.
29+
// In case of collision precedence is given to tags present in the first
30+
// parameter 'a'.
31+
func Merge(a Tags, b Tags) Tags {
32+
var result Tags
33+
// Initialize result with the first set of tags 'a'.
34+
// If first set is nil, initialize result with empty set of tags.
35+
if a == nil {
36+
result = NewTags()
37+
} else {
38+
result = a
39+
}
40+
if b != nil && len(b) > 0 {
41+
// Add all the tags which are not already present in result
42+
for tk, tv := range b {
43+
if _, found := result[tk]; !found {
44+
result[tk] = tv
45+
}
46+
}
47+
}
48+
return result
49+
}

0 commit comments

Comments
 (0)