Skip to content

feat(xrd): Add new subcommand crossplane xrd convert#9

Closed
tampakrap wants to merge 31 commits into
crossplane:mainfrom
tampakrap:theo/feat_xrd_convert
Closed

feat(xrd): Add new subcommand crossplane xrd convert#9
tampakrap wants to merge 31 commits into
crossplane:mainfrom
tampakrap:theo/feat_xrd_convert

Conversation

@tampakrap
Copy link
Copy Markdown
Collaborator

Description of your changes

Introduce an XRD to CRD converter: crossplane xrd convert. It takes an XRD either on stdin or as a file argument and emits the equivalent Kubernetes CustomResourceDefinition that Crossplane derives from it.

The output CRD is what Crossplane uses internally to validate composite resources. This is useful for inspecting the CRD shape, feeding it into kubectl-based tooling that doesn't understand XRDs, or for debugging purposes.

The top-level crossplane xrd command is marked as maturity "beta", which applies to the subtree as well.

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.

adamwg and others added 30 commits April 17, 2026 15:50
This commit adds build infrastructure for the standalone CLI repository, with a
stub cmd/crossplane. We'll add the existing crank code from crossplane in a
subsequent commit.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
This is all copied from crossplane/crossplane and updated to remove the parts we
don't need (e.g., pushing container images).

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
This commit contains the current `cmd/crank` from c/c and the supporting
`internal/docker` package, with imports updated as necessary.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
The CLI's version will not be guaranteed to match a Crossplane version going
forward, so we can't use the CLI's version number as the default image tag for
render. Use the `stable` tag instead, so that we always use the latest stable
Crossplane.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
We'll fill out more content in the README later, but bootstrap it for now.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
We don't actually have any fuzz tests yet, and this job relies on some
configuration existing in the google oss-fuzz repository.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Copy Crossplane's coderabbit config as a starting point and remove irrelevant
parts.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Historically, we've introduced new commands in the `crossplane alpha` or
`crossplane beta` trees and moved them as they matured. This makes it awkward to
introduce new commands in existing trees, since the tree gets split across
maturity levels, and also breaks users when commands mature since their
invocation changes.

Move all the existing alpha and beta commands to the top level (or into the
right tree, in the case of `crossplane render op`). Start indicating maturity
using kong tags. Alpha and beta features are hidden from help by default but can
be enabled by configuration and have a maturity indicator added to their help
automatically.

Make `crossplane render xr` the default subcommand for `crossplane render` so
that existing render users are not broken by this change.

Fixes crossplane/crossplane#7309

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Now that we have a config file, we need commands to manage it. Add basic
`crossplane config view` and `crossplane config set` commands so we can view and
update the config.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
This repository has additional maintainers beyond the
@crossplane/crossplane-maintainers group.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
The protos in this repository are vendored from crossplane/crossplane, so no
need to lint them or push them to the Buf schema registry. This avoids the need
for a `BUF_TOKEN` secret in this repo.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Add coderabbit config and clean up configuration
As proposed in the DevEx design doc, reorganize our command tree so that it's
noun-centric. Leave `crossplane render` as an alias for `crossplane xr render`
since that command is GA and we don't want to break any users/scripts. Allow
alpha and beta commands to move without aliases.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Show only the top-level nouns in the help. The user can use the `--help` flag on
any of these commands to see the subcommands. This will help keep our help tidy
and readable as we expand functionality.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
In keeping with how Crossplane handles pre-GA features, enable beta features by
default (and have a config option to disable them), but leave alpha features
disabled by default (with a config option to enable).

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Reorganize the command tree and introduce config
Export the entire nixpkgs-unstable rather than just its go package so that our
nix setup can use arbitrary packages from unstable as needed.

Matches the change in crossplane/crossplane commit 744190edf.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Upcoming developer experience changes require golang 1.26. Do the update
separately and resolve new lint issues so that we don't mix the changes into the
bigger DevEx PR.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
We use the function protobufs in the function we implement for injecting and
extracting context during render. When this code lived in crossplane/crossplane
it made sense to use the protobufs from there, but now that we're separate it
would be better to use the versions in function-sdk-go like a normal function.

This removes our dependency on crossplane/crossplane, which is nice.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Use function protobufs from function-sdk-go
While reviewing/testing crossplane#3 I noticed that the space-separated `--config
/path/to/config.yaml` ignores the config. Reproducer:

```bash
  # Create a new config file
  go run ./cmd/crossplane --config=/tmp/xpcfg.yaml config set features.enableAlpha true

  # Correctly prints the full content of the config
  go run ./cmd/crossplane --config=/tmp/xpcfg.yaml config view

  # (before the fix) prints only "version: 1" ignoring the config
  go run ./cmd/crossplane --config /tmp/xpcfg.yaml config view
