| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 | 
							- /*
 
-    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 (
 
- 	"context"
 
- 	"errors"
 
- 	"fmt"
 
- 	"os"
 
- 	"path/filepath"
 
- 	"github.com/moby/buildkit/util/progress/progressui"
 
- 	"github.com/compose-spec/compose-go/v2/types"
 
- 	"github.com/containerd/containerd/platforms"
 
- 	"github.com/docker/buildx/build"
 
- 	"github.com/docker/buildx/builder"
 
- 	"github.com/docker/buildx/controller/pb"
 
- 	"github.com/docker/buildx/store/storeutil"
 
- 	"github.com/docker/buildx/util/buildflags"
 
- 	xprogress "github.com/docker/buildx/util/progress"
 
- 	"github.com/docker/cli/cli/command"
 
- 	cliopts "github.com/docker/cli/opts"
 
- 	"github.com/docker/compose/v2/internal/tracing"
 
- 	"github.com/docker/compose/v2/pkg/api"
 
- 	"github.com/docker/compose/v2/pkg/progress"
 
- 	"github.com/docker/compose/v2/pkg/utils"
 
- 	"github.com/docker/docker/builder/remotecontext/urlutil"
 
- 	"github.com/docker/go-units"
 
- 	bclient "github.com/moby/buildkit/client"
 
- 	"github.com/moby/buildkit/session"
 
- 	"github.com/moby/buildkit/session/auth/authprovider"
 
- 	"github.com/moby/buildkit/session/secrets/secretsprovider"
 
- 	"github.com/moby/buildkit/session/sshforward/sshprovider"
 
- 	"github.com/moby/buildkit/util/entitlements"
 
- 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 
- 	"github.com/sirupsen/logrus"
 
- 	// required to get default driver registered
 
- 	_ "github.com/docker/buildx/driver/docker"
 
- )
 
- func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
 
- 	err := options.Apply(project)
 
- 	if err != nil {
 
- 		return err
 
- 	}
 
- 	return progress.RunWithTitle(ctx, func(ctx context.Context) error {
 
- 		_, err := s.build(ctx, project, options, nil)
 
- 		return err
 
- 	}, 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{}
 
- 	err = project.WithServices(options.Services, func(service types.ServiceConfig) error {
 
- 		if service.Build == nil {
 
- 			return nil
 
- 		}
 
- 		image := api.GetImageNameOrDefault(service, project.Name)
 
- 		_, localImagePresent := localImages[image]
 
- 		if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
 
- 			return nil
 
- 		}
 
- 		name := service.Name
 
- 		serviceToBeBuild[name] = serviceToBuild{name: name, service: service}
 
- 		return nil
 
- 	}, types.IgnoreDependencies)
 
- 	if err != nil || len(serviceToBeBuild) == 0 {
 
- 		return imageIDs, err
 
- 	}
 
- 	// Initialize buildkit nodes
 
- 	var (
 
- 		b     *builder.Builder
 
- 		nodes []builder.Node
 
- 		w     *xprogress.Printer
 
- 	)
 
- 	if buildkitEnabled {
 
- 		builderName := options.Builder
 
- 		if builderName == "" {
 
- 			builderName = os.Getenv("BUILDX_BUILDER")
 
- 		}
 
- 		b, err = builder.New(s.dockerCli, builder.WithName(builderName))
 
- 		if err != nil {
 
- 			return nil, err
 
- 		}
 
- 		nodes, err = b.LoadNodes(ctx)
 
- 		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, os.Stdout, progressui.DisplayMode(options.Progress),
 
- 			xprogress.WithDesc(
 
- 				fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
 
- 				fmt.Sprintf("%s:%s", b.Driver, b.Name),
 
- 			))
 
- 		if err != nil {
 
- 			return nil, err
 
- 		}
 
- 	}
 
- 	// we use a pre-allocated []string to collect build digest by service index while running concurrent goroutines
 
- 	builtDigests := make([]string, len(project.Services))
 
- 	names := project.ServiceNames()
 
- 	getServiceIndex := func(name string) int {
 
- 		for idx, n := range names {
 
- 			if n == name {
 
- 				return idx
 
- 			}
 
- 		}
 
- 		return -1
 
- 	}
 
- 	err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
 
- 		if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
 
- 			return nil
 
- 		}
 
- 		serviceToBuild, ok := serviceToBeBuild[name]
 
- 		if !ok {
 
- 			return nil
 
- 		}
 
- 		service := serviceToBuild.service
 
- 		if !buildkitEnabled {
 
- 			id, err := s.doBuildClassic(ctx, project, service, options)
 
- 			if err != nil {
 
- 				return err
 
- 			}
 
- 			builtDigests[getServiceIndex(name)] = id
 
- 			if options.Push {
 
- 				return s.push(ctx, project, api.PushOptions{})
 
- 			}
 
- 			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
 
- 		}
 
- 		digest, err := s.doBuildBuildkit(ctx, name, buildOptions, w, nodes)
 
- 		if err != nil {
 
- 			return err
 
- 		}
 
- 		builtDigests[getServiceIndex(name)] = digest
 
- 		return nil
 
- 	}, func(traversal *graphTraversal) {
 
- 		traversal.maxConcurrency = s.maxConcurrency
 
- 	})
 
- 	// enforce all build event get consumed
 
- 	if buildkitEnabled {
 
- 		if errw := w.Wait(); errw != nil {
 
- 			return nil, errw
 
- 		}
 
- 	}
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	for i, imageDigest := range builtDigests {
 
- 		if imageDigest != "" {
 
- 			imageRef := api.GetImageNameOrDefault(project.Services[names[i]], project.Name)
 
- 			imageIDs[imageRef] = imageDigest
 
- 		}
 
- 	}
 
- 	return imageIDs, err
 
- }
 
- func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {
 
- 	for name, service := range project.Services {
 
- 		if service.Image == "" && service.Build == nil {
 
- 			return fmt.Errorf("invalid service %q. Must specify either image or build", name)
 
- 		}
 
- 	}
 
- 	images, err := s.getLocalImagesDigests(ctx, project)
 
- 	if err != nil {
 
- 		return err
 
- 	}
 
- 	err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(project),
 
- 		func(ctx context.Context) error {
 
- 			return s.pullRequiredImages(ctx, project, images, quietPull)
 
- 		},
 
- 	)(ctx)
 
- 	if err != nil {
 
- 		return err
 
- 	}
 
- 	if buildOpts != nil {
 
- 		err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
 
- 			func(ctx context.Context) error {
 
- 				builtImages, err := s.build(ctx, project, *buildOpts, images)
 
- 				if err != nil {
 
- 					return err
 
- 				}
 
- 				for name, digest := range builtImages {
 
- 					images[name] = digest
 
- 				}
 
- 				return nil
 
- 			},
 
- 		)(ctx)
 
- 		if err != nil {
 
- 			return err
 
- 		}
 
- 	}
 
- 	// set digest as com.docker.compose.image label so we can detect outdated containers
 
- 	for name, service := range project.Services {
 
- 		image := api.GetImageNameOrDefault(service, project.Name)
 
- 		digest, ok := images[image]
 
- 		if ok {
 
- 			if service.Labels == nil {
 
- 				service.Labels = types.Labels{}
 
- 			}
 
- 			service.CustomLabels.Add(api.ImageDigestLabel, digest)
 
- 		}
 
- 		project.Services[name] = service
 
- 	}
 
- 	return nil
 
- }
 
- func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
 
- 	var imageNames []string
 
- 	for _, s := range project.Services {
 
- 		imgName := api.GetImageNameOrDefault(s, project.Name)
 
- 		if !utils.StringContains(imageNames, imgName) {
 
- 			imageNames = append(imageNames, imgName)
 
- 		}
 
- 	}
 
- 	imgs, err := s.getImages(ctx, imageNames)
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	images := map[string]string{}
 
- 	for name, info := range imgs {
 
- 		images[name] = info.ID
 
- 	}
 
- 	for i, service := range project.Services {
 
- 		imgName := api.GetImageNameOrDefault(service, project.Name)
 
- 		digest, ok := images[imgName]
 
- 		if !ok {
 
- 			continue
 
- 		}
 
- 		if service.Platform != "" {
 
- 			platform, err := platforms.Parse(service.Platform)
 
- 			if err != nil {
 
- 				return nil, err
 
- 			}
 
- 			inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, digest)
 
- 			if err != nil {
 
- 				return nil, err
 
- 			}
 
- 			actual := specs.Platform{
 
- 				Architecture: inspect.Architecture,
 
- 				OS:           inspect.Os,
 
- 				Variant:      inspect.Variant,
 
- 			}
 
- 			if !platforms.NewMatcher(platform).Match(actual) {
 
- 				// there is a local image, but it's for the wrong platform, so
 
- 				// pretend it doesn't exist so that we can pull/build an image
 
- 				// for the correct platform instead
 
- 				delete(images, imgName)
 
- 			}
 
- 		}
 
- 		project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
 
- 	}
 
- 	return images, nil
 
- }
 
- // resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build.
 
- //
 
- // First, args directly defined via `build.args` in YAML are considered.
 
- // Then, any explicitly passed args in opts (e.g. via `--build-arg` on the CLI) are merged, overwriting any
 
- // keys that already exist.
 
- // Next, any keys without a value are resolved using the project environment.
 
- //
 
- // 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 {
 
- 	result := make(types.MappingWithEquals).
 
- 		OverrideBy(service.Build.Args).
 
- 		OverrideBy(opts.Args).
 
- 		Resolve(envResolver(project.Environment))
 
- 	// proxy arguments do NOT override and should NOT have env resolution applied,
 
- 	// so they're handled last
 
- 	for k, v := range storeutil.GetProxyConfig(dockerCli) {
 
- 		if _, ok := result[k]; !ok {
 
- 			v := v
 
- 			result[k] = &v
 
- 		}
 
- 	}
 
- 	return result
 
- }
 
- func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
 
- 	plats, err := parsePlatforms(service)
 
- 	if err != nil {
 
- 		return build.Options{}, err
 
- 	}
 
- 	cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
 
- 	if err != nil {
 
- 		return build.Options{}, err
 
- 	}
 
- 	cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
 
- 	if err != nil {
 
- 		return build.Options{}, err
 
- 	}
 
- 	sessionConfig := []session.Attachable{
 
- 		authprovider.NewDockerAuthProvider(s.configFile(), nil),
 
- 	}
 
- 	if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
 
- 		sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
 
- 		if err != nil {
 
- 			return build.Options{}, err
 
- 		}
 
- 		sessionConfig = append(sessionConfig, sshAgentProvider)
 
- 	}
 
- 	if len(service.Build.Secrets) > 0 {
 
- 		secretsProvider, err := addSecretsConfig(project, service)
 
- 		if err != nil {
 
- 			return build.Options{}, err
 
- 		}
 
- 		sessionConfig = append(sessionConfig, secretsProvider)
 
- 	}
 
- 	tags := []string{api.GetImageNameOrDefault(service, project.Name)}
 
- 	if len(service.Build.Tags) > 0 {
 
- 		tags = append(tags, service.Build.Tags...)
 
- 	}
 
- 	var allow []entitlements.Entitlement
 
- 	if service.Build.Privileged {
 
- 		allow = append(allow, entitlements.EntitlementSecurityInsecure)
 
- 	}
 
- 	imageLabels := getImageBuildLabels(project, service)
 
- 	push := options.Push && service.Image != ""
 
- 	exports := []bclient.ExportEntry{{
 
- 		Type: "docker",
 
- 		Attrs: map[string]string{
 
- 			"load": "true",
 
- 			"push": fmt.Sprint(push),
 
- 		},
 
- 	}}
 
- 	if len(service.Build.Platforms) > 1 {
 
- 		exports = []bclient.ExportEntry{{
 
- 			Type: "image",
 
- 			Attrs: map[string]string{
 
- 				"push": fmt.Sprint(push),
 
- 			},
 
- 		}}
 
- 	}
 
- 	return build.Options{
 
- 		Inputs: build.Inputs{
 
- 			ContextPath:      service.Build.Context,
 
- 			DockerfileInline: service.Build.DockerfileInline,
 
- 			DockerfilePath:   dockerFilePath(service.Build.Context, service.Build.Dockerfile),
 
- 			NamedContexts:    toBuildContexts(service.Build.AdditionalContexts),
 
- 		},
 
- 		CacheFrom:   pb.CreateCaches(cacheFrom),
 
- 		CacheTo:     pb.CreateCaches(cacheTo),
 
- 		NoCache:     service.Build.NoCache,
 
- 		Pull:        service.Build.Pull,
 
- 		BuildArgs:   flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
 
- 		Tags:        tags,
 
- 		Target:      service.Build.Target,
 
- 		Exports:     exports,
 
- 		Platforms:   plats,
 
- 		Labels:      imageLabels,
 
- 		NetworkMode: service.Build.Network,
 
- 		ExtraHosts:  service.Build.ExtraHosts.AsList(":"),
 
- 		Ulimits:     toUlimitOpt(service.Build.Ulimits),
 
- 		Session:     sessionConfig,
 
- 		Allow:       allow,
 
- 	}, nil
 
- }
 
- func toUlimitOpt(ulimits map[string]*types.UlimitsConfig) *cliopts.UlimitOpt {
 
- 	ref := map[string]*units.Ulimit{}
 
- 	for _, limit := range toUlimits(ulimits) {
 
- 		ref[limit.Name] = &units.Ulimit{
 
- 			Name: limit.Name,
 
- 			Hard: limit.Hard,
 
- 			Soft: limit.Soft,
 
- 		}
 
- 	}
 
- 	return cliopts.NewUlimitOpt(&ref)
 
- }
 
- func flatten(in types.MappingWithEquals) types.Mapping {
 
- 	out := types.Mapping{}
 
- 	if len(in) == 0 {
 
- 		return out
 
- 	}
 
- 	for k, v := range in {
 
- 		if v == nil {
 
- 			continue
 
- 		}
 
- 		out[k] = *v
 
- 	}
 
- 	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 {
 
- 		sshConfig = append(sshConfig, sshprovider.AgentConfig{
 
- 			ID:    sshKey.ID,
 
- 			Paths: []string{sshKey.Path},
 
- 		})
 
- 	}
 
- 	return sshprovider.NewSSHAgentProvider(sshConfig)
 
- }
 
- func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
 
- 	var sources []secretsprovider.Source
 
- 	for _, secret := range service.Build.Secrets {
 
- 		config := project.Secrets[secret.Source]
 
- 		id := secret.Source
 
- 		if secret.Target != "" {
 
- 			id = secret.Target
 
- 		}
 
- 		switch {
 
- 		case config.File != "":
 
- 			sources = append(sources, secretsprovider.Source{
 
- 				ID:       id,
 
- 				FilePath: config.File,
 
- 			})
 
- 		case config.Environment != "":
 
- 			sources = append(sources, secretsprovider.Source{
 
- 				ID:  id,
 
- 				Env: config.Environment,
 
- 			})
 
- 		default:
 
- 			return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
 
- 		}
 
- 		if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
 
- 			logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
 
- 		}
 
- 	}
 
- 	store, err := secretsprovider.NewStore(sources)
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	return secretsprovider.NewSecretProvider(store), nil
 
- }
 
- func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
 
- 	ret := make(types.Labels)
 
- 	if service.Build != nil {
 
- 		for k, v := range service.Build.Labels {
 
- 			ret.Add(k, v)
 
- 		}
 
- 	}
 
- 	ret.Add(api.VersionLabel, api.ComposeVersion)
 
- 	ret.Add(api.ProjectLabel, project.Name)
 
- 	ret.Add(api.ServiceLabel, service.Name)
 
- 	return ret
 
- }
 
- func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
 
- 	namedContexts := map[string]build.NamedContext{}
 
- 	for name, context := range additionalContexts {
 
- 		namedContexts[name] = build.NamedContext{Path: context}
 
- 	}
 
- 	return namedContexts
 
- }
 
- func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
 
- 	if service.Build == nil || len(service.Build.Platforms) == 0 {
 
- 		return nil, nil
 
- 	}
 
- 	var errs []error
 
- 	ret := make([]specs.Platform, len(service.Build.Platforms))
 
- 	for i := range service.Build.Platforms {
 
- 		p, err := platforms.Parse(service.Build.Platforms[i])
 
- 		if err != nil {
 
- 			errs = append(errs, err)
 
- 		} else {
 
- 			ret[i] = p
 
- 		}
 
- 	}
 
- 	if err := errors.Join(errs...); err != nil {
 
- 		return nil, err
 
- 	}
 
- 	return ret, nil
 
- }
 
 
  |