From 04ed63dace6e596e227ff20cfa0a6f6c03b02d98 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:22:22 +0530 Subject: [PATCH 1/8] feat(xpkg): add parseAnnotations helper for OCI manifest annotations Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/annotations.go | 37 +++++++++++ cmd/crossplane/xpkg/annotations_test.go | 86 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 cmd/crossplane/xpkg/annotations.go create mode 100644 cmd/crossplane/xpkg/annotations_test.go diff --git a/cmd/crossplane/xpkg/annotations.go b/cmd/crossplane/xpkg/annotations.go new file mode 100644 index 00000000..4f45073e --- /dev/null +++ b/cmd/crossplane/xpkg/annotations.go @@ -0,0 +1,37 @@ +/* +Copyright 2025 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 xpkg + +import ( + "strings" + + "github.com/crossplane/crossplane-runtime/v2/pkg/errors" +) + +// parseAnnotations parses a slice of "key=value" strings into a map. Returns +// an error if any entry is not in key=value format. +func parseAnnotations(kvs []string) (map[string]string, error) { + anns := make(map[string]string, len(kvs)) + for _, kv := range kvs { + k, v, ok := strings.Cut(kv, "=") + if !ok { + return nil, errors.Errorf("invalid annotation %q: must be in key=value format", kv) + } + anns[k] = v + } + return anns, nil +} diff --git a/cmd/crossplane/xpkg/annotations_test.go b/cmd/crossplane/xpkg/annotations_test.go new file mode 100644 index 00000000..0114ae37 --- /dev/null +++ b/cmd/crossplane/xpkg/annotations_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2025 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 xpkg + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestParseAnnotations(t *testing.T) { + type args struct { + kvs []string + } + type want struct { + anns map[string]string + err error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "EmptySlice": { + reason: "Empty input should return an empty map with no error.", + args: args{kvs: []string{}}, + want: want{anns: map[string]string{}}, + }, + "SingleEntry": { + reason: "A single valid key=value entry should be parsed correctly.", + args: args{kvs: []string{"org.example/key=value"}}, + want: want{anns: map[string]string{"org.example/key": "value"}}, + }, + "MultipleEntries": { + reason: "Multiple valid key=value entries should all be parsed.", + args: args{kvs: []string{ + "org.opencontainers.image.source=https://github.com/example/pkg", + "org.opencontainers.image.version=v1.0.0", + }}, + want: want{anns: map[string]string{ + "org.opencontainers.image.source": "https://github.com/example/pkg", + "org.opencontainers.image.version": "v1.0.0", + }}, + }, + "ValueContainsEquals": { + reason: "Values that contain '=' characters should be preserved intact.", + args: args{kvs: []string{"key=val=ue"}}, + want: want{anns: map[string]string{"key": "val=ue"}}, + }, + "MissingEquals": { + reason: "An entry without '=' should return an error.", + args: args{kvs: []string{"invalid-no-equals"}}, + want: want{err: cmpopts.AnyError}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, err := parseAnnotations(tc.args.kvs) + + if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("\n%s\nparseAnnotations(...): -want error, +got error:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.anns, got); diff != "" { + t.Errorf("\n%s\nparseAnnotations(...): -want, +got:\n%s", tc.reason, diff) + } + }) + } +} From da78a01ea0e71a95ff0011986870e9fd2e458e37 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:24:15 +0530 Subject: [PATCH 2/8] feat(xpkg): add --annotation flag to xpkg build Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/build.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cmd/crossplane/xpkg/build.go b/cmd/crossplane/xpkg/build.go index 83c209a8..652f3464 100644 --- a/cmd/crossplane/xpkg/build.go +++ b/cmd/crossplane/xpkg/build.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" + "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/spf13/afero" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -50,6 +51,7 @@ const ( errPullRuntimeImage = "failed to pull runtime image" errLoadRuntimeTarball = "failed to load runtime tarball" errGetRuntimeBaseImageOpts = "failed to get runtime base image options" + errParseAnnotations = "failed to parse annotations" ) // AfterApply constructs and binds context to any subcommands @@ -99,12 +101,13 @@ func (c *buildCmd) AfterApply() error { // buildCmd builds a crossplane package. type buildCmd struct { // Flags. Keep sorted alphabetically. - EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` - EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` - ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` - Ignore []string `help:"comma-separated list of globs specifying files to exclude from the build, relative to --package-root." placeholder:"PATH"` - PackageFile string `help:"The file to write the package to. Defaults to a generated filename in --package-root." placeholder:"PATH" predictor:"xpkg_file" short:"o" type:"path"` - PackageRoot string `default:"." help:"The directory that contains the package's crossplane.yaml file." predictor:"directory" short:"f" type:"existingdir"` + Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` + EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` + EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` + ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` + Ignore []string `help:"Comma-separated file paths, specified relative to --package-root, to exclude from the package. Wildcards are supported. Directories cannot be excluded." placeholder:"PATH"` + PackageFile string `help:"The file to write the package to. Defaults to a generated filename in --package-root." placeholder:"PATH" predictor:"xpkg_file" short:"o" type:"path"` + PackageRoot string `default:"." help:"The directory that contains the package's crossplane.yaml file." predictor:"directory" short:"f" type:"existingdir"` // Internal state. These aren't part of the user-exposed CLI structure. fs afero.Fs @@ -175,6 +178,14 @@ func (c *buildCmd) Run(logger logging.Logger) error { return errors.Wrap(err, errBuildPackage) } + anns, err := parseAnnotations(c.Annotation) + if err != nil { + return errors.Wrap(err, errParseAnnotations) + } + if len(anns) > 0 { + img = mutate.Annotations(img, anns).(v1.Image) + } + hash, err := img.Digest() if err != nil { return errors.Wrap(err, errImageDigest) From 397fef249b23277eb8b77e3f4dff2cbd2ae20854 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:27:18 +0530 Subject: [PATCH 3/8] feat(xpkg): add --annotation flag to xpkg push Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/batch.go | 2 +- cmd/crossplane/xpkg/push.go | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cmd/crossplane/xpkg/batch.go b/cmd/crossplane/xpkg/batch.go index 4aec2613..952e8d38 100644 --- a/cmd/crossplane/xpkg/batch.go +++ b/cmd/crossplane/xpkg/batch.go @@ -281,7 +281,7 @@ func (c *batchCmd) pushWithRetry(logger logging.Logger, imgs []packageImage, s s retryMsg := "" for i := range tries { logger.Info(fmt.Sprintf("Pushing xpkg to %s.%s", t, retryMsg)) - err := pushImages(logger, imgs, t) + err := pushImages(logger, imgs, t, nil) if err == nil { break } diff --git a/cmd/crossplane/xpkg/push.go b/cmd/crossplane/xpkg/push.go index 42b6ddb7..f6266c91 100644 --- a/cmd/crossplane/xpkg/push.go +++ b/cmd/crossplane/xpkg/push.go @@ -65,6 +65,7 @@ type pushCmd struct { Package string `arg:"" help:"Where to push the package. Must be a fully qualified OCI tag, including the registry, repository, and tag." placeholder:"REGISTRY/REPOSITORY:TAG"` // Flags. Keep sorted alphabetically. + Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` InsecureSkipTLSVerify bool `help:"[INSECURE] Skip verifying TLS certificates."` PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` @@ -126,7 +127,12 @@ func (c *pushCmd) Run(logger logging.Logger) error { remote.WithTransport(t), } - return pushImages(logger, images, c.Package, options...) + anns, err := parseAnnotations(c.Annotation) + if err != nil { + return errors.Wrap(err, errParseAnnotations) + } + + return pushImages(logger, images, c.Package, anns, options...) } // packageImage describes a package image that will be pushed. @@ -140,7 +146,7 @@ type packageImage struct { } // pushImages pushes package images to the given URL using the provided options. -func pushImages(logger logging.Logger, images []packageImage, url string, options ...remote.Option) error { +func pushImages(logger logging.Logger, images []packageImage, url string, annotations map[string]string, options ...remote.Option) error { if len(options) == 0 { options = []remote.Option{ remote.WithAuthFromKeychain(authn.DefaultKeychain), @@ -161,6 +167,10 @@ func pushImages(logger logging.Logger, images []packageImage, url string, option return errors.Wrapf(err, errAnnotateLayers) } + if len(annotations) > 0 { + img = mutate.Annotations(img, annotations).(v1.Image) + } + if err := remote.Write(tag, img, options...); err != nil { return errors.Wrapf(err, errFmtPushPackage, pi.Path) } @@ -183,6 +193,10 @@ func pushImages(logger logging.Logger, images []packageImage, url string, option return errors.Wrapf(err, errAnnotateLayers) } + if len(annotations) > 0 { + img = mutate.Annotations(img, annotations).(v1.Image) + } + d, err := img.Digest() if err != nil { return errors.Wrapf(err, errFmtGetDigest, pi.Path) From b248f8a3e4dbfd6b79a570f514975de2006b1d4c Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:55:55 +0530 Subject: [PATCH 4/8] fix(xpkg): address golangci-lint issues in annotation support Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/annotations.go | 12 ++++++++++++ cmd/crossplane/xpkg/build.go | 7 ++----- cmd/crossplane/xpkg/push.go | 10 +++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cmd/crossplane/xpkg/annotations.go b/cmd/crossplane/xpkg/annotations.go index 4f45073e..992a3a08 100644 --- a/cmd/crossplane/xpkg/annotations.go +++ b/cmd/crossplane/xpkg/annotations.go @@ -19,6 +19,9 @@ package xpkg import ( "strings" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/crossplane/crossplane-runtime/v2/pkg/errors" ) @@ -35,3 +38,12 @@ func parseAnnotations(kvs []string) (map[string]string, error) { } return anns, nil } + +// annotateImage applies annotations to an OCI image manifest. It is a no-op +// when annotations is empty or nil. +func annotateImage(img v1.Image, annotations map[string]string) v1.Image { + if len(annotations) == 0 { + return img + } + return mutate.Annotations(img, annotations).(v1.Image) //nolint:forcetypeassert // mutate.Annotations always returns v1.Image when given v1.Image input +} diff --git a/cmd/crossplane/xpkg/build.go b/cmd/crossplane/xpkg/build.go index 652f3464..8d3e913c 100644 --- a/cmd/crossplane/xpkg/build.go +++ b/cmd/crossplane/xpkg/build.go @@ -23,7 +23,6 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" - "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/spf13/afero" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -101,7 +100,7 @@ func (c *buildCmd) AfterApply() error { // buildCmd builds a crossplane package. type buildCmd struct { // Flags. Keep sorted alphabetically. - Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` + Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` @@ -182,9 +181,7 @@ func (c *buildCmd) Run(logger logging.Logger) error { if err != nil { return errors.Wrap(err, errParseAnnotations) } - if len(anns) > 0 { - img = mutate.Annotations(img, anns).(v1.Image) - } + img = annotateImage(img, anns) hash, err := img.Digest() if err != nil { diff --git a/cmd/crossplane/xpkg/push.go b/cmd/crossplane/xpkg/push.go index f6266c91..ceb9948a 100644 --- a/cmd/crossplane/xpkg/push.go +++ b/cmd/crossplane/xpkg/push.go @@ -67,7 +67,7 @@ type pushCmd struct { // Flags. Keep sorted alphabetically. Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` InsecureSkipTLSVerify bool `help:"[INSECURE] Skip verifying TLS certificates."` - PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` + PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` // Internal state. These aren't part of the user-exposed CLI structure. fs afero.Fs @@ -167,9 +167,7 @@ func pushImages(logger logging.Logger, images []packageImage, url string, annota return errors.Wrapf(err, errAnnotateLayers) } - if len(annotations) > 0 { - img = mutate.Annotations(img, annotations).(v1.Image) - } + img = annotateImage(img, annotations) if err := remote.Write(tag, img, options...); err != nil { return errors.Wrapf(err, errFmtPushPackage, pi.Path) @@ -193,9 +191,7 @@ func pushImages(logger logging.Logger, images []packageImage, url string, annota return errors.Wrapf(err, errAnnotateLayers) } - if len(annotations) > 0 { - img = mutate.Annotations(img, annotations).(v1.Image) - } + img = annotateImage(img, annotations) d, err := img.Digest() if err != nil { From d01fdc03334d39c506ec6a6fb0023668012fc66a Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sat, 6 Jun 2026 23:50:21 +0530 Subject: [PATCH 5/8] fix(xpkg): address PR review feedback on annotation support - Rename --annotation to --oci-annotation in xpkg build and push to distinguish from Kubernetes metadata.annotations - Apply OCI annotations to the image index in the multi-platform push path, not only to individual manifests - Fix copyright year in annotations.go and annotations_test.go Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/annotations.go | 11 ++++++++++- cmd/crossplane/xpkg/annotations_test.go | 2 +- cmd/crossplane/xpkg/build.go | 4 ++-- cmd/crossplane/xpkg/push.go | 7 ++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cmd/crossplane/xpkg/annotations.go b/cmd/crossplane/xpkg/annotations.go index 992a3a08..141b0ecb 100644 --- a/cmd/crossplane/xpkg/annotations.go +++ b/cmd/crossplane/xpkg/annotations.go @@ -1,5 +1,5 @@ /* -Copyright 2025 The Crossplane Authors. +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. @@ -47,3 +47,12 @@ func annotateImage(img v1.Image, annotations map[string]string) v1.Image { } return mutate.Annotations(img, annotations).(v1.Image) //nolint:forcetypeassert // mutate.Annotations always returns v1.Image when given v1.Image input } + +// annotateIndex applies annotations to an OCI image index manifest. It is a +// no-op when annotations is empty or nil. +func annotateIndex(idx v1.ImageIndex, annotations map[string]string) v1.ImageIndex { + if len(annotations) == 0 { + return idx + } + return mutate.Annotations(idx, annotations).(v1.ImageIndex) //nolint:forcetypeassert // mutate.Annotations always returns v1.ImageIndex when given v1.ImageIndex input +} diff --git a/cmd/crossplane/xpkg/annotations_test.go b/cmd/crossplane/xpkg/annotations_test.go index 0114ae37..f3def9c5 100644 --- a/cmd/crossplane/xpkg/annotations_test.go +++ b/cmd/crossplane/xpkg/annotations_test.go @@ -1,5 +1,5 @@ /* -Copyright 2025 The Crossplane Authors. +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. diff --git a/cmd/crossplane/xpkg/build.go b/cmd/crossplane/xpkg/build.go index 8d3e913c..15a98870 100644 --- a/cmd/crossplane/xpkg/build.go +++ b/cmd/crossplane/xpkg/build.go @@ -100,7 +100,7 @@ func (c *buildCmd) AfterApply() error { // buildCmd builds a crossplane package. type buildCmd struct { // Flags. Keep sorted alphabetically. - Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` + OCIAnnotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." name:"oci-annotation" placeholder:"KEY=VALUE" short:"a"` EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` @@ -177,7 +177,7 @@ func (c *buildCmd) Run(logger logging.Logger) error { return errors.Wrap(err, errBuildPackage) } - anns, err := parseAnnotations(c.Annotation) + anns, err := parseAnnotations(c.OCIAnnotation) if err != nil { return errors.Wrap(err, errParseAnnotations) } diff --git a/cmd/crossplane/xpkg/push.go b/cmd/crossplane/xpkg/push.go index ceb9948a..04501371 100644 --- a/cmd/crossplane/xpkg/push.go +++ b/cmd/crossplane/xpkg/push.go @@ -65,7 +65,7 @@ type pushCmd struct { Package string `arg:"" help:"Where to push the package. Must be a fully qualified OCI tag, including the registry, repository, and tag." placeholder:"REGISTRY/REPOSITORY:TAG"` // Flags. Keep sorted alphabetically. - Annotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." placeholder:"KEY=VALUE" short:"a"` + OCIAnnotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." name:"oci-annotation" placeholder:"KEY=VALUE" short:"a"` InsecureSkipTLSVerify bool `help:"[INSECURE] Skip verifying TLS certificates."` PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` @@ -127,7 +127,7 @@ func (c *pushCmd) Run(logger logging.Logger) error { remote.WithTransport(t), } - anns, err := parseAnnotations(c.Annotation) + anns, err := parseAnnotations(c.OCIAnnotation) if err != nil { return errors.Wrap(err, errParseAnnotations) } @@ -240,7 +240,8 @@ func pushImages(logger logging.Logger, images []packageImage, url string, annota return err } - if err := remote.WriteIndex(tag, mutate.AppendManifests(empty.Index, adds...), options...); err != nil { + idx := annotateIndex(mutate.AppendManifests(empty.Index, adds...), annotations) + if err := remote.WriteIndex(tag, idx, options...); err != nil { return errors.Wrapf(err, errFmtWriteIndex, len(adds)) } From 37a0a2f23ebbf0f8cf6198fa585be161d58c01f1 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sun, 7 Jun 2026 13:26:54 +0530 Subject: [PATCH 6/8] fix(xpkg): reject empty keys in parseAnnotations Annotations like "=value" passed strings.Cut with ok=true, silently inserting an empty string key into the map. Add an explicit check and return a clear error when the key is empty. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/annotations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/crossplane/xpkg/annotations.go b/cmd/crossplane/xpkg/annotations.go index 141b0ecb..80109cb7 100644 --- a/cmd/crossplane/xpkg/annotations.go +++ b/cmd/crossplane/xpkg/annotations.go @@ -34,6 +34,9 @@ func parseAnnotations(kvs []string) (map[string]string, error) { if !ok { return nil, errors.Errorf("invalid annotation %q: must be in key=value format", kv) } + if k == "" { + return nil, errors.Errorf("invalid annotation %q: key must not be empty", kv) + } anns[k] = v } return anns, nil From d79c9452f70055f7c633cb02655fe4c12fa29f22 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sat, 13 Jun 2026 21:29:59 +0530 Subject: [PATCH 7/8] fix(xpkg): remove --oci-annotation flag from xpkg build Annotations applied during build are not persisted in the tarball written by tarball.Write, so keeping the flag on build could mislead users into thinking they don't need to provide annotations to push. Move the flag and errParseAnnotations to push only. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/build.go | 8 -------- cmd/crossplane/xpkg/push.go | 9 +++++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/cmd/crossplane/xpkg/build.go b/cmd/crossplane/xpkg/build.go index 15a98870..1e33556d 100644 --- a/cmd/crossplane/xpkg/build.go +++ b/cmd/crossplane/xpkg/build.go @@ -50,7 +50,6 @@ const ( errPullRuntimeImage = "failed to pull runtime image" errLoadRuntimeTarball = "failed to load runtime tarball" errGetRuntimeBaseImageOpts = "failed to get runtime base image options" - errParseAnnotations = "failed to parse annotations" ) // AfterApply constructs and binds context to any subcommands @@ -100,7 +99,6 @@ func (c *buildCmd) AfterApply() error { // buildCmd builds a crossplane package. type buildCmd struct { // Flags. Keep sorted alphabetically. - OCIAnnotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." name:"oci-annotation" placeholder:"KEY=VALUE" short:"a"` EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` @@ -177,12 +175,6 @@ func (c *buildCmd) Run(logger logging.Logger) error { return errors.Wrap(err, errBuildPackage) } - anns, err := parseAnnotations(c.OCIAnnotation) - if err != nil { - return errors.Wrap(err, errParseAnnotations) - } - img = annotateImage(img, anns) - hash, err := img.Digest() if err != nil { return errors.Wrap(err, errImageDigest) diff --git a/cmd/crossplane/xpkg/push.go b/cmd/crossplane/xpkg/push.go index 04501371..b1bd6a7a 100644 --- a/cmd/crossplane/xpkg/push.go +++ b/cmd/crossplane/xpkg/push.go @@ -45,9 +45,10 @@ import ( var helpPush string const ( - errGetwd = "failed to get working directory while searching for package" - errFindPackageinWd = "failed to find a package in current working directory" - errAnnotateLayers = "failed to propagate xpkg annotations from OCI image config file to image layers" + errGetwd = "failed to get working directory while searching for package" + errFindPackageinWd = "failed to find a package in current working directory" + errAnnotateLayers = "failed to propagate xpkg annotations from OCI image config file to image layers" + errParseAnnotations = "failed to parse annotations" errFmtNewTag = "failed to parse package tag %q" errFmtReadPackage = "failed to read package file %s" @@ -67,7 +68,7 @@ type pushCmd struct { // Flags. Keep sorted alphabetically. OCIAnnotation []string `help:"An OCI manifest annotation to add to the package in key=value format. Repeatable." name:"oci-annotation" placeholder:"KEY=VALUE" short:"a"` InsecureSkipTLSVerify bool `help:"[INSECURE] Skip verifying TLS certificates."` - PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` + PackageFiles []string `help:"A comma-separated list of xpkg files to push." placeholder:"PATH" predictor:"xpkg_file" short:"f" type:"existingfile"` // Internal state. These aren't part of the user-exposed CLI structure. fs afero.Fs From 5c915f035f6cf8a79f656514879d4816a2b861de Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Wed, 17 Jun 2026 16:58:44 +0530 Subject: [PATCH 8/8] docs(xpkg): fix vale lint errors in --ignore flag help text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the Ignore flag help string to satisfy the vale rules enforced by CI: - Microsoft.Contractions: 'cannot' → 'can't' - write-good.Passive: active voice for 'are supported' and 'be excluded' No functional change; the generated command-reference.md is the only artefact affected. Signed-off-by: Chaitanya Maili --- cmd/crossplane/xpkg/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/crossplane/xpkg/build.go b/cmd/crossplane/xpkg/build.go index 1e33556d..e34c9e18 100644 --- a/cmd/crossplane/xpkg/build.go +++ b/cmd/crossplane/xpkg/build.go @@ -102,7 +102,7 @@ type buildCmd struct { EmbedRuntimeImage string `help:"An OCI image to embed in the package as its runtime." placeholder:"NAME" xor:"runtime-image"` EmbedRuntimeImageTarball string `help:"An OCI image tarball to embed in the package as its runtime." placeholder:"PATH" predictor:"file" type:"existingfile" xor:"runtime-image"` ExamplesRoot string `default:"./examples" help:"A directory of example YAML files to include in the package." predictor:"directory" short:"e" type:"path"` - Ignore []string `help:"Comma-separated file paths, specified relative to --package-root, to exclude from the package. Wildcards are supported. Directories cannot be excluded." placeholder:"PATH"` + Ignore []string `help:"Comma-separated file paths, specified relative to --package-root, to exclude from the package. Crossplane supports wildcards. You can't exclude directories." placeholder:"PATH"` PackageFile string `help:"The file to write the package to. Defaults to a generated filename in --package-root." placeholder:"PATH" predictor:"xpkg_file" short:"o" type:"path"` PackageRoot string `default:"." help:"The directory that contains the package's crossplane.yaml file." predictor:"directory" short:"f" type:"existingdir"`