diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index 495521ecb..e06f3008d 100644 --- a/cmd/compose/publish.go +++ b/cmd/compose/publish.go @@ -20,6 +20,7 @@ import ( "context" "errors" "strings" + "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" diff --git a/cmd/compose/publish_test.go b/cmd/compose/publish_test.go new file mode 100644 index 000000000..1539db769 --- /dev/null +++ b/cmd/compose/publish_test.go @@ -0,0 +1,42 @@ +/* + Copyright 2020 Docker Compose CLI 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 compose + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestParseAnnotations(t *testing.T) { + t.Run("valid annotation", func(t *testing.T) { + result, err := parseAnnotations([]string{"foo=bar"}) + assert.NilError(t, err) + assert.DeepEqual(t, result, map[string]string{"foo": "bar"}) + }) + + t.Run("invalid annotation missing equals sign", func(t *testing.T) { + _, err := parseAnnotations([]string{"foobar"}) + assert.Error(t, err, `invalid annotation "foobar": expected format key=value`) + }) + + t.Run("empty slice", func(t *testing.T) { + result, err := parseAnnotations([]string{}) + assert.NilError(t, err) + assert.Check(t, result == nil) + }) +} diff --git a/docs/reference/compose_publish.md b/docs/reference/compose_publish.md index 9a82fc260..880b5fa24 100644 --- a/docs/reference/compose_publish.md +++ b/docs/reference/compose_publish.md @@ -5,14 +5,15 @@ Publish compose application ### Options -| Name | Type | Default | Description | -|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------| -| `--app` | `bool` | | Published compose application (includes referenced images) | -| `--dry-run` | `bool` | | Execute command in dry run mode | -| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) | -| `--resolve-image-digests` | `bool` | | Pin image tags to digests | -| `--with-env` | `bool` | | Include environment variables in the published OCI artifact | -| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts | +| Name | Type | Default | Description | +|:--------------------------|:--------------|:--------|:-------------------------------------------------------------------------------| +| `--annotation` | `stringArray` | | Add custom metadata to the published OCI artifact (format: key=value) | +| `--app` | `bool` | | Published compose application (includes referenced images) | +| `--dry-run` | `bool` | | Execute command in dry run mode | +| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) | +| `--resolve-image-digests` | `bool` | | Pin image tags to digests | +| `--with-env` | `bool` | | Include environment variables in the published OCI artifact | +| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts | diff --git a/docs/reference/docker_compose_alpha_publish.yaml b/docs/reference/docker_compose_alpha_publish.yaml index 9059cbf48..1686e14ab 100644 --- a/docs/reference/docker_compose_alpha_publish.yaml +++ b/docs/reference/docker_compose_alpha_publish.yaml @@ -5,6 +5,17 @@ usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG] pname: docker compose alpha plink: docker_compose_alpha.yaml options: + - option: annotation + value_type: stringArray + default_value: '[]' + description: | + Add custom metadata to the published OCI artifact (format: key=value) + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: app value_type: bool default_value: "false" diff --git a/docs/reference/docker_compose_publish.yaml b/docs/reference/docker_compose_publish.yaml index c3189d89c..5947612d1 100644 --- a/docs/reference/docker_compose_publish.yaml +++ b/docs/reference/docker_compose_publish.yaml @@ -5,6 +5,17 @@ usage: docker compose publish [OPTIONS] REPOSITORY[:TAG] pname: docker compose plink: docker_compose.yaml options: + - option: annotation + value_type: stringArray + default_value: '[]' + description: | + Add custom metadata to the published OCI artifact (format: key=value) + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: app value_type: bool default_value: "false" diff --git a/internal/oci/push.go b/internal/oci/push.go index 848080663..5abc1fc70 100644 --- a/internal/oci/push.go +++ b/internal/oci/push.go @@ -94,7 +94,7 @@ func DescriptorForEnvFile(path string, content []byte) v1.Descriptor { } } -func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, error) { +func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion, extraAnnotations map[string]string) (v1.Descriptor, error) { // Check if we need an extra empty layer for the manifest config if ociVersion == api.OCIVersion1_1 || ociVersion == "" { err := push(ctx, resolver, named, v1.DescriptorEmptyJSON) @@ -113,17 +113,17 @@ func PushManifest(ctx context.Context, resolver remotes.Resolver, named referenc if ociVersion != "" { // if a version was explicitly specified, use it - return createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion) + return createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion, extraAnnotations) } // try to push in the OCI 1.1 format but fallback to OCI 1.0 on 4xx errors // (other than auth) since it's most likely the result of the registry not // having support - descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1) + descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1, extraAnnotations) var pushErr pusherrors.ErrUnexpectedStatus if errors.As(err, &pushErr) && isNonAuthClientError(pushErr.StatusCode) { // TODO(milas): show a warning here (won't work with logrus) - return createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0) + return createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0, extraAnnotations) } return descriptor, err } @@ -137,8 +137,8 @@ func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, d return Push(ctx, resolver, fullRef, descriptor) } -func createAndPushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, error) { - descriptor, toPush, err := generateManifest(layers, ociVersion) +func createAndPushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion, extraAnnotations map[string]string) (v1.Descriptor, error) { + descriptor, toPush, err := generateManifest(layers, ociVersion, extraAnnotations) if err != nil { return v1.Descriptor{}, err } @@ -159,7 +159,7 @@ func isNonAuthClientError(statusCode int) bool { return !slices.Contains(clientAuthStatusCodes, statusCode) } -func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) (v1.Descriptor, []v1.Descriptor, error) { +func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion, extraAnnotations map[string]string) (v1.Descriptor, []v1.Descriptor, error) { var toPush []v1.Descriptor var config v1.Descriptor var artifactType string