Limitations¶
This page is deliberately blunt about what the operator does not do today, so you can decide up front whether it fits your use case.
Scope¶
- Crossplane XRDs and plain native CRDs, nothing else.
pkg/engine(the actual conversion logic) is intentionally kept agnostic of any particular resource type — it only depends on standard Kubernetes OpenAPI schema types and a smallSchemaSourceinterface — with two adapters implementing it today:pkg/xrdadapter(Crossplane XRDs) andpkg/crdadapter(native CRDs). Both are independently toggleable — see Installation: Feature toggles. - Crossplane v2 (
apiextensions.crossplane.io/v2) is the targeted API. The operator reads/patchesspec.conversionthe same way on v1 and v2 XRDs, but it's only tested against the currentv2API in CI. - The hub version must be the target resource's storage version (
referenceable: trueon an XRD,storage: trueon a native CRD). This is enforced as a hard validation error — you can't designate an arbitrary spoke as the conversion hub. - One config per target resource. Enforced structurally by a unique field index plus the admission webhook, not a runtime lock — this is a design constraint, not a current gap, but it does mean you can't split one resource's conversion logic across multiple
XRDConversionConfig/CRDConversionConfigobjects.
Rule-authoring constraints¶
forEachnesting is capped at depth 2. AforEachmay wrap anotherforEachfor arrays-of-arrays; a third level is rejected at compile and admission time (not silently truncated). This bounds the CRD schema recursion the engine has to reason about. See For Each.forEachrequires strict positional correspondence between the hub and spoke arrays — same length, same order. If both the hub and spoke item paths are present on the input as arrays of different lengths, conversion fails with a hard runtime error (it does not silently coerce to the source length). When the destination path is absent, the output array is sized from the source alone.- Free-form maps (
additionalProperties: true) and preserve-unknown-fields subtrees are opaque, all-or-nothing units. A rule must claim the whole subtree; the engine does not reason about individual keys inside one. If you need field-level control over part of a free-form map, model those fields explicitly in the schema instead. oneOf/anyOf/allOf/$refnodes are treated opaquely. Analyze does not flatten through those JSON Schema constructs. An uncovered leaf sitting inside one is reported with the construct name in the diagnostic (not a generic "uncovered field" message). Transforming across aoneOfbranch is out of scope.arrayToMapByKeyrequires unique key values. A duplicatekeyFieldvalue across array elements is a runtime conversion error, not a silent overwrite.- Some strategies can never be statically proven lossless, because the engine can't reason about arbitrary regexes, templates, JSON Patch documents, or CEL at compile time:
jsonPatch,scalarToFields/fieldsToScalar(unless you setlosslessOverride: trueand back that claim up withconvctl testagainst representative data),cel(always lossy — nolosslessOverride), andmapToArrayByKey/round-tripping through a sorted array (always lossy — see Array ⇄ Map by Key).
Operational¶
- CRD schema changes require a manual step on Helm upgrade. CRDs are installed once at
helm installand never touched byhelm upgrade/helm uninstall(Helm's own recommended convention for CRD-heavy charts) — see Upgrading. - Per-pod webhook-server state isn't surfaced back into
ConversionWebhookServer.status.status.assignedConfigsreflects desired assignment computed by the shared resolver, not confirmation that every replica has actually finished compiling and loading a given config — that's a deliberate trade-off to avoid the operator's reconcile loop depending on network calls to webhook-server pods. Check each pod's/debug/registryendpoint or metrics for real per-pod state. - Spoke-to-spoke conversions always route through the hub — two
Convertcalls, never a direct spoke-to-spoke path. This keeps compilation cost linear in the number of spoke versions. A 1000-elementforEachspoke-to-spoke convert is ~2.3× a single hop and still under 1 ms (Capacity planning); shortcut plans are not implemented. - No cross-cluster or multi-region coordination. Every
ConversionWebhookServerinstance and every operator replica assumes a single Kubernetes cluster. A webhook-server in cluster A serving conversions for a resource in cluster B is an explicit non-goal — see Architecture: One cluster, one install. Keep configs consistent across a fleet with CI (convctl test --live/diff --liveper kubecontext), not by sharing webhook endpoints.
Scale envelopes¶
Microbenchmarks for compile time vs schema size and Convert vs array length
live in pkg/engine/*_bench_test.go
and are summarized in Capacity planning. Extremely
large or deeply nested schemas beyond the published 1000-leaf / 1000-element
points are still unvalidated against a live apiserver. Re-run make bench
locally; make test-e2e-load posts synthetic ConversionReview batches at a
live webhook-server; make test-e2e-scale drives real Get/List through the
apiserver conversion path against a generated CRD fleet (up to 100×100).
If something here blocks you, please open an issue — several of these are natural extension points the design was deliberately seamed for (see Roadmap), not fundamental barriers.