diff --git a/pkg/compose/build.go b/pkg/compose/build.go index b2392ca6fa..7028bace96 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -19,6 +19,7 @@ package compose import ( "context" "fmt" + "os" "path/filepath" "github.com/compose-spec/compose-go/types" @@ -53,68 +54,83 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti }, s.stderr(), "Building") } -func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) { +func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo args := options.Args.Resolve(envResolver(project.Environment)) buildkitEnabled, err := s.dockerCli.BuildKitEnabled() if err != nil { return nil, err } + + // Progress needs its own context that lives longer than the + // build one otherwise it won't read all the messages from + // build and will lock + progressCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress) + if err != nil { + return nil, err + } + builtIDs := make([]string, len(project.Services)) err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error { if len(options.Services) > 0 && !utils.Contains(options.Services, name) { return nil } - for i, service := range project.Services { - if service.Name != name { - continue - } - - if service.Build == nil { - return nil - } + service, idx := getServiceIndex(project, name) - if !buildkitEnabled { - if service.Build.Args == nil { - service.Build.Args = args - } else { - service.Build.Args = service.Build.Args.OverrideBy(args) - } - id, err := s.doBuildClassic(ctx, service, options) - if err != nil { - return err - } - builtIDs[i] = id - - if options.Push { - return s.push(ctx, project, api.PushOptions{}) - } - return nil - } + if service.Build == nil { + return nil + } - if options.Memory != 0 { - fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored.") + if !buildkitEnabled { + if service.Build.Args == nil { + service.Build.Args = args + } else { + service.Build.Args = service.Build.Args.OverrideBy(args) } - - buildOptions, err := s.toBuildOptions(project, service, options) + id, err := s.doBuildClassic(ctx, service, options) if err != nil { return err } - buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args)) - opts := map[string]build.Options{service.Name: buildOptions} + builtIDs[idx] = id - ids, err := s.doBuildBuildkit(ctx, opts, options.Progress) - if err != nil { - return err + if options.Push { + return s.push(ctx, project, api.PushOptions{}) } - builtIDs[i] = ids[service.Name] return nil } + + if options.Memory != 0 { + fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored.") + } + + buildOptions, err := s.toBuildOptions(project, service, options) + if err != nil { + return err + } + buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args)) + + ids, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w) + if err != nil { + return err + } + builtIDs[idx] = ids[service.Name] + return nil }, func(traversal *graphTraversal) { traversal.maxConcurrency = s.maxConcurrency }) + // enforce all build event get consumed + if errw := w.Wait(); errw != nil { + return nil, errw + } + + if err != nil { + return nil, err + } + imageIDs := map[string]string{} for i, d := range builtIDs { if d != "" { @@ -124,6 +140,18 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti return imageIDs, err } +func getServiceIndex(project *types.Project, name string) (types.ServiceConfig, int) { + var service types.ServiceConfig + var idx int + for i, s := range project.Services { + if s.Name == name { + idx, service = i, s + break + } + } + return service, idx +} + func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error { for _, service := range project.Services { if service.Image == "" && service.Build == nil { diff --git a/pkg/compose/build_buildkit.go b/pkg/compose/build_buildkit.go index 7ee35b51d3..1204b1348e 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -20,23 +20,22 @@ import ( "context" "crypto/sha1" "fmt" - "os" "path/filepath" _ "github.com/docker/buildx/driver/docker" //nolint:blank-imports _ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports _ "github.com/docker/buildx/driver/kubernetes" //nolint:blank-imports _ "github.com/docker/buildx/driver/remote" //nolint:blank-imports + buildx "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" "github.com/docker/buildx/build" "github.com/docker/buildx/builder" "github.com/docker/buildx/util/dockerutil" - xprogress "github.com/docker/buildx/util/progress" "github.com/docker/compose/v2/pkg/progress" ) -func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]build.Options, mode string) (map[string]string, error) { +func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer) (map[string]string, error) { b, err := builder.New(s.dockerCli) if err != nil { return nil, err @@ -49,22 +48,9 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]bu var response map[string]*client.SolveResponse if s.dryRun { - response = s.dryRunBuildResponse(ctx, opts) + response = s.dryRunBuildResponse(ctx, service, opts) } else { - // Progress needs its own context that lives longer than the - // build one otherwise it won't read all the messages from - // build and will lock - progressCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode) - if err != nil { - return nil, err - } - response, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(s.dockerCli), filepath.Dir(s.configFile().Filename), w) - errW := w.Wait() - if err == nil { - err = errW - } + response, err = build.Build(ctx, nodes, map[string]build.Options{service: opts}, dockerutil.NewClient(s.dockerCli), filepath.Dir(s.configFile().Filename), buildx.WithPrefix(p, service, true)) if err != nil { return nil, WrapCategorisedComposeError(err, BuildFailure) } @@ -85,29 +71,27 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]bu return imagesBuilt, err } -func (s composeService) dryRunBuildResponse(ctx context.Context, options map[string]build.Options) map[string]*client.SolveResponse { +func (s composeService) dryRunBuildResponse(ctx context.Context, name string, options build.Options) map[string]*client.SolveResponse { w := progress.ContextWriter(ctx) buildResponse := map[string]*client.SolveResponse{} - for name, option := range options { - dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name))) - w.Event(progress.Event{ - ID: " ", - Status: progress.Done, - Text: fmt.Sprintf("build service %s", name), - }) - w.Event(progress.Event{ - ID: "==>", - Status: progress.Done, - Text: fmt.Sprintf("==> writing image %s", dryRunUUID), - }) - w.Event(progress.Event{ - ID: "==> ==>", - Status: progress.Done, - Text: fmt.Sprintf(`naming to %s`, option.Tags[0]), - }) - buildResponse[name] = &client.SolveResponse{ExporterResponse: map[string]string{ - "containerimage.digest": dryRunUUID, - }} - } + dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name))) + w.Event(progress.Event{ + ID: " ", + Status: progress.Done, + Text: fmt.Sprintf("build service %s", name), + }) + w.Event(progress.Event{ + ID: "==>", + Status: progress.Done, + Text: fmt.Sprintf("==> writing image %s", dryRunUUID), + }) + w.Event(progress.Event{ + ID: "==> ==>", + Status: progress.Done, + Text: fmt.Sprintf(`naming to %s`, options.Tags[0]), + }) + buildResponse[name] = &client.SolveResponse{ExporterResponse: map[string]string{ + "containerimage.digest": dryRunUUID, + }} return buildResponse } diff --git a/pkg/e2e/cancel_test.go b/pkg/e2e/cancel_test.go index 67ab9fd18d..6e34bd8d2d 100644 --- a/pkg/e2e/cancel_test.go +++ b/pkg/e2e/cancel_test.go @@ -59,8 +59,8 @@ func TestComposeCancel(t *testing.T) { }, 30*time.Second, 1*time.Second) err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default - assert.NilError(t, err) + c.WaitForCondition(t, func() (bool, string) { out := stdout.String() errors := stderr.String()