Skip to content

feat(xr): Convert a Claim to XR via crossplane xr generate#13

Open
tampakrap wants to merge 1 commit into
crossplane:mainfrom
tampakrap:theo/feat_claimtoxr_v2
Open

feat(xr): Convert a Claim to XR via crossplane xr generate#13
tampakrap wants to merge 1 commit into
crossplane:mainfrom
tampakrap:theo/feat_claimtoxr_v2

Conversation

@tampakrap
Copy link
Copy Markdown
Collaborator

@tampakrap tampakrap commented May 12, 2026

Description of your changes

Introduce a Claim to XR converter: crossplane xr generate claim.yaml. Similarly to other converter functions/tools, it takes a Claim either on stdin or as file argument and generates an equivalent XR YAML.

The parent crossplane xr is marked as maturity "alpha", which applies to the subtree as well.

This new tool can be used together with the render command as shown in the example below:

crossplane render <(crossplane xr generate claim.yaml) composition.yaml functions.yaml

The library function ConvertClaimToXR can be used by downstream tools (eg crossplane-diff) that rely on Claim to XR conversion.

The tool (and as a result the converter function, via the Options struct) is configurable and provides the following flags:

  • --direct: omit spec.claimRef and the equivalent labels.
  • --name: specify an explicit XR name, skipping either the random suffix in the non-direct mode or the Claim name fallback in direct mode.
  • --kind: override the derived "X" default.
  • --gen-uid: populate metadata.uid with a fresh random UUID.

I didn't create new docs issue/PR, we can use crossplane/docs#1088

I have:

Need help with this checklist? See the cheat sheet.

Introduce a Claim to XR converter: `crossplane xr generate claim.yaml`.
Similarly to other converter functions/tools, it takes a Claim either on
stdin or as file argument and generates an equivalent XR YAML.

The parent `crossplane xr` is marked as maturity "alpha", which applies
to the subtree as well.

This new tool can be used together with the render command as shown in
the example below:

```bash
crossplane render <(crossplane xr generate claim.yaml) composition.yaml functions.yaml
```

The library function ConvertClaimToXR can be used by downstream tools
(eg crossplane-diff) that rely on Claim to XR conversion.

The tool (and as a result the converter function, via the Options
struct) is configurable and provides the following flags:
- `--direct`: omit spec.claimRef and the equivalent labels.
- `--name`: specify an explicit XR name, skipping either the random
  suffix in the non-direct mode or the Claim name fallback in direct
  mode.
- `--kind`: override the derived "X<Kind>" default.
- `--gen-uid`: populate metadata.uid with a fresh random UUID.

Signed-off-by: Theo Chatzimichos <tampakrap@gmail.com>
@tampakrap tampakrap requested review from adamwg and haarchri May 12, 2026 14:33
@tampakrap tampakrap requested review from a team and jcogilvie as code owners May 12, 2026 14:33
@tampakrap tampakrap requested review from jbw976 and removed request for a team May 12, 2026 14:33
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a new crossplane xr generate CLI command that converts Crossplane Claims to Composite Resources (XRs). The implementation includes command registration, a conversion function with validation and name/kind derivation, and comprehensive test coverage for all scenarios.

Changes

XR Generate Command

