Repository: aws-controllers-k8s/runtime
Pull request #179: feat: Adopt-or-Create
Author: login: michaelhtm
Created: 2025-04-04T22:58:54Z | Merged: 2025-04-25T21:19:47Z

Issue [#2406](https://github.com/aws-controllers-k8s/community/issues/2406)

Description of changes:
The changes handle the addition of a new `adoption-policy` called `adopt-or-create`.
The adopt-or-create policy requires users to define the Spec fields in the resource Spec,
and the Status fields in the `adoption-fields` annotation. Requiring the Spec fields be in Spec
for adopt-or-create ensures that the users provide fields that may also be required for a create
operation. 

Here is the controller lifecycle of adopt-or-create policy:
```
Controller looks for resource in AWS
If found
   check if there are any differences between user resource definition (desired) and 
    resource definition in AWS (latest). Update if there are any differences
If not Found
  create the resource
```
Unlike the `adopt` adoption policy, if a resource has `adopt-or-create` policy and `read-only`, and it is not found 
in AWS, the controller will mark that resource with a terminal error.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Diff:
diff --git a/pkg/runtime/reconciler.go b/pkg/runtime/reconciler.go
index ac5e5885..75ad3f66 100644
--- a/pkg/runtime/reconciler.go
+++ b/pkg/runtime/reconciler.go
@@ -303,54 +303,46 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
+// Handles the population of ACK Resource fields from annotation.
+func (r *resourceReconciler) handlePopulation(
 	ctx context.Context,
-	rm acktypes.AWSResourceManager,
 	desired acktypes.AWSResource,
-	rlog acktypes.Logger,
 ) (acktypes.AWSResource, error) {
+	var err error
+	rlog := ackrtlog.FromContext(ctx)
+	exit := rlog.Trace("r.handlePopulation")
+	defer func() {
+		exit(err)
+	}()
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
-
-	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
-	if err != nil {
-		return desired, ackerr.NewTerminalError(err)
-	}
-	if len(extractedFields) == 0 {
-		// TODO(michaelhtm) Here we need to figure out if we want to have an
-		// error or not. should we consider accepting values from Spec?
-		// And then we can let the ReadOne figure out if we have missing
-		// required fields for a Read
-		return nil, fmt.Errorf("failed extracting fields from annotation")
-	}
-	resolved := desired.DeepCopy()
-	err = resolved.PopulateResourceFromAnnotation(extractedFields)
+	rlog.Debug("Populating Resource")
+	adoptionPolicy, err := GetAdoptionPolicy(desired)
 	if err != nil {
 		return nil, err
 	}
-
-	rlog.Enter("rm.ReadOne")
-	latest, err := rm.ReadOne(ctx, resolved)
-	rlog.Exit("rm.ReadOne", err)
-	rm.FilterSystemTags(latest)
+	adoptionFields, err := ExtractAdoptionFields(desired)
 	if err != nil {
-		return latest, err
-	}
-
-	if err = r.setResourceManaged(ctx, rm, latest); err != nil {
-		return latest, err
+		// if the user does not provide adoption fields and the policy
+		// is adopt-or-create, we will assume the adoption fields are passed
+		// in Spec
+		if adoptionPolicy == AdoptionPolicy_AdoptOrCreate {
+			return desired, nil
+		}
+		return desired, ackerr.NewTerminalError(err)
 	}
 
-	r.rd.MarkAdopted(latest)
-	rlog.WithValues("is_adopted", "true")
-	latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
-	if err != nil {
-		return latest, err
+	populated := desired.DeepCopy()
+	err = populated.PopulateResourceFromAnnotation(adoptionFields)
+	// maybe don't return errors when it's adopt-or-create?
+	//
+	// TODO (michaelhtm) change PopulateResourceFromAnnotation to understand
+	// adopt-or-create, and validate Spec fields are not nil...
+	if err != nil && adoptionPolicy != AdoptionPolicy_AdoptOrCreate {
+		return nil, err
 	}
 
-	rlog.Info("Resource Adopted")
-	return latest, nil
+	return populated, nil
 }
 
 // reconcile either cleans up a deleted resource or ensures that the supplied
@@ -419,92 +411,54 @@ func (r *resourceReconciler) Sync(
 	isAdopted := IsAdopted(desired)
 	rlog.WithValues("is_adopted", isAdopted)
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
-		if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
-			latest, err := r.handleAdoption(ctx, rm, desired, rlog)
-
-			if err != nil {
-				// If we get an error, we want to return here
-				// TODO(michaelhtm): Change the handling of
-				// the error to allow Adopt or Create here
-				// when supported
-				return latest, err
-			}
-			return latest, nil
-		}
+	// If AdoptionAnnotation feature gate is enabled, and the resource has an adoption
+	// annotation and doesn't hold an ACK finalizer, trigger the adoption path.
+	needAdoption := NeedAdoption(desired) && !r.rd.IsManaged(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption)
+	adoptionPolicy, err := GetAdoptionPolicy(desired)
+	if err != nil {
+		return nil, err
+	}
+	if !needAdoption {
+		adoptionPolicy = ""
 	}
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources) {
-		isReadOnly := IsReadOnly(desired)
+	// find out if this is read only
+	isReadOnly := IsReadOnly(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources)
+	if isReadOnly {
 		rlog.WithValues("is_read_only", isReadOnly)
-
-		// NOTE(a-hilaly): When the time comes to support adopting resources
-		// using annotations, we will need to think a little bit more about
-		// the case where a user, wants to adopt a resource as read-only.
-		//
-		// NOTE(michaelhtm): Done, tnx :)
-
-		// If the resource is read-only, we enter a different code path where we
-		// only read the resource and patch the metadata and spec.
-		if isReadOnly {
-			rlog.Enter("rm.ResolveReferences")
-			resolved, _, _ := rm.ResolveReferences(ctx, r.apiReader, desired)
-			// NOTE(a-hilaly): One might be wondering why are we ignoring the
-			// error here. The reason is that the current implementation of
-			// ResolveReference doesn't take into consideration the read-only
-			// resources (and their fields).
-			//
-			// In a nutshell, `ResolveReferences` implementation was designed to
-			// be used for the create/update operations, and not for the read-only
-			// resources. This means that the implementation of `ResolveReferences`
-			// returns an error when any of the required references (for create)
-			// are not resolved. This is not helpful for read-only resources.
-			//
-			// We need to refactor the `ResolveReferences` implementation to
-			// be able to resolve read-only required fields. For example, EKS
-			// NodeGroups, only require the `ClusterName` field to be resolved.
-			// This is not the case for create/update operations where we need to
-			// resolve all the references (e.g. `ClusterName`, `NodeRoleArn`, etc.).
-			// See https://github.com/aws-controllers-k8s/eks-controller/blob/main/pkg/resource/nodegroup/references.go#L72-L114
-			//
-			// What's the worst that can happen? If we don't resolve the references
-			// for read-only resources, ReadOne call will fail with an error, causing
-			// the resource to be requeued.
-			//
-			// TODO(a-hilaly): Implement resolve references for read-only resources,
-			// maybe part of the DynamicReferences work.
-			rlog.Exit("rm.ResolveReferences", err)
-
-			rlog.Enter("rm.ReadOne")
-			latest, err = rm.ReadOne(ctx, resolved)
-			rlog.Exit("rm.ReadOne", err)
-			if err != nil {
-				if err == ackerr.NotFound {
-					return nil, ackerr.ReadOnlyResourceNotFound
-				}
-				return latest, err
-			}
-
-			err = r.patchResourceStatus(ctx, desired, latest)
-			return latest, err
-		}
 	}
 
 	rlog.Enter("rm.ResolveReferences")
 	resolved, hasReferences, err := rm.ResolveReferences(ctx, r.apiReader, desired)
 	rlog.Exit("rm.ResolveReferences", err)
-	if err != nil {
+	// TODO (michaelhtm): should we fail here for `adopt-or-create` adoption policy?
+	if err != nil && !needAdoption && !isReadOnly {
 		return ackcondition.WithReferencesResolvedCondition(desired, err), err
 	}
 	if hasReferences {
 		resolved = ackcondition.WithReferencesResolvedCondition(resolved, err)
 	}
 
-	rlog.Enter("rm.EnsureTags")
-	err = rm.EnsureTags(ctx, resolved, r.sc.GetMetadata())
-	rlog.Exit("rm.EnsureTags", err)
-	if err != nil {
-		return resolved, err
+	if needAdoption {
+		populated, err := r.handlePopulation(ctx, desired)
+		if err != nil {
+			return nil, err
+		}
+		if adoptionPolicy == AdoptionPolicy_AdoptOrCreate {
+			// here we assume the spec fields are provided in the spec.
+			resolved.SetStatus(populated)
+		} else {
+			resolved = populated
+		}
+	}
+
+	if !isReadOnly {
+		rlog.Enter("rm.EnsureTags")
+		err = rm.EnsureTags(ctx, resolved, r.sc.GetMetadata())
+		rlog.Exit("rm.EnsureTags", err)
+		if err != nil {
+			return resolved, err
+		}
 	}
 
 	rlog.Enter("rm.ReadOne")
@@ -514,13 +468,32 @@ func (r *resourceReconciler) Sync(
 		if err != ackerr.NotFound {
 			return latest, err
 		}
-		if isAdopted {
+		if adoptionPolicy == AdoptionPolicy_Adopt || isAdopted {
 			return nil, ackerr.AdoptedResourceNotFound
 		}
+		if isReadOnly {
+			return nil, ackerr.ReadOnlyResourceNotFound
+		}
 		if latest, err = r.createResource(ctx, rm, resolved); err != nil {
 			return latest, err
 		}
-	} else {
+	} else if adoptionPolicy == AdoptionPolicy_Adopt {
+		rm.FilterSystemTags(latest)
+		if err = r.setResourceManaged(ctx, rm, latest); err != nil {
+			return latest, err
+		}
+		latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
+		if err != nil {
+			return latest, err
+		}
+	} else if !isReadOnly {
+		if adoptionPolicy == AdoptionPolicy_AdoptOrCreate {
+			// set adopt-or-create resource as managed before attempting
+			// update
+			if err = r.setResourceManaged(ctx, rm, latest); err != nil {
+				return latest, err
+			}
+		}
 		if latest, err = r.updateResource(ctx, rm, resolved, latest); err != nil {
 			return latest, err
 		}
diff --git a/pkg/runtime/reconciler_test.go b/pkg/runtime/reconciler_test.go
index 15d4e0ae..d5df3142 100644
--- a/pkg/runtime/reconciler_test.go
+++ b/pkg/runtime/reconciler_test.go
@@ -262,6 +262,201 @@ func TestReconcilerReadOnlyResource(t *testing.T) {
 	rm.AssertNotCalled(t, "Delta", 0)
 }
 
+func TestReconcilerAdoptResource(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	kc.On("Status").Return(statusWriter)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Create", 0)
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Create(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Identifiers").Return(ids)
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		nil, ackerr.NotFound,
+	).Once()
+	rm.On("ReadOne", ctx, latest).Return(
+		latest, nil,
+	)
+	rm.On("Create", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false).Once()
+	rd.On("IsManaged", desired).Return(true)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Status").Return(statusWriter)
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 2)
+	rm.AssertNumberOfCalls(t, "Create", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Adopt(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+	delta := ackcompare.NewDelta()
+	delta.Add("Spec.A", "val1", "val2")
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Identifiers").Return(ids)
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("Update", ctx, desired, latest, delta).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(true)
+	rd.On("Delta", desired, latest).Return(
+		delta,
+	).Once()
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Status").Return(statusWriter)
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	rm.AssertCalled(t, "Update", ctx, desired, latest, delta)
+	rd.AssertCalled(t, "Delta", desired, latest)
+	// Assert that the resource is not created or updated
+	rm.AssertNumberOfCalls(t, "Create", 0)
+}
+
 func TestReconcilerCreate_UnManagedResource_CheckReferencesResolveOnce(t *testing.T) {
 	require := require.New(t)
 
@@ -464,11 +659,13 @@ func TestReconcilerUpdate(t *testing.T) {
 	rm.On("ReadOne", ctx, desired).Return(
 		latest, nil,
 	)
+	rm.On("FilterSystemTags", latest)
 	rm.On("Update", ctx, desired, latest, delta).Return(
 		latest, nil,
 	)
 	rm.On("IsSynced", ctx, latest).Return(true, nil)
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	).Once()
@@ -555,6 +752,7 @@ func TestReconcilerUpdate_ResourceNotSynced(t *testing.T) {
 	rm.On("IsSynced", ctx, latest).Return(false, nil)
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	).Once()
@@ -635,6 +833,7 @@ func TestReconcilerUpdate_NoDelta_ResourceNotSynced(t *testing.T) {
 	rm.On("IsSynced", ctx, latest).Return(false, nil)
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(delta)
 
 	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
@@ -713,6 +912,7 @@ func TestReconcilerUpdate_NoDelta_ResourceSynced(t *testing.T) {
 	rm.On("IsSynced", ctx, latest).Return(true, nil)
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(delta)
 
 	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
@@ -799,6 +999,7 @@ func TestReconcilerUpdate_IsSyncedError(t *testing.T) {
 		true, syncedError)
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	).Once()
@@ -862,6 +1063,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInMetadata(t *testing.T) {
 	latestMetaObj.SetAnnotations(map[string]string{"a": "b"})
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	).Once()
@@ -942,6 +1144,7 @@ func TestReconcilerUpdate_PatchMetadataAndSpec_DiffInSpec(t *testing.T) {
 	// Note no change to metadata...
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	)
@@ -1103,6 +1306,7 @@ func TestReconcilerUpdate_ErrorInLateInitialization(t *testing.T) {
 	)
 
 	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+	rd.On("IsManaged", desired).Return(true)
 	rd.On("Delta", desired, latest).Return(
 		delta,
 	).Once()
@@ -1209,6 +1413,12 @@ func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
 	})
 
 	rm := &ackmocks.AWSResourceManager{}
+	rmf, rd := managerFactoryMocks(desired, latest, false)
+
+	r, _, scmd := reconcilerMocks(rmf)
+	rd.On("IsManaged", desired).Return(false)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+
 	rm.On("ResolveReferences", ctx, nil, desired).Return(
 		desired, false, nil,
 	)
@@ -1219,11 +1429,6 @@ func TestReconcilerUpdate_ResourceNotManaged(t *testing.T) {
 	)
 	rm.On("IsSynced", ctx, latest).Return(true, nil)
 
-	rmf, rd := managerFactoryMocks(desired, latest, false)
-
-	r, _, scmd := reconcilerMocks(rmf)
-	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
-
 	_, err := r.Sync(ctx, rm, desired)
 	require.NotNil(err)
 	assert.Equal(ackerr.Terminal, err)
diff --git a/pkg/runtime/util.go b/pkg/runtime/util.go
index 21c04817..adc994aa 100644
--- a/pkg/runtime/util.go
+++ b/pkg/runtime/util.go
@@ -15,6 +15,7 @@ package runtime
 
 import (
 	"encoding/json"
+	"fmt"
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
@@ -24,6 +25,21 @@ import (
 )
 
 // TODO(jaypipes): Place this code somewhere separate
+//     (michaelhtm)             ^ +1
+
+// AdoptionPolicy stores adoptionPolicy values we expect users to
+// provide in the resources `adoption-policy` annotation
+//
+// TODO(michaelhtm) Maybe we need a different place for this...
+// next refactor maybe? 🤷‍♂️
+type AdoptionPolicy string
+
+const (
+	// AdoptPolicy is ...
+	AdoptionPolicy_Adopt AdoptionPolicy = "adopt"
+	// AdoptPolicy is ...
+	AdoptionPolicy_AdoptOrCreate AdoptionPolicy = "adopt-or-create"
+)
 
 // IsAdopted returns true if the supplied AWSResource was created with a
 // non-nil ARN annotation, which indicates that the Kubernetes user who created
@@ -71,28 +87,32 @@ func IsReadOnly(res acktypes.AWSResource) bool {
 }
 
 // GetAdoptionPolicy returns the Adoption Policy of the resource
-// defined by the user in annotation. Possible values are: 
+// defined by the user in annotation. Possible values are:
 // adopt-only | adopt-or-create
 // adopt-only keeps requing until the resource is found
 // adopt-or-create creates the resource if does not exist
-func GetAdoptionPolicy(res acktypes.AWSResource) string {
+func GetAdoptionPolicy(res acktypes.AWSResource) (AdoptionPolicy, error) {
 	mo := res.MetaObject()
 	if mo == nil {
 		panic("getAdoptionPolicy received resource with nil RuntimeObject")
 	}
-	for k, v := range mo.GetAnnotations() {
-		if k == ackv1alpha1.AnnotationAdoptionPolicy {
-			return v
-		}
+	policy, ok := mo.GetAnnotations()[ackv1alpha1.AnnotationAdoptionPolicy]
+	if !ok {
+		return "", nil
 	}
 
-	return ""
+	if policy != string(AdoptionPolicy_Adopt) && policy != string(AdoptionPolicy_AdoptOrCreate) {
+		return "", fmt.Errorf("unrecognized adoption policy")
+	}
+
+	return AdoptionPolicy(policy), nil
 }
 
 // NeedAdoption returns true when the resource has
 // adopt annotation but is not yet adopted
 func NeedAdoption(res acktypes.AWSResource) bool {
-	return GetAdoptionPolicy(res) != "" && !IsAdopted(res) 
+	adoptionPolicy, _ := GetAdoptionPolicy(res)
+	return adoptionPolicy != "" && !IsAdopted(res)
 }
 
 func ExtractAdoptionFields(res acktypes.AWSResource) (map[string]string, error) {
diff --git a/pkg/runtime/util_test.go b/pkg/runtime/util_test.go
index cf871e2d..74589e3a 100644
--- a/pkg/runtime/util_test.go
+++ b/pkg/runtime/util_test.go
@@ -74,7 +74,7 @@ func TestIsForcedAdoption(t *testing.T) {
 	res := &mocks.AWSResource{}
 	res.On("MetaObject").Return(&metav1.ObjectMeta{
 		Annotations: map[string]string{
-			ackv1alpha1.AnnotationAdoptionPolicy: "true",
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
 			ackv1alpha1.AnnotationAdopted:        "false",
 		},
 	})
@@ -83,7 +83,7 @@ func TestIsForcedAdoption(t *testing.T) {
 	res = &mocks.AWSResource{}
 	res.On("MetaObject").Return(&metav1.ObjectMeta{
 		Annotations: map[string]string{
-			ackv1alpha1.AnnotationAdoptionPolicy: "true",
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
 			ackv1alpha1.AnnotationAdopted:        "true",
 		},
 	})
@@ -92,7 +92,6 @@ func TestIsForcedAdoption(t *testing.T) {
 	res = &mocks.AWSResource{}
 	res.On("MetaObject").Return(&metav1.ObjectMeta{
 		Annotations: map[string]string{
-			ackv1alpha1.AnnotationAdoptionPolicy: "false",
 			ackv1alpha1.AnnotationAdopted:        "true",
 		},
 	})


Reviews:
user: login: a-hilaly
body: Thanks @michaelhtm ! Looks good overall, I left a few comments below
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:34:40Z
user: login: a-hilaly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:36:31Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:39:17Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:41:52Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:42:36Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:49:31Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:53:38Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T18:54:29Z
user: login: rushmash91
body: Nice @michaelhtm !! would be great to have more details in the PR description.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T19:58:35Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T20:09:29Z

Review comments:
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: interesting rename, can you please add a comment explaining what is the purpose (and non-purpose) of this function?
created_at: 2025-04-22T18:22:26Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: can we get the logger from the context instead? (following the existing pattern)
created_at: 2025-04-22T18:22:53Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: can you get the adoption policy from the desired object?
created_at: 2025-04-22T18:23:14Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: does `ExtractAdoptionFields` need to be public?
created_at: 2025-04-22T18:23:38Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
 	if err != nil {
+		if adoptionPolicy == AdoptOrCreatePolicy {
+			return desired, nil
+		}
 		return desired, ackerr.NewTerminalError(err)
 	}
-	if len(extractedFields) == 0 {
-		// TODO(michaelhtm) Here we need to figure out if we want to have an
-		// error or not. should we consider accepting values from Spec?
-		// And then we can let the ReadOne figure out if we have missing
-		// required fields for a Read
-		return nil, fmt.Errorf("failed extracting fields from annotation")
-	}
-	resolved := desired.DeepCopy()
-	err = resolved.PopulateResourceFromAnnotation(extractedFields)
-	if err != nil {
-		return nil, err
-	}
 
-	rlog.Enter("rm.ReadOne")
-	latest, err := rm.ReadOne(ctx, resolved)
-	rlog.Exit("rm.ReadOne", err)
-	rm.FilterSystemTags(latest)
-	if err != nil {
-		return latest, err
-	}
-
-	if err = r.setResourceManaged(ctx, rm, latest); err != nil {
-		return latest, err
-	}
-
-	r.rd.MarkAdopted(latest)
-	rlog.WithValues("is_adopted", "true")
-	latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
-	if err != nil {
-		return latest, err
+	populated := desired.DeepCopy()
+	err = populated.PopulateResourceFromAnnotation(adoptionFields)
+	// maybe don't return errors when it's adopt-or-create?
+	// TODO (michaelhtm) change PopulateResourceFromAnnotation to understand
+	// adopt-or-create, and validate Spec fields are not nil...
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: yes 💯 
created_at: 2025-04-22T18:25:06Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
 	if err != nil {
+		if adoptionPolicy == AdoptOrCreatePolicy {
+			return desired, nil
+		}
 		return desired, ackerr.NewTerminalError(err)
 	}
-	if len(extractedFields) == 0 {
-		// TODO(michaelhtm) Here we need to figure out if we want to have an
-		// error or not. should we consider accepting values from Spec?
-		// And then we can let the ReadOne figure out if we have missing
-		// required fields for a Read
-		return nil, fmt.Errorf("failed extracting fields from annotation")
-	}
-	resolved := desired.DeepCopy()
-	err = resolved.PopulateResourceFromAnnotation(extractedFields)
-	if err != nil {
-		return nil, err
-	}
 
-	rlog.Enter("rm.ReadOne")
-	latest, err := rm.ReadOne(ctx, resolved)
-	rlog.Exit("rm.ReadOne", err)
-	rm.FilterSystemTags(latest)
-	if err != nil {
-		return latest, err
-	}
-
-	if err = r.setResourceManaged(ctx, rm, latest); err != nil {
-		return latest, err
-	}
-
-	r.rd.MarkAdopted(latest)
-	rlog.WithValues("is_adopted", "true")
-	latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
-	if err != nil {
-		return latest, err
+	populated := desired.DeepCopy()
+	err = populated.PopulateResourceFromAnnotation(adoptionFields)
+	// maybe don't return errors when it's adopt-or-create?
+	// TODO (michaelhtm) change PopulateResourceFromAnnotation to understand
+	// adopt-or-create, and validate Spec fields are not nil...
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: or maybe `PopulateResourceFromAnnotation` shouldn't return an error? how about a boolean, stating whether a resource got populated or not.
created_at: 2025-04-22T18:25:55Z
author_association: MEMBER
diff_hunk: @@ -419,92 +398,53 @@ func (r *resourceReconciler) Sync(
 	isAdopted := IsAdopted(desired)
 	rlog.WithValues("is_adopted", isAdopted)
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
-		if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
-			latest, err := r.handleAdoption(ctx, rm, desired, rlog)
-
-			if err != nil {
-				// If we get an error, we want to return here
-				// TODO(michaelhtm): Change the handling of
-				// the error to allow Adopt or Create here
-				// when supported
-				return latest, err
-			}
-			return latest, nil
-		}
+	// find out if we need to adopt early on
+	needAdoption := NeedAdoption(desired) && !r.rd.IsManaged(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption)
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: ```suggestion
    // If AdoptionAnnotation feature gate is enabled, and the resource has an adoption
    // annotation and doesn't hold an ACK finalizer, trigger the adoption path.
	needAdoption := r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) && NeedAdoption(desired) && !r.rd.IsManaged(desired)
```
created_at: 2025-04-22T18:27:27Z
author_association: MEMBER
diff_hunk: @@ -24,6 +25,20 @@ import (
 )
 
 // TODO(jaypipes): Place this code somewhere separate
+//     (michaelhtm)             ^ +1
+
+// AdoptionPolicy stores adoptionPolicy values we expect users to
+// provide in the resources `adoption-policy` annotation
+// TODO(michaelhtm) Maybe we need a different place for this...
+// next refactor maybe? 🤷‍♂️
path: pkg/runtime/util.go
user: login: a-hilaly
body: ```suggestion
// AdoptionPolicy stores adoptionPolicy values we expect users to
// provide in the resources `adoption-policy` annotation
//
// TODO(michaelhtm) Maybe we need a different place for this...
// next refactor maybe? 🤷‍♂️
```
created_at: 2025-04-22T18:28:28Z
author_association: MEMBER
diff_hunk: @@ -24,6 +25,20 @@ import (
 )
 
 // TODO(jaypipes): Place this code somewhere separate
+//     (michaelhtm)             ^ +1
+
+// AdoptionPolicy stores adoptionPolicy values we expect users to
+// provide in the resources `adoption-policy` annotation
+// TODO(michaelhtm) Maybe we need a different place for this...
+// next refactor maybe? 🤷‍♂️
path: pkg/runtime/util.go
user: login: a-hilaly
body: An yes, i think you can move a lot of this logic to an `adoption.go` file
created_at: 2025-04-22T18:28:52Z
author_association: MEMBER
diff_hunk: @@ -24,6 +25,20 @@ import (
 )
 
 // TODO(jaypipes): Place this code somewhere separate
+//     (michaelhtm)             ^ +1
+
+// AdoptionPolicy stores adoptionPolicy values we expect users to
+// provide in the resources `adoption-policy` annotation
+// TODO(michaelhtm) Maybe we need a different place for this...
+// next refactor maybe? 🤷‍♂️
+type AdoptionPolicy string
+
+const (
+	AdoptPolicy AdoptionPolicy = "adopt"
+	// calling this policy constant `AdoptOrCreatePolicy`
+	//  and see if we need to rename it!!!
+	AdoptOrCreatePolicy AdoptionPolicy = "adopt-or-create"
+)
path: pkg/runtime/util.go
user: login: a-hilaly
body: ```suggestion
const (
    // AdoptPolicy is ...
	AdoptPolicy AdoptionPolicy = "adopt"
	// AdoptOrCreatePolicy is ...
	AdoptOrCreatePolicy AdoptionPolicy = "adopt-or-create"
)
```
created_at: 2025-04-22T18:30:17Z
author_association: MEMBER
diff_hunk: @@ -262,6 +262,205 @@ func TestReconcilerReadOnlyResource(t *testing.T) {
 	rm.AssertNotCalled(t, "Delta", 0)
 }
 
+func TestReconcilerAdoptResource(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	kc.On("Status").Return(statusWriter)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Create", 0)
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Create(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+	// ids.On("ARN").Return(&arn)
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Identifiers").Return(ids)
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		nil, ackerr.NotFound,
+	).Once()
+	rm.On("ReadOne", ctx, latest).Return(
+		latest, nil,
+	)
+	rm.On("Create", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false).Once()
+	rd.On("IsManaged", desired).Return(true)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Status").Return(statusWriter)
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 2)
+	rm.AssertNumberOfCalls(t, "Create", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Adopt(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
path: pkg/runtime/reconciler_test.go
user: login: a-hilaly
body: ```suggestion
```
created_at: 2025-04-22T18:31:10Z
author_association: MEMBER
diff_hunk: @@ -262,6 +262,205 @@ func TestReconcilerReadOnlyResource(t *testing.T) {
 	rm.AssertNotCalled(t, "Delta", 0)
 }
 
+func TestReconcilerAdoptResource(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	kc.On("Status").Return(statusWriter)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Create", 0)
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Create(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+	// ids.On("ARN").Return(&arn)
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Identifiers").Return(ids)
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		nil, ackerr.NotFound,
+	).Once()
+	rm.On("ReadOne", ctx, latest).Return(
+		latest, nil,
+	)
+	rm.On("Create", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false).Once()
+	rd.On("IsManaged", desired).Return(true)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Status").Return(statusWriter)
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 2)
+	rm.AssertNumberOfCalls(t, "Create", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Adopt(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+	// ids.On("ARN").Return(&arn)
path: pkg/runtime/reconciler_test.go
user: login: a-hilaly
body: ```suggestion
```
created_at: 2025-04-22T18:31:15Z
author_association: MEMBER
diff_hunk: @@ -262,6 +262,205 @@ func TestReconcilerReadOnlyResource(t *testing.T) {
 	rm.AssertNotCalled(t, "Delta", 0)
 }
 
+func TestReconcilerAdoptResource(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	kc.On("Status").Return(statusWriter)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Create", 0)
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Create(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt-or-create",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	ids := &ackmocks.AWSResourceIdentifiers{}
+	// ids.On("ARN").Return(&arn)
path: pkg/runtime/reconciler_test.go
user: login: a-hilaly
body: ```suggestion
```
created_at: 2025-04-22T18:31:22Z
author_association: MEMBER
diff_hunk: @@ -262,6 +262,205 @@ func TestReconcilerReadOnlyResource(t *testing.T) {
 	rm.AssertNotCalled(t, "Delta", 0)
 }
 
+func TestReconcilerAdoptResource(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+
+	desired, _, metaObj := resourceMocks()
+	desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return()
+	metaObj.SetAnnotations(map[string]string{
+		ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+		ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+	})
+
+	latest, latestRTObj, _ := resourceMocks()
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On("MetaObject").Return(metav1.ObjectMeta{
+		Annotations: map[string]string{
+			ackv1alpha1.AnnotationAdoptionPolicy: "adopt",
+			ackv1alpha1.AnnotationAdoptionFields: "{\"arn\": \"my-adopt-book-arn\"}",
+		},
+	})
+	latest.On("Conditions").Return([]*ackv1alpha1.Condition{})
+	latest.On(
+		"ReplaceConditions",
+		mock.AnythingOfType("[]*v1alpha1.Condition"),
+	).Return()
+
+	rm := &ackmocks.AWSResourceManager{}
+	rm.On("ResolveReferences", ctx, nil, desired).Return(
+		desired, false, nil,
+	).Times(2)
+	rm.On("ClearResolvedReferences", desired).Return(desired)
+	rm.On("ClearResolvedReferences", latest).Return(latest)
+	rm.On("ReadOne", ctx, desired).Return(
+		latest, nil,
+	).Once()
+	rm.On("IsSynced", ctx, latest).Return(true, nil)
+	rmf, rd := managedResourceManagerFactoryMocks(desired, latest)
+
+	rm.On("LateInitialize", ctx, latest).Return(latest, nil)
+	rd.On("IsManaged", desired).Return(false)
+	rd.On("Delta", desired, latest).Return(ackcompare.NewDelta())
+	rd.On("Delta", latest, latest).Return(ackcompare.NewDelta())
+
+	r, kc, scmd := reconcilerMocks(rmf)
+	rm.On("EnsureTags", ctx, desired, scmd).Return(nil)
+	statusWriter := &ctrlrtclientmock.SubResourceWriter{}
+	kc.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	kc.On("Status").Return(statusWriter)
+	statusWriter.On("Patch", ctx, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil)
+	_, err := r.Sync(ctx, rm, desired)
+	require.Nil(err)
+	rm.AssertNumberOfCalls(t, "ReadOne", 1)
+	// Assert that the resource is not created or updated
+	rm.AssertNotCalled(t, "Create", 0)
+	rm.AssertNotCalled(t, "Update", 0)
+	rm.AssertNotCalled(t, "Delta", 0)
+}
+
+func TestReconcilerAdoptOrCreateResource_Create(t *testing.T) {
+	require := require.New(t)
+
+	ctx := context.TODO()
+	// arn := ackv1alpha1.AWSResourceName("my-adopt-book-arn")
path: pkg/runtime/reconciler_test.go
user: login: a-hilaly
body: ```suggestion
```
created_at: 2025-04-22T18:31:31Z
author_association: MEMBER
diff_hunk: @@ -514,13 +454,30 @@ func (r *resourceReconciler) Sync(
 		if err != ackerr.NotFound {
 			return latest, err
 		}
-		if isAdopted {
+		if adoptionPolicy == AdoptPolicy || isAdopted {
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: do you also want to check for `needAdoption`
created_at: 2025-04-22T18:32:28Z
author_association: MEMBER
diff_hunk: @@ -514,13 +454,30 @@ func (r *resourceReconciler) Sync(
 		if err != ackerr.NotFound {
 			return latest, err
 		}
-		if isAdopted {
+		if adoptionPolicy == AdoptPolicy || isAdopted {
 			return nil, ackerr.AdoptedResourceNotFound
 		}
+		if isReadOnly {
+			return nil, ackerr.ReadOnlyResourceNotFound
+		}
 		if latest, err = r.createResource(ctx, rm, resolved); err != nil {
 			return latest, err
 		}
-	} else {
+	} else if adoptionPolicy == AdoptPolicy {
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: ditto
created_at: 2025-04-22T18:32:44Z
author_association: MEMBER
path: pkg/runtime/reconciler.go
user: login: a-hilaly
body: nit: maybe it's about time to factor the Sync function in multiple small functions? maybe in a seperate PR? looks like it's growing exponentially, and most of it's logic is really the core of ACK.
created_at: 2025-04-22T18:36:31Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: not really. privatizing...
created_at: 2025-04-22T18:39:17Z
author_association: MEMBER
diff_hunk: @@ -514,13 +454,30 @@ func (r *resourceReconciler) Sync(
 		if err != ackerr.NotFound {
 			return latest, err
 		}
-		if isAdopted {
+		if adoptionPolicy == AdoptPolicy || isAdopted {
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: i check that early on..`adoptionPolicy = ""` if `!needAdoption`
created_at: 2025-04-22T18:41:52Z
author_association: MEMBER
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: ➕1
created_at: 2025-04-22T18:42:36Z
author_association: MEMBER
diff_hunk: @@ -419,92 +398,53 @@ func (r *resourceReconciler) Sync(
 	isAdopted := IsAdopted(desired)
 	rlog.WithValues("is_adopted", isAdopted)
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
-		if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
-			latest, err := r.handleAdoption(ctx, rm, desired, rlog)
-
-			if err != nil {
-				// If we get an error, we want to return here
-				// TODO(michaelhtm): Change the handling of
-				// the error to allow Adopt or Create here
-				// when supported
-				return latest, err
-			}
-			return latest, nil
-		}
+	// find out if we need to adopt early on
path: pkg/runtime/reconciler.go
user: login: rushmash91
body: nit
```suggestion
	// find out if we need to adopt early
```
created_at: 2025-04-22T18:42:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: Actually there's more functions in `util.go` that are public without needing to be..we can change their signature during the next refactor..
created_at: 2025-04-22T18:49:31Z
author_association: MEMBER
diff_hunk: @@ -419,92 +398,53 @@ func (r *resourceReconciler) Sync(
 	isAdopted := IsAdopted(desired)
 	rlog.WithValues("is_adopted", isAdopted)
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
-		if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
-			latest, err := r.handleAdoption(ctx, rm, desired, rlog)
-
-			if err != nil {
-				// If we get an error, we want to return here
-				// TODO(michaelhtm): Change the handling of
-				// the error to allow Adopt or Create here
-				// when supported
-				return latest, err
-			}
-			return latest, nil
-		}
+	// find out if we need to adopt early on
+	needAdoption := NeedAdoption(desired) && !r.rd.IsManaged(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption)
+	adoptionPolicy, err := GetAdoptionPolicy(desired)
+	if err != nil {
+		return nil, err
+	}
+	if !needAdoption {
+		adoptionPolicy = ""
 	}
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources) {
-		isReadOnly := IsReadOnly(desired)
+	// find out if this is read only
+	isReadOnly := IsReadOnly(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources)
+	if isReadOnly {
 		rlog.WithValues("is_read_only", isReadOnly)
-
-		// NOTE(a-hilaly): When the time comes to support adopting resources
-		// using annotations, we will need to think a little bit more about
-		// the case where a user, wants to adopt a resource as read-only.
-		//
-		// NOTE(michaelhtm): Done, tnx :)
-
-		// If the resource is read-only, we enter a different code path where we
-		// only read the resource and patch the metadata and spec.
-		if isReadOnly {
-			rlog.Enter("rm.ResolveReferences")
-			resolved, _, _ := rm.ResolveReferences(ctx, r.apiReader, desired)
-			// NOTE(a-hilaly): One might be wondering why are we ignoring the
-			// error here. The reason is that the current implementation of
-			// ResolveReference doesn't take into consideration the read-only
-			// resources (and their fields).
-			//
-			// In a nutshell, `ResolveReferences` implementation was designed to
-			// be used for the create/update operations, and not for the read-only
-			// resources. This means that the implementation of `ResolveReferences`
-			// returns an error when any of the required references (for create)
-			// are not resolved. This is not helpful for read-only resources.
-			//
-			// We need to refactor the `ResolveReferences` implementation to
-			// be able to resolve read-only required fields. For example, EKS
-			// NodeGroups, only require the `ClusterName` field to be resolved.
-			// This is not the case for create/update operations where we need to
-			// resolve all the references (e.g. `ClusterName`, `NodeRoleArn`, etc.).
-			// See https://github.com/aws-controllers-k8s/eks-controller/blob/main/pkg/resource/nodegroup/references.go#L72-L114
-			//
-			// What's the worst that can happen? If we don't resolve the references
-			// for read-only resources, ReadOne call will fail with an error, causing
-			// the resource to be requeued.
-			//
-			// TODO(a-hilaly): Implement resolve references for read-only resources,
-			// maybe part of the DynamicReferences work.
-			rlog.Exit("rm.ResolveReferences", err)
-
-			rlog.Enter("rm.ReadOne")
-			latest, err = rm.ReadOne(ctx, resolved)
-			rlog.Exit("rm.ReadOne", err)
-			if err != nil {
-				if err == ackerr.NotFound {
-					return nil, ackerr.ReadOnlyResourceNotFound
-				}
-				return latest, err
-			}
-
-			err = r.patchResourceStatus(ctx, desired, latest)
-			return latest, err
-		}
 	}
 
 	rlog.Enter("rm.ResolveReferences")
 	resolved, hasReferences, err := rm.ResolveReferences(ctx, r.apiReader, desired)
 	rlog.Exit("rm.ResolveReferences", err)
-	if err != nil {
+	// TODO (michaelhtm): should we fail here for `adopt-or-create` adoption policy?
path: pkg/runtime/reconciler.go
user: login: rushmash91
body: Failing is consistent here.. this is why we see the issues like the adoption fails in VPC Peering in ec2 (for just adopt)?
created_at: 2025-04-22T18:50:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
 	if err != nil {
+		if adoptionPolicy == AdoptOrCreatePolicy {
+			return desired, nil
+		}
 		return desired, ackerr.NewTerminalError(err)
 	}
-	if len(extractedFields) == 0 {
-		// TODO(michaelhtm) Here we need to figure out if we want to have an
-		// error or not. should we consider accepting values from Spec?
-		// And then we can let the ReadOne figure out if we have missing
-		// required fields for a Read
-		return nil, fmt.Errorf("failed extracting fields from annotation")
-	}
-	resolved := desired.DeepCopy()
-	err = resolved.PopulateResourceFromAnnotation(extractedFields)
-	if err != nil {
-		return nil, err
-	}
 
-	rlog.Enter("rm.ReadOne")
-	latest, err := rm.ReadOne(ctx, resolved)
-	rlog.Exit("rm.ReadOne", err)
-	rm.FilterSystemTags(latest)
-	if err != nil {
-		return latest, err
-	}
-
-	if err = r.setResourceManaged(ctx, rm, latest); err != nil {
-		return latest, err
-	}
-
-	r.rd.MarkAdopted(latest)
-	rlog.WithValues("is_adopted", "true")
-	latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
-	if err != nil {
-		return latest, err
+	populated := desired.DeepCopy()
+	err = populated.PopulateResourceFromAnnotation(adoptionFields)
+	// maybe don't return errors when it's adopt-or-create?
+	// TODO (michaelhtm) change PopulateResourceFromAnnotation to understand
+	// adopt-or-create, and validate Spec fields are not nil...
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: For this, i was mostly thinking we can pass the `adoptionPolicy` to `PopulateResourceFromAnnotation`. Doing so, we can decide not to populate Spec fields if the policy is Adopt-Or-Create...
created_at: 2025-04-22T18:53:38Z
author_association: MEMBER
diff_hunk: @@ -303,54 +303,33 @@ func (r *resourceReconciler) handleCacheError(
 	return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
 }
 
-func (r *resourceReconciler) handleAdoption(
-	ctx context.Context,
-	rm acktypes.AWSResourceManager,
-	desired acktypes.AWSResource,
+func (r *resourceReconciler) handlePopulation(
 	rlog acktypes.Logger,
+	desired acktypes.AWSResource,
+	adoptionPolicy AdoptionPolicy,
 ) (acktypes.AWSResource, error) {
 	// If the resource is being adopted by force, we need to access
 	// the required field passed by annotation and attempt a read.
 
 	rlog.Info("Adopting Resource")
-	extractedFields, err := ExtractAdoptionFields(desired)
+	adoptionFields, err := ExtractAdoptionFields(desired)
 	if err != nil {
+		if adoptionPolicy == AdoptOrCreatePolicy {
+			return desired, nil
+		}
 		return desired, ackerr.NewTerminalError(err)
 	}
-	if len(extractedFields) == 0 {
-		// TODO(michaelhtm) Here we need to figure out if we want to have an
-		// error or not. should we consider accepting values from Spec?
-		// And then we can let the ReadOne figure out if we have missing
-		// required fields for a Read
-		return nil, fmt.Errorf("failed extracting fields from annotation")
-	}
-	resolved := desired.DeepCopy()
-	err = resolved.PopulateResourceFromAnnotation(extractedFields)
-	if err != nil {
-		return nil, err
-	}
 
-	rlog.Enter("rm.ReadOne")
-	latest, err := rm.ReadOne(ctx, resolved)
-	rlog.Exit("rm.ReadOne", err)
-	rm.FilterSystemTags(latest)
-	if err != nil {
-		return latest, err
-	}
-
-	if err = r.setResourceManaged(ctx, rm, latest); err != nil {
-		return latest, err
-	}
-
-	r.rd.MarkAdopted(latest)
-	rlog.WithValues("is_adopted", "true")
-	latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
-	if err != nil {
-		return latest, err
+	populated := desired.DeepCopy()
+	err = populated.PopulateResourceFromAnnotation(adoptionFields)
+	// maybe don't return errors when it's adopt-or-create?
+	// TODO (michaelhtm) change PopulateResourceFromAnnotation to understand
+	// adopt-or-create, and validate Spec fields are not nil...
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: Or..we can divide this up into two..PopulateSpecFromAnnotations, and PopulateStatusFromAnnotation..(we need a better name)
created_at: 2025-04-22T18:54:28Z
author_association: MEMBER
diff_hunk: @@ -419,92 +398,53 @@ func (r *resourceReconciler) Sync(
 	isAdopted := IsAdopted(desired)
 	rlog.WithValues("is_adopted", isAdopted)
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
-		if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
-			latest, err := r.handleAdoption(ctx, rm, desired, rlog)
-
-			if err != nil {
-				// If we get an error, we want to return here
-				// TODO(michaelhtm): Change the handling of
-				// the error to allow Adopt or Create here
-				// when supported
-				return latest, err
-			}
-			return latest, nil
-		}
+	// find out if we need to adopt early on
+	needAdoption := NeedAdoption(desired) && !r.rd.IsManaged(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption)
+	adoptionPolicy, err := GetAdoptionPolicy(desired)
+	if err != nil {
+		return nil, err
+	}
+	if !needAdoption {
+		adoptionPolicy = ""
 	}
 
-	if r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources) {
-		isReadOnly := IsReadOnly(desired)
+	// find out if this is read only
+	isReadOnly := IsReadOnly(desired) && r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources)
+	if isReadOnly {
 		rlog.WithValues("is_read_only", isReadOnly)
-
-		// NOTE(a-hilaly): When the time comes to support adopting resources
-		// using annotations, we will need to think a little bit more about
-		// the case where a user, wants to adopt a resource as read-only.
-		//
-		// NOTE(michaelhtm): Done, tnx :)
-
-		// If the resource is read-only, we enter a different code path where we
-		// only read the resource and patch the metadata and spec.
-		if isReadOnly {
-			rlog.Enter("rm.ResolveReferences")
-			resolved, _, _ := rm.ResolveReferences(ctx, r.apiReader, desired)
-			// NOTE(a-hilaly): One might be wondering why are we ignoring the
-			// error here. The reason is that the current implementation of
-			// ResolveReference doesn't take into consideration the read-only
-			// resources (and their fields).
-			//
-			// In a nutshell, `ResolveReferences` implementation was designed to
-			// be used for the create/update operations, and not for the read-only
-			// resources. This means that the implementation of `ResolveReferences`
-			// returns an error when any of the required references (for create)
-			// are not resolved. This is not helpful for read-only resources.
-			//
-			// We need to refactor the `ResolveReferences` implementation to
-			// be able to resolve read-only required fields. For example, EKS
-			// NodeGroups, only require the `ClusterName` field to be resolved.
-			// This is not the case for create/update operations where we need to
-			// resolve all the references (e.g. `ClusterName`, `NodeRoleArn`, etc.).
-			// See https://github.com/aws-controllers-k8s/eks-controller/blob/main/pkg/resource/nodegroup/references.go#L72-L114
-			//
-			// What's the worst that can happen? If we don't resolve the references
-			// for read-only resources, ReadOne call will fail with an error, causing
-			// the resource to be requeued.
-			//
-			// TODO(a-hilaly): Implement resolve references for read-only resources,
-			// maybe part of the DynamicReferences work.
-			rlog.Exit("rm.ResolveReferences", err)
-
-			rlog.Enter("rm.ReadOne")
-			latest, err = rm.ReadOne(ctx, resolved)
-			rlog.Exit("rm.ReadOne", err)
-			if err != nil {
-				if err == ackerr.NotFound {
-					return nil, ackerr.ReadOnlyResourceNotFound
-				}
-				return latest, err
-			}
-
-			err = r.patchResourceStatus(ctx, desired, latest)
-			return latest, err
-		}
 	}
 
 	rlog.Enter("rm.ResolveReferences")
 	resolved, hasReferences, err := rm.ResolveReferences(ctx, r.apiReader, desired)
 	rlog.Exit("rm.ResolveReferences", err)
-	if err != nil {
+	// TODO (michaelhtm): should we fail here for `adopt-or-create` adoption policy?
path: pkg/runtime/reconciler.go
user: login: michaelhtm
body: The VPCPeering error was mostly happening after the adoption path was successful..
created_at: 2025-04-22T20:09:29Z
author_association: MEMBER

PR comments:
user: login: ack-prow[bot]
created_at: 2025-04-04T22:58:58Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/aws-controllers-k8s/runtime/pull/179#" title="Author self-approved">michaelhtm</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign a-hilaly for approval by writing `/assign @a-hilaly` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=aws-controllers-k8s%2Fruntime).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/aws-controllers-k8s/runtime/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["a-hilaly"]} -->
author_association: NONE
user: login: gfaraj
created_at: 2025-04-07T16:21:18Z
body: Hi @michaelhtm! Quick question -- will this overwrite the entire Kubernetes manifest if it's able to adopt an existing resource? From the code I'm seeing, it seems like that's the case.

In my case, I would like to avoid that if possible. Overwriting the manifest from the adopted resource means I have to hit "sync" again for my argo app. Really, I just want to upsert my resource to match my ACK spec.

While I think overwriting the whole thing does seem like a valid use case, maybe it's possible to support both cases? Might be looking at a separate adoption policy of "upsert" or something like that?
author_association: NONE
user: login: michaelhtm
created_at: 2025-04-07T16:24:57Z
body: @gfaraj Hello, The entire manifest would be replaced with the spec defined in AWS if the `adoption-policy` is `adopt`. With the current implementation, if it's `adopt-or-create`, the controller will do an update after the adoption, if not it will create the resource. Maybe we need a better name than `adopt-or-create`..
author_association: MEMBER
user: login: gfaraj
created_at: 2025-04-07T16:56:32Z
body: @michaelhtm oh nice, thanks for confirming that, I may have misread part of the changes. Yeah then this seems more like an "upsert" -- maybe that should be the policy name instead?
author_association: NONE
user: login: michaelhtm
created_at: 2025-04-21T21:03:43Z
body: /test sagemaker-controller-test

author_association: MEMBER
user: login: michaelhtm
created_at: 2025-04-25T18:13:36Z
body: /test sagemaker-controller-test
author_association: MEMBER
user: login: ack-prow[bot]
created_at: 2025-04-25T18:45:14Z
body: @michaelhtm: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
verify-attribution | 6f7e297bc3825f197a0fa129b260fb9ec7860f26 | [link](https://prow.ack.aws.dev/view/s3/ack-prow-logs/pr-logs/pull/aws-controllers-k8s_runtime/179/verify-attribution/1914774836786237440) | false | `/test verify-attribution`
sagemaker-controller-test | 6f7e297bc3825f197a0fa129b260fb9ec7860f26 | [link](https://prow.ack.aws.dev/view/s3/ack-prow-logs/pr-logs/pull/aws-controllers-k8s_runtime/179/sagemaker-controller-test/1915831586931085312) | true | `/test sagemaker-controller-test`

[Full PR test history](https://prow.ack.aws.dev/pr-history?org=aws-controllers-k8s&repo=runtime&pr=179). [Your PR dashboard](https://prow.ack.aws.dev/pr?query=is:pr+state:open+author:michaelhtm).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE