diff --git a/docs/cmd/tkn_bundle_push.md b/docs/cmd/tkn_bundle_push.md index 7654aa708..dcdd3b6fd 100644 --- a/docs/cmd/tkn_bundle_push.md +++ b/docs/cmd/tkn_bundle_push.md @@ -36,6 +36,7 @@ Created time: --ctime string YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS or RFC3339 formatted created time to set, defaults to current time. In non RFC3339 syntax dates are in UTC timezone. -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 + --label strings OCI Config labels in the form of key=value to be added to the OCI image. Can be provided multiple times to add multiple labels. --remote-bearer string A Bearer token to authenticate against the repository --remote-password string A password to pass to the registry for basic auth. Must be used with --remote-username --remote-skip-tls If set to true, skips TLS check when connecting to the registry diff --git a/docs/man/man1/tkn-bundle-push.1 b/docs/man/man1/tkn-bundle-push.1 index 0a4a2eac4..bc935cfae 100644 --- a/docs/man/man1/tkn-bundle-push.1 +++ b/docs/man/man1/tkn-bundle-push.1 @@ -61,6 +61,10 @@ Created time: \fB\-h\fP, \fB\-\-help\fP[=false] help for push +.PP +\fB\-\-label\fP=[] + OCI Config labels in the form of key=value to be added to the OCI image. Can be provided multiple times to add multiple labels. + .PP \fB\-\-remote\-bearer\fP="" A Bearer token to authenticate against the repository diff --git a/pkg/bundle/builder.go b/pkg/bundle/builder.go index 6abb1e22d..1b4cffc2d 100644 --- a/pkg/bundle/builder.go +++ b/pkg/bundle/builder.go @@ -23,8 +23,14 @@ 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, annotations map[string]string, ctime time.Time, log io.Writer) (v1.Image, error) { +func BuildTektonBundle(contents []string, annotations, labels map[string]string, ctime time.Time, log io.Writer) (v1.Image, error) { img := mutate.Annotations(empty.Image, annotations).(v1.Image) + img, err := mutate.Config(img, v1.Config{ + Labels: labels, + }) + if err != nil { + return nil, fmt.Errorf("setting labels: %w", err) + } if len(contents) > tkremote.MaximumBundleObjects { return nil, fmt.Errorf("bundle contains more than the maximum %d allow objects", tkremote.MaximumBundleObjects) @@ -96,7 +102,7 @@ func BuildTektonBundle(contents []string, annotations map[string]string, ctime t } // Set created time for bundle image - img, err := mutate.CreatedAt(img, v1.Time{Time: ctime}) + img, err = mutate.CreatedAt(img, v1.Time{Time: ctime}) if err != nil { return nil, fmt.Errorf("failed to add created time to image: %w", err) } diff --git a/pkg/bundle/builder_test.go b/pkg/bundle/builder_test.go index 2d96f2821..17134c4cc 100644 --- a/pkg/bundle/builder_test.go +++ b/pkg/bundle/builder_test.go @@ -69,7 +69,8 @@ func TestBuildTektonBundle(t *testing.T) { } annotations := map[string]string{"org.opencontainers.image.license": "Apache-2.0", "org.opencontainers.image.url": "https://example.org"} - img, err := BuildTektonBundle([]string{string(raw)}, annotations, time.Now(), &bytes.Buffer{}) + labels := map[string]string{"version": "1.0", "quay.expires-after": "7d"} + img, err := BuildTektonBundle([]string{string(raw)}, annotations, labels, time.Now(), &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -82,6 +83,9 @@ func TestBuildTektonBundle(t *testing.T) { if cfg.Created.IsZero() { t.Error("Created time of image was not set") } + if !cmp.Equal(cfg.Config.Labels, labels) { + t.Errorf("Expected labels were not set got: %+v", cfg.Config.Labels) + } manifest, err := img.Manifest() if err != nil { @@ -163,7 +167,7 @@ func TestBadObj(t *testing.T) { t.Error(err) return } - _, err = BuildTektonBundle([]string{string(raw)}, nil, time.Now(), &bytes.Buffer{}) + _, err = BuildTektonBundle([]string{string(raw)}, nil, nil, time.Now(), &bytes.Buffer{}) noNameErr := errors.New("kubernetes resources should have a name") if err == nil { t.Errorf("expected error: %v", noNameErr) @@ -186,7 +190,7 @@ func TestLessThenMaxBundle(t *testing.T) { return } // no error for less then max - _, err = BuildTektonBundle([]string{string(raw)}, nil, time.Now(), &bytes.Buffer{}) + _, err = BuildTektonBundle([]string{string(raw)}, nil, nil, time.Now(), &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -214,7 +218,7 @@ func TestJustEnoughBundleSize(t *testing.T) { justEnoughObj = append(justEnoughObj, string(raw)) } // no error for the max - _, err := BuildTektonBundle(justEnoughObj, nil, time.Now(), &bytes.Buffer{}) + _, err := BuildTektonBundle(justEnoughObj, nil, nil, time.Now(), &bytes.Buffer{}) if err != nil { t.Error(err) } @@ -243,14 +247,14 @@ func TestTooManyInBundle(t *testing.T) { } // expect error when we hit the max - _, err := BuildTektonBundle(toMuchObj, nil, time.Now(), &bytes.Buffer{}) + _, err := BuildTektonBundle(toMuchObj, nil, nil, time.Now(), &bytes.Buffer{}) if err == nil { t.Errorf("expected error: %v", toManyObjErr) } } func TestDeterministicLayers(t *testing.T) { - img, err := BuildTektonBundle(threeTasks, nil, time.Now(), &bytes.Buffer{}) + img, err := BuildTektonBundle(threeTasks, nil, nil, time.Now(), &bytes.Buffer{}) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -282,7 +286,7 @@ func TestDeterministicLayers(t *testing.T) { } func TestDeterministicManifest(t *testing.T) { - img, err := BuildTektonBundle(threeTasks, nil, time.Time{}, &bytes.Buffer{}) + img, err := BuildTektonBundle(threeTasks, nil, nil, time.Time{}, &bytes.Buffer{}) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/pkg/cmd/bundle/list_test.go b/pkg/cmd/bundle/list_test.go index 1bc6d5c06..97d92ed3d 100644 --- a/pkg/cmd/bundle/list_test.go +++ b/pkg/cmd/bundle/list_test.go @@ -105,7 +105,7 @@ func TestListCommand(t *testing.T) { t.Fatal(err) } - img, err := bundle.BuildTektonBundle([]string{examplePullTask, examplePullPipeline}, nil, time.Now(), &bytes.Buffer{}) + img, err := bundle.BuildTektonBundle([]string{examplePullTask, examplePullPipeline}, nil, nil, time.Now(), &bytes.Buffer{}) if err != nil { t.Fatal(err) } diff --git a/pkg/cmd/bundle/push.go b/pkg/cmd/bundle/push.go index 9f25fe966..03c1353c0 100644 --- a/pkg/cmd/bundle/push.go +++ b/pkg/cmd/bundle/push.go @@ -42,6 +42,8 @@ type pushOptions struct { remoteOptions bundle.RemoteOptions annotationParams []string annotations map[string]string + labelParams []string + labels map[string]string ctimeParam string ctime time.Time } @@ -101,6 +103,7 @@ Created time: 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", "", []string{}, "OCI Manifest annotation in the form of key=value to be added to the OCI image. Can be provided multiple times to add multiple annotations.") c.Flags().StringVar(&opts.ctimeParam, "ctime", "", "YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS or RFC3339 formatted created time to set, defaults to current time. In non RFC3339 syntax dates are in UTC timezone.") + c.Flags().StringSliceVarP(&opts.labelParams, "label", "", []string{}, "OCI Config labels in the form of key=value to be added to the OCI image. Can be provided multiple times to add multiple labels.") bundle.AddRemoteFlags(c.Flags(), &opts.remoteOptions) return c @@ -137,6 +140,10 @@ func (p *pushOptions) parseArgsAndFlags(args []string) (err error) { return err } + if p.labels, err = params.ParseParams(p.labelParams); err != nil { + return err + } + if p.ctime, err = determineCTime(p.ctimeParam); err != nil { return err } @@ -150,7 +157,7 @@ func (p *pushOptions) Run(args []string) error { return err } - img, err := bundle.BuildTektonBundle(p.bundleContents, p.annotations, p.ctime, p.stream.Out) + img, err := bundle.BuildTektonBundle(p.bundleContents, p.annotations, p.labels, p.ctime, 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 92972dea8..aa3179b4e 100644 --- a/pkg/cmd/bundle/push_test.go +++ b/pkg/cmd/bundle/push_test.go @@ -62,6 +62,8 @@ func TestPushCommand(t *testing.T) { expectedAnnotations map[string]string ctime string expectedCTime time.Time + labels []string + expectedLabels map[string]string }{ { name: "single-input", @@ -102,6 +104,16 @@ func TestPushCommand(t *testing.T) { ctime: fixedTime.Format(time.RFC3339), expectedCTime: fixedTime, }, + { + name: "with-labels", + files: map[string]string{ + "simple.yaml": exampleTask, + }, + expectedContents: map[string]expected{exampleTaskExpected.name: exampleTaskExpected}, + labels: []string{"version=1.0", "quay.expires-after=7d"}, + expectedLabels: map[string]string{"version": "1.0", "quay.expires-after": "7d"}, + expectedCTime: time.Unix(defaultTimestamp, 0), + }, } for _, tc := range testcases { @@ -149,6 +161,7 @@ func TestPushCommand(t *testing.T) { annotationParams: tc.annotations, remoteOptions: bundle.RemoteOptions{}, ctimeParam: tc.ctime, + labelParams: tc.labels, } if err := opts.Run([]string{ref}); err != nil { @@ -174,6 +187,10 @@ func TestPushCommand(t *testing.T) { t.Errorf("Expected created time to be %s, but it was %s", tc.expectedCTime, config.Created.Time) } + if !cmp.Equal(config.Config.Labels, tc.expectedLabels) { + t.Errorf("Expected labels to be %+v, but it was %+v", tc.expectedLabels, config.Config.Labels) + } + layers, err := img.Layers() if err != nil { t.Fatal(err)