@@ -303,7 +303,7 @@ func (p *DefaultDiffProcessor) DiffSingleResource(ctx context.Context, res *un.U
303303 // Check for nested XRs in the composed resources and process them recursively
304304 p .config .Logger .Debug ("Checking for nested XRs" , "resource" , resourceID , "composedCount" , len (desired .ComposedResources ))
305305
306- nestedDiffs , err := p .ProcessNestedXRs (ctx , desired .ComposedResources , compositionProvider , resourceID , 1 )
306+ nestedDiffs , err := p .ProcessNestedXRs (ctx , desired .ComposedResources , compositionProvider , resourceID , xr , 1 )
307307 if err != nil {
308308 p .config .Logger .Debug ("Error processing nested XRs" , "resource" , resourceID , "error" , err )
309309 return nil , errors .Wrap (err , "cannot process nested XRs" )
@@ -329,6 +329,7 @@ func (p *DefaultDiffProcessor) ProcessNestedXRs(
329329 composedResources []cpd.Unstructured ,
330330 compositionProvider types.CompositionProvider ,
331331 parentResourceID string ,
332+ parentXR * cmp.Unstructured ,
332333 depth int ,
333334) (map [string ]* dt.ResourceDiff , error ) {
334335 if depth > p .config .MaxNestedDepth {
@@ -345,27 +346,88 @@ func (p *DefaultDiffProcessor) ProcessNestedXRs(
345346 "composedResourceCount" , len (composedResources ),
346347 "depth" , depth )
347348
349+ // Fetch observed resources from parent XR to find existing nested XRs
350+ // This allows us to preserve the identity of nested XRs that already exist in the cluster
351+ var observedResources []cpd.Unstructured
352+ if parentXR != nil {
353+ obs , err := p .diffCalculator .FetchObservedResources (ctx , parentXR )
354+ if err != nil {
355+ // Log but continue - nested XRs without existing cluster state will show as new (with "(generated)")
356+ p .config .Logger .Debug ("Could not fetch observed resources for parent XR (continuing)" ,
357+ "parentResource" , parentResourceID ,
358+ "error" , err )
359+ } else {
360+ observedResources = obs
361+ }
362+ }
363+
348364 allDiffs := make (map [string ]* dt.ResourceDiff )
349365
350366 for _ , composed := range composedResources {
351- un := & un.Unstructured {Object : composed .UnstructuredContent ()}
367+ nestedXR := & un.Unstructured {Object : composed .UnstructuredContent ()}
352368
353369 // Check if this composed resource is itself an XR
354- isXR , _ := p .getCompositeResourceXRD (ctx , un )
370+ isXR , _ := p .getCompositeResourceXRD (ctx , nestedXR )
355371
356372 if ! isXR {
357373 // Skip non-XR resources
358374 continue
359375 }
360376
361- nestedResourceID := fmt .Sprintf ("%s/%s (nested depth %d)" , un .GetKind (), un .GetName (), depth )
377+ nestedResourceID := fmt .Sprintf ("%s/%s (nested depth %d)" , nestedXR .GetKind (), nestedXR .GetName (), depth )
362378 p .config .Logger .Debug ("Found nested XR, processing recursively" ,
363379 "nestedXR" , nestedResourceID ,
364380 "parentXR" , parentResourceID ,
365381 "depth" , depth )
366382
383+ // Find the matching existing nested XR in observed resources (if it exists)
384+ // Match by composition-resource-name annotation to find the correct existing resource
385+ var existingNestedXR * un.Unstructured
386+ compositionResourceName := nestedXR .GetAnnotations ()["crossplane.io/composition-resource-name" ]
387+ if compositionResourceName != "" {
388+ for _ , obs := range observedResources {
389+ obsUnstructured := & un.Unstructured {Object : obs .UnstructuredContent ()}
390+ obsCompResName := obsUnstructured .GetAnnotations ()["crossplane.io/composition-resource-name" ]
391+
392+ // Match by composition-resource-name annotation and kind
393+ if obsCompResName == compositionResourceName && obsUnstructured .GetKind () == nestedXR .GetKind () {
394+ existingNestedXR = obsUnstructured
395+ p .config .Logger .Debug ("Found existing nested XR in cluster" ,
396+ "nestedXR" , nestedResourceID ,
397+ "existingName" , existingNestedXR .GetName (),
398+ "compositionResourceName" , compositionResourceName )
399+ break
400+ }
401+ }
402+ }
403+
404+ // If we found an existing nested XR, preserve its identity (name, composite label)
405+ // This ensures its managed resources can be matched correctly
406+ if existingNestedXR != nil {
407+ // Preserve the actual cluster name
408+ nestedXR .SetName (existingNestedXR .GetName ())
409+ nestedXR .SetGenerateName (existingNestedXR .GetGenerateName ())
410+
411+ // Preserve the composite label so child resources get matched correctly
412+ if labels := existingNestedXR .GetLabels (); labels != nil {
413+ if compositeLabel , exists := labels ["crossplane.io/composite" ]; exists {
414+ nestedXRLabels := nestedXR .GetLabels ()
415+ if nestedXRLabels == nil {
416+ nestedXRLabels = make (map [string ]string )
417+ }
418+ nestedXRLabels ["crossplane.io/composite" ] = compositeLabel
419+ nestedXR .SetLabels (nestedXRLabels )
420+
421+ p .config .Logger .Debug ("Preserved nested XR identity" ,
422+ "nestedXR" , nestedResourceID ,
423+ "preservedName" , nestedXR .GetName (),
424+ "preservedCompositeLabel" , compositeLabel )
425+ }
426+ }
427+ }
428+
367429 // Recursively process this nested XR
368- nestedDiffs , err := p .DiffSingleResource (ctx , un , compositionProvider )
430+ nestedDiffs , err := p .DiffSingleResource (ctx , nestedXR , compositionProvider )
369431 if err != nil {
370432 // Check if the error is due to missing composition
371433 // Note: It's valid to have an XRD in Crossplane without a composition attached to it.
@@ -376,7 +438,7 @@ func (p *DefaultDiffProcessor) ProcessNestedXRs(
376438 p .config .Logger .Info ("Skipping nested XR processing due to missing composition" ,
377439 "nestedXR" , nestedResourceID ,
378440 "parentXR" , parentResourceID ,
379- "gvk" , un .GroupVersionKind ().String ())
441+ "gvk" , nestedXR .GroupVersionKind ().String ())
380442 // Continue processing other nested XRs
381443 continue
382444 }
0 commit comments