@@ -7,25 +7,36 @@ import (
77 "sync"
88
99 "github.com/crossplane-contrib/crossplane-diff/cmd/diff/client/core"
10+ extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1011 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1112 un "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+ "k8s.io/apimachinery/pkg/runtime"
1214 "k8s.io/apimachinery/pkg/runtime/schema"
1315 "k8s.io/client-go/dynamic"
1416
1517 "github.com/crossplane/crossplane-runtime/v2/pkg/errors"
1618 "github.com/crossplane/crossplane-runtime/v2/pkg/logging"
19+
20+ xpextv1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1"
21+ xpextv2 "github.com/crossplane/crossplane/v2/apis/apiextensions/v2"
1722)
1823
1924// SchemaClient handles operations related to Kubernetes schemas and CRDs.
2025type SchemaClient interface {
2126 // GetCRD gets the CustomResourceDefinition for a given GVK
22- GetCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* un.Unstructured , error )
27+ GetCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* extv1.CustomResourceDefinition , error )
28+
29+ // GetCRDByName gets the CustomResourceDefinition by its name
30+ GetCRDByName (name string ) (* extv1.CustomResourceDefinition , error )
2331
2432 // IsCRDRequired checks if a GVK requires a CRD
2533 IsCRDRequired (ctx context.Context , gvk schema.GroupVersionKind ) bool
2634
27- // ValidateResource validates a resource against its schema
28- ValidateResource (ctx context.Context , resource * un.Unstructured ) error
35+ // LoadCRDsFromXRDs converts XRDs to CRDs and caches them
36+ LoadCRDsFromXRDs (ctx context.Context , xrds []* un.Unstructured ) error
37+
38+ // GetAllCRDs returns all cached CRDs (needed for external validation library)
39+ GetAllCRDs () []* extv1.CustomResourceDefinition
2940}
3041
3142// DefaultSchemaClient implements SchemaClient.
@@ -37,6 +48,12 @@ type DefaultSchemaClient struct {
3748 // Resource type caching
3849 resourceTypeMap map [schema.GroupVersionKind ]bool
3950 resourceMapMu sync.RWMutex
51+
52+ // CRD caching - consolidated from SchemaValidator
53+ crds []* extv1.CustomResourceDefinition
54+ crdsMu sync.RWMutex
55+ crdByName map [string ]* extv1.CustomResourceDefinition // for fast lookup by name
56+ xrdToCRDName map [string ]string // maps XRD name to CRD name
4057}
4158
4259// NewSchemaClient creates a new DefaultSchemaClient.
@@ -46,39 +63,64 @@ func NewSchemaClient(clients *core.Clients, typeConverter TypeConverter, logger
4663 typeConverter : typeConverter ,
4764 logger : logger ,
4865 resourceTypeMap : make (map [schema.GroupVersionKind ]bool ),
66+ crds : []* extv1.CustomResourceDefinition {},
67+ crdByName : make (map [string ]* extv1.CustomResourceDefinition ),
68+ xrdToCRDName : make (map [string ]string ),
4969 }
5070}
5171
5272// GetCRD gets the CustomResourceDefinition for a given GVK.
53- func (c * DefaultSchemaClient ) GetCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* un. Unstructured , error ) {
54- // Get the pluralized resource name
73+ func (c * DefaultSchemaClient ) GetCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* extv1. CustomResourceDefinition , error ) {
74+ // Get the pluralized resource name to construct CRD name
5575 resourceName , err := c .typeConverter .GetResourceNameForGVK (ctx , gvk )
5676 if err != nil {
5777 return nil , errors .Wrapf (err , "cannot determine CRD name for %s" , gvk .String ())
5878 }
5979
60- c .logger .Debug ("Looking up CRD" , "gvk" , gvk .String (), "crdName" , resourceName )
61-
62- // Construct the CRD name using the resource name and group
80+ // Construct the full CRD name
6381 crdName := fmt .Sprintf ("%s.%s" , resourceName , gvk .Group )
6482
83+ // Check cache first
84+ c .crdsMu .RLock ()
85+
86+ if cached , ok := c .crdByName [crdName ]; ok {
87+ c .crdsMu .RUnlock ()
88+ c .logger .Debug ("Using cached CRD" , "gvk" , gvk .String (), "crdName" , crdName )
89+
90+ return cached , nil
91+ }
92+
93+ c .crdsMu .RUnlock ()
94+
95+ c .logger .Debug ("Looking up CRD" , "gvk" , gvk .String (), "crdName" , resourceName )
96+
6597 // Define the CRD GVR directly to avoid recursion
6698 crdGVR := schema.GroupVersionResource {
6799 Group : "apiextensions.k8s.io" ,
68100 Version : "v1" ,
69101 Resource : "customresourcedefinitions" ,
70102 }
71103
72- // Fetch the CRD
73- crd , err := c .dynamicClient .Resource (crdGVR ).Get (ctx , crdName , metav1.GetOptions {})
104+ // Fetch the CRD from cluster
105+ crdObj , err := c .dynamicClient .Resource (crdGVR ).Get (ctx , crdName , metav1.GetOptions {})
74106 if err != nil {
75107 c .logger .Debug ("Failed to get CRD" , "gvk" , gvk .String (), "crdName" , crdName , "error" , err )
76108 return nil , errors .Wrapf (err , "cannot get CRD %s for %s" , crdName , gvk .String ())
77109 }
78110
79111 c .logger .Debug ("Successfully retrieved CRD" , "gvk" , gvk .String (), "crdName" , resourceName )
80112
81- return crd , nil
113+ // Convert to typed CRD
114+ crdTyped := & extv1.CustomResourceDefinition {}
115+ if err := runtime .DefaultUnstructuredConverter .FromUnstructured (crdObj .Object , crdTyped ); err != nil {
116+ c .logger .Debug ("Error converting CRD" , "gvk" , gvk .String (), "crdName" , crdName , "error" , err )
117+ return nil , errors .Wrapf (err , "cannot convert CRD %s to typed" , crdName )
118+ }
119+
120+ // Add to cache
121+ c .addCRD (crdTyped )
122+
123+ return crdTyped , nil
82124}
83125
84126// IsCRDRequired checks if a GVK requires a CRD.
@@ -135,17 +177,222 @@ func (c *DefaultSchemaClient) IsCRDRequired(ctx context.Context, gvk schema.Grou
135177 return true
136178}
137179
138- // ValidateResource validates a resource against its schema.
139- func (c * DefaultSchemaClient ) ValidateResource (_ context.Context , resource * un.Unstructured ) error {
140- // This would use OpenAPI validation - simplified for now
141- c .logger .Debug ("Validating resource" , "kind" , resource .GetKind (), "name" , resource .GetName ())
142- return nil
143- }
144-
145180// Helper to cache resource type requirements.
146181func (c * DefaultSchemaClient ) cacheResourceType (gvk schema.GroupVersionKind , requiresCRD bool ) {
147182 c .resourceMapMu .Lock ()
148183 defer c .resourceMapMu .Unlock ()
149184
150185 c .resourceTypeMap [gvk ] = requiresCRD
151186}
187+
188+ // extractGVKsFromXRDs extracts GVKs from multiple XRDs. This is a pure function with no side effects.
189+ func extractGVKsFromXRDs (xrds []* un.Unstructured ) ([]schema.GroupVersionKind , error ) {
190+ var allGVKs []schema.GroupVersionKind
191+
192+ for _ , xrd := range xrds {
193+ gvks , err := extractGVKsFromXRD (xrd )
194+ if err != nil {
195+ return nil , errors .Wrapf (err , "failed to extract GVKs from XRD %s" , xrd .GetName ())
196+ }
197+
198+ allGVKs = append (allGVKs , gvks ... )
199+ }
200+
201+ return allGVKs , nil
202+ }
203+
204+ // extractGVKsFromXRD extracts all GroupVersionKinds from an XRD using strongly-typed conversion.
205+ // This method handles both v1 and v2 XRDs and leverages Kubernetes runtime conversion.
206+ func extractGVKsFromXRD (xrd * un.Unstructured ) ([]schema.GroupVersionKind , error ) {
207+ apiVersion := xrd .GetAPIVersion ()
208+
209+ switch apiVersion {
210+ case "apiextensions.crossplane.io/v1" :
211+ return extractGVKsFromV1XRD (xrd )
212+ case "apiextensions.crossplane.io/v2" :
213+ return extractGVKsFromV2XRD (xrd )
214+ default :
215+ return nil , errors .Errorf ("unsupported XRD apiVersion %s in XRD %s" , apiVersion , xrd .GetName ())
216+ }
217+ }
218+
219+ // extractGVKsFromV1XRD extracts GVKs from a v1 XRD using strongly-typed conversion.
220+ func extractGVKsFromV1XRD (xrd * un.Unstructured ) ([]schema.GroupVersionKind , error ) {
221+ typedXRD := & xpextv1.CompositeResourceDefinition {}
222+ if err := runtime .DefaultUnstructuredConverter .FromUnstructured (xrd .Object , typedXRD ); err != nil {
223+ return nil , errors .Wrapf (err , "cannot convert XRD %s to v1 typed object" , xrd .GetName ())
224+ }
225+
226+ // Extract GVKs for each version - no validation needed since XRDs from server are guaranteed valid
227+ gvks := make ([]schema.GroupVersionKind , 0 , len (typedXRD .Spec .Versions ))
228+ for _ , version := range typedXRD .Spec .Versions {
229+ gvks = append (gvks , schema.GroupVersionKind {
230+ Group : typedXRD .Spec .Group ,
231+ Version : version .Name ,
232+ Kind : typedXRD .Spec .Names .Kind ,
233+ })
234+ }
235+
236+ return gvks , nil
237+ }
238+
239+ // extractGVKsFromV2XRD extracts GVKs from a v2 XRD using strongly-typed conversion.
240+ func extractGVKsFromV2XRD (xrd * un.Unstructured ) ([]schema.GroupVersionKind , error ) {
241+ typedXRD := & xpextv2.CompositeResourceDefinition {}
242+ if err := runtime .DefaultUnstructuredConverter .FromUnstructured (xrd .Object , typedXRD ); err != nil {
243+ return nil , errors .Wrapf (err , "cannot convert XRD %s to v2 typed object" , xrd .GetName ())
244+ }
245+
246+ // Extract GVKs for each version - no validation needed since XRDs from server are guaranteed valid
247+ gvks := make ([]schema.GroupVersionKind , 0 , len (typedXRD .Spec .Versions ))
248+ for _ , version := range typedXRD .Spec .Versions {
249+ gvks = append (gvks , schema.GroupVersionKind {
250+ Group : typedXRD .Spec .Group ,
251+ Version : version .Name ,
252+ Kind : typedXRD .Spec .Names .Kind ,
253+ })
254+ }
255+
256+ return gvks , nil
257+ }
258+
259+ // LoadCRDsFromXRDs fetches corresponding CRDs from the cluster for the given XRDs and caches them.
260+ // Instead of converting XRDs to CRDs, this method fetches the actual CRDs that should already
261+ // exist in the cluster since the Crossplane control plane manages both XRDs and their corresponding CRDs.
262+ func (c * DefaultSchemaClient ) LoadCRDsFromXRDs (ctx context.Context , xrds []* un.Unstructured ) error {
263+ c .logger .Debug ("Loading CRDs from cluster for XRDs" , "xrdCount" , len (xrds ))
264+
265+ if len (xrds ) == 0 {
266+ c .logger .Debug ("No XRDs provided, nothing to load" )
267+ return nil
268+ }
269+
270+ // Extract GVKs from XRDs using the pure function
271+ gvks , err := extractGVKsFromXRDs (xrds )
272+ if err != nil {
273+ return err // Error already wrapped with context from ExtractGVKsFromXRDs
274+ }
275+
276+ // Build XRD-to-CRD name mappings for later use
277+ xrdToCRDMappings := make (map [string ]string ) // XRD name -> CRD name
278+
279+ for _ , xrd := range xrds {
280+ // Extract the CRD name from XRD spec (format: {plural}.{group})
281+ group , _ , _ := un .NestedString (xrd .Object , "spec" , "group" )
282+
283+ plural , _ , _ := un .NestedString (xrd .Object , "spec" , "names" , "plural" )
284+ if group != "" && plural != "" {
285+ crdName := plural + "." + group
286+ xrdName := xrd .GetName ()
287+ xrdToCRDMappings [xrdName ] = crdName
288+ c .logger .Debug ("Mapped XRD to CRD" , "xrdName" , xrdName , "crdName" , crdName )
289+ }
290+ }
291+
292+ // Load CRDs from the extracted GVKs
293+ err = c .loadCRDsFromGVKs (ctx , gvks )
294+ if err != nil {
295+ return err // Error already wrapped with context from LoadCRDsFromGVKs
296+ }
297+
298+ // Store XRD-to-CRD name mappings
299+ c .crdsMu .Lock ()
300+
301+ for xrdName , crdName := range xrdToCRDMappings {
302+ c .xrdToCRDName [xrdName ] = crdName
303+ }
304+
305+ c .crdsMu .Unlock ()
306+
307+ c .logger .Debug ("Successfully stored XRD-to-CRD mappings" , "count" , len (xrdToCRDMappings ))
308+
309+ return nil
310+ }
311+
312+ // loadCRDsFromGVKs fetches CRDs from the cluster for the given GVKs and caches them.
313+ // This method fetches the actual CRDs from the cluster for each provided GVK.
314+ func (c * DefaultSchemaClient ) loadCRDsFromGVKs (ctx context.Context , gvks []schema.GroupVersionKind ) error {
315+ c .logger .Debug ("Loading CRDs from cluster for GVKs" , "gvkCount" , len (gvks ))
316+
317+ if len (gvks ) == 0 {
318+ c .logger .Debug ("No GVKs provided, nothing to load" )
319+ return nil
320+ }
321+
322+ // TODO: Consider parallel fetching of CRDs to improve performance for large numbers of GVKs.
323+ // This could significantly speed up initialization when dealing with many XRDs.
324+ // For now, we fetch sequentially to keep the implementation simple.
325+
326+ // Fetch CRDs from cluster for each GVK - fail fast if any CRD is missing
327+ // Per repository guidelines: never continue in a degraded state
328+ fetchedCRDs := make ([]* extv1.CustomResourceDefinition , 0 , len (gvks ))
329+
330+ for _ , gvk := range gvks {
331+ crd , err := c .GetCRD (ctx , gvk )
332+ if err != nil {
333+ c .logger .Debug ("Failed to fetch required CRD for GVK" , "gvk" , gvk .String (), "error" , err )
334+ return errors .Wrapf (err , "cannot fetch required CRD for %s" , gvk .String ())
335+ }
336+
337+ fetchedCRDs = append (fetchedCRDs , crd )
338+ }
339+
340+ c .logger .Debug ("Successfully fetched all required CRDs from cluster" , "count" , len (fetchedCRDs ))
341+
342+ return nil
343+ }
344+
345+ // GetCRDByName gets a CRD by its name from the cache.
346+ // If the name is not found directly, it will also check if it's an XRD name
347+ // that maps to a different CRD name (e.g., claim XRDs).
348+ func (c * DefaultSchemaClient ) GetCRDByName (name string ) (* extv1.CustomResourceDefinition , error ) {
349+ c .crdsMu .RLock ()
350+ defer c .crdsMu .RUnlock ()
351+
352+ // First, try direct lookup by CRD name
353+ if crd , exists := c .crdByName [name ]; exists {
354+ return crd , nil
355+ }
356+
357+ // If not found, check if this is an XRD name that maps to a different CRD name
358+ if crdName , exists := c .xrdToCRDName [name ]; exists {
359+ if crd , exists := c .crdByName [crdName ]; exists {
360+ c .logger .Debug ("Found CRD for XRD via name mapping" , "xrdName" , name , "crdName" , crdName )
361+ return crd , nil
362+ }
363+ }
364+
365+ return nil , errors .Errorf ("CRD with name %s not found in cache" , name )
366+ }
367+
368+ // GetAllCRDs returns all cached CRDs.
369+ func (c * DefaultSchemaClient ) GetAllCRDs () []* extv1.CustomResourceDefinition {
370+ c .crdsMu .RLock ()
371+ defer c .crdsMu .RUnlock ()
372+
373+ // Return a copy to prevent external modification
374+ result := make ([]* extv1.CustomResourceDefinition , len (c .crds ))
375+ copy (result , c .crds )
376+
377+ return result
378+ }
379+
380+ // addCRD adds a CRD to the cache.
381+ func (c * DefaultSchemaClient ) addCRD (crd * extv1.CustomResourceDefinition ) {
382+ c .crdsMu .Lock ()
383+ defer c .crdsMu .Unlock ()
384+
385+ // Check if already cached to avoid duplicates
386+ if _ , exists := c .crdByName [crd .Name ]; exists {
387+ c .logger .Debug ("CRD already in cache, skipping" , "crdName" , crd .Name )
388+ return
389+ }
390+
391+ // Add to slice
392+ c .crds = append (c .crds , crd )
393+
394+ // Add to name lookup map
395+ c .crdByName [crd .Name ] = crd
396+
397+ c .logger .Debug ("Added CRD to cache" , "crdName" , crd .Name )
398+ }
0 commit comments