-
Notifications
You must be signed in to change notification settings - Fork 8
feat(xr): Introduce xr patch --xrd
#61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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 | ||
| ``` |
| 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"` | ||
|
|
||
| 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 | ||
| } | ||
| 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 { | ||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll already have broken them by moving it from Might also be a good time to move this code out of
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against a Since 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| crdWithDefaults, err := schema.NewStructural(&k) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| structuraldefaulting.Default(xr, crdWithDefaults) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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-xrdor just--xrd-defaults? It's not really clear from the flag name--xrdthat it indicates an action.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose
xr patch --xrdto be the same asrender --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 upcomingtestsubcommand, which will havepatch.xrdas 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.