diff --git a/local/compose/build.go b/local/compose/build.go index eb78869dc..aa81516c3 100644 --- a/local/compose/build.go +++ b/local/compose/build.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "os" - "strings" "github.com/compose-spec/compose-go/types" "github.com/containerd/containerd/platforms" @@ -87,7 +86,18 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti } func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, observedState Containers, quietPull bool) error { - images, err := s.getImageDigests(ctx, project) + for _, service := range project.Services { + if service.Image == "" && service.Build == nil { + return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name) + } + } + + images, err := s.getLocalImagesDigests(ctx, project) + if err != nil { + return err + } + + err = s.pullRequiredImages(ctx, project, images, quietPull) if err != nil { return err } @@ -122,9 +132,6 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types. } func (s *composeService) getBuildOptions(project *types.Project, images map[string]string) (map[string]build.Options, []string, error) { - session := []session.Attachable{ - authprovider.NewDockerAuthProvider(os.Stderr), - } opts := map[string]build.Options{} imagesToBuild := []string{} for _, service := range project.Services { @@ -146,30 +153,12 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri opts[imageName] = opt continue } - if service.Image != "" { - if localImagePresent { - continue - } - } - // Buildx has no command to "just pull", see - // so we bake a temporary dockerfile that will just pull and export pulled image - opts[service.Name] = build.Options{ - Inputs: build.Inputs{ - ContextPath: ".", - DockerfilePath: "-", - InStream: strings.NewReader("FROM " + service.Image), - }, - Tags: []string{service.Image}, // Used to retrieve image to pull in case of windows engine - Pull: true, - Session: session, - } - } return opts, imagesToBuild, nil } -func (s *composeService) getImageDigests(ctx context.Context, project *types.Project) (map[string]string, error) { +func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) { imageNames := []string{} for _, s := range project.Services { imgName := getImageName(s, project.Name) @@ -289,6 +278,9 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}}, Platforms: plats, Labels: service.Build.Labels, + Session: []session.Attachable{ + authprovider.NewDockerAuthProvider(os.Stderr), + }, }, nil } diff --git a/local/compose/pull.go b/local/compose/pull.go index 0df5e45c7..d62937815 100644 --- a/local/compose/pull.go +++ b/local/compose/pull.go @@ -61,7 +61,7 @@ func (s *composeService) Pull(ctx context.Context, project *types.Project, opts continue } eg.Go(func() error { - err := s.pullServiceImage(ctx, service, info, s.configFile, w) + err := s.pullServiceImage(ctx, service, info, s.configFile, w, false) if err != nil { if !opts.IgnoreFailures { return err @@ -75,7 +75,7 @@ func (s *composeService) Pull(ctx context.Context, project *types.Project, opts return eg.Wait() } -func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer) error { +func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPull bool) error { w.Event(progress.Event{ ID: service.Name, Status: progress.Working, @@ -131,7 +131,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser if jm.Error != nil { return metrics.WrapCategorisedComposeError(errors.New(jm.Error.Message), metrics.PullFailure) } - toPullProgressEvent(service.Name, jm, w) + if !quietPull { + toPullProgressEvent(service.Name, jm, w) + } } w.Event(progress.Event{ ID: service.Name, @@ -141,6 +143,47 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser return nil } +func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error { + info, err := s.apiClient.Info(ctx) + if err != nil { + return err + } + + if info.IndexServerAddress == "" { + info.IndexServerAddress = registry.IndexServer + } + + return progress.Run(ctx, func(ctx context.Context) error { + w := progress.ContextWriter(ctx) + eg, ctx := errgroup.WithContext(ctx) + for _, service := range project.Services { + if service.Image == "" { + continue + } + switch service.PullPolicy { + case types.PullPolicyMissing, types.PullPolicyIfNotPresent: + if _, ok := images[service.Image]; ok { + continue + } + case types.PullPolicyNever, types.PullPolicyBuild: + continue + case types.PullPolicyAlways: + // force pull + } + service := service + eg.Go(func() error { + err := s.pullServiceImage(ctx, service, info, s.configFile, w, quietPull) + if err != nil && service.Build != nil { + // image can be built, so we can ignore pull failure + return nil + } + return err + }) + } + return eg.Wait() + }) +} + func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) { if jm.ID == "" || jm.Progress == nil { return diff --git a/local/e2e/compose/metrics_test.go b/local/e2e/compose/metrics_test.go index ab761702e..140da80e0 100644 --- a/local/e2e/compose/metrics_test.go +++ b/local/e2e/compose/metrics_test.go @@ -65,14 +65,14 @@ func TestComposeMetrics(t *testing.T) { res.Assert(t, icmd.Expected{ExitCode: 16, Err: "unknown flag: --file"}) res = c.RunDockerOrExitError("compose", "donw", "--file", "../compose/fixtures/wrong-composefile/compose.yml") res.Assert(t, icmd.Expected{ExitCode: 16, Err: `unknown docker command: "compose donw"`}) - res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/unknown-image.yml", "pull") - res.Assert(t, icmd.Expected{ExitCode: 18, Err: `pull access denied for unknownimage, repository does not exist or may require 'docker login'`}) res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/build-error.yml", "build") res.Assert(t, icmd.Expected{ExitCode: 17, Err: `line 17: unknown instruction: WRONG`}) res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/build-error.yml", "up") res.Assert(t, icmd.Expected{ExitCode: 17, Err: `line 17: unknown instruction: WRONG`}) + res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/unknown-image.yml", "pull") + res.Assert(t, icmd.Expected{ExitCode: 18, Err: `pull access denied for unknownimage, repository does not exist or may require 'docker login'`}) res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/unknown-image.yml", "up") - res.Assert(t, icmd.Expected{ExitCode: 17, Err: `pull access denied, repository does not exist or may require authorization`}) + res.Assert(t, icmd.Expected{ExitCode: 18, Err: `pull access denied for unknownimage, repository does not exist or may require 'docker login'`}) usage := s.GetUsage() assert.DeepEqual(t, []string{ @@ -82,10 +82,10 @@ func TestComposeMetrics(t *testing.T) { `{"command":"compose up","context":"moby","source":"cli","status":"failure-cmd-syntax"}`, `{"command":"compose up","context":"moby","source":"cli","status":"failure-cmd-syntax"}`, `{"command":"compose","context":"moby","source":"cli","status":"failure-cmd-syntax"}`, - `{"command":"compose pull","context":"moby","source":"cli","status":"failure-pull"}`, `{"command":"compose build","context":"moby","source":"cli","status":"failure-build"}`, `{"command":"compose up","context":"moby","source":"cli","status":"failure-build"}`, - `{"command":"compose up","context":"moby","source":"cli","status":"failure-build"}`, + `{"command":"compose pull","context":"moby","source":"cli","status":"failure-pull"}`, + `{"command":"compose up","context":"moby","source":"cli","status":"failure-pull"}`, }, usage) }) }