@@ -23,6 +23,7 @@ import (
2323 "github.com/google/go-cmp/cmp"
2424 kerrors "k8s.io/apimachinery/pkg/api/errors"
2525 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2627 "k8s.io/apimachinery/pkg/runtime"
2728 "k8s.io/apimachinery/pkg/runtime/schema"
2829 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -503,6 +504,39 @@ func TestPrepareJSONMerge(t *testing.T) {
503504 err error
504505 }
505506
507+ newManagedFieldsObj := func () * unstructured.Unstructured {
508+ u := & unstructured.Unstructured {}
509+ u .SetGroupVersionKind (schema.GroupVersionKind {
510+ Group : "example.com" ,
511+ Version : "v1" ,
512+ Kind : "TestResource" ,
513+ })
514+ u .SetName ("test-resource" )
515+ _ = unstructured .SetNestedField (u .Object , "new-ref-id" , "spec" , "forProvider" , "someRefId" )
516+ u .SetManagedFields ([]metav1.ManagedFieldsEntry {
517+ {
518+ Manager : fieldOwnerAPISimpleRefResolver ,
519+ Operation : metav1 .ManagedFieldsOperationApply ,
520+ FieldsType : "FieldsV1" ,
521+ FieldsV1 : & metav1.FieldsV1 {
522+ Raw : []byte (`{"f:spec":{"f:forProvider":{"f:otherRefId":{}}}}` ),
523+ },
524+ },
525+ })
526+ return u
527+ }
528+
529+ withoutManagedFields := func () * unstructured.Unstructured {
530+ u := & unstructured.Unstructured {}
531+ u .SetGroupVersionKind (schema.GroupVersionKind {
532+ Group : "example.com" ,
533+ Version : "v1" ,
534+ Kind : "TestResource" ,
535+ })
536+ u .SetName ("test-resource" )
537+ return u
538+ }
539+
506540 cases := map [string ]struct {
507541 reason string
508542 args args
@@ -522,6 +556,43 @@ func TestPrepareJSONMerge(t *testing.T) {
522556 patch : `{"name":"resolved"}` ,
523557 },
524558 },
559+ "PatchWithNoChanges" : {
560+ reason : "Should return empty patch when existing and resolved are the same." ,
561+ args : args {
562+ existing : & fake.LegacyManaged {
563+ ObjectMeta : metav1.ObjectMeta {
564+ Name : "same" ,
565+ },
566+ },
567+ resolved : & fake.LegacyManaged {
568+ ObjectMeta : metav1.ObjectMeta {
569+ Name : "same" ,
570+ },
571+ },
572+ },
573+ want : want {
574+ patch : `{}` ,
575+ },
576+ },
577+ "PatchWithManagedFieldsMerged" : {
578+ // Verifies that managed fields referencing fields not in the current patch
579+ // are still included in the final patch to maintain SSA field ownership.
580+ // Note: The expected patch includes managedFields metadata because existing
581+ // has none while resolved does - this is a test artifact. In production,
582+ // both objects would have identical managedFields so the diff wouldn't include them.
583+ reason : "Should merge previously managed fields into the patch to maintain SSA field ownership." ,
584+ args : args {
585+ existing : withoutManagedFields (),
586+ resolved : newManagedFieldsObj (),
587+ },
588+ want : want {
589+ patch : `{"apiVersion":"example.com/v1","kind":"TestResource",` +
590+ `"metadata":{"managedFields":[{"fieldsType":"FieldsV1",` +
591+ `"fieldsV1":{"f:spec":{"f:forProvider":{"f:otherRefId":{}}}},` +
592+ `"manager":"managed.crossplane.io/api-simple-reference-resolver",` +
593+ `"operation":"Apply"}]},"spec":{"forProvider":{"someRefId":"new-ref-id"}}}` ,
594+ },
595+ },
525596 }
526597
527598 for name , tc := range cases {
@@ -538,6 +609,127 @@ func TestPrepareJSONMerge(t *testing.T) {
538609 }
539610}
540611
612+ func TestMergeWithManagedFields (t * testing.T ) {
613+ type args struct {
614+ patch []byte
615+ obj runtime.Object
616+ fieldManager string
617+ }
618+
619+ type want struct {
620+ patch string
621+ err error
622+ }
623+
624+ withManagedFields := func () * unstructured.Unstructured {
625+ u := & unstructured.Unstructured {}
626+ u .SetGroupVersionKind (schema.GroupVersionKind {
627+ Group : "example.com" ,
628+ Version : "v1" ,
629+ Kind : "TestResource" ,
630+ })
631+ u .SetName ("test-resource" )
632+ _ = unstructured .SetNestedField (u .Object , "old-ref-value" , "spec" , "forProvider" , "someRefId" )
633+ u .SetManagedFields ([]metav1.ManagedFieldsEntry {
634+ {
635+ Manager : fieldOwnerAPISimpleRefResolver ,
636+ Operation : metav1 .ManagedFieldsOperationApply ,
637+ FieldsType : "FieldsV1" ,
638+ FieldsV1 : & metav1.FieldsV1 {
639+ Raw : []byte (`{"f:spec":{"f:forProvider":{"f:someRefId":{}}}}` ),
640+ },
641+ },
642+ })
643+ return u
644+ }
645+
646+ withDifferentManager := func () * unstructured.Unstructured {
647+ u := & unstructured.Unstructured {}
648+ u .SetGroupVersionKind (schema.GroupVersionKind {
649+ Group : "example.com" ,
650+ Version : "v1" ,
651+ Kind : "TestResource" ,
652+ })
653+ u .SetName ("test-resource" )
654+ _ = unstructured .SetNestedField (u .Object , "other-ref-value" , "spec" , "forProvider" , "otherRefId" )
655+ u .SetManagedFields ([]metav1.ManagedFieldsEntry {
656+ {
657+ Manager : "different-manager" ,
658+ Operation : metav1 .ManagedFieldsOperationApply ,
659+ FieldsType : "FieldsV1" ,
660+ FieldsV1 : & metav1.FieldsV1 {
661+ Raw : []byte (`{"f:spec":{"f:forProvider":{"f:otherRefId":{}}}}` ),
662+ },
663+ },
664+ })
665+ return u
666+ }
667+
668+ cases := map [string ]struct {
669+ reason string
670+ args args
671+ want want
672+ }{
673+ "NoManagedFields" : {
674+ reason : "Should return the original patch when there are no managed fields." ,
675+ args : args {
676+ patch : []byte (`{"metadata":{"name":"resolved"}}` ),
677+ obj : & fake.LegacyManaged {},
678+ fieldManager : fieldOwnerAPISimpleRefResolver ,
679+ },
680+ want : want {
681+ patch : `{"metadata":{"name":"resolved"}}` ,
682+ },
683+ },
684+ "EmptyPatch" : {
685+ reason : "Should return empty patch when patch is empty and no managed fields." ,
686+ args : args {
687+ patch : []byte (`{}` ),
688+ obj : & fake.LegacyManaged {},
689+ fieldManager : fieldOwnerAPISimpleRefResolver ,
690+ },
691+ want : want {
692+ patch : `{}` ,
693+ },
694+ },
695+ "WithManagedFields" : {
696+ reason : "Should merge previously managed fields into the patch to maintain field ownership." ,
697+ args : args {
698+ patch : []byte (`{"spec":{"forProvider":{"newRefId":"new-value"}}}` ),
699+ obj : withManagedFields (),
700+ fieldManager : fieldOwnerAPISimpleRefResolver ,
701+ },
702+ want : want {
703+ patch : `{"apiVersion":"example.com/v1","kind":"TestResource","spec":{"forProvider":{"newRefId":"new-value","someRefId":"old-ref-value"}}}` ,
704+ },
705+ },
706+ "DifferentFieldManager" : {
707+ reason : "Should not include managed fields from a different field manager." ,
708+ args : args {
709+ patch : []byte (`{"metadata":{"name":"resolved"}}` ),
710+ obj : withDifferentManager (),
711+ fieldManager : fieldOwnerAPISimpleRefResolver ,
712+ },
713+ want : want {
714+ patch : `{"metadata":{"name":"resolved"}}` ,
715+ },
716+ },
717+ }
718+
719+ for name , tc := range cases {
720+ t .Run (name , func (t * testing.T ) {
721+ patch , err := mergeWithManagedFields (tc .args .patch , tc .args .obj , tc .args .fieldManager )
722+ if diff := cmp .Diff (tc .want .err , err , test .EquateErrors ()); diff != "" {
723+ t .Errorf ("\n %s\n mergeWithManagedFields(...): -wantErr, +gotErr:\n %s" , tc .reason , diff )
724+ }
725+
726+ if diff := cmp .Diff (tc .want .patch , string (patch )); diff != "" {
727+ t .Errorf ("\n %s\n mergeWithManagedFields(...): -want, +got:\n %s" , tc .reason , diff )
728+ }
729+ })
730+ }
731+ }
732+
541733func TestRetryingCriticalAnnotationUpdater (t * testing.T ) {
542734 errBoom := errors .New ("boom" )
543735
0 commit comments