diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index 244724e8f..71f43ac03 100644 --- a/cmd/compose/publish.go +++ b/cmd/compose/publish.go @@ -25,11 +25,16 @@ import ( "github.com/docker/compose/v2/pkg/api" ) +type publishOptions struct { + *ProjectOptions + resolveImageDigests bool +} + func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { - opts := pushOptions{ + opts := publishOptions{ ProjectOptions: p, } - publishCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "publish [OPTIONS] [REPOSITORY]", Short: "Publish compose application", RunE: Adapt(func(ctx context.Context, args []string) error { @@ -37,14 +42,18 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic }), Args: cobra.ExactArgs(1), } - return publishCmd + flags := cmd.Flags() + flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.") + return cmd } -func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, repository string) error { +func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error { project, err := opts.ToProject(dockerCli, nil) if err != nil { return err } - return backend.Publish(ctx, project, repository, api.PublishOptions{}) + return backend.Publish(ctx, project, repository, api.PublishOptions{ + ResolveImageDigests: opts.resolveImageDigests, + }) } diff --git a/docs/reference/compose_alpha_publish.md b/docs/reference/compose_alpha_publish.md index 056d211b0..ba58d73c8 100644 --- a/docs/reference/compose_alpha_publish.md +++ b/docs/reference/compose_alpha_publish.md @@ -5,9 +5,10 @@ Publish compose application ### Options -| Name | Type | Default | Description | -|:------------|:-----|:--------|:--------------------------------| -| `--dry-run` | | | Execute command in dry run mode | +| Name | Type | Default | Description | +|:--------------------------|:-----|:--------|:--------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--resolve-image-digests` | | | Pin image tags to digests. | diff --git a/docs/reference/docker_compose_alpha_publish.yaml b/docs/reference/docker_compose_alpha_publish.yaml index ba01e166a..266e894c4 100644 --- a/docs/reference/docker_compose_alpha_publish.yaml +++ b/docs/reference/docker_compose_alpha_publish.yaml @@ -4,6 +4,17 @@ long: Publish compose application usage: docker compose alpha publish [OPTIONS] [REPOSITORY] pname: docker compose alpha plink: docker_compose_alpha.yaml +options: + - option: resolve-image-digests + value_type: bool + default_value: "false" + description: Pin image tags to digests. + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false inherited_options: - option: dry-run value_type: bool diff --git a/pkg/api/api.go b/pkg/api/api.go index 8bbd2026e..42578638a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -363,6 +363,7 @@ type PortOptions struct { // PublishOptions group options of the Publish API type PublishOptions struct { + ResolveImageDigests bool } func (e Event) String() string { diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 47b508149..02fd62604 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -63,37 +63,24 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re return err } - w.Event(progress.Event{ - ID: file, - Text: "publishing", - Status: progress.Working, - }) - layer := v1.Descriptor{ - MediaType: "application/vnd.docker.compose.file+yaml", - Digest: digest.FromString(string(f)), - Size: int64(len(f)), - Annotations: map[string]string{ - "com.docker.compose.version": api.ComposeVersion, - "com.docker.compose.file": filepath.Base(file), - }, + layer, err := s.pushComposeFile(ctx, file, f, resolver, named) + if err != nil { + return err } layers = append(layers, layer) - err = resolver.Push(ctx, named, layer, f) - if err != nil { - w.Event(progress.Event{ - ID: file, - Text: "publishing", - Status: progress.Error, - }) + } + if options.ResolveImageDigests { + yaml, err := s.generateImageDigestsOverride(ctx, project) + if err != nil { return err } - w.Event(progress.Event{ - ID: file, - Text: "published", - Status: progress.Done, - }) + layer, err := s.pushComposeFile(ctx, "image-digests.yaml", yaml, resolver, named) + if err != nil { + return err + } + layers = append(layers, layer) } emptyConfig, err := json.Marshal(v1.ImageConfig{}) @@ -157,3 +144,61 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re }) return nil } + +func (s *composeService) generateImageDigestsOverride(ctx context.Context, project *types.Project) ([]byte, error) { + project.ApplyProfiles([]string{"*"}) + err := project.ResolveImages(func(named reference.Named) (digest.Digest, error) { + auth, err := encodedAuth(named, s.configFile()) + if err != nil { + return "", err + } + inspect, err := s.apiClient().DistributionInspect(ctx, named.String(), auth) + if err != nil { + return "", err + } + return inspect.Descriptor.Digest, nil + }) + if err != nil { + return nil, err + } + override := types.Project{} + for _, service := range project.Services { + override.Services = append(override.Services, types.ServiceConfig{ + Name: service.Name, + Image: service.Image, + }) + } + return override.MarshalYAML() +} + +func (s *composeService) pushComposeFile(ctx context.Context, file string, content []byte, resolver *imagetools.Resolver, named reference.Named) (v1.Descriptor, error) { + w := progress.ContextWriter(ctx) + w.Event(progress.Event{ + ID: file, + Text: "publishing", + Status: progress.Working, + }) + layer := v1.Descriptor{ + MediaType: "application/vnd.docker.compose.file+yaml", + Digest: digest.FromString(string(content)), + Size: int64(len(content)), + Annotations: map[string]string{ + "com.docker.compose.version": api.ComposeVersion, + "com.docker.compose.file": filepath.Base(file), + }, + } + err := resolver.Push(ctx, named, layer, content) + w.Event(progress.Event{ + ID: file, + Text: "published", + Status: statusFor(err), + }) + return layer, err +} + +func statusFor(err error) progress.EventStatus { + if err != nil { + return progress.Error + } + return progress.Done +}