Skip to content

Commit a06c483

Browse files
authored
Fix claim label tracking, name and generateName preservation (#55)
* Fix claim label tracking, name and generateName preservation Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com> * Remove unnecessary test comment Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com> * Fix multiple ownerrefs problem with claims Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com> * Add claim e2e Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com> * Add note for potential inconsistency in resource matching logic Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com> --------- Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
1 parent 4ee0016 commit a06c483

24 files changed

+1219
-64
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
- We should never silently continue a diff in the face of a failure, and we should strive not to make best-guesses.
22
- The diff command should strive for accuracy above all else, so in cases where errors or guesses may compromise accuracy, we should fail the diff completely.
33
- We should not emit partial results for a given XR; if given multiple XRs, it's okay to emit results only for those that pass so long as we call attention to the others that failed.
4-
- We should always emit useful logging with appropriate contextual objects attached.
4+
- We should always emit useful logging with appropriate contextual objects attached.
5+
- the earthly `reviewable` command should always be run with a long timeout. it runs all of our ITs and they can take several minutes.

cmd/diff/diff_integration_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,14 +1222,12 @@ Summary: 2 added`,
12221222
metadata:
12231223
annotations:
12241224
crossplane.io/composition-resource-name: nop-resource
1225-
- generateName: test-claim-82crv-
1225+
generateName: test-claim-82crv-
12261226
labels:
12271227
crossplane.io/claim-name: test-claim
12281228
crossplane.io/claim-namespace: existing-namespace
1229-
- crossplane.io/composite: test-claim-82crv
1230-
- name: test-claim-82crv
1231-
+ crossplane.io/composite: test-claim
1232-
+ name: test-claim
1229+
crossplane.io/composite: test-claim-82crv
1230+
name: test-claim-82crv
12331231
spec:
12341232
forProvider:
12351233
- configData: existing-value

cmd/diff/diffprocessor/diff_calculator.go

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,23 +93,14 @@ func (c *DefaultDiffCalculator) CalculateDiff(ctx context.Context, composite *un
9393
"resource", current)
9494
}
9595

96-
// Update owner references if needed
97-
c.resourceManager.UpdateOwnerRefs(composite, desired)
98-
99-
// If this is an existing resource but our desired object uses generateName,
100-
// set the name from the current resource for the dry-run apply
101-
if current != nil && name == "" && generateName != "" {
102-
currentName := current.GetName()
103-
c.logger.Debug("Using existing resource name for desired object with generateName",
104-
"resource", resourceID,
105-
"currentName", currentName)
96+
// Preserve existing resource identity for resources with generateName
97+
desired = c.preserveExistingResourceIdentity(current, desired, resourceID, name)
10698

107-
// Create a copy of the desired object and set the name from the current resource
108-
// This is needed for server-side apply which requires a name
109-
desiredCopy := desired.DeepCopy()
110-
desiredCopy.SetName(currentName)
111-
desired = desiredCopy
112-
}
99+
// Update owner references if needed (done after preserving existing labels)
100+
// IMPORTANT: For composed resources, the owner should be the XR, not a Claim.
101+
// When composite is the current XR from the cluster, we use it as the owner.
102+
// This ensures composed resources only have the XR as their controller owner.
103+
c.resourceManager.UpdateOwnerRefs(ctx, composite, desired)
113104

114105
// Determine what the resource would look like after application
115106
wouldBeResult := desired
@@ -306,3 +297,50 @@ func (c *DefaultDiffCalculator) CalculateRemovedResourceDiffs(ctx context.Contex
306297

307298
return removedDiffs, nil
308299
}
300+
301+
// preserveExistingResourceIdentity preserves the identity (name, generateName, labels) from an existing
302+
// resource with generateName to ensure dry-run apply works on the correct resource identity.
303+
// This is critical for claim scenarios where the rendered name differs from the generated name.
304+
func (c *DefaultDiffCalculator) preserveExistingResourceIdentity(current, desired *un.Unstructured, resourceID, renderedName string) *un.Unstructured {
305+
// Only preserve identity for existing resources with both generateName and name
306+
if current == nil || current.GetGenerateName() == "" || current.GetName() == "" {
307+
return desired
308+
}
309+
310+
currentName := current.GetName()
311+
currentGenerateName := current.GetGenerateName()
312+
313+
c.logger.Debug("Using existing resource identity for dry-run apply",
314+
"resource", resourceID,
315+
"renderedName", renderedName,
316+
"currentName", currentName,
317+
"currentGenerateName", currentGenerateName)
318+
319+
// Create a copy of the desired object and set the identity from the current resource
320+
// This ensures dry-run apply works on the correct resource identity
321+
desiredCopy := desired.DeepCopy()
322+
desiredCopy.SetName(currentName)
323+
desiredCopy.SetGenerateName(currentGenerateName)
324+
325+
// Preserve important labels from the existing resource, particularly the composite label
326+
// This ensures the dry-run apply gets the right labels and preserves them correctly
327+
currentLabels := current.GetLabels()
328+
if currentLabels != nil {
329+
desiredLabels := desiredCopy.GetLabels()
330+
if desiredLabels == nil {
331+
desiredLabels = make(map[string]string)
332+
}
333+
334+
// Preserve the composite label if it exists (critical for claims)
335+
if compositeLabel, exists := currentLabels["crossplane.io/composite"]; exists {
336+
desiredLabels["crossplane.io/composite"] = compositeLabel
337+
c.logger.Debug("Preserved composite label for dry-run apply",
338+
"resource", resourceID,
339+
"compositeLabel", compositeLabel)
340+
}
341+
342+
desiredCopy.SetLabels(desiredLabels)
343+
}
344+
345+
return desiredCopy
346+
}

cmd/diff/diffprocessor/resource_manager.go

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
un "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1313
"k8s.io/apimachinery/pkg/runtime/schema"
1414
"k8s.io/apimachinery/pkg/util/uuid"
15+
"k8s.io/utils/ptr"
1516

1617
"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
1718
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
@@ -24,7 +25,7 @@ type ResourceManager interface {
2425
FetchCurrentObject(ctx context.Context, composite *un.Unstructured, desired *un.Unstructured) (*un.Unstructured, bool, error)
2526

2627
// UpdateOwnerRefs ensures all OwnerReferences have valid UIDs
27-
UpdateOwnerRefs(parent *un.Unstructured, child *un.Unstructured)
28+
UpdateOwnerRefs(ctx context.Context, parent *un.Unstructured, child *un.Unstructured)
2829
}
2930

3031
// DefaultResourceManager implements ResourceManager interface.
@@ -289,6 +290,9 @@ func (m *DefaultResourceManager) findMatchingResource(
289290
continue
290291
}
291292

293+
// TODO: is this logic correct? we should definitely match composition-resource-name, even if the generateName
294+
// doesn't match. maybe as a fallback if we don't have the annotation?
295+
292296
// If we have a generateName, verify the match has the right prefix
293297
if generateName != "" {
294298
resName := res.GetName()
@@ -333,17 +337,79 @@ func (m *DefaultResourceManager) hasMatchingResourceName(annotations map[string]
333337
}
334338

335339
// UpdateOwnerRefs ensures all OwnerReferences have valid UIDs.
336-
func (m *DefaultResourceManager) UpdateOwnerRefs(parent *un.Unstructured, child *un.Unstructured) {
340+
// It handles Claims and XRs differently according to Crossplane's ownership model:
341+
// - Claims should never be controller owners of composed resources.
342+
// - XRs should be controller owners and use their UID for matching references.
343+
func (m *DefaultResourceManager) UpdateOwnerRefs(ctx context.Context, parent *un.Unstructured, child *un.Unstructured) {
337344
// if there's no parent, we are the parent.
338345
if parent == nil {
339346
m.logger.Debug("No parent provided for owner references update")
340347
return
341348
}
342349

343-
uid := parent.GetUID()
350+
// Check if the parent is a claim and dispatch to appropriate handler
351+
isParentAClaim := m.defClient.IsClaimResource(ctx, parent)
352+
353+
switch {
354+
case isParentAClaim:
355+
m.updateOwnerRefsForClaim(parent, child)
356+
default:
357+
m.updateOwnerRefsForXR(parent, child)
358+
}
359+
360+
// Update composite owner label (common for both Claims and XRs)
361+
m.updateCompositeOwnerLabel(ctx, parent, child)
362+
}
363+
364+
// updateOwnerRefsForClaim handles owner reference updates when the parent is a Claim.
365+
// Claims should not be controller owners of composed resources per Crossplane's model:
366+
// Claim -> owns -> XR -> owns -> Composed Resources.
367+
func (m *DefaultResourceManager) updateOwnerRefsForClaim(claim *un.Unstructured, child *un.Unstructured) {
368+
m.logger.Debug("Processing owner references with claim parent",
369+
"parentKind", claim.GetKind(),
370+
"parentName", claim.GetName(),
371+
"childKind", child.GetKind(),
372+
"childName", child.GetName())
373+
374+
// Get the current owner references
375+
refs := child.GetOwnerReferences()
376+
updatedRefs := make([]metav1.OwnerReference, 0, len(refs))
377+
378+
for _, ref := range refs {
379+
// Generate UIDs for any owner references that don't have them
380+
// For Claims, we never use the Claim's UID even for matching refs
381+
if ref.UID == "" {
382+
ref.UID = uuid.NewUUID()
383+
m.logger.Debug("Generated UID for owner reference",
384+
"refKind", ref.Kind,
385+
"refName", ref.Name,
386+
"newUID", ref.UID)
387+
}
388+
389+
// Ensure Claims are never controller owners
390+
if ref.Kind == claim.GetKind() && ref.Name == claim.GetName() {
391+
if ref.Controller != nil && *ref.Controller {
392+
ref.Controller = ptr.To(false)
393+
m.logger.Debug("Set Controller to false for claim owner reference",
394+
"refKind", ref.Kind,
395+
"refName", ref.Name)
396+
}
397+
}
398+
399+
updatedRefs = append(updatedRefs, ref)
400+
}
401+
402+
child.SetOwnerReferences(updatedRefs)
403+
}
404+
405+
// updateOwnerRefsForXR handles owner reference updates when the parent is an XR.
406+
// XRs should be controller owners of their composed resources and use their UID
407+
// for matching owner references.
408+
func (m *DefaultResourceManager) updateOwnerRefsForXR(xr *un.Unstructured, child *un.Unstructured) {
409+
uid := xr.GetUID()
344410
m.logger.Debug("Updating owner references",
345-
"parentKind", parent.GetKind(),
346-
"parentName", parent.GetName(),
411+
"parentKind", xr.GetKind(),
412+
"parentName", xr.GetName(),
347413
"parentUID", uid,
348414
"childKind", child.GetKind(),
349415
"childName", child.GetName())
@@ -359,11 +425,11 @@ func (m *DefaultResourceManager) UpdateOwnerRefs(parent *un.Unstructured, child
359425
for _, ref := range refs {
360426
originalUID := ref.UID
361427

362-
// if there is an owner ref on the dependent that we are pretty sure comes from us,
363-
// point the UID to the parent.
364-
if ref.Name == parent.GetName() &&
365-
ref.APIVersion == parent.GetAPIVersion() &&
366-
ref.Kind == parent.GetKind() &&
428+
// If there is an owner ref on the dependent that matches the parent XR,
429+
// use the parent's UID
430+
if ref.Name == xr.GetName() &&
431+
ref.APIVersion == xr.GetAPIVersion() &&
432+
ref.Kind == xr.GetKind() &&
367433
ref.UID == "" {
368434
ref.UID = uid
369435
m.logger.Debug("Updated matching owner reference with parent UID",
@@ -372,7 +438,7 @@ func (m *DefaultResourceManager) UpdateOwnerRefs(parent *un.Unstructured, child
372438
"newUID", ref.UID)
373439
}
374440

375-
// if we have a non-matching owner ref don't use the parent UID.
441+
// For non-matching owner refs, generate a random UID
376442
if ref.UID == "" {
377443
ref.UID = uuid.NewUUID()
378444
m.logger.Debug("Generated new random UID for owner reference",
@@ -387,17 +453,14 @@ func (m *DefaultResourceManager) UpdateOwnerRefs(parent *un.Unstructured, child
387453
// Update the object with the modified owner references
388454
child.SetOwnerReferences(updatedRefs)
389455

390-
// Update composite owner label
391-
m.updateCompositeOwnerLabel(parent, child)
392-
393456
m.logger.Debug("Updated owner references and labels",
394457
"newCount", len(updatedRefs))
395458
}
396459

397460
// updateCompositeOwnerLabel updates the ownership labels on the child resource.
398461
// For Claims, it sets claim-name and claim-namespace labels.
399462
// For XRs, it sets the composite label.
400-
func (m *DefaultResourceManager) updateCompositeOwnerLabel(parent, child *un.Unstructured) {
463+
func (m *DefaultResourceManager) updateCompositeOwnerLabel(ctx context.Context, parent, child *un.Unstructured) {
401464
if parent == nil {
402465
return
403466
}
@@ -419,18 +482,32 @@ func (m *DefaultResourceManager) updateCompositeOwnerLabel(parent, child *un.Uns
419482
}
420483

421484
// Check if the parent is a claim
422-
ctx := context.Background()
423485
isParentAClaim := m.defClient.IsClaimResource(ctx, parent)
424486

425487
switch {
426488
case isParentAClaim:
427489
// For claims, set claim-specific labels (all claims are namespaced)
428490
labels["crossplane.io/claim-name"] = parentName
429491
labels["crossplane.io/claim-namespace"] = parent.GetNamespace()
430-
m.logger.Debug("Updated claim owner labels",
431-
"claimName", parentName,
432-
"claimNamespace", parent.GetNamespace(),
433-
"child", child.GetName())
492+
493+
// For claims, only set the composite label if it doesn't already exist
494+
// If it exists, it likely points to a generated XR name which we should preserve
495+
existingComposite, hasComposite := labels["crossplane.io/composite"]
496+
if !hasComposite {
497+
// No existing composite label, set it to the claim name
498+
labels["crossplane.io/composite"] = parentName
499+
m.logger.Debug("Set composite label for new claim resource",
500+
"claimName", parentName,
501+
"claimNamespace", parent.GetNamespace(),
502+
"child", child.GetName())
503+
} else {
504+
// Preserve existing composite label (likely a generated XR name)
505+
m.logger.Debug("Preserved existing composite label for claim resource",
506+
"claimName", parentName,
507+
"claimNamespace", parent.GetNamespace(),
508+
"existingComposite", existingComposite,
509+
"child", child.GetName())
510+
}
434511
default:
435512
// For XRs, set the composite label
436513
labels["crossplane.io/composite"] = parentName

0 commit comments

Comments
 (0)