```

The problem is that `strings.TrimPrefix` returns the original string
unchanged when the prefix doesn't match, so the `v != ""` check can not
distinguish "prefix matched and value extracted" from "prefix not
matched". For `--config /path` this makes `configFlag` return "--config"
as the path instead of falling through to read the next argument.

Switching to `strings.CutPrefix`, which returns an ok bool that makes
the distinction clear.

Signed-off-by: Theo Chatzimichos <tampakrap@gmail.com>
Adds a table-driven test for configFlag

The `EmptyEquals` case revealed that `--config=` silently fell back to
the default config path, while a trailing `--config` errored cleanly.

Treat both empty cases the same way and error out, so the user gets
immediate feedback either way.

Signed-off-by: Theo Chatzimichos <tampakrap@gmail.com>
fix(config): Fix flag parsing with space-separated value
Introduce an XRD to CRD converter: `crossplane xrd convert`. It takes
an XRD either on stdin or as a file argument and emits the equivalent
Kubernetes CustomResourceDefinition that Crossplane derives from it.

The output CRD is what Crossplane uses internally to validate composite
resources. This is useful for inspecting the CRD shape, feeding it into
kubectl-based tooling that doesn't understand XRDs, or for debugging
purposes.

The top-level `crossplane xrd` command is marked as maturity "beta",
which applies to the subtree as well.

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

coderabbitai Bot commented May 10, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2fa280fd-cef0-41b3-b95f-dc9caae812f1

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb65fc and 6293241.

📒 Files selected for processing (2)
  • cmd/crossplane/xrd/convert.go
  • cmd/crossplane/xrd/convert_test.go

📝 Walkthrough

Walkthrough

Adds an xrd convert subcommand to the Crossplane CLI that reads a CompositeResourceDefinition (XRD), derives one or two CustomResourceDefinition (CRD) objects, and emits them as YAML to stdout, a file, or a directory. The new command is registered under the root CLI with beta maturity.

Changes

XRD Convert Command

Layer / File(s) Summary
Command Container Structure
cmd/crossplane/xrd/xrd.go
Defines the Cmd struct as a container for XRD subcommands, with a Convert field wired to convertCmd.
Convert Command Setup
cmd/crossplane/xrd/convert.go
Adds convertCmd fields (InputFile, OutputFile, OutputDir, Fs), Help() usage text, and AfterApply() filesystem initialization.
Command Execution
cmd/crossplane/xrd/convert.go
Run() reads/unmarshals XRD YAML, validates GVK, calls ToCRDs(), and writes CRD YAML as multi-document stream or to files/directories.
CRD Emission
cmd/crossplane/xrd/convert.go
writeCRDs() marshals CRDs to YAML with ---\n separators; writeFile() writes output files.
Conversion Helper
cmd/crossplane/xrd/convert.go
Exported ToCRDs() derives XR (and optional Claim) CustomResourceDefinition objects and sets their APIVersion/Kind.
Unit Tests
cmd/crossplane/xrd/convert_test.go
TestToCRDs exercises ToCRDs for multiple scope/claim scenarios; minimalXRD helper constructs minimal XRD inputs.
CLI Integration
cmd/crossplane/main.go
Imports cmd/crossplane/xrd and registers the XRD subcommand field (maturity beta) in the root cli struct.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Would you like me to run a focused pass on the new conversion logic and tests for edge cases or style nitpicks? Thank you for the contribution.

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a new crossplane xrd convert subcommand, and meets the 72-character length requirement at 54 characters.
Description check ✅ Passed The description clearly explains the purpose of the XRD to CRD converter, its use cases, and implementation details, all of which align with the changeset.
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.
Breaking Changes ✅ Passed No breaking changes found. PR adds new xrd CLI subcommand. The private cli struct modification and new exports do not break existing public APIs.
Feature Gate Requirement ✅ Passed The XRD command is marked maturity:beta and properly gated via the existing feature flag system. No APIs affected; CLI-only changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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: 2

🤖 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/xrd/convert.go`:
- Around line 81-87: The YAML unmarshal and type-check errors in the XRD
conversion flow (the yaml.Unmarshal call and the GroupVersionKind check using
xrd.GroupVersionKind()) produce terse messages; update these error returns to be
actionable for operators by wrapping the original error and adding guidance such
as what was being attempted (parsing XRD input), what to verify next (confirm
the file contains valid YAML, correct apiVersion/kind — expected
apiextensionsv1.CompositeResourceDefinitionGroupVersionKind — and that the file
path/permissions are correct), and include the original error or the offending
GroupVersionKind value for context (replace errors.Wrap(err, "cannot unmarshal
XRD") and errors.Errorf("input is not a %s; got %s", ...) with richer messages).
Apply the same pattern to the related messages around lines 91-97 and 103-123
(the CRD marshal/unmarshal/error returns) so all CLI errors are user-guiding and
include original error details and suggested next steps.
- Around line 101-103: The file-open call for writing the output in convert.go
uses c.fs.OpenFile(c.OutputFile, os.O_CREATE|os.O_WRONLY, 0o644) which doesn't
truncate existing files; change the flags passed to OpenFile (where c.OutputFile
is used) to include os.O_TRUNC so the output is fully replaced (i.e., use
os.O_CREATE|os.O_WRONLY|os.O_TRUNC) when creating/writing the CRD YAML.
🪄 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: 4e81b265-d567-4c8e-a083-b50e8b1224b3

📥 Commits

Reviewing files that changed from the base of the PR and between eb4d56a and 7eb65fc.

📒 Files selected for processing (3)
  • cmd/crossplane/main.go
  • cmd/crossplane/xrd/convert.go
  • cmd/crossplane/xrd/xrd.go

Comment thread cmd/crossplane/xrd/convert.go
Comment thread cmd/crossplane/xrd/convert.go Outdated
Comment thread cmd/crossplane/xrd/convert.go Outdated
output = f
}

outputW := bufio.NewWriter(output)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why Not using here afero.WriteFile ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I copy-pasted it from the older/existing converters eg

outputW := bufio.NewWriter(output)
but I don't mind changing it, what would you prefer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

regarding the implementation for devex features i would tent to using afero.WriteFile - as we interacting with an aferoFS anyways - #7

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed in #12

Comment thread cmd/crossplane/xrd/convert.go Outdated
Copy link
Copy Markdown
Member

@haarchri haarchri left a comment

Choose a reason for hiding this comment

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

thanks, can you add also unit-tests for the convert command? xrd for namespace scope, Cluster scope and legacy with / without claimNames

Comment thread cmd/crossplane/xrd/convert.go Outdated
Support legacy XRDs that offer a Claim. `ToCRD` is renamed to `ToCRDs`,
wwhich now returns the XR CRD followed by the Claim CRD when
`xrd.OffersClaims()`. Non-legacy v2 XRDs still produce a single CRD.

Also, add a `--output-dir` flag that is mutually exclusive with
`--output-file`, that writes each CRD to its own file named after its
`.metadata.name`. The default stdout or `--output-file` modes output a
multi-doc yaml.

Finally, add unit tests for the converter covering the v2 namespaced,
cluster-scoped, or legacy with or without ClaimNames.

Signed-off-by: Theo Chatzimichos <tampakrap@gmail.com>
Comment on lines +53 to +63
Convert a Crossplane CompositeResourceDefinition (XRD) into the Kubernetes
CustomResourceDefinition (CRD) that describes the composite resource type.

The output CRD(s) are what Crossplane derives internally from the XRD. This is
useful for inspecting the CRD shape, feeding it into kubectl-based tooling that
doesn't understand XRDs, or as a debugging aid.

For legacy XRDs that offer a Claim (spec.claimNames set, typically with
spec.scope: LegacyCluster) two CRDs are produced: one cluster-scoped CRD for
the XR and one namespaced CRD for the Claim. For namespaced XRDs only the XR
CRD is produced. The detection is automatic; no flag is needed.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we make this here a bit clearer ?

Suggested change
Convert a Crossplane CompositeResourceDefinition (XRD) into the Kubernetes
CustomResourceDefinition (CRD) that describes the composite resource type.
The output CRD(s) are what Crossplane derives internally from the XRD. This is
useful for inspecting the CRD shape, feeding it into kubectl-based tooling that
doesn't understand XRDs, or as a debugging aid.
For legacy XRDs that offer a Claim (spec.claimNames set, typically with
spec.scope: LegacyCluster) two CRDs are produced: one cluster-scoped CRD for
the XR and one namespaced CRD for the Claim. For namespaced XRDs only the XR
CRD is produced. The detection is automatic; no flag is needed.
Convert a CompositeResourceDefinition (XRD) into the CustomResourceDefinition(s)
that Crossplane derives from it internally.
Useful for inspecting the generated CRD shape, feeding it into kubectl-based
tooling that doesn't understand XRDs, or debugging composition behavior.
Output depends on the XRD type, detected automatically:
* Namespaced XRD -> one CRD for the XR
* Legacy XRD with a Claim -> two CRDs: cluster-scoped XR + namespaced Claim

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed in #12

return errors.Wrapf(err, "cannot marshal CRD %q", crd.GetName())
}

if _, err := outputW.WriteString("---\n"); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

my comment is still valid - can we use here afero.WriteFile like we doing in #7

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed in #12

//
// Callers that consume the CRD in-memory should call xcrd.ForCompositeResource
// (and, for legacy XRDs, xcrd.ForCompositeResourceClaim) directly.
func ToCRDs(xrd *apiextensionsv1.CompositeResourceDefinition) ([]*extv1.CustomResourceDefinition, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this something we need to export ? wonder if this is something we can move to Internal

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree that it doesn't need to be exported, but I didn't move it to internal since no other package needs it, at least for now. If any other package needs it in the future we can move it to internal. The change is done in #12

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.

3 participants