Layer / File(s) Summary
XR Command Structure and CLI Registration
cmd/crossplane/xr/xr.go, cmd/crossplane/main.go
The xr package is created with a public Cmd struct that registers a Generate subcommand, then wired into the main CLI as a top-level XR command with alpha maturity.
Generate Implementation and Conversion Logic
cmd/crossplane/xr/generate.go
The generateCmd CLI command reads Claim YAML (from file or stdin), converts it to an XR via ConvertClaimToXR, and writes the result as YAML to an output file or stdout. ConvertClaimToXR validates the input Claim, parses apiVersion, derives XR kind from the Claim kind (or override via options), computes metadata name with optional suffix/UID generation, applies label/annotation semantics for direct vs linked modes, copies the Claim spec, and optionally sets a fresh metadata.uid.
Generate Tests and Helpers
cmd/crossplane/xr/generate_test.go
Comprehensive table-driven test suite with helper builders for Claim fixtures, test case modifiers (labels, annotations, complex specs), and expected XR generation logic. Tests cover error paths (nil/empty/missing fields), name derivation with/without direct mode, label/annotation merging, spec field type preservation, kind/name overrides, and UID generation semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Breaking Changes ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and descriptively summarizes the main change: introducing a new CLI command to convert Claims to Composite Resources (XRs), and it stays well within the 72-character limit at 60 characters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Feature Gate Requirement ✅ Passed The xr generate command is a client-side CLI utility that does not affect apis/** or core behavior. It is properly gated with maturity:"alpha"; alpha commands remain hidden unless explicitly enabled.
Description check ✅ Passed The PR description clearly explains the new Claim-to-XR converter feature, its purpose, configurable flags, usage examples, and testing approach.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
cmd/crossplane/xr/generate.go (1)

116-116: 💤 Low value

Make error message consistent with codebase style.

For consistency with other error messages in the codebase (which use lowercase and "cannot"), consider changing "Unable to marshal back to yaml" to "cannot marshal XR to YAML".

♻️ Suggested change
-	if err != nil {
-		return errors.Wrap(err, "Unable to marshal back to yaml")
-	}
+	if err != nil {
+		return errors.Wrap(err, "cannot marshal XR to YAML")
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/xr/generate.go` at line 116, Change the error string passed to
errors.Wrap in the return statement that currently reads errors.Wrap(err,
"Unable to marshal back to yaml") so it follows project style; replace the
message with "cannot marshal XR to YAML" (i.e., use errors.Wrap(err, "cannot
marshal XR to YAML")) in the function in generate.go where the XR marshaling
failure is handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/crossplane/xr/generate.go`:
- Around line 136-143: Update the user-facing error message constants
(errNoAPIVersion, errNoKind, errNoSpecSection and optionally errEmptyClaimYAML
and errNilInput) to be user-friendly and actionable: replace terse developer
phrases with full-sentence guidance that names the missing field and suggests a
next step (for example: indicate the Claim YAML must include an
apiVersion/kind/spec and ask the user to check their Claim definition or
validate their YAML). Locate these constants by name in the file
(errNoAPIVersion, errNoKind, errNoSpecSection, errEmptyClaimYAML, errNilInput)
and update their string values accordingly so CLI output gives clear, helpful
instructions to the user.
- Around line 244-257: When building the XR in non-Direct mode (the block
guarded by if !opts.Direct), validate that claim.GetNamespace() is non-empty
before setting labels[labelClaimNamespace] or calling
xrPaved.SetValue("spec.claimRef", ...); if the namespace is empty return a
wrapped error (e.g. "claim namespace required in non-Direct mode") so we don't
create an XR with an empty claimRef namespace; perform this check early in the
if !opts.Direct block (before using claim.GetNamespace()) and only proceed to
set labels and call xrPaved.SetValue when the namespace is present.
- Line 100: Replace the generic errors.Wrap(err, "Unmarshalling Error") call
with a user-friendly, specific CLI message such as errors.Wrap(err, "cannot
parse Claim YAML") or errors.Wrap(err, "invalid Claim YAML format"); update the
string in the existing errors.Wrap(err, "...") expression so the returned error
refers to the Claim YAML parsing problem (keep the wrapped internal err for
diagnostics but avoid exposing raw internal details in the top-level message).

---

Nitpick comments:
In `@cmd/crossplane/xr/generate.go`:
- Line 116: Change the error string passed to errors.Wrap in the return
statement that currently reads errors.Wrap(err, "Unable to marshal back to
yaml") so it follows project style; replace the message with "cannot marshal XR
to YAML" (i.e., use errors.Wrap(err, "cannot marshal XR to YAML")) in the
function in generate.go where the XR marshaling failure is handled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c84bb47-bee6-4223-abcf-f9d69ec8d963

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3542b and 1b30eaf.

📒 Files selected for processing (4)
  • cmd/crossplane/main.go
  • cmd/crossplane/xr/generate.go
  • cmd/crossplane/xr/generate_test.go
  • cmd/crossplane/xr/xr.go


claim := &unstructured.Unstructured{}
if err := yaml.Unmarshal(claimData, claim); err != nil {
return errors.Wrap(err, "Unmarshalling Error")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Improve error message specificity.

The error message "Unmarshalling Error" doesn't tell the user what went wrong with their Claim YAML. Consider making it more specific and helpful, such as "cannot parse Claim YAML" or "invalid Claim YAML format".

As per coding guidelines, CLI error messages must be especially user-friendly and avoid internal error details.

💬 Suggested improvement
-	if err := yaml.Unmarshal(claimData, claim); err != nil {
-		return errors.Wrap(err, "Unmarshalling Error")
-	}
+	if err := yaml.Unmarshal(claimData, claim); err != nil {
+		return errors.Wrap(err, "cannot parse Claim YAML")
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/xr/generate.go` at line 100, Replace the generic
errors.Wrap(err, "Unmarshalling Error") call with a user-friendly, specific CLI
message such as errors.Wrap(err, "cannot parse Claim YAML") or errors.Wrap(err,
"invalid Claim YAML format"); update the string in the existing errors.Wrap(err,
"...") expression so the returned error refers to the Claim YAML parsing problem
(keep the wrapped internal err for diagnostics but avoid exposing raw internal
details in the top-level message).

Comment on lines +136 to +143
const (
// Error messages.
errNilInput = "input is nil"
errEmptyClaimYAML = "invalid Claim YAML: parsed object is empty"
errNoAPIVersion = "Claim has no apiVersion"
errParseAPIVersion = "failed to parse Claim APIVersion"
errNoKind = "Claim has no kind section"
errNoSpecSection = "Claim has no spec section"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make error messages more user-friendly and actionable.

The error message constants are developer-focused (e.g., "Claim has no apiVersion") rather than user-friendly. They don't provide context about what the user was trying to do or suggest next steps.

Consider messages like:

  • errNoAPIVersion: "The Claim YAML must include an 'apiVersion' field. Please check your Claim definition."
  • errNoKind: "The Claim YAML must include a 'kind' field. Please check your Claim definition."
  • errNoSpecSection: "The Claim YAML must include a 'spec' section. Please check your Claim definition."

As per coding guidelines, error messages must be meaningful to end users, not just developers—avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible. CLI error messages must be especially user-friendly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/xr/generate.go` around lines 136 - 143, Update the user-facing
error message constants (errNoAPIVersion, errNoKind, errNoSpecSection and
optionally errEmptyClaimYAML and errNilInput) to be user-friendly and
actionable: replace terse developer phrases with full-sentence guidance that
names the missing field and suggests a next step (for example: indicate the
Claim YAML must include an apiVersion/kind/spec and ask the user to check their
Claim definition or validate their YAML). Locate these constants by name in the
file (errNoAPIVersion, errNoKind, errNoSpecSection, errEmptyClaimYAML,
errNilInput) and update their string values accordingly so CLI output gives
clear, helpful instructions to the user.

Comment on lines +244 to +257
if !opts.Direct {
xrName = names.SimpleNameGenerator.GenerateName(claimName + "-")
labels[labelClaimName] = claim.GetName()

labels[labelClaimNamespace] = claim.GetNamespace()
if err := xrPaved.SetValue("spec.claimRef", map[string]any{
"apiVersion": apiVersion,
"kind": claimKind,
"name": claimName,
"namespace": claim.GetNamespace(),
}); err != nil {
return nil, errors.Wrap(err, "failed to set claimRef")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate namespace in non-Direct mode.

In non-Direct mode, the code uses claim.GetNamespace() (lines 246, 248, 253) without validating that the namespace is non-empty. Claims are namespace-scoped resources, but if a Claim is created without a namespace (or read from a YAML without one), this could result in an XR with empty namespace in spec.claimRef and labels, which would be semantically incorrect.

Should we validate that the namespace is present when not in Direct mode? Or document the expected behavior when namespace is empty?

🛡️ Suggested validation
 	if !opts.Direct {
+		claimNS := claim.GetNamespace()
+		if claimNS == "" {
+			return nil, errors.New("Claim must have a namespace when generating a non-direct XR")
+		}
 		xrName = names.SimpleNameGenerator.GenerateName(claimName + "-")
 		labels[labelClaimName] = claim.GetName()
 
-		labels[labelClaimNamespace] = claim.GetNamespace()
+		labels[labelClaimNamespace] = claimNS
 		if err := xrPaved.SetValue("spec.claimRef", map[string]any{
 			"apiVersion": apiVersion,
 			"kind":       claimKind,
 			"name":       claimName,
-			"namespace":  claim.GetNamespace(),
+			"namespace":  claimNS,
 		}); err != nil {
 			return nil, errors.Wrap(err, "failed to set claimRef")
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !opts.Direct {
xrName = names.SimpleNameGenerator.GenerateName(claimName + "-")
labels[labelClaimName] = claim.GetName()
labels[labelClaimNamespace] = claim.GetNamespace()
if err := xrPaved.SetValue("spec.claimRef", map[string]any{
"apiVersion": apiVersion,
"kind": claimKind,
"name": claimName,
"namespace": claim.GetNamespace(),
}); err != nil {
return nil, errors.Wrap(err, "failed to set claimRef")
}
}
if !opts.Direct {
claimNS := claim.GetNamespace()
if claimNS == "" {
return nil, errors.New("Claim must have a namespace when generating a non-direct XR")
}
xrName = names.SimpleNameGenerator.GenerateName(claimName + "-")
labels[labelClaimName] = claim.GetName()
labels[labelClaimNamespace] = claimNS
if err := xrPaved.SetValue("spec.claimRef", map[string]any{
"apiVersion": apiVersion,
"kind": claimKind,
"name": claimName,
"namespace": claimNS,
}); err != nil {
return nil, errors.Wrap(err, "failed to set claimRef")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/xr/generate.go` around lines 244 - 257, When building the XR
in non-Direct mode (the block guarded by if !opts.Direct), validate that
claim.GetNamespace() is non-empty before setting labels[labelClaimNamespace] or
calling xrPaved.SetValue("spec.claimRef", ...); if the namespace is empty return
a wrapped error (e.g. "claim namespace required in non-Direct mode") so we don't
create an XR with an empty claimRef namespace; perform this check early in the
if !opts.Direct block (before using claim.GetNamespace()) and only proceed to
set labels and call xrPaved.SetValue when the namespace is present.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant