Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions cmd/crossplane/render/xr/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ import (
"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
"github.com/crossplane/crossplane-runtime/v2/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/v2/pkg/xcrd"

apiextensionsv1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1"
pkgv1 "github.com/crossplane/crossplane/apis/v2/pkg/v1"

"github.com/crossplane/cli/v2/cmd/crossplane/render"
"github.com/crossplane/cli/v2/cmd/crossplane/render/contextfn"
xrcmd "github.com/crossplane/cli/v2/cmd/crossplane/xr"
"github.com/crossplane/cli/v2/internal/async"
"github.com/crossplane/cli/v2/internal/dependency"
"github.com/crossplane/cli/v2/internal/project"
Expand Down Expand Up @@ -169,13 +169,8 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinte
return errors.Wrapf(err, "cannot load XRD from %q", c.XRD)
}

crd, err := xcrd.ForCompositeResource(xrd)
if err != nil {
return errors.Wrapf(err, "cannot derive composite CRD from XRD %q", xrd.GetName())
}

if err := render.DefaultValues(xr.UnstructuredContent(), xr.GetAPIVersion(), *crd); err != nil {
return errors.Wrapf(err, "cannot default values for XR %q", xr.GetName())
if err := xrcmd.ApplyXRDDefaults(xr.GetUnstructured(), xrd); err != nil {
return errors.Wrapf(err, "cannot apply XRD defaults to XR %q", xr.GetName())
}
}

Expand Down
43 changes: 0 additions & 43 deletions cmd/crossplane/render/xrd.go

This file was deleted.

26 changes: 26 additions & 0 deletions cmd/crossplane/xr/help/patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
The `xr patch` command applies XR-level patches to a Composite Resource (XR).

It reads the XR from a file (or stdin), applies the requested patches, and
writes the result to stdout or to a file. At least one patching flag must be
set; today the only one is `--xrd`, which applies default values from an XRD's
`openAPIV3Schema` to the XR. More patching flags will be added in the future.

## Examples

Apply default values from an XRD to an XR:

```shell
crossplane xr patch xr.yaml --xrd xrd.yaml
```

Patch an XR from stdin:

```shell
cat xr.yaml | crossplane xr patch - --xrd xrd.yaml
```

Write the patched XR to a file:

```shell
crossplane xr patch xr.yaml --xrd xrd.yaml -o patched.yaml
```
104 changes: 104 additions & 0 deletions cmd/crossplane/xr/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
Copyright 2026 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package xr

import (
"github.com/alecthomas/kong"
"github.com/spf13/afero"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"

commonIO "github.com/crossplane/cli/v2/cmd/crossplane/convert/io"
"github.com/crossplane/cli/v2/cmd/crossplane/render"

_ "embed"
)

//go:embed help/patch.md
var patchHelp string

type patchCmd struct {
// Arguments.
InputFile string `arg:"" default:"-" help:"The XR YAML file to patch, or '-' for stdin." optional:"" predictor:"file" type:"path"`

// Output flags.
OutputFile string `help:"The file to write the patched XR YAML to. Defaults to stdout." placeholder:"PATH" predictor:"file" short:"o" type:"path"`

// Patching flags.
XRD string `help:"A YAML file specifying the CompositeResourceDefinition (XRD) whose schema defaults are applied to the XR." name:"xrd" placeholder:"PATH" predictor:"file" type:"path"`
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 give this a more descriptive name like --defaults-from-xrd or just --xrd-defaults? It's not really clear from the flag name --xrd that it indicates an action.

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 chose xr patch --xrd to be the same as render --xrd, which is not descriptive either, but people know it very well. In theory, in the future passing the XRD could mean additional things apart from applying the defaults. Furthermore, I don't expect this command to be heavily used, I believe it will be used only for debugging purposes. The libraries behind this command though are going to be used in the upcoming test subcommand, which will have patch.xrd as defined in our design doc.

That said, I'm fine using a more descriptive name to the flag, I just wanted to give that background before we take a decision though.


fs afero.Fs
}

func (c *patchCmd) Help() string {
return patchHelp
}

// AfterApply implements kong.AfterApply.
func (c *patchCmd) AfterApply() error {
c.fs = afero.NewOsFs()
return nil
}

// Run runs the patch command.
func (c *patchCmd) Run(k *kong.Context) error {
if c.XRD == "" {
return errors.New("no patching flag provided: at least one of --xrd must be set")
}

xrData, err := commonIO.Read(c.fs, c.InputFile)
if err != nil {
return err
}

xr := &unstructured.Unstructured{}
if err := yaml.Unmarshal(xrData, xr); err != nil {
return errors.Wrap(err, "cannot unmarshal XR")
}

xrd, err := render.LoadXRD(c.fs, c.XRD)
if err != nil {
return errors.Wrapf(err, "cannot load XRD from %q", c.XRD)
}

if err := ApplyXRDDefaults(xr, xrd); err != nil {
return errors.Wrapf(err, "cannot apply XRD defaults to XR %q", xr.GetName())
}

b, err := yaml.Marshal(xr)
if err != nil {
return errors.Wrap(err, "cannot marshal patched XR")
}

data := append([]byte("---\n"), b...)

if c.OutputFile != "" {
if err := afero.WriteFile(c.fs, c.OutputFile, data, 0o644); err != nil {
return errors.Wrapf(err, "cannot write output file %q", c.OutputFile)
}

return nil
}

if _, err := k.Stdout.Write(data); err != nil {
return errors.Wrap(err, "cannot write output")
}

return nil
}
1 change: 1 addition & 0 deletions cmd/crossplane/xr/xr.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ package xr
// Cmd contains XR subcommands.
type Cmd struct {
Generate generateCmd `cmd:"" help:"Generate a Composite Resource (XR) from a Claim."`
Patch patchCmd `cmd:"" help:"Patch a Composite Resource (XR) with additional configurations."`
}
81 changes: 81 additions & 0 deletions cmd/crossplane/xr/xrd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright 2026 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package xr

import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
schema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
"github.com/crossplane/crossplane-runtime/v2/pkg/xcrd"

apiextensionsv1 "github.com/crossplane/crossplane/apis/v2/apiextensions/v1"
)

// ApplyXRDDefaults applies default values from an XRD's openAPIV3Schema to an
// XR. The XR is mutated in place.
//
// This is the canonical XRD-defaulting entry point for the cli; downstream
// commands and tools (e.g. `crossplane render xr --xrd`) call into this
// function rather than re-implementing the schema-defaulting routine.
func ApplyXRDDefaults(xr *unstructured.Unstructured, xrdef *apiextensionsv1.CompositeResourceDefinition) error {
crd, err := xcrd.ForCompositeResource(xrdef)
if err != nil {
return errors.Wrapf(err, "cannot derive CRD from XRD %q", xrdef.GetName())
}

return DefaultValues(xr.UnstructuredContent(), xr.GetAPIVersion(), *crd)
}

// DefaultValues sets default values on the XR based on the CRD schema.
//
// Callers starting from an XRD should prefer ApplyXRDDefaults; this is the
// lower-level routine for callers that already have a CRD in hand.
func DefaultValues(xr map[string]any, apiVersion string, crd extv1.CustomResourceDefinition) error {
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 kept the same name and code that it was there. But maybe we should rename this function to ApplyCRDDefaults to match the other? If we do that though, we might break someone's setup that might be using it.

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.

We'll already have broken them by moving it from cmd/crossplane/render to cmd/crossplane/xrd, so I'd be in favor of renaming as well.

Might also be a good time to move this code out of cmd/ since multiple commands rely on it. Maybe a new exported package like pkg/xr or pkg/composite.

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.

good idea, what is your preference?

var (
k apiextensions.JSONSchemaProps
version *extv1.CustomResourceDefinitionVersion
)

for _, vr := range crd.Spec.Versions {
checkAPIVersion := crd.Spec.Group + "/" + vr.Name
if checkAPIVersion == apiVersion {
version = &vr
break
}
}

if version == nil {
return errors.Errorf("the specified API version '%s' does not exist in the XRD", apiVersion)
}

if err := extv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(version.Schema.OpenAPIV3Schema, &k, nil); err != nil {
Comment on lines +65 to +69
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 | 🟡 Minor | ⚡ Quick win

Guard against a nil version.Schema before dereferencing?

Since DefaultValues is exported as the "lower-level routine for callers that already have a CRD in hand," a CRD version without a Schema would panic at version.Schema.OpenAPIV3Schema on Line 69. The ApplyXRDDefaults path via xcrd.ForCompositeResource always populates it, so this is purely defensive for direct callers — but a small nil check would turn a panic into a friendly error.

🛡️ Suggested guard
 	if version == nil {
 		return errors.Errorf("the specified API version '%s' does not exist in the XRD", apiVersion)
 	}
+
+	if version.Schema == nil || version.Schema.OpenAPIV3Schema == nil {
+		return errors.Errorf("the specified API version '%s' has no schema in the XRD", apiVersion)
+	}
📝 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 version == nil {
return errors.Errorf("the specified API version '%s' does not exist in the XRD", apiVersion)
}
if err := extv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(version.Schema.OpenAPIV3Schema, &k, nil); err != nil {
if version == nil {
return errors.Errorf("the specified API version '%s' does not exist in the XRD", apiVersion)
}
if version.Schema == nil || version.Schema.OpenAPIV3Schema == nil {
return errors.Errorf("the specified API version '%s' has no schema in the XRD", apiVersion)
}
if err := extv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(version.Schema.OpenAPIV3Schema, &k, nil); err != nil {
🤖 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/xrd.go` around lines 65 - 69, The code dereferences
version.Schema.OpenAPIV3Schema before checking for a nil Schema; update
DefaultValues (the routine using version) to validate that version.Schema is
non-nil and return a clear error if it is nil instead of panicking.
Specifically, before calling
extv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps with
version.Schema.OpenAPIV3Schema, add a guard that checks if version.Schema == nil
and return an errors.Errorf (or wrap) stating the CRD version lacks a Schema;
keep references to Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps,
version.Schema.OpenAPIV3Schema, and the
DefaultValues/ApplyXRDDefaults/xcrd.ForCompositeResource call sites in mind so
callers that already populate Schema remain unchanged.

return err
}

crdWithDefaults, err := schema.NewStructural(&k)
if err != nil {
return err
}

structuraldefaulting.Default(xr, crdWithDefaults)

return nil
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
package render
/*
Copyright 2026 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package xr

import (
"testing"
Expand Down
Loading