publish Compose application as compose.yaml + images

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2025-09-29 09:48:34 +02:00 committed by Nicolas De loof
parent cf7e31f731
commit 07602f2070
9 changed files with 189 additions and 42 deletions

View file

@ -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
}

View file

@ -20,6 +20,7 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
@ -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"
)
@ -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 {
@ -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,
@ -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,

View file

@ -26,11 +26,12 @@ import (
"strings"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/oci"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
spec "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
@ -67,6 +68,7 @@ func (g ociRemoteLoader) Accept(path string) bool {
return strings.HasPrefix(path, OciPrefix)
}
//nolint:gocyclo
func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
enabled, err := ociRemoteLoaderEnabled()
if err != nil {
@ -91,7 +93,7 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
descriptor, content, err := oci.Get(ctx, resolver, ref)
if err != nil {
return "", err
return "", fmt.Errorf("failed to pull OCI resource %q: %w", ref, err)
}
cache, err := cacheDir()
@ -101,7 +103,35 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
local = filepath.Join(cache, descriptor.Digest.Hex())
if _, err = os.Stat(local); os.IsNotExist(err) {
var manifest v1.Manifest
// a Compose application bundle is published as image index
if images.IsIndexType(descriptor.MediaType) {
var index spec.Index
err = json.Unmarshal(content, &index)
if err != nil {
return "", err
}
found := false
for _, manifest := range index.Manifests {
if manifest.ArtifactType != oci.ComposeProjectArtifactType {
continue
}
found = true
digested, err := reference.WithDigest(ref, manifest.Digest)
if err != nil {
return "", err
}
descriptor, content, err = oci.Get(ctx, resolver, digested)
if err != nil {
return "", fmt.Errorf("failed to pull OCI resource %q: %w", ref, err)
}
}
if !found {
return "", fmt.Errorf("OCI index %s doesn't refer to compose artifacts", ref)
}
}
var manifest spec.Manifest
err = json.Unmarshal(content, &manifest)
if err != nil {
return "", err
@ -123,7 +153,7 @@ func (g ociRemoteLoader) Dir(path string) string {
return g.known[path]
}
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest v1.Manifest, ref reference.Named, resolver remotes.Resolver) error { //nolint:gocyclo
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error { //nolint:gocyclo
err := os.MkdirAll(local, 0o700)
if err != nil {
return err
@ -173,7 +203,7 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, man
return nil
}
func writeComposeFile(layer v1.Descriptor, i int, f *os.File, content []byte) error {
func writeComposeFile(layer spec.Descriptor, i int, f *os.File, content []byte) error {
if _, ok := layer.Annotations["com.docker.compose.file"]; i > 0 && ok {
_, err := f.Write([]byte("\n---\n"))
if err != nil {
@ -184,7 +214,7 @@ func writeComposeFile(layer v1.Descriptor, i int, f *os.File, content []byte) er
return err
}
func writeEnvFile(layer v1.Descriptor, local string, content []byte) error {
func writeEnvFile(layer spec.Descriptor, local string, content []byte) error {
envfilePath, ok := layer.Annotations["com.docker.compose.envfile"]
if !ok {
return fmt.Errorf("missing annotation com.docker.compose.envfile in layer %q", layer.Digest)