From cf573840844c43d78b01be6bb1e0a3915ba9e7d2 Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Thu, 24 Aug 2023 14:08:32 +0200 Subject: [PATCH] Support for Manifest annotations in `bundle push` Adds `-a` or `--annotate` parameter to `tkn bundle push` to set any OCI Manifest annotations on the bundle image. Fixes #1933 --- docs/cmd/tkn_bundle_push.md | 1 + docs/man/man1/tkn-bundle-push.1 | 4 ++++ pkg/bundle/builder.go | 4 ++-- pkg/bundle/builder_test.go | 16 +++++++++++----- pkg/cmd/bundle/list_test.go | 2 +- pkg/cmd/bundle/push.go | 15 ++++++++++++++- pkg/cmd/bundle/push_test.go | 27 +++++++++++++++++++++++---- 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/docs/cmd/tkn_bundle_push.md b/docs/cmd/tkn_bundle_push.md index 2bd2265db5..c324e30c9a 100644 --- a/docs/cmd/tkn_bundle_push.md +++ b/docs/cmd/tkn_bundle_push.md @@ -29,6 +29,7 @@ Input: ### Options ``` + -a, --annotate strings OCI Manifest annotations in the form of key=value to be added to the OCI image -f, --filenames strings List of fully-qualified file paths containing YAML or JSON defined Tekton objects to include in this bundle -h, --help help for push --remote-bearer string A Bearer token to authenticate against the repository diff --git a/docs/man/man1/tkn-bundle-push.1 b/docs/man/man1/tkn-bundle-push.1 index 3c99f611ec..f3d364dc33 100644 --- a/docs/man/man1/tkn-bundle-push.1 +++ b/docs/man/man1/tkn-bundle-push.1 @@ -41,6 +41,10 @@ Input: .SH OPTIONS +.PP +\fB\-a\fP, \fB\-\-annotate\fP=[] + OCI Manifest annotations in the form of key=value to be added to the OCI image + .PP \fB\-f\fP, \fB\-\-filenames\fP=[] List of fully\-qualified file paths containing YAML or JSON defined Tekton objects to include in this bundle diff --git a/pkg/bundle/builder.go b/pkg/bundle/builder.go index 45d8938eca..1319d6a038 100644 --- a/pkg/bundle/builder.go +++ b/pkg/bundle/builder.go @@ -21,8 +21,8 @@ import ( // BuildTektonBundle will return a complete OCI Image usable as a Tekton Bundle built by parsing, decoding, and // compressing the provided contents as Tekton objects. -func BuildTektonBundle(contents []string, log io.Writer) (v1.Image, error) { - img := empty.Image +func BuildTektonBundle(contents []string, annotations map[string]string, log io.Writer) (v1.Image, error) { + img := mutate.Annotations(empty.Image, annotations).(v1.Image) if len(contents) > tkremote.MaximumBundleObjects { return nil, fmt.Errorf("bundle contains more than the maximum %d allow objects", tkremote.MaximumBundleObjects) diff --git a/pkg/bundle/builder_test.go b/pkg/bundle/builder_test.go index c8bb27b732..b4c193f626 100644 --- a/pkg/bundle/builder_test.go +++ b/pkg/bundle/builder_test.go @@ -34,7 +34,8 @@ func TestBuildTektonBundle(t *testing.T) { return } - img, err := BuildTektonBundle([]string{string(raw)}, &bytes.Buffer{}) + annotations := map[string]string{"a1": "v1", "a2": "v2"} + img, err := BuildTektonBundle([]string{string(raw)}, annotations, &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -54,6 +55,11 @@ func TestBuildTektonBundle(t *testing.T) { return } + ann := manifest.Annotations + if len(ann) != len(annotations) || fmt.Sprint(ann) != fmt.Sprint(annotations) { + t.Errorf("Requested annotations were not set wanted: %s, got %s", annotations, ann) + } + if len(manifest.Layers) != 1 { t.Errorf("Unexpected number of layers %d", len(manifest.Layers)) } @@ -123,7 +129,7 @@ func TestBadObj(t *testing.T) { t.Error(err) return } - _, err = BuildTektonBundle([]string{string(raw)}, &bytes.Buffer{}) + _, err = BuildTektonBundle([]string{string(raw)}, nil, &bytes.Buffer{}) noNameErr := errors.New("kubernetes resources should have a name") if err == nil { t.Errorf("expected error: %v", noNameErr) @@ -146,7 +152,7 @@ func TestLessThenMaxBundle(t *testing.T) { return } // no error for less then max - _, err = BuildTektonBundle([]string{string(raw)}, &bytes.Buffer{}) + _, err = BuildTektonBundle([]string{string(raw)}, nil, &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -174,7 +180,7 @@ func TestJustEnoughBundleSize(t *testing.T) { justEnoughObj = append(justEnoughObj, string(raw)) } // no error for the max - _, err := BuildTektonBundle(justEnoughObj, &bytes.Buffer{}) + _, err := BuildTektonBundle(justEnoughObj, nil, &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -203,7 +209,7 @@ func TestTooManyInBundle(t *testing.T) { } // expect error when we hit the max - _, err := BuildTektonBundle(toMuchObj, &bytes.Buffer{}) + _, err := BuildTektonBundle(toMuchObj, nil, &bytes.Buffer{}) if err == nil { t.Errorf("expected error: %v", toManyObjErr) } diff --git a/pkg/cmd/bundle/list_test.go b/pkg/cmd/bundle/list_test.go index d42fd9d59c..d597a1c91a 100644 --- a/pkg/cmd/bundle/list_test.go +++ b/pkg/cmd/bundle/list_test.go @@ -104,7 +104,7 @@ func TestListCommand(t *testing.T) { t.Fatal(err) } - img, err := bundle.BuildTektonBundle([]string{examplePullTask, examplePullPipeline}, &bytes.Buffer{}) + img, err := bundle.BuildTektonBundle([]string{examplePullTask, examplePullPipeline}, nil, &bytes.Buffer{}) if err != nil { t.Fatal(err) } diff --git a/pkg/cmd/bundle/push.go b/pkg/cmd/bundle/push.go index 559160fc1f..aef634b41c 100644 --- a/pkg/cmd/bundle/push.go +++ b/pkg/cmd/bundle/push.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" @@ -31,6 +32,8 @@ type pushOptions struct { bundleContents []string bundleContentPaths []string remoteOptions bundle.RemoteOptions + annotationParams []string + annotations map[string]string } func pushCommand(_ cli.Params) *cobra.Command { @@ -80,6 +83,7 @@ Input: }, } c.Flags().StringSliceVarP(&opts.bundleContentPaths, "filenames", "f", []string{}, "List of fully-qualified file paths containing YAML or JSON defined Tekton objects to include in this bundle") + c.Flags().StringSliceVarP(&opts.annotationParams, "annotate", "a", []string{}, "OCI Manifest annotations in the form of key=value to be added to the OCI image") bundle.AddRemoteFlags(c.Flags(), &opts.remoteOptions) return c @@ -109,6 +113,15 @@ func (p *pushOptions) parseArgsAndFlags(args []string) error { p.bundleContents = append(p.bundleContents, string(contents)) } + p.annotations = map[string]string{} + for _, annParam := range p.annotationParams { + if k, v, ok := strings.Cut(annParam, "="); ok { + p.annotations[strings.TrimSpace(k)] = strings.TrimSpace(v) + } else { + return fmt.Errorf("annotation parameter not in key=value syntax: %q", annParam) + } + } + return nil } @@ -118,7 +131,7 @@ func (p *pushOptions) Run(args []string) error { return err } - img, err := bundle.BuildTektonBundle(p.bundleContents, p.stream.Out) + img, err := bundle.BuildTektonBundle(p.bundleContents, p.annotations, p.stream.Out) if err != nil { return err } diff --git a/pkg/cmd/bundle/push_test.go b/pkg/cmd/bundle/push_test.go index 77aa08a6c2..8db0d5c7c8 100644 --- a/pkg/cmd/bundle/push_test.go +++ b/pkg/cmd/bundle/push_test.go @@ -51,10 +51,12 @@ var ( func TestPushCommand(t *testing.T) { testcases := []struct { - name string - files map[string]string - stdin string - expectedContents map[string]expected + name string + files map[string]string + stdin string + annotations []string + expectedContents map[string]expected + expectedAnnotations map[string]string }{ { name: "single-input", @@ -71,6 +73,18 @@ func TestPushCommand(t *testing.T) { stdin: exampleTask, expectedContents: map[string]expected{exampleTaskExpected.name: exampleTaskExpected}, }, + { + name: "with-annotations", + files: map[string]string{ + "simple.yaml": exampleTask, + }, + annotations: []string{"a1=k1", "a2 = k2"}, + expectedContents: map[string]expected{exampleTaskExpected.name: exampleTaskExpected}, + expectedAnnotations: map[string]string{ + "a1": "k1", + "a2": "k2", + }, + }, } for _, tc := range testcases { @@ -112,6 +126,7 @@ func TestPushCommand(t *testing.T) { Err: &bytes.Buffer{}, }, bundleContentPaths: paths, + annotationParams: tc.annotations, remoteOptions: bundle.RemoteOptions{}, } if err := opts.Run([]string{ref}); err != nil { @@ -138,6 +153,10 @@ func TestPushCommand(t *testing.T) { t.Errorf("Expected %d layers but found %d", len(tc.expectedContents), len(manifest.Layers)) } + if len(manifest.Annotations) != len(tc.expectedAnnotations) || fmt.Sprint(manifest.Annotations) != fmt.Sprint(tc.expectedAnnotations) { + t.Errorf("Requested annotations were not set wanted: %s, got %s", tc.expectedAnnotations, manifest.Annotations) + } + for i, l := range manifest.Layers { title, ok := l.Annotations[tkremote.TitleAnnotation] if !ok {