Skip to content

Commit 3608800

Browse files
authored
Use xrd defaults on xrs (#45)
* Apply defaults from XRDs to XRs Signed-off-by: Jonathan Ogilvie <[email protected]> * Apply defaults from XRDs to XRs Signed-off-by: Jonathan Ogilvie <[email protected]> * Refactor interfaces for lint Signed-off-by: Jonathan Ogilvie <[email protected]> * Refactor XRD -> CRD logic to use the real CRDs from the cluster, which the crossplane runtime has already processed Signed-off-by: Jonathan Ogilvie <[email protected]> * Add test builder; push name-to-CRD function down a layer Signed-off-by: Jonathan Ogilvie <[email protected]> * Extend builder and use in more places Signed-off-by: Jonathan Ogilvie <[email protected]> * Fix lint Signed-off-by: Jonathan Ogilvie <[email protected]> * A little more builderizing Signed-off-by: Jonathan Ogilvie <[email protected]> * Lint Signed-off-by: Jonathan Ogilvie <[email protected]> * Little more cleanup Signed-off-by: Jonathan Ogilvie <[email protected]> * Formatting Signed-off-by: Jonathan Ogilvie <[email protected]> --------- Signed-off-by: Jonathan Ogilvie <[email protected]>
1 parent b94d8c6 commit 3608800

19 files changed

+2054
-395
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ gitlab/
3030

3131
# go build output (from go build ./cmd/crank etc)
3232
/crossplane-diff
33+
/diff
3334

3435
# ignore the cluster dir since it's pulled from crossplane/crossplane by earthly
3536
cluster/

cmd/diff/client/kubernetes/schema_client.go

Lines changed: 265 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2025
type 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.
146181
func (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

Comments
 (0)