diff --git a/pkg/shp/cmd/build/create.go b/pkg/shp/cmd/build/create.go index 09194693e..cb09183f6 100644 --- a/pkg/shp/cmd/build/create.go +++ b/pkg/shp/cmd/build/create.go @@ -72,6 +72,24 @@ func (c *CreateCommand) Run(params *params.Params, io *genericclioptions.IOStrea } b.Spec.Env = append(b.Spec.Env, parsedEnvs...) + labels, err := c.cmd.Flags().GetStringArray(flags.OutputImageLabelsFlag) + if err != nil { + return err + } + b.Spec.Output.Labels, err = util.StringSliceToMap(labels) + if err != nil { + return err + } + + annotations, err := c.cmd.Flags().GetStringArray(flags.OutputImageAnnotationsFlag) + if err != nil { + return err + } + b.Spec.Output.Annotations, err = util.StringSliceToMap(annotations) + if err != nil { + return err + } + flags.SanitizeBuildSpec(&b.Spec) clientset, err := params.ShipwrightClientSet() diff --git a/pkg/shp/flags/build.go b/pkg/shp/flags/build.go index 3c6b59ddd..edca89392 100644 --- a/pkg/shp/flags/build.go +++ b/pkg/shp/flags/build.go @@ -39,6 +39,8 @@ func BuildSpecFromFlags(flags *pflag.FlagSet) *buildv1alpha1.BuildSpec { imageFlags(flags, "output", &spec.Output) timeoutFlags(flags, spec.Timeout) envFlags(flags, spec.Env) + imageLabelsFlags(flags, spec.Output.Labels) + imageAnnotationsFlags(flags, spec.Output.Annotations) return spec } diff --git a/pkg/shp/flags/flags.go b/pkg/shp/flags/flags.go index 2553f1130..b31238fc0 100644 --- a/pkg/shp/flags/flags.go +++ b/pkg/shp/flags/flags.go @@ -46,6 +46,10 @@ const ( ServiceAccountGenerateFlag = "sa-generate" // TimeoutFlag command-line flag. TimeoutFlag = "timeout" + // OutputImageLabelsFlag command-line flag. + OutputImageLabelsFlag = "output-image-label" + // OutputImageAnnotationsFlag command-line flag. + OutputImageAnnotationsFlag = "output-image-annotation" ) // sourceFlags flags for ".spec.source" @@ -176,3 +180,28 @@ func envFlags(flags *pflag.FlagSet, envs []corev1.EnvVar) { "specify a key-value pair for an environment variable to set for the build container", ) } + +// imageLabelsFlags registers flags for output image labels. +func imageLabelsFlags(flags *pflag.FlagSet, labels map[string]string) { + var l []string + flags.StringArrayVarP( + &l, + OutputImageLabelsFlag, + "", + []string{}, + "specify a set of key-value pairs that correspond to labels to set on the output image", + ) + +} + +// imageLabelsFlags registers flags for output image annotations. +func imageAnnotationsFlags(flags *pflag.FlagSet, annotations map[string]string) { + var a []string + flags.StringArrayVarP( + &a, + OutputImageAnnotationsFlag, + "", + []string{}, + "specify a set of key-value pairs that correspond to annotations to set on the output image", + ) +} diff --git a/pkg/shp/util/env.go b/pkg/shp/util/env.go index f91c36cc8..591c1cab9 100644 --- a/pkg/shp/util/env.go +++ b/pkg/shp/util/env.go @@ -20,3 +20,17 @@ func StringSliceToEnvVarSlice(envs []string) ([]corev1.EnvVar, error) { return envVars, nil } + +func StringSliceToMap(kvPairs []string) (map[string]string, error) { + m := map[string]string{} + + for _, l := range kvPairs { + parts := strings.SplitN(l, "=", 2) + if len(parts) == 1 { + return nil, fmt.Errorf("failed to parse key-value pair %q, not enough parts", l) + } + m[parts[0]] = parts[1] + } + + return m, nil +} diff --git a/pkg/shp/util/env_test.go b/pkg/shp/util/env_test.go index a486b71eb..6970a7e92 100644 --- a/pkg/shp/util/env_test.go +++ b/pkg/shp/util/env_test.go @@ -76,3 +76,80 @@ func TestStringSliceToEnvVar_ErrorCases(t *testing.T) { }) } } + +func TestStringSliceToMap(t *testing.T) { + type args struct { + keyVal []string + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "no envs", + args: args{ + keyVal: []string{}, + }, + want: map[string]string{}, + }, { + name: "value containing '='", + args: args{ + keyVal: []string{"my-label=foo=bar"}, + }, + want: map[string]string{ + "my-label": "foo=bar", + }, + }, { + name: "one env", + args: args{ + keyVal: []string{"my-name=my-value"}, + }, + want: map[string]string{ + "my-name": "my-value", + }, + }, + { + name: "multiple envs", + args: args{ + keyVal: []string{"name-one=value-one", "name-two=value-two", "name-three=value-three"}, + }, + want: map[string]string{ + "name-one": "value-one", + "name-two": "value-two", + "name-three": "value-three", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := StringSliceToMap(tt.args.keyVal); !reflect.DeepEqual(got, tt.want) { + t.Errorf("StringSliceToMap() = %#v, want %#v", got, tt.want) + } + }) + } +} + +func TestStringSliceToMap_ErrorCases(t *testing.T) { + type args struct { + keyVal []string + } + tests := []struct { + name string + args args + }{ + { + name: "value part missing", + args: args{ + keyVal: []string{"abc"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := StringSliceToMap(tt.args.keyVal); err == nil { + t.Error("StringSliceToMap() = want error but got nil") + } + }) + } +} diff --git a/test/e2e/output-image-labels-annotations.bats b/test/e2e/output-image-labels-annotations.bats new file mode 100644 index 000000000..23b74e7cb --- /dev/null +++ b/test/e2e/output-image-labels-annotations.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats + +source test/e2e/helpers.sh + +setup() { + load 'bats/support/load' + load 'bats/assert/load' + load 'bats/file/load' +} + +teardown() { + run kubectl delete builds.shipwright.io --all + run kubectl delete buildruns.shipwright.io --all +} + +@test "shp output image labels and annotation lifecycle" { + # generate random names for our build and buildrun + build_name=$(random_name) + buildrun_name=$(random_name) + + # create a Build with a label and an annotation + run shp build create ${build_name} --source-url=https://github.com/shipwright-io/sample-go --output-image=my-image --output-image-label=foo=bar --output-image-annotation=created-by=shipwright + assert_success + + # ensure that the build was successfully created + assert_output --partial "Created build \"${build_name}\"" + + # get the yaml for the Build object + run kubectl get builds.shipwright.io/${build_name} -o yaml + assert_success + + # ensure that the label and annotation were inserted into the Build object + assert_output --partial "foo: bar" + assert_output --partial "created-by: shipwright" +} \ No newline at end of file