@@ -12,6 +12,7 @@ import (
1212 un "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1313 "k8s.io/apimachinery/pkg/runtime"
1414 "k8s.io/apimachinery/pkg/runtime/schema"
15+ k8stypes "k8s.io/apimachinery/pkg/types"
1516
1617 "github.com/crossplane/crossplane-runtime/v2/pkg/errors"
1718 "github.com/crossplane/crossplane-runtime/v2/pkg/logging"
@@ -20,8 +21,6 @@ import (
2021)
2122
2223// CompositionClient handles operations related to Compositions.
23- //
24- //nolint:interfacebloat // Composition operations are cohesive; splitting would fragment the API.
2524type CompositionClient interface {
2625 core.Initializable
2726
@@ -34,16 +33,16 @@ type CompositionClient interface {
3433 // GetComposition gets a composition by name
3534 GetComposition (ctx context.Context , name string ) (* apiextensionsv1.Composition , error )
3635
37- // FindCompositesUsingComposition finds all composites (XRs and Claims) that use the specified composition
38- FindCompositesUsingComposition ( ctx context. Context , compositionName string , namespace string ) ([] * un. Unstructured , error )
39-
40- // GetCompositesByName fetches the user-named composites (XRs or Claims) for a composition.
41- // For each ResourceRef, the XR GVK derived from comp.Spec.CompositeTypeRef is tried first, then the
42- // claim GVK derived from the XRD if defined. A ref is "matched" only when (a) the cluster lookup
43- // succeeds AND (b) the resource references this composition by name. Refs whose lookups all 404, or
44- // that exist but reference a different composition, are returned in `unmatched` (not as an error) .
36+ // FindComposites locates composites (XRs and Claims) that reference a composition.
37+ // When opts.Refs is non-empty, it performs ref-based lookup: each ref is resolved against the
38+ // composition's XR GVK (then claim GVK on 404 if the XRD defines a claim type). A ref is included
39+ // in the result only when (a) the cluster lookup succeeds AND (b) the resource references this
40+ // composition by name. Refs that don't satisfy both are silently omitted; callers derive
41+ // "unmatched" from the diff between input refs and returned objects.
42+ // When opts.Refs is empty, it performs default discovery scoped to opts.Namespace, listing all
43+ // XRs (and Claims, if the XRD defines them) of the appropriate GVK and filtering by composition .
4544 // NotFound responses are tolerated; non-NotFound transport errors propagate.
46- GetCompositesByName (ctx context.Context , comp * apiextensionsv1.Composition , refs [] dtypes.ResourceRef ) (matched []* un.Unstructured , unmatched []dtypes. ResourceRef , err error )
45+ FindComposites (ctx context.Context , comp * apiextensionsv1.Composition , opts dtypes.FindCompositesOptions ) ([]* un.Unstructured , error )
4746}
4847
4948// DefaultCompositionClient implements CompositionClient.
@@ -702,7 +701,11 @@ func (c *DefaultCompositionClient) findByTypeReference(ctx context.Context, _ *u
702701}
703702
704703// FindCompositesUsingComposition finds all composites (XRs and Claims) that use the specified composition.
705- func (c * DefaultCompositionClient ) FindCompositesUsingComposition (ctx context.Context , compositionName string , namespace string ) ([]* un.Unstructured , error ) {
704+ // findByListing implements default-discovery for FindComposites: list every XR (and Claim, if the
705+ // XRD defines one) of the composition's target GVK, scoped to namespace, and filter by composition.
706+ // Pre-existing behavior: if the composition itself isn't in the cluster (net-new), the GetComposition
707+ // lookup fails and the error propagates to the caller, which is expected to handle "net-new" gracefully.
708+ func (c * DefaultCompositionClient ) findByListing (ctx context.Context , compositionName string , namespace string ) ([]* un.Unstructured , error ) {
706709 c .logger .Debug ("Finding composites using composition" ,
707710 "compositionName" , compositionName ,
708711 "namespace" , namespace )
@@ -837,106 +840,150 @@ func (c *DefaultCompositionClient) resourceUsesComposition(resource *un.Unstruct
837840
838841// GetCompositesByName fetches the user-named composites for a composition.
839842// See the CompositionClient interface for the full contract.
840- func (c * DefaultCompositionClient ) GetCompositesByName (ctx context.Context , comp * apiextensionsv1.Composition , refs []dtypes.ResourceRef ) ([]* un.Unstructured , []dtypes.ResourceRef , error ) {
843+ // findByRefs implements ref-based lookup for FindComposites: each ref is resolved against the
844+ // composition's XR GVK first, then the claim GVK on 404 if the XRD defines a claim type. Refs that
845+ // fail both lookups, or that exist but reference a different composition, are silently dropped from
846+ // the result. NotFound responses are tolerated; other errors propagate.
847+ //
848+ // Step 4 will split this into resolveCompositeTypes + lookupRef.
849+ // findByRefs implements ref-based lookup for FindComposites: each ref is resolved against the
850+ // composition's XR GVK first, then the claim GVK on 404 if the XRD defines a claim type.
851+ func (c * DefaultCompositionClient ) findByRefs (ctx context.Context , comp * apiextensionsv1.Composition , refs []k8stypes.NamespacedName ) ([]* un.Unstructured , error ) {
841852 if len (refs ) == 0 {
842- return nil , nil , nil
853+ return nil , nil
843854 }
844855
845- // Resolve XR GVK from the (possibly net-new) composition file. No cluster lookup needed.
846- gv , err := schema .ParseGroupVersion (comp .Spec .CompositeTypeRef .APIVersion )
856+ types , err := c .resolveCompositeTypes (ctx , comp )
847857 if err != nil {
848- return nil , nil , errors . Wrapf ( err , "cannot parse compositeTypeRef apiVersion %q" , comp . Spec . CompositeTypeRef . APIVersion )
858+ return nil , err
849859 }
850860
851- xrGVK := schema.GroupVersionKind {
852- Group : gv .Group ,
853- Version : gv .Version ,
854- Kind : comp .Spec .CompositeTypeRef .Kind ,
861+ var matched []* un.Unstructured
862+
863+ for _ , ref := range refs {
864+ obj , err := c .lookupRef (ctx , ref , types , comp .GetName ())
865+ if err != nil {
866+ return nil , err
867+ }
868+
869+ if obj != nil {
870+ matched = append (matched , obj )
871+ }
855872 }
856873
857- // Resolve claim GVK best-effort. Missing XRD or no claimNames → claim lookups are skipped.
858- var claimGVK schema.GroupVersionKind
874+ return matched , nil
875+ }
876+
877+ // compositeTypes holds the GVKs needed to look up a composite by name. claimGVK is empty
878+ // when the XRD has no claimNames or could not be retrieved (graceful degradation).
879+ type compositeTypes struct {
880+ xrGVK schema.GroupVersionKind
881+ claimGVK schema.GroupVersionKind
882+ }
883+
884+ // resolveCompositeTypes derives the XR GVK from the composition's CompositeTypeRef and
885+ // best-effort resolves the claim GVK from the XRD. XRD lookup failures and missing claimNames
886+ // produce an empty claim GVK without erroring (graceful degradation).
887+ func (c * DefaultCompositionClient ) resolveCompositeTypes (ctx context.Context , comp * apiextensionsv1.Composition ) (compositeTypes , error ) {
888+ gv , err := schema .ParseGroupVersion (comp .Spec .CompositeTypeRef .APIVersion )
889+ if err != nil {
890+ return compositeTypes {}, errors .Wrapf (err , "cannot parse compositeTypeRef apiVersion %q" , comp .Spec .CompositeTypeRef .APIVersion )
891+ }
892+
893+ types := compositeTypes {
894+ xrGVK : schema.GroupVersionKind {
895+ Group : gv .Group ,
896+ Version : gv .Version ,
897+ Kind : comp .Spec .CompositeTypeRef .Kind ,
898+ },
899+ }
859900
860- xrd , xrdErr := c .definitionClient .GetXRDForXR (ctx , xrGVK )
901+ xrd , xrdErr := c .definitionClient .GetXRDForXR (ctx , types . xrGVK )
861902
862903 switch {
863904 case xrdErr != nil :
864905 c .logger .Debug ("XRD lookup failed; skipping claim-GVK fallback for --resource lookups" ,
865- "xrGVK" , xrGVK .String (), "error" , xrdErr )
906+ "xrGVK" , types . xrGVK .String (), "error" , xrdErr )
866907 case xrd != nil :
867908 gvk , err := c .getClaimTypeFromXRD (xrd )
868909 if err != nil {
869910 c .logger .Debug ("could not extract claim type from XRD; skipping claim-GVK fallback" ,
870911 "xrd" , xrd .GetName (), "error" , err )
871912 } else {
872- claimGVK = gvk
913+ types . claimGVK = gvk
873914 }
874915 }
875916
876- var (
877- matched []* un.Unstructured
878- unmatched []dtypes.ResourceRef
879- )
917+ return types , nil
918+ }
880919
881- for _ , ref := range refs {
882- // Try XR-GVK first.
883- obj , err := c .resourceClient .GetResource (ctx , xrGVK , ref .Namespace , ref .Name )
920+ // lookupRef resolves a single ref against (compName, types). Tries the XR GVK first, then the
921+ // claim GVK on 404 if non-empty. Returns the matched object, or nil if not found anywhere or
922+ // found but referencing a different composition. Non-NotFound cluster errors propagate.
923+ //
924+ // NOTE: when XR GVK returns a hit but the resource references a different composition, this
925+ // returns nil WITHOUT trying the claim GVK. Follow-up F1 (Copilot review on PR #322) tracks
926+ // switching this to fall through to the claim path so same-name XR+Claim collisions resolve
927+ // correctly. Step 4 preserves the existing behavior.
928+ func (c * DefaultCompositionClient ) lookupRef (ctx context.Context , ref k8stypes.NamespacedName , types compositeTypes , compName string ) (* un.Unstructured , error ) {
929+ // Try XR GVK first.
930+ obj , err := c .resourceClient .GetResource (ctx , types .xrGVK , ref .Namespace , ref .Name )
884931
885- switch {
886- case err == nil :
887- if c .resourceUsesComposition (obj , comp . GetName () ) {
888- c .logger .Debug ("matched ref via XR GVK" ,
889- "ref" , ref .String (), "composition" , comp . GetName () )
932+ switch {
933+ case err == nil :
934+ if c .resourceUsesComposition (obj , compName ) {
935+ c .logger .Debug ("matched ref via XR GVK" ,
936+ "ref" , ref .String (), "composition" , compName )
890937
891- matched = append (matched , obj )
938+ return obj , nil
939+ }
892940
893- continue
894- }
941+ c . logger . Debug ( "ref exists as XR but does not reference this composition" ,
942+ "ref" , ref . String (), "composition" , compName )
895943
896- c .logger .Debug ("ref exists as XR but does not reference this composition" ,
897- "ref" , ref .String (), "composition" , comp .GetName ())
944+ return nil , nil
945+ case ! apierrors .IsNotFound (err ):
946+ return nil , errors .Wrapf (err , "cannot fetch composite %s as %s" , ref .String (), types .xrGVK )
947+ }
898948
899- unmatched = append (unmatched , ref )
949+ // XR GVK was 404. Try claim GVK if available.
950+ if types .claimGVK .Empty () {
951+ c .logger .Debug ("ref not found as XR and no claim GVK to try" ,
952+ "ref" , ref .String ())
900953
901- continue
902- case ! apierrors .IsNotFound (err ):
903- return nil , nil , errors .Wrapf (err , "cannot fetch composite %s as %s" , ref .String (), xrGVK )
904- }
954+ return nil , nil
955+ }
905956
906- // XR-GVK was 404. Try claim GVK if available.
907- if claimGVK .Empty () {
908- c .logger .Debug ("ref not found as XR and no claim GVK to try" ,
909- "ref" , ref .String ())
957+ obj , err = c .resourceClient .GetResource (ctx , types .claimGVK , ref .Namespace , ref .Name )
910958
911- unmatched = append (unmatched , ref )
959+ switch {
960+ case err == nil :
961+ if c .resourceUsesComposition (obj , compName ) {
962+ c .logger .Debug ("matched ref via claim GVK" ,
963+ "ref" , ref .String (), "composition" , compName )
912964
913- continue
965+ return obj , nil
914966 }
915967
916- obj , err = c .resourceClient .GetResource (ctx , claimGVK , ref .Namespace , ref .Name )
968+ c .logger .Debug ("ref exists as claim but does not reference this composition" ,
969+ "ref" , ref .String (), "composition" , compName )
917970
918- switch {
919- case err == nil :
920- if c .resourceUsesComposition (obj , comp .GetName ()) {
921- c .logger .Debug ("matched ref via claim GVK" ,
922- "ref" , ref .String (), "composition" , comp .GetName ())
971+ return nil , nil
972+ case apierrors .IsNotFound (err ):
973+ c .logger .Debug ("ref not found as XR or claim" , "ref" , ref .String ())
923974
924- matched = append (matched , obj )
925-
926- continue
927- }
928-
929- c .logger .Debug ("ref exists as claim but does not reference this composition" ,
930- "ref" , ref .String (), "composition" , comp .GetName ())
931-
932- unmatched = append (unmatched , ref )
933- case apierrors .IsNotFound (err ):
934- c .logger .Debug ("ref not found as XR or claim" , "ref" , ref .String ())
935- unmatched = append (unmatched , ref )
936- default :
937- return nil , nil , errors .Wrapf (err , "cannot fetch composite %s as %s" , ref .String (), claimGVK )
938- }
975+ return nil , nil
976+ default :
977+ return nil , errors .Wrapf (err , "cannot fetch composite %s as %s" , ref .String (), types .claimGVK )
939978 }
979+ }
940980
941- return matched , unmatched , nil
981+ // FindComposites dispatches to ref-based or listing-based discovery based on opts.Refs.
982+ func (c * DefaultCompositionClient ) FindComposites (ctx context.Context , comp * apiextensionsv1.Composition , opts dtypes.FindCompositesOptions ) ([]* un.Unstructured , error ) {
983+ switch {
984+ case len (opts .Refs ) > 0 :
985+ return c .findByRefs (ctx , comp , opts .Refs )
986+ default :
987+ return c .findByListing (ctx , comp .GetName (), opts .Namespace )
988+ }
942989}
0 commit comments