Browse Source

introduce --resolve-image-digests for publish to seal service images by digest

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 years ago
parent
commit
6727908803

+ 14 - 5
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,
+	})
 }

+ 4 - 3
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.      |
 
 
 <!---MARKER_GEN_END-->

+ 11 - 0
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

+ 1 - 0
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 {

+ 70 - 25
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
+}