@@ -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