Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type publishOptions struct {
ociVersion string
withEnvironment bool
assumeYes bool
app bool
}

func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
Expand All @@ -53,6 +54,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
flags.BoolVar(&opts.app, "app", false, "Published compose application (includes referenced images)")
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
Expand All @@ -76,7 +78,8 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
}

return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
ResolveImageDigests: opts.resolveImageDigests || opts.app,
Application: opts.app,
OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
AssumeYes: opts.assumeYes,
Expand Down
1 change: 1 addition & 0 deletions docs/reference/compose_publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Publish compose application

| 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 |
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/docker_compose_alpha_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: app
value_type: bool
default_value: "false"
description: Published compose application (includes referenced images)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: oci-version
value_type: string
description: |
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/docker_compose_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ usage: docker compose publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose
plink: docker_compose.yaml
options:
- option: app
value_type: bool
default_value: "false"
description: Published compose application (includes referenced images)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: oci-version
value_type: string
description: |
Expand Down
48 changes: 15 additions & 33 deletions internal/oci/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

"github.com/containerd/containerd/v2/core/remotes"
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/compose/v2/pkg/api"
"github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -94,20 +93,20 @@ 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) error {
func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (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)
if err != nil {
return err
return v1.Descriptor{}, err
}
}
// prepare to push the manifest by pushing the layers
layerDescriptors := make([]v1.Descriptor, len(layers))
for i := range layers {
layerDescriptors[i] = layers[i]
if err := push(ctx, resolver, named, layers[i]); err != nil {
return err
return v1.Descriptor{}, err
}
}

Expand All @@ -119,13 +118,13 @@ func PushManifest(ctx context.Context, resolver remotes.Resolver, named referenc
// 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
err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1)
descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1)
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 err
return descriptor, err
}

func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor v1.Descriptor) error {
Expand All @@ -134,37 +133,21 @@ func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, d
return err
}

pusher, err := resolver.Pusher(ctx, fullRef.String())
if err != nil {
return err
}
push, err := pusher.Push(ctx, descriptor)
if errdefs.IsAlreadyExists(err) {
return nil
}
if err != nil {
return err
}
defer func() {
_ = push.Close()
}()

_, err = push.Write(descriptor.Data)
return err
return Push(ctx, resolver, fullRef, descriptor)
}

func createAndPushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) error {
toPush, err := generateManifest(layers, ociVersion)
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)
if err != nil {
return err
return v1.Descriptor{}, err
}
for _, p := range toPush {
err = push(ctx, resolver, named, p)
if err != nil {
return err
return v1.Descriptor{}, err
}
}
return nil
return descriptor, nil
}

func isNonAuthClientError(statusCode int) bool {
Expand All @@ -175,7 +158,7 @@ func isNonAuthClientError(statusCode int) bool {
return !slices.Contains(clientAuthStatusCodes, statusCode)
}

func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]v1.Descriptor, error) {
func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) (v1.Descriptor, []v1.Descriptor, error) {
var toPush []v1.Descriptor
var config v1.Descriptor
var artifactType string
Expand Down Expand Up @@ -205,10 +188,9 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]v1.De
case api.OCIVersion1_1:
config = v1.DescriptorEmptyJSON
artifactType = ComposeProjectArtifactType
// N.B. the descriptor has the data embedded in it
toPush = append(toPush, config)
default:
return nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
return v1.Descriptor{}, nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
}

manifest, err := json.Marshal(v1.Manifest{
Expand All @@ -222,7 +204,7 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]v1.De
},
})
if err != nil {
return nil, err
return v1.Descriptor{}, nil, err
}

manifestDescriptor := v1.Descriptor{
Expand All @@ -236,5 +218,5 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]v1.De
Data: manifest,
}
toPush = append(toPush, manifestDescriptor)
return toPush, nil
return manifestDescriptor, toPush, nil
}
62 changes: 62 additions & 0 deletions internal/oci/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ package oci
import (
"context"
"io"
"net/url"
"strings"

"github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/containerd/containerd/v2/pkg/labels"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/compose/v2/internal/registry"
"github.com/moby/buildkit/util/contentutil"
spec "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand Down Expand Up @@ -70,3 +75,60 @@ func Get(ctx context.Context, resolver remotes.Resolver, ref reference.Named) (s
}
return descriptor, content, nil
}

func Copy(ctx context.Context, resolver remotes.Resolver, image reference.Named, named reference.Named) (spec.Descriptor, error) {
src, desc, err := resolver.Resolve(ctx, image.String())
if err != nil {
return spec.Descriptor{}, err
}
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
// set LabelDistributionSource so push will actually use a registry mount
refspec := reference.TrimNamed(image).String()
u, err := url.Parse("dummy://" + refspec)
if err != nil {
return spec.Descriptor{}, err
}
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
desc.Annotations[labels.LabelDistributionSource+"."+source] = repo

p, err := resolver.Pusher(ctx, named.Name())
if err != nil {
return spec.Descriptor{}, err
}
f, err := resolver.Fetcher(ctx, src)
if err != nil {
return spec.Descriptor{}, err
}

err = contentutil.CopyChain(ctx,
contentutil.FromPusher(p),
contentutil.FromFetcher(f), desc)
return desc, err
}

func Push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor spec.Descriptor) error {
pusher, err := resolver.Pusher(ctx, ref.String())
if err != nil {
return err
}
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeYAMLMediaType, "artifact-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeEnvFileMediaType, "artifact-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, ComposeEmptyConfigMediaType, "config-")
ctx = remotes.WithMediaTypeKeyPrefix(ctx, spec.MediaTypeEmptyJSON, "config-")

push, err := pusher.Push(ctx, descriptor)
if errdefs.IsAlreadyExists(err) {
return nil
}
if err != nil {
return err
}
defer func() {
_ = push.Close()
}()

_, err = push.Write(descriptor.Data)
return err
}
3 changes: 2 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,10 @@ const (
// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool
Application bool
WithEnvironment bool
AssumeYes bool

AssumeYes bool
OCIVersion OCIVersion
}

Expand Down
50 changes: 49 additions & 1 deletion pkg/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -36,6 +37,8 @@ import (
"github.com/docker/compose/v2/pkg/compose/transform"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand All @@ -45,6 +48,7 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re
}, s.stdinfo(), "Publishing")
}

//nolint:gocyclo
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
accept, err := s.preChecks(project, options)
if err != nil {
Expand Down Expand Up @@ -106,7 +110,7 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
Status: progress.Working,
})
if !s.dryRun {
err = oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
if err != nil {
w.Event(progress.Event{
ID: repository,
Expand All @@ -115,6 +119,50 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
})
return err
}

if options.Application {
manifests := []v1.Descriptor{}
for _, service := range project.Services {
ref, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}

manifest, err := oci.Copy(ctx, resolver, ref, named)
if err != nil {
return err
}
manifests = append(manifests, manifest)
}

descriptor.Data = nil
index, err := json.Marshal(v1.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Subject: &descriptor,
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
})
if err != nil {
return err
}
imagesDescriptor := v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
ArtifactType: oci.ComposeProjectArtifactType,
Digest: digest.FromString(string(index)),
Size: int64(len(index)),
Annotations: map[string]string{
"com.docker.compose.version": api.ComposeVersion,
},
Data: index,
}
err = oci.Push(ctx, resolver, reference.TrimNamed(named), imagesDescriptor)
if err != nil {
return err
}
}
}
w.Event(progress.Event{
ID: repository,
Expand Down
Loading