@@ -27,6 +27,7 @@ import (
2727 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2929 "k8s.io/apimachinery/pkg/runtime/schema"
30+ "k8s.io/apimachinery/pkg/types"
3031 "k8s.io/client-go/dynamic/dynamiclister"
3132 "k8s.io/client-go/tools/record"
3233 ctrl "sigs.k8s.io/controller-runtime"
@@ -128,17 +129,18 @@ func (r *specReconciler) Reconcile(ctx context.Context, req reconcile.Request) (
128129 switch {
129130 case getErr == nil :
130131 if owner , ours := ownershipOf (existing , r .localUID , string (obj .GetUID ())); ! ours {
131- reason := conflictReason (owner )
132- msg := fmt .Sprintf ("provider object %s already exists and is %s; not overwriting (conflictPolicy: Fail)" , client .ObjectKeyFromObject (obj ), owner )
133- log .Info ("conflict: refusing to overwrite provider object" , "owner" , owner )
134- // Surface on the object's own condition (best-effort: pruned if the
135- // CRD's status schema has no conditions) AND as an Event (always).
136- setObjCondition (obj , metav1 .ConditionFalse , reason , msg )
137- _ = r .consumerClient .Status ().Update (ctx , obj )
138- if r .recorder != nil {
139- r .recorder .Event (obj , corev1 .EventTypeWarning , reason , msg )
132+ // Adopt only takes an un-owned (markerless) object; it never steals
133+ // one already owned by another binding/consumer.
134+ if owner != ownerForeignUnmanaged || res .ConflictPolicy != corev1alpha1 .ConflictPolicyAdopt {
135+ reason := conflictReason (owner )
136+ msg := fmt .Sprintf ("provider object %s already exists and is %s; not overwriting (conflictPolicy: %s)" , client .ObjectKeyFromObject (obj ), owner , policyOrFail (res .ConflictPolicy ))
137+ log .Info ("conflict: refusing to overwrite provider object" , "owner" , owner )
138+ if err := r .markConflict (ctx , obj , reason , msg ); err != nil {
139+ return reconcile.Result {}, err
140+ }
141+ return reconcile.Result {}, nil
140142 }
141- return reconcile. Result {}, nil
143+ log . Info ( "adopting un-owned provider object" , "key" , client . ObjectKeyFromObject ( obj ))
142144 }
143145 case apierrors .IsNotFound (getErr ):
144146 if r .scope == apiextensionsv1 .NamespaceScoped {
@@ -150,6 +152,11 @@ func (r *specReconciler) Reconcile(ctx context.Context, req reconcile.Request) (
150152 return reconcile.Result {}, fmt .Errorf ("provider get: %w" , getErr )
151153 }
152154
155+ // We are syncing — clear any stale conflict marker from a previous round.
156+ if err := r .clearConflict (ctx , obj ); err != nil {
157+ return reconcile.Result {}, err
158+ }
159+
153160 // Spec consumer -> provider (SSA).
154161 patch := r .providerPatch (obj , r .localUID )
155162 if err := r .providerClient .Patch (ctx , patch , client .Apply , client .FieldOwner (fieldOwner ), client .ForceOwnership ); err != nil {
@@ -168,6 +175,36 @@ func (r *specReconciler) Reconcile(ctx context.Context, req reconcile.Request) (
168175 return reconcile.Result {RequeueAfter : statusResyncBackstop }, nil
169176}
170177
178+ // markConflict records a conflict durably (an annotation that survives status
179+ // pruning, used by bindings to count conflicts), best-effort on the object's
180+ // own condition, and as an Event.
181+ func (r * specReconciler ) markConflict (ctx context.Context , obj * unstructured.Unstructured , reason , msg string ) error {
182+ if obj .GetAnnotations ()[corev1alpha1 .AnnotationConflict ] != reason {
183+ p := fmt .Appendf (nil , `{"metadata":{"annotations":{%q:%q}}}` , corev1alpha1 .AnnotationConflict , reason )
184+ if err := r .consumerClient .Patch (ctx , obj , client .RawPatch (types .MergePatchType , p )); client .IgnoreNotFound (err ) != nil {
185+ return fmt .Errorf ("marking conflict: %w" , err )
186+ }
187+ }
188+ setObjCondition (obj , metav1 .ConditionFalse , reason , msg )
189+ _ = r .consumerClient .Status ().Update (ctx , obj )
190+ if r .recorder != nil {
191+ r .recorder .Event (obj , corev1 .EventTypeWarning , reason , msg )
192+ }
193+ return nil
194+ }
195+
196+ // clearConflict removes the conflict marker once the object syncs cleanly.
197+ func (r * specReconciler ) clearConflict (ctx context.Context , obj * unstructured.Unstructured ) error {
198+ if obj .GetAnnotations ()[corev1alpha1 .AnnotationConflict ] == "" {
199+ return nil
200+ }
201+ p := fmt .Appendf (nil , `{"metadata":{"annotations":{%q:null}}}` , corev1alpha1 .AnnotationConflict )
202+ if err := r .consumerClient .Patch (ctx , obj , client .RawPatch (types .MergePatchType , p )); client .IgnoreNotFound (err ) != nil {
203+ return fmt .Errorf ("clearing conflict: %w" , err )
204+ }
205+ return nil
206+ }
207+
171208func (r * specReconciler ) reconcileDelete (ctx context.Context , obj * unstructured.Unstructured ) (reconcile.Result , error ) {
172209 if ! controllerutil .ContainsFinalizer (obj , corev1alpha1 .FinalizerSyncer ) {
173210 return reconcile.Result {}, nil
@@ -241,28 +278,42 @@ func newUnstructured(gvk schema.GroupVersionKind) *unstructured.Unstructured {
241278 return u
242279}
243280
281+ // Ownership classifications returned by ownershipOf.
282+ const (
283+ ownerForeignUnmanaged = "foreign-unmanaged"
284+ ownerOurs = "ours"
285+ ownerByAnother = "owned-by-another"
286+ )
287+
244288// ownershipOf reports the marker owner of a provider object and whether it is
245289// ours (our consumer cluster + our consumer object UID).
246290func ownershipOf (obj * unstructured.Unstructured , localUID , consumerObjUID string ) (string , bool ) {
247291 ann := obj .GetAnnotations ()
248292 cluster := ann [corev1alpha1 .AnnotationConsumerClusterUID ]
249293 objUID := ann [corev1alpha1 .AnnotationConsumerObjectUID ]
250294 if cluster == "" && objUID == "" {
251- return "foreign-unmanaged" , false
295+ return ownerForeignUnmanaged , false
252296 }
253297 if cluster == localUID && objUID == consumerObjUID {
254- return "ours" , true
298+ return ownerOurs , true
255299 }
256- return "owned-by-another" , false
300+ return ownerByAnother , false
257301}
258302
259303func conflictReason (owner string ) string {
260- if owner == "foreign-unmanaged" {
304+ if owner == ownerForeignUnmanaged {
261305 return corev1alpha1 .ReasonForeignObjectExists
262306 }
263307 return corev1alpha1 .ReasonOwnedByAnother
264308}
265309
310+ func policyOrFail (p corev1alpha1.ConflictPolicy ) corev1alpha1.ConflictPolicy {
311+ if p == "" {
312+ return corev1alpha1 .ConflictPolicyFail
313+ }
314+ return p
315+ }
316+
266317func setObjCondition (obj * unstructured.Unstructured , status metav1.ConditionStatus , reason , msg string ) {
267318 cond := map [string ]any {
268319 "type" : corev1alpha1 .ConditionSynced ,
0 commit comments