From 4386cea93c49a68fc7baf438f2083fa0049e9528 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:22:22 +0530 Subject: [PATCH 1/6] 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 0000000..4f45073 --- /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 0000000..0114ae3 --- /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 7ec8790186ab8b7d4165fba97cc17c9fa8a3907d Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:24:15 +0530 Subject: [PATCH 2/6] 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 83c209a..652f346 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 a119d6e278f9a7650bf4469a06c3b49cc0b8e764 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:27:18 +0530 Subject: [PATCH 3/6] 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 4aec261..952e8d3 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 42b6ddb..f6266c9 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 73f8bf99839bc657cf4b6cebdc8034c5ec9cf8f8 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Tue, 12 May 2026 10:55:55 +0530 Subject: [PATCH 4/6] 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 4f45073..992a3a0 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 652f346..8d3e913 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 f6266c9..ceb9948 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 29c10b29a852d355f78ea2c9ba9c4724e1fbdd55 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sat, 6 Jun 2026 23:50:21 +0530 Subject: [PATCH 5/6] 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 992a3a0..141b0ec 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 0114ae3..f3def9c 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 8d3e913..15a9887 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 ceb9948..0450137 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 f40a61fa7bb1862fc101da8f4cc6883800062b59 Mon Sep 17 00:00:00 2001 From: Chaitanya Maili Date: Sun, 7 Jun 2026 13:26:54 +0530 Subject: [PATCH 6/6] 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 141b0ec..80109cb 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