Pārlūkot izejas kodu

delegate build to buildx bake

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 1 gadu atpakaļ
vecāks
revīzija
095f65cb42
5 mainītis faili ar 329 papildinājumiem un 40 dzēšanām
  1. 1 1
      go.mod
  2. 3 2
      go.sum
  3. 20 36
      pkg/compose/build.go
  4. 305 0
      pkg/compose/build_bake.go
  5. 0 1
      pkg/compose/watch_test.go

+ 1 - 1
go.mod

@@ -136,7 +136,7 @@ require (
 	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
 	github.com/moby/sys/signal v0.7.1 // indirect
-	github.com/moby/sys/symlink v0.3.0 // indirect
+	github.com/moby/sys/symlink v0.2.0 // indirect
 	github.com/moby/sys/user v0.3.0 // indirect
 	github.com/moby/sys/userns v0.1.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

+ 3 - 2
go.sum

@@ -337,8 +337,8 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z
 github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
 github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
 github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
-github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
-github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
+github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
+github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
 github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
 github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
@@ -582,6 +582,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 20 - 36
pkg/compose/build.go

@@ -21,7 +21,6 @@ import (
 	"errors"
 	"fmt"
 	"os"
-	"path/filepath"
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/containerd/platforms"
@@ -38,7 +37,6 @@ import (
 	"github.com/docker/compose/v2/pkg/progress"
 	"github.com/docker/compose/v2/pkg/utils"
 	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/builder/remotecontext/urlutil"
 	bclient "github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/session"
 	"github.com/moby/buildkit/session/auth/authprovider"
@@ -64,26 +62,16 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
 	}, s.stdinfo(), "Building")
 }
 
-type serviceToBuild struct {
-	name    string
-	service types.ServiceConfig
-}
-
 //nolint:gocyclo
 func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]string) (map[string]string, error) {
-	buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
-	if err != nil {
-		return nil, err
-	}
-
 	imageIDs := map[string]string{}
-	serviceToBeBuild := map[string]serviceToBuild{}
+	serviceToBuild := types.Services{}
 
 	var policy types.DependencyOption = types.IgnoreDependencies
 	if options.Deps {
 		policy = types.IncludeDependencies
 	}
-	err = project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
+	err := project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
 		if service.Build == nil {
 			return nil
 		}
@@ -92,14 +80,26 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
 			return nil
 		}
-		serviceToBeBuild[serviceName] = serviceToBuild{name: serviceName, service: *service}
+		serviceToBuild[serviceName] = *service
 		return nil
 	}, policy)
