From 55dc7613a3287520fcf4022e4d5608fb5ccc3ec6 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:22:22 +0530 Subject: [PATCH 1/7] 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 5198bf37bb5cc49b3b443a6d24b4945e0a802405 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:24:15 +0530 Subject: [PATCH 2/7] 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 16a474ac7ca2dd6b63e02652f200bd08cfca0fd4 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:27:18 +0530 Subject: [PATCH 3/7] 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 fe4fd52cbe5c259f27e70153a838ae71971854c4 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:55:55 +0530 Subject: [PATCH 4/7] 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 47841c0f59d99d6cf39978c142404b5dcb2bc0b1 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sat, 6 Jun 2026 23:50:21 +0530 Subject: [PATCH 5/7] 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 71a0c5014b6b50873a6d2b67248fc6510262edca Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sun, 7 Jun 2026 13:26:54 +0530 Subject: [PATCH 6/7] 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 22e4bff591df3aa4326596aa4c49b529c2a9610e Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sat, 13 Jun 2026 21:29:59 +0530 Subject: [PATCH 7/7] 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