Kitchen sink: every strategy at once¶
internal/cli/testdata/full/
is the one place in the repository where all 29 built-in strategies are
exercised against a single schema: a three-version XRD with a hub and two
spokes, 30 rules covering all 29 strategies, plus sample objects at every
version. It is the
fixture the CLI's own end-to-end tests and the e2e suite run against, so it is
correct by construction — if a strategy's YAML shape ever changed, this fixture
would break first.
It is a reference, not a tutorial. Read it when you want to see a strategy's exact YAML in the context of a whole config rather than in isolation. If you want a config to copy and adapt, start with the examples gallery instead — those are written to be read top to bottom.
What's in it¶
| File | What it is |
|---|---|
xrd.yaml |
xwidgets.example.org with three served versions: v3 (hub, referenceable: true), v2, and v1. |
config.yaml |
The XRDConversionConfig: 14 rules for the v2 spoke, 16 for v1, 30 in total covering 29 distinct strategies. |
config-norules.yaml |
The same config with every rule stripped — the "new spoke version, no mapping yet" starting point for convctl suggest. |
samples/ |
One object per version: hub-v3.yaml, spoke-v2.yaml, spoke-v1.yaml. |
Run it¶
git clone https://github.com/terasky-oss/declarative-conversion-operator
cd declarative-conversion-operator
go run ./cmd/convctl test \
--xrd internal/cli/testdata/full/xrd.yaml \
--config internal/cli/testdata/full/config.yaml \
--samples internal/cli/testdata/full/samples/
With convctl already installed, drop the go run ./cmd/ prefix. The run ends
with:
and exits 0.
Nine paths is three samples across three served versions. The three PASS
results are the identity paths (v3→v3, v2→v2, v1→v1); the other six carry
acknowledged loss, because this fixture deliberately includes every strategy
that is always lossy in one direction — delete, constant, defaultValue,
numericScale onto an integer, mapToArrayByKey, quantity/duration
canonical string re-formatting, and cel (always lossy). Each of those rules states
its reason in the config, and acknowledged loss never fails a run at any
--fail-on threshold. What would fail is loss nobody declared.
Two more things the report shows that are easy to miss:
- Spoke-to-spoke paths match rules from both spokes.
v1→v2listsv2:andv1:rules because every spoke-to-spoke conversion routes through the hub — two conversions, exactly as in a live cluster. RULE COVERAGElists all 30 rules with a match count. A rule no sample exercised would show up here as a warning, which--strict(or--fail-on warn) escalates to a failure.
Rule index¶
Where to find each strategy in
config.yaml.
Rule numbers are the indices the convctl test report prints
(v2:rule[4]:FieldsToMap), so a report line points straight at a block of YAML.
v2 spoke — 14 rules¶
| # | Strategy | What it maps here |
|---|---|---|
| 0 | FieldRename |
spec.storageGB → spec.storageSize |
| 1 | FieldRename |
status.phase → status.state — the same strategy on a status path |
| 2 | ScalarToObject |
spec.replicaCount → spec.replicas.count |
| 3 | ObjectToScalar |
spec.network.cidr → spec.networkCIDR |
| 4 | FieldsToMap |
spec.cpuLimit + spec.memoryLimit → spec.limits map |
| 5 | ToAnnotation |
spec.description → annotation, restoreOnReverse: true |
| 6 | FromAnnotation |
hub annotation → spec.operatorNote, stashOnReverse: true |
| 7 | EnumRemap |
spec.size: Small/Medium/Large ⇄ S/M/L |
| 8 | Constant |
forces spec.schemaVersion: "v2" on the spoke — always lossy |
| 9 | JSONPatch |
escape hatch: move spec.legacyFlag ⇄ spec.legacyFlagV2, with losslessOverride |
| 10 | TypeCoerce |
spec.priority: integer on the hub, string on the spoke |
| 11 | ScalarToFields |
spec.diskSize: "50Gi" → spec.diskSizeValue + spec.diskSizeUnit |
| 12 | NumericScale |
spec.memoryMB ⇄ spec.memoryGB, factor: 1024 — lossy on the integer side |
| 13 | ListJoin |
spec.dnsServers array ⇄ spec.dnsServersCSV string |
v1 spoke — 16 rules¶
| # | Strategy | What it maps here |
|---|---|---|
| 0 | SingletonArrayToObject |
spec.zones (one-element array) → spec.zone |
| 1 | ObjectToSingletonArray |
spec.primaryRegion → spec.regions |
| 2 | MapToFields |
spec.tags map → spec.envTag + spec.teamTag |
| 3 | ToLabel |
spec.tier → label, serialization: String |
| 4 | FromLabel |
hub label → spec.operatorTier, stashOnReverse: true |
| 5 | DefaultValue |
spec.computeUnits, spoke-only, defaults to 1 |
| 6 | Delete |
drops spec.debugMode, a v1-only escape hatch |
| 7 | ForEach |
per-element renames inside spec.volumes |
| 8 | FieldsToScalar |
spec.contactName + spec.contactEmail → spec.contact |
| 9 | ArrayToMapByKey |
spec.endpoints array ⇄ map keyed by name |
| 10 | MapToArrayByKey |
spec.limitsByTier map ⇄ spec.tierLimits array keyed by tier |
| 11 | ListSplit |
spec.allowedCIDRsCSV string ⇄ spec.allowedCIDRs array |
| 12 | Quantity |
spec.cpuRequest Quantity string ⇄ spec.cpuMillis millivalue — lossy on the string side |
| 13 | Duration |
spec.timeout duration string ⇄ spec.timeoutSeconds — lossy on the string side |
| 14 | MapKeyRename |
spec.extraLabels: rename app ⇄ application, other keys pass through |
| 15 | CEL |
spec.packed ⇄ spec.bitHigh + spec.bitLow (always lossy) |
What the schema is quietly demonstrating¶
Reading xrd.yaml
alongside the config explains two things no single strategy page can:
- Fields with an identical shape on both sides need no rule. Each spoke mirrors the fields the other spoke has rules for, keeping them byte-identical to the hub — and they are covered automatically. That is why 30 rules are enough for a schema this wide, and why fail-closed coverage isn't as noisy in practice as it sounds.
statusis not special.v2mapsstatus.phase→status.statewith an ordinaryFieldRename, whilev1leavesstatusuntouched because its shape already matches. A conversion webhook receives the whole stored object, sostatusfollows exactly the same coverage rules asspec.
Other commands worth running against it¶
Schema-only analysis, no samples needed — this is where the three rules the
engine cannot verify (JSONPatch, ScalarToFields, FieldsToScalar) surface
as warnings:
go run ./cmd/convctl analyze \
--xrd internal/cli/testdata/full/xrd.yaml \
--config internal/cli/testdata/full/config.yaml
convctl suggest against the rule-stripped config, to see how much of a
mapping this wide can be bootstrapped automatically (a handful of
FieldRenames and one TypeCoerce — everything else is a design decision the
tool deliberately won't guess at):
go run ./cmd/convctl suggest \
--xrd internal/cli/testdata/full/xrd.yaml \
--config internal/cli/testdata/full/config-norules.yaml
And convctl diff between the two configs, which reads as "what did writing all
30 rules actually accomplish?" — every field that stopped being uncovered, per
spoke:
go run ./cmd/convctl diff \
--xrd internal/cli/testdata/full/xrd.yaml \
--config internal/cli/testdata/full/config-norules.yaml \
--config internal/cli/testdata/full/config.yaml -o table
See the CLI Reference for every command and flag.
It's a test fixture
Unit tests and the e2e suite assert against these files, so treat them as
read-only when experimenting — copy the directory elsewhere before editing,
or point --config at a copy.