Skip to content

Commit 7ec3ad0

Browse files
authored
fix: Remove Finalizer from resource if create returns error (#185)
Issue [#2406#issuecomment-2860631351](aws-controllers-k8s/community#2406 (comment)) Description of changes: The reason marking a resource as managed (putting the finalizer) before attempting a create is a general practice in kubernetes. The main reason we do it is to protect against deletion protection. If we don't put the finalizer, there is no deletion protection against the resource. The current adoption logic expects the resource to not be managed (no finalizer) to trigger an adoption. If the initial creation attempt of a resource fails due to any AWS error, any subsequent reconciliations attempting to adopt an existing resource will not succeed. These changes set the resource as unmanaged if for any reason there is an error during the create call, which will allow the adoption logic to run in subsequent reconciliations. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 6a1bc57 commit 7ec3ad0

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

pkg/runtime/reconciler.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,16 @@ func (r *resourceReconciler) createResource(
636636
latest, err = rm.Create(ctx, desired)
637637
rlog.Exit("rm.Create", err)
638638
if err != nil {
639+
// Here we're deciding to set a resource as unmanaged
640+
// if the error is an AWS API Error. This will ensure
641+
// that we're only managing (put finalizer) the resources
642+
// that actually exist in AWS.
643+
if _, ok := ackerr.AWSError(err); ok {
644+
mErr := r.setResourceUnmanaged(ctx, rm, desired)
645+
if mErr != nil {
646+
return latest, err
647+
}
648+
}
639649
return latest, err
640650
}
641651

pkg/runtime/reconciler_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222
"time"
2323

24+
"github.com/aws/smithy-go"
2425
"github.com/stretchr/testify/assert"
2526
"github.com/stretchr/testify/mock"
2627
"github.com/stretchr/testify/require"
@@ -224,6 +225,68 @@ func TestReconcilerCreate_BackoffRetries(t *testing.T) {
224225
rm.AssertNumberOfCalls(t, "ReadOne", 6)
225226
}
226227

228+
type awsError struct {
229+
smithy.APIError
230+
}
231+
232+
func (err awsError) Error() string {
233+
return "mock error"
234+
}
235+
236+
func TestReconcilerCreate_UnmanageResourceOnAWSErrors(t *testing.T) {
237+
require := require.New(t)
238+
239+
ctx := context.TODO()
240+
arn := ackv1alpha1.AWSResourceName("mybook-arn")
241+
242+
desired, desiredRTObj, _ := resourceMocks()
243+
desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
244+
245+
ids := &ackmocks.AWSResourceIdentifiers{}
246+
ids.On("ARN").Return(&arn)
247+
248+
latest, latestRTObj, _ := resourceMocks()
249+
latest.On("Identifiers").Return(ids)
250+
251+
latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
252+
latest.On(
253+
"ReplaceConditions",
254+
mock.AnythingOfType("[]*v1alpha1.Condition"),
255+
).Return()
256+
257+
rm := &ackmocks.AWSResourceManager{}
258+
rm.On("ResolveReferences", ctx, nil, desired).Return(
259+
desired, false, nil,
260+
).Times(2)
261+
rm.On("ClearResolvedReferences", desired).Return(desired)
262+
rm.On("ClearResolvedReferences", latest).Return(latest)
263+
rm.On("ReadOne", ctx, desired).Return(
264+
latest, ackerr.NotFound,
265+
).Once()
266+
rm.On("Create", ctx, desired).Return(
267+
latest, awsError{},
268+
)
269+
rm.On("IsSynced", ctx, latest).Return(false, nil)
270+
rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
271+
rd.On("IsManaged", desired).Return(false).Twice()
272+
rd.On("IsManaged", desired).Return(true)
273+
rd.On("MarkUnmanaged", desired)
274+
rd.On("MarkManaged", desired)
275+
rd.On("MarkUnmanaged", desired)
276+
rd.On("ResourceFromRuntimeObject", desiredRTObj).Return(desired)
277+
rd.On("Delta", desired, desired).Return(ackcompare.NewDelta())
278+
279+
r, kc, scmd := reconcilerMocks(rmf)
280+
rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
281+
// Use specific matcher for WithoutCancel context instead of mock.Anything
282+
kc.On("Patch", withoutCancelContextMatcher, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
283+
_, err := r.Sync(ctx, rm, desired)
284+
require.NotNil(err)
285+
rm.AssertNumberOfCalls(t, "ReadOne", 1)
286+
rd.AssertCalled(t, "MarkUnmanaged", desired)
287+
rd.AssertCalled(t, "MarkManaged", desired)
288+
}
289+
227290
func TestReconcilerReadOnlyResource(t *testing.T) {
228291
require := require.New(t)
229292

@@ -1249,7 +1312,7 @@ func TestReconcilerHandleReconcilerError_PatchStatus_Latest(t *testing.T) {
12491312

12501313
_, err := r.HandleReconcileError(ctx, desired, latest, nil)
12511314
require.Nil(err)
1252-
statusWriter.AssertCalled(t, "Patch", withoutCancelContextMatcher, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch"))
1315+
statusWriter.AssertCalled(t, "Patch", withoutCancelContextMatcher, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch"))
12531316
// The HandleReconcilerError function never updates spec or metadata, so
12541317
// even though there is a change to the annotations we expect no call to
12551318
// patch the spec/metadata...

0 commit comments

Comments
 (0)