Skip to content

Commit 7fbba5c

Browse files
Set Terminal condition on updating unmanaged (#45)
ACK uses finalizers to determine whether it is currently managing a k8s resource. We set the finalizer when creating the resource and delete it when we have deleted the underlying AWS resource. The current code path also sets the finalizer if one is not applied when attempting to update a resource. If a user were to create a resource that has the same name as an existing AWS resource, it will attempt to call `updateResource` which would set the finalizer and treat the resource as if ACK created it. This behaviour circumvents the safer adoption mechanisms put in place to prevent overriding existing resources. This pull request ensures that any resource which does not have a finalizer, when attempting to call `updateResource`, will be marked with a terminal condition. Adopted resources are now marked with the finalizer when they are created.
1 parent 5e11a07 commit 7fbba5c

File tree

4 files changed

+112
-36
lines changed

4 files changed

+112
-36
lines changed

pkg/condition/condition.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import (
2121
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
2222
)
2323

24+
var (
25+
NotManagedMessage = "Resource already exists"
26+
NotManagedReason = "This resource already exists but is not managed by ACK. " +
27+
"To bring the resource under ACK management, you should explicitly adopt " +
28+
"the resource by creating a services.k8s.aws/AdoptedResource"
29+
)
30+
2431
// Synced returns the Condition in the resource's Conditions collection that is
2532
// of type ConditionTypeResourceSynced. If no such condition is found, returns
2633
// nil.

pkg/runtime/adoption_reconciler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ func (r *adoptionReconciler) sync(
209209
}
210210

211211
described.SetObjectMeta(*targetMeta)
212+
targetDescriptor.MarkManaged(described)
212213
targetDescriptor.MarkAdopted(described)
213214

214215
if err := r.kc.Create(ctx, described.RuntimeObject()); err != nil {

pkg/runtime/reconciler.go

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
3030
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
31+
"github.com/aws-controllers-k8s/runtime/pkg/condition"
3132
ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config"
3233
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
3334
ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics"
@@ -312,8 +313,8 @@ func (r *resourceReconciler) updateResource(
312313
exit := rlog.Trace("r.updateResource")
313314
defer exit(err)
314315

315-
// Ensure the resource is always managed (adopted resources apply)
316-
if err = r.setResourceManaged(ctx, desired); err != nil {
316+
// Ensure the resource is managed
317+
if err = r.failOnResourceUnmanaged(ctx, latest); err != nil {
317318
return latest, err
318319
}
319320

@@ -333,7 +334,7 @@ func (r *resourceReconciler) updateResource(
333334
}
334335
// Ensure that we are patching any changes to the annotations/metadata and
335336
// the Spec that may have been set by the resource manager's successful
336-
// Create call above.
337+
// Update call above.
337338
err = r.patchResourceMetadataAndSpec(ctx, desired, latest)
338339
if err != nil {
339340
return latest, err
@@ -402,13 +403,9 @@ func (r *resourceReconciler) patchResourceMetadataAndSpec(
402403
}
403404

404405
rlog.Enter("kc.Patch (metadata + spec)")
405-
// It is necessary to use `DeepCopyObject` versions of `latest` when calling
406-
// `Patch` as this method overrides all values as merged from `desired`.
407-
// This may affect `latest` in later execution, as otherwise this would set
408-
//`desired` == `latest` after calling this method.
409406
err = r.kc.Patch(
410407
ctx,
411-
latest.RuntimeObject().DeepCopyObject(),
408+
latest.RuntimeObject(),
412409
client.MergeFrom(desired.RuntimeObject().DeepCopyObject()),
413410
)
414411
rlog.Exit("kc.Patch (metadata + spec)", err)
@@ -432,13 +429,9 @@ func (r *resourceReconciler) patchResourceStatus(
432429
defer exit(err)
433430

434431
rlog.Enter("kc.Patch (status)")
435-
// It is necessary to use `DeepCopyObject` versions of `latest` when calling
436-
// `Patch` as this method overrides all values as merged from `desired`.
437-
// This may affect `latest` in later execution, as otherwise this would set
438-
//`desired` == `latest` after calling this method.
439432
err = r.kc.Status().Patch(
440433
ctx,
441-
latest.RuntimeObject().DeepCopyObject(),
434+
latest.RuntimeObject(),
442435
client.MergeFrom(desired.RuntimeObject().DeepCopyObject()),
443436
)
444437
rlog.Exit("kc.Patch (status)", err)
@@ -517,21 +510,14 @@ func (r *resourceReconciler) setResourceManaged(
517510
if r.rd.IsManaged(res) {
518511
return nil
519512
}
520-
521513
var err error
522514
rlog := ackrtlog.FromContext(ctx)
523515
exit := rlog.Trace("r.setResourceManaged")
524516
defer exit(err)
525517

526518
orig := res.RuntimeObject().DeepCopyObject()
527519
r.rd.MarkManaged(res)
528-
rlog.Enter("kc.Patch (metadata + spec)")
529-
err = r.kc.Patch(
530-
ctx,
531-
res.RuntimeObject(),
532-
client.MergeFrom(orig),
533-
)
534-
rlog.Exit("kc.Patch (metadata + spec)", err)
520+
err = r.patchResourceMetadataAndSpec(ctx, r.rd.ResourceFromRuntimeObject(orig), res)
535521
if err != nil {
536522
return err
537523
}
@@ -557,20 +543,29 @@ func (r *resourceReconciler) setResourceUnmanaged(
557543

558544
orig := res.RuntimeObject().DeepCopyObject()
559545
r.rd.MarkUnmanaged(res)
560-
rlog.Enter("kc.Patch (metadata + spec)")
561-
err = r.kc.Patch(
562-
ctx,
563-
res.RuntimeObject(),
564-
client.MergeFrom(orig),
565-
)
566-
rlog.Exit("kc.Patch (metadata + spec)", err)
546+
err = r.patchResourceMetadataAndSpec(ctx, r.rd.ResourceFromRuntimeObject(orig), res)
567547
if err != nil {
568548
return err
569549
}
570550
rlog.Debug("removed resource from management")
571551
return nil
572552
}
573553

554+
// failOnResourceUnmanaged ensures that the underlying CR in the supplied
555+
// AWSResource has a finalizer. If it does not, it will set a Terminal condition
556+
// and return with an error
557+
func (r *resourceReconciler) failOnResourceUnmanaged(
558+
ctx context.Context,
559+
res acktypes.AWSResource,
560+
) error {
561+
if r.rd.IsManaged(res) {
562+
return nil
563+
}
564+
565+
condition.SetTerminal(res, corev1.ConditionTrue, &condition.NotManagedMessage, &condition.NotManagedReason)
566+
return ackerr.Terminal
567+
}
568+
574569
// getAWSResource returns an AWSResource representing the requested Kubernetes
575570
// namespaced object
576571
func (r *resourceReconciler) getAWSResource(

pkg/runtime/reconciler_test.go

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"time"
2121

2222
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/mock"
2324
"github.com/stretchr/testify/require"
2425
"go.uber.org/zap/zapcore"
26+
corev1 "k8s.io/api/core/v1"
2527
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2628
k8sobj "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2729
k8srtschema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -30,7 +32,9 @@ import (
3032

3133
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
3234
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
35+
"github.com/aws-controllers-k8s/runtime/pkg/condition"
3336
ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config"
37+
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
3438
ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics"
3539
"github.com/aws-controllers-k8s/runtime/pkg/requeue"
3640
ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime"
@@ -96,10 +100,20 @@ func reconcilerMocks(
96100
), kc
97101
}
98102

103+
func managedResourceManagerFactoryMocks(
104+
desired acktypes.AWSResource,
105+
latest acktypes.AWSResource,
106+
) (
107+
*ackmocks.AWSResourceManagerFactory,
108+
*ackmocks.AWSResourceDescriptor,
109+
) {
110+
return managerFactoryMocks(desired, latest, true)
111+
}
112+
99113
func managerFactoryMocks(
100114
desired acktypes.AWSResource,
101115
latest acktypes.AWSResource,
102-
delta *ackcompare.Delta,
116+
isManaged bool,
103117
) (
104118
*ackmocks.AWSResourceManagerFactory,
105119
*ackmocks.AWSResourceDescriptor,
@@ -114,7 +128,7 @@ func managerFactoryMocks(
114128
rd.On("EmptyRuntimeObject").Return(
115129
&fakeBook{},
116130
)
117-
rd.On("IsManaged", desired).Return(true)
131+
rd.On("IsManaged", latest).Return(isManaged)
118132

119133
rmf := &ackmocks.AWSResourceManagerFactory{}
120134
rmf.On("ResourceDescriptor").Return(rd)
@@ -150,7 +164,7 @@ func TestReconcilerUpdate(t *testing.T) {
150164
latest, nil,
151165
)
152166

153-
rmf, rd := managerFactoryMocks(desired, latest, delta)
167+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
154168
rd.On("Delta", desired, latest).Return(
155169
delta,
156170
).Once()
@@ -201,7 +215,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInMetadata(t *testing.T) {
201215
// Note the change in annotations
202216
latestMetaObj.SetAnnotations(map[string]string{"a": "b"})
203217

204-
rmf, rd := managerFactoryMocks(desired, latest, delta)
218+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
205219
rd.On("Delta", desired, latest).Return(
206220
delta,
207221
).Once()
@@ -251,7 +265,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInSpec(t *testing.T) {
251265
latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
252266
// Note no change to metadata...
253267

254-
rmf, rd := managerFactoryMocks(desired, latest, delta)
268+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
255269
rd.On("Delta", desired, latest).Return(
256270
delta,
257271
)
@@ -301,7 +315,7 @@ func TestReconcilerHandleReconcilerError_PatchStatus_Latest(t *testing.T) {
301315

302316
latestMetaObj.SetAnnotations(map[string]string{"a": "b"})
303317

304-
rmf, _ := managerFactoryMocks(desired, latest, delta)
318+
rmf, _ := managedResourceManagerFactoryMocks(desired, latest)
305319
r, kc := reconcilerMocks(rmf)
306320

307321
statusWriter := &ctrlrtclientmock.StatusWriter{}
@@ -325,7 +339,7 @@ func TestReconcilerHandleReconcilerError_NoPatchStatus_NoLatest(t *testing.T) {
325339

326340
desired, _, _ := resourceMocks()
327341

328-
rmf, _ := managerFactoryMocks(desired, nil, nil)
342+
rmf, _ := managedResourceManagerFactoryMocks(desired, nil)
329343
r, kc := reconcilerMocks(rmf)
330344

331345
statusWriter := &ctrlrtclientmock.StatusWriter{}
@@ -368,7 +382,7 @@ func TestReconcilerUpdate_ErrorInLateInitialization(t *testing.T) {
368382
latest, nil,
369383
)
370384

371-
rmf, rd := managerFactoryMocks(desired, latest, delta)
385+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
372386
rd.On("Delta", desired, latest).Return(
373387
delta,
374388
).Once()
@@ -393,3 +407,62 @@ func TestReconcilerUpdate_ErrorInLateInitialization(t *testing.T) {
393407
kc.AssertNotCalled(t, "Patch", ctx, latestRTObj, client.MergeFrom(desiredRTObj))
394408
rm.AssertCalled(t, "LateInitialize", ctx, latest)
395409
}
410+
411+
func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
412+
require := require.New(t)
413+
assert := assert.New(t)
414+
415+
ctx := context.TODO()
416+
arn := ackv1alpha1.AWSResourceName("mybook-arn")
417+
418+
delta := ackcompare.NewDelta()
419+
420+
desired, _, _ := resourceMocks()
421+
422+
ids := &ackmocks.AWSResourceIdentifiers{}
423+
ids.On("ARN").Return(&arn)
424+
425+
latest, _, _ := resourceMocks()
426+
latest.On("Identifiers").Return(ids)
427+
latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
428+
429+
terminalCondition := ackv1alpha1.Condition{
430+
Type: ackv1alpha1.ConditionTypeTerminal,
431+
Status: corev1.ConditionTrue,
432+
Reason: &condition.NotManagedReason,
433+
Message: &condition.NotManagedMessage,
434+
}
435+
latest.On("ReplaceConditions", mock.AnythingOfType("[]*v1alpha1.Condition")).Return([]*ackv1alpha1.Condition{&terminalCondition}).Run(func(args mock.Arguments) {
436+
conditions := args.Get(0).([]*ackv1alpha1.Condition)
437+
hasTerminal := false
438+
for _, condition := range conditions {
439+
if condition.Type != ackv1alpha1.ConditionTypeTerminal {
440+
continue
441+
}
442+
443+
hasTerminal = true
444+
assert.Equal(condition.Message, terminalCondition.Message)
445+
assert.Equal(condition.Reason, terminalCondition.Reason)
446+
}
447+
448+
assert.True(hasTerminal)
449+
})
450+
451+
rm := &ackmocks.AWSResourceManager{}
452+
rm.On("ReadOne", ctx, desired).Return(
453+
latest, nil,
454+
)
455+
456+
rmf, rd := managerFactoryMocks(desired, latest, false)
457+
458+
r, _ := reconcilerMocks(rmf)
459+
460+
_, err := r.Sync(ctx, rm, desired)
461+
// Assert the error from late initialization
462+
require.NotNil(err)
463+
assert.Equal(ackerr.Terminal, err)
464+
rm.AssertCalled(t, "ReadOne", ctx, desired)
465+
rd.AssertNotCalled(t, "Delta", desired, latest)
466+
rm.AssertNotCalled(t, "Update", ctx, desired, latest, delta)
467+
rm.AssertNotCalled(t, "LateInitialize", ctx, latest)
468+
}

0 commit comments

Comments
 (0)