-	if err != nil || len(serviceToBeBuild) == 0 {
+	if err != nil || len(serviceToBuild) == 0 {
 		return imageIDs, err
 	}
 
+	bake, err := buildWithBake(s.dockerCli)
+	if err != nil {
+		return nil, err
+	}
+	if bake {
+		return s.doBuildBake(ctx, project, serviceToBuild, options)
+	}
+
 	// Initialize buildkit nodes
+	buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
+	if err != nil {
+		return nil, err
+	}
 	var (
 		b     *builder.Builder
 		nodes []builder.Node
@@ -152,12 +152,10 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		return -1
 	}
 	err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
-		serviceToBuild, ok := serviceToBeBuild[name]
+		service, ok := serviceToBuild[name]
 		if !ok {
 			return nil
 		}
-		service := serviceToBuild.service
-
 		cw := progress.ContextWriter(ctx)
 		serviceName := fmt.Sprintf("Service %s", name)
 
@@ -211,7 +209,8 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 
 	for i, imageDigest := range builtDigests {
 		if imageDigest != "" {
-			imageRef := api.GetImageNameOrDefault(project.Services[names[i]], project.Name)
+			service := project.Services[names[i]]
+			imageRef := api.GetImageNameOrDefault(service, project.Name)
 			imageIDs[imageRef] = imageDigest
 		}
 	}
@@ -334,12 +333,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
 //
 // Finally, standard proxy variables based on the Docker client configuration are added, but will not overwrite
 // any values if already present.
-func resolveAndMergeBuildArgs(
-	dockerCli command.Cli,
-	project *types.Project,
-	service types.ServiceConfig,
-	opts api.BuildOptions,
-) types.MappingWithEquals {
+func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, opts api.BuildOptions) types.MappingWithEquals {
 	result := make(types.MappingWithEquals).
 		OverrideBy(service.Build.Args).
 		OverrideBy(opts.Args).
@@ -479,16 +473,6 @@ func flatten(in types.MappingWithEquals) types.Mapping {
 	return out
 }
 
-func dockerFilePath(ctxName string, dockerfile string) string {
-	if dockerfile == "" {
-		return ""
-	}
-	if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
-		return dockerfile
-	}
-	return filepath.Join(ctxName, dockerfile)
-}
-
 func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
 	sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
 	for _, sshKey := range sshKeys {

+ 305 - 0
pkg/compose/build_bake.go

@@ -0,0 +1,305 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/docker/cli/cli-plugins/manager"
+	"github.com/docker/cli/cli-plugins/socket"
+	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/progress"
+	"github.com/docker/docker/builder/remotecontext/urlutil"
+	"github.com/moby/buildkit/client"
+	"github.com/moby/buildkit/util/progress/progressui"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/propagation"
+	"golang.org/x/sync/errgroup"
+)
+
+func buildWithBake(dockerCli command.Cli) (bool, error) {
+	b, ok := os.LookupEnv("COMPOSE_BAKE")
+	if !ok {
+		if dockerCli.ConfigFile().Plugins["compose"]["build"] == "bake" {
+			b, ok = "true", true
+		}
+	}
+	if !ok {
+		return false, nil
+	}
+	bake, err := strconv.ParseBool(b)
+	if err != nil {
+		return false, err
+	}
+	if !bake {
+		return false, nil
+	}
+
+	enabled, err := dockerCli.BuildKitEnabled()
+	if err != nil {
+		return false, err
+	}
+	if !enabled {
+		logrus.Warnf("Docker Compose is configured to build using Bake, but buildkit isn't enabled")
+	}
+
+	_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
+	if err != nil {
+		if manager.IsNotFound(err) {
+			logrus.Warnf("Docker Compose is configured to build using Bake, but buildx isn't installed")
+			return false, nil
+		}
+		return false, err
+	}
+	return true, err
+}
+
+// We _could_ use bake.* types from github.com/docker/buildx but long term plan is to remove buildx as a dependency
+type bakeConfig struct {
+	Groups  map[string]bakeGroup  `json:"group"`
+	Targets map[string]bakeTarget `json:"target"`
+}
+
+type bakeGroup struct {
+	Targets []string `json:"targets"`
+}
+
+type bakeTarget struct {
+	Context    string            `json:"context,omitempty"`
+	Dockerfile string            `json:"dockerfile,omitempty"`
+	Args       map[string]string `json:"args,omitempty"`
+	Labels     map[string]string `json:"labels,omitempty"`
+	Tags       []string          `json:"tags,omitempty"`
+	CacheFrom  []string          `json:"cache-from,omitempty"`
+	CacheTo    []string          `json:"cache-to,omitempty"`
+	Secrets    []string          `json:"secret,omitempty"`
+	SSH        []string          `json:"ssh,omitempty"`
+	Platforms  []string          `json:"platforms,omitempty"`
+	Target     string            `json:"target,omitempty"`
+	Pull       bool              `json:"pull,omitempty"`
+	NoCache    bool              `json:"no-cache,omitempty"`
+}
+
+type bakeMetadata map[string]buildStatus
+
+type buildStatus struct {
+	Digest string `json:"containerimage.digest"`
+}
+
+func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
+	cw := progress.ContextWriter(ctx)
+	for name := range serviceToBeBuild {
+		cw.Event(progress.BuildingEvent(name))
+	}
+
+	eg := errgroup.Group{}
+	ch := make(chan *client.SolveStatus)
+	display, err := progressui.NewDisplay(os.Stdout, progressui.DisplayMode(options.Progress))
+	if err != nil {
+		return nil, err
+	}
+	eg.Go(func() error {
+		_, err := display.UpdateFrom(ctx, ch)
+		return err
+	})
+
+	cfg := bakeConfig{
+		Groups:  map[string]bakeGroup{},
+		Targets: map[string]bakeTarget{},
+	}
+	var group bakeGroup
+
+	for name, service := range serviceToBeBuild {
+		if service.Build == nil {
+			continue
+		}
+		build := *service.Build
+
+		args := types.Mapping{}
+		for k, v := range resolveAndMergeBuildArgs(s.dockerCli, project, service, options) {
+			if v == nil {
+				continue
+			}
+			args[k] = *v
+		}
+
+		cfg.Targets[name] = bakeTarget{
+			Context:    build.Context,
+			Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
+			Args:       args,
+			Labels:     build.Labels,
+			Tags:       build.Tags,
+
+			CacheFrom: build.CacheFrom,
+			// CacheTo:    TODO
+			Platforms: build.Platforms,
+			Target:    build.Target,
+			Secrets:   toBakeSecrets(project, build.Secrets),
+			SSH:       toBakeSSH(build.SSH),
+			Pull:      options.Pull,
+			NoCache:   options.NoCache,
+		}
+		group.Targets = append(group.Targets, name)
+	}
+
+	cfg.Groups["default"] = group
+
+	b, err := json.Marshal(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	metadata, err := os.CreateTemp(os.TempDir(), "compose")
+	if err != nil {
+		return nil, err
+	}
+
+	buildx, err := manager.GetPlugin("buildx", s.dockerCli, &cobra.Command{})
+	if err != nil {
+		return nil, err
+	}
+	cmd := exec.CommandContext(ctx, buildx.Path, "bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadata.Name())
+	// Remove DOCKER_CLI_PLUGIN... variable so buildx can detect it run standalone
+	cmd.Env = filter(os.Environ(), manager.ReexecEnvvar)
+
+	// Use docker/cli mechanism to propagate termination signal to child process
+	server, err := socket.NewPluginServer(nil)
+	if err != nil {
+		defer server.Close() //nolint:errcheck
+		cmd.Cancel = server.Close
+		cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
+	}
+
+	cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
+
+	// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
+	carrier := propagation.MapCarrier{}
+	otel.GetTextMapPropagator().Inject(ctx, &carrier)
+	cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
+
+	cmd.Stdout = s.stdout()
+	cmd.Stdin = bytes.NewBuffer(b)
+	pipe, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, err
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		return nil, err
+	}
+	eg.Go(cmd.Wait)
+	for {
+		decoder := json.NewDecoder(pipe)
+		var s client.SolveStatus
+		err := decoder.Decode(&s)
+		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
+			// bake displays build details at the end of a build, which isn't a json SolveStatus
+			continue
+		}
+		ch <- &s
+	}
+	close(ch) // stop build progress UI
+
+	err = eg.Wait()
+	if err != nil {
+		return nil, err
+	}
+
+	b, err = os.ReadFile(metadata.Name())
+	if err != nil {
+		return nil, err
+	}
+
+	var md bakeMetadata
+	err = json.Unmarshal(b, &md)
+	if err != nil {
+		return nil, err
+	}
+
+	results := map[string]string{}
+	for name, m := range md {
+		results[name] = m.Digest
+		cw.Event(progress.BuiltEvent(name))
+	}
+	return results, nil
+}
+
+func toBakeSSH(ssh types.SSHConfig) []string {
+	var s []string
+	for _, key := range ssh {
+		s = append(s, fmt.Sprintf("%s=%s", key.ID, key.Path))
+	}
+	return s
+}
+
+func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig) []string {
+	var s []string
+	for _, ref := range secrets {
+		def := project.Secrets[ref.Source]
+		switch {
+		case def.Environment != "":
+			s = append(s, fmt.Sprintf("id=%s,type=env,env=%s", ref.Source, def.Environment))
+		case def.File != "":
+			s = append(s, fmt.Sprintf("id=%s,type=file,src=%s", ref.Source, def.File))
+		}
+	}
+	return s
+}
+
+func filter(environ []string, variable string) []string {
+	prefix := variable + "="
+	filtered := make([]string, 0, len(environ))
+	for _, val := range environ {
+		if !strings.HasPrefix(val, prefix) {
+			filtered = append(filtered, val)
+		}
+	}
+	return filtered
+}
+
+func replace(environ []string, variable, value string) []string {
+	filtered := filter(environ, variable)
+	return append(filtered, fmt.Sprintf("%s=%s", variable, value))
+}
+
+func dockerFilePath(ctxName string, dockerfile string) string {
+	if dockerfile == "" {
+		return ""
+	}
+	if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
+		return dockerfile
+	}
+	return filepath.Join(ctxName, dockerfile)
+}

+ 0 - 1
pkg/compose/watch_test.go

@@ -117,7 +117,6 @@ func TestWatch_Sync(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	cli := mocks.NewMockCli(mockCtrl)
 	cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
-	cli.EXPECT().BuildKitEnabled().Return(true, nil)
 	apiClient := mocks.NewMockAPIClient(mockCtrl)
 	apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return([]moby.Container{
 		testContainer("test", "123", false),