-
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
Open
tampakrap
wants to merge
3
commits into
crossplane:main
Choose a base branch
from
tampakrap:theo/feat_patchxr_xrd
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. Pass at least one patching flag; | ||
| today the only one is `--xrd`, which applies default values from an XRD's | ||
| `openAPIV3Schema` to the XR. Future releases add more patching flags. | ||
|
|
||
| ## 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 | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| /* | ||
| 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" | ||
| xrpkg "github.com/crossplane/cli/v2/pkg/xr" | ||
|
|
||
| _ "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) that provides schema defaults for 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 := xrpkg.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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /* | ||
| 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 provides library functions for working with XRs. | ||
| 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 ApplyCRDDefaults(xr.UnstructuredContent(), xr.GetAPIVersion(), *crd) | ||
| } | ||
|
|
||
| // ApplyCRDDefaults 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 ApplyCRDDefaults(xr map[string]any, apiVersion string, crd extv1.CustomResourceDefinition) error { | ||
| 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 { | ||
| return err | ||
| } | ||
|
|
||
| crdWithDefaults, err := schema.NewStructural(&k) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| structuraldefaulting.Default(xr, crdWithDefaults) | ||
|
|
||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Guard against a
nilversion.Schemabefore dereferencing?Since
DefaultValuesis exported as the "lower-level routine for callers that already have a CRD in hand," a CRD version without aSchemawould panic atversion.Schema.OpenAPIV3Schemaon Line 69. TheApplyXRDDefaultspath viaxcrd.ForCompositeResourcealways 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
🤖 Prompt for AI Agents