build.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "github.com/compose-spec/compose-go/types"
  20. "github.com/containerd/containerd/platforms"
  21. "github.com/docker/buildx/build"
  22. "github.com/docker/buildx/builder"
  23. "github.com/docker/buildx/controller/pb"
  24. "github.com/docker/buildx/store/storeutil"
  25. "github.com/docker/buildx/util/buildflags"
  26. xprogress "github.com/docker/buildx/util/progress"
  27. "github.com/docker/cli/cli/command"
  28. "github.com/docker/compose/v2/internal/tracing"
  29. "github.com/docker/compose/v2/pkg/api"
  30. "github.com/docker/compose/v2/pkg/progress"
  31. "github.com/docker/compose/v2/pkg/utils"
  32. "github.com/docker/docker/builder/remotecontext/urlutil"
  33. bclient "github.com/moby/buildkit/client"
  34. "github.com/moby/buildkit/session"
  35. "github.com/moby/buildkit/session/auth/authprovider"
  36. "github.com/moby/buildkit/session/secrets/secretsprovider"
  37. "github.com/moby/buildkit/session/sshforward/sshprovider"
  38. "github.com/moby/buildkit/util/entitlements"
  39. specs "github.com/opencontainers/image-spec/specs-go/v1"
  40. "github.com/pkg/errors"
  41. "github.com/sirupsen/logrus"
  42. // required to get default driver registered
  43. _ "github.com/docker/buildx/driver/docker"
  44. )
  45. func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
  46. err := options.Apply(project)
  47. if err != nil {
  48. return err
  49. }
  50. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  51. _, err := s.build(ctx, project, options)
  52. return err
  53. }, s.stdinfo(), "Building")
  54. }
  55. //nolint:gocyclo
  56. func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) {
  57. buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
  58. if err != nil {
  59. return nil, err
  60. }
  61. // Initialize buildkit nodes
  62. var (
  63. b *builder.Builder
  64. nodes []builder.Node
  65. w *xprogress.Printer
  66. )
  67. if buildkitEnabled {
  68. builderName := options.Builder
  69. if builderName == "" {
  70. builderName = os.Getenv("BUILDX_BUILDER")
  71. }
  72. b, err = builder.New(s.dockerCli, builder.WithName(builderName))
  73. if err != nil {
  74. return nil, err
  75. }
  76. nodes, err = b.LoadNodes(ctx, false)
  77. if err != nil {
  78. return nil, err
  79. }
  80. // Progress needs its own context that lives longer than the
  81. // build one otherwise it won't read all the messages from
  82. // build and will lock
  83. progressCtx, cancel := context.WithCancel(context.Background())
  84. defer cancel()
  85. w, err = xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress,
  86. xprogress.WithDesc(
  87. fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
  88. fmt.Sprintf("%s:%s", b.Driver, b.Name),
  89. ))
  90. if err != nil {
  91. return nil, err
  92. }
  93. }
  94. builtDigests := make([]string, len(project.Services))
  95. err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
  96. if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
  97. return nil
  98. }
  99. service, idx := getServiceIndex(project, name)
  100. if service.Build == nil {
  101. return nil
  102. }
  103. if !buildkitEnabled {
  104. id, err := s.doBuildClassic(ctx, project, service, options)
  105. if err != nil {
  106. return err
  107. }
  108. builtDigests[idx] = id
  109. if options.Push {
  110. return s.push(ctx, project, api.PushOptions{})
  111. }
  112. return nil
  113. }
  114. if options.Memory != 0 {
  115. fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored.")
  116. }
  117. buildOptions, err := s.toBuildOptions(project, service, options)
  118. if err != nil {
  119. return err
  120. }
  121. digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, nodes)
  122. if err != nil {
  123. return err
  124. }
  125. builtDigests[idx] = digest
  126. return nil
  127. }, func(traversal *graphTraversal) {
  128. traversal.maxConcurrency = s.maxConcurrency
  129. })
  130. // enforce all build event get consumed
  131. if buildkitEnabled {
  132. if errw := w.Wait(); errw != nil {
  133. return nil, errw
  134. }
  135. }
  136. if err != nil {
  137. return nil, err
  138. }
  139. imageIDs := map[string]string{}
  140. for i, imageDigest := range builtDigests {
  141. if imageDigest != "" {
  142. imageRef := api.GetImageNameOrDefault(project.Services[i], project.Name)
  143. imageIDs[imageRef] = imageDigest
  144. }
  145. }
  146. return imageIDs, err
  147. }
  148. func getServiceIndex(project *types.Project, name string) (types.ServiceConfig, int) {
  149. var service types.ServiceConfig
  150. var idx int
  151. for i, s := range project.Services {
  152. if s.Name == name {
  153. idx, service = i, s
  154. break
  155. }
  156. }
  157. return service, idx
  158. }
  159. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  160. for _, service := range project.Services {
  161. if service.Image == "" && service.Build == nil {
  162. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  163. }
  164. }
  165. images, err := s.getLocalImagesDigests(ctx, project)
  166. if err != nil {
  167. return err
  168. }
  169. err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(project),
  170. func(ctx context.Context) error {
  171. return s.pullRequiredImages(ctx, project, images, quietPull)
  172. },
  173. )(ctx)
  174. if err != nil {
  175. return err
  176. }
  177. mode := xprogress.PrinterModeAuto
  178. if quietPull {
  179. mode = xprogress.PrinterModeQuiet
  180. }
  181. buildRequired, err := s.prepareProjectForBuild(project, images)
  182. if err != nil {
  183. return err
  184. }
  185. if buildRequired {
  186. err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
  187. func(ctx context.Context) error {
  188. builtImages, err := s.build(ctx, project, api.BuildOptions{
  189. Progress: mode,
  190. })
  191. if err != nil {
  192. return err
  193. }
  194. for name, digest := range builtImages {
  195. images[name] = digest
  196. }
  197. return nil
  198. },
  199. )(ctx)
  200. if err != nil {
  201. return err
  202. }
  203. }
  204. // set digest as com.docker.compose.image label so we can detect outdated containers
  205. for i, service := range project.Services {
  206. image := api.GetImageNameOrDefault(service, project.Name)
  207. digest, ok := images[image]
  208. if ok {
  209. if project.Services[i].Labels == nil {
  210. project.Services[i].Labels = types.Labels{}
  211. }
  212. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  213. }
  214. }
  215. return nil
  216. }
  217. func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
  218. buildRequired := false
  219. err := api.BuildOptions{}.Apply(project)
  220. if err != nil {
  221. return false, err
  222. }
  223. for i, service := range project.Services {
  224. if service.Build == nil {
  225. continue
  226. }
  227. image := api.GetImageNameOrDefault(service, project.Name)
  228. _, localImagePresent := images[image]
  229. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  230. service.Build = nil
  231. project.Services[i] = service
  232. continue
  233. }
  234. if service.Platform == "" {
  235. // let builder to build for default platform
  236. service.Build.Platforms = nil
  237. } else {
  238. service.Build.Platforms = []string{service.Platform}
  239. }
  240. project.Services[i] = service
  241. buildRequired = true
  242. }
  243. return buildRequired, nil
  244. }
  245. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  246. var imageNames []string
  247. for _, s := range project.Services {
  248. imgName := api.GetImageNameOrDefault(s, project.Name)
  249. if !utils.StringContains(imageNames, imgName) {
  250. imageNames = append(imageNames, imgName)
  251. }
  252. }
  253. imgs, err := s.getImages(ctx, imageNames)
  254. if err != nil {
  255. return nil, err
  256. }
  257. images := map[string]string{}
  258. for name, info := range imgs {
  259. images[name] = info.ID
  260. }
  261. for i, service := range project.Services {
  262. imgName := api.GetImageNameOrDefault(service, project.Name)
  263. digest, ok := images[imgName]
  264. if !ok {
  265. continue
  266. }
  267. if service.Platform != "" {
  268. platform, err := platforms.Parse(service.Platform)
  269. if err != nil {
  270. return nil, err
  271. }
  272. inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, digest)
  273. if err != nil {
  274. return nil, err
  275. }
  276. actual := specs.Platform{
  277. Architecture: inspect.Architecture,
  278. OS: inspect.Os,
  279. Variant: inspect.Variant,
  280. }
  281. if !platforms.NewMatcher(platform).Match(actual) {
  282. return nil, errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s",
  283. imgName, platforms.Format(platform), platforms.Format(actual))
  284. }
  285. }
  286. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  287. }
  288. return images, nil
  289. }
  290. // resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build.
  291. //
  292. // First, args directly defined via `build.args` in YAML are considered.
  293. // Then, any explicitly passed args in opts (e.g. via `--build-arg` on the CLI) are merged, overwriting any
  294. // keys that already exist.
  295. // Next, any keys without a value are resolved using the project environment.
  296. //
  297. // Finally, standard proxy variables based on the Docker client configuration are added, but will not overwrite
  298. // any values if already present.
  299. func resolveAndMergeBuildArgs(
  300. dockerCli command.Cli,
  301. project *types.Project,
  302. service types.ServiceConfig,
  303. opts api.BuildOptions,
  304. ) types.MappingWithEquals {
  305. result := make(types.MappingWithEquals).
  306. OverrideBy(service.Build.Args).
  307. OverrideBy(opts.Args).
  308. Resolve(envResolver(project.Environment))
  309. // proxy arguments do NOT override and should NOT have env resolution applied,
  310. // so they're handled last
  311. for k, v := range storeutil.GetProxyConfig(dockerCli) {
  312. if _, ok := result[k]; !ok {
  313. v := v
  314. result[k] = &v
  315. }
  316. }
  317. return result
  318. }
  319. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
  320. plats, err := addPlatforms(project, service)
  321. if err != nil {
  322. return build.Options{}, err
  323. }
  324. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  325. if err != nil {
  326. return build.Options{}, err
  327. }
  328. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  329. if err != nil {
  330. return build.Options{}, err
  331. }
  332. sessionConfig := []session.Attachable{
  333. authprovider.NewDockerAuthProvider(s.configFile()),
  334. }
  335. if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
  336. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
  337. if err != nil {
  338. return build.Options{}, err
  339. }
  340. sessionConfig = append(sessionConfig, sshAgentProvider)
  341. }
  342. if len(service.Build.Secrets) > 0 {
  343. secretsProvider, err := addSecretsConfig(project, service)
  344. if err != nil {
  345. return build.Options{}, err
  346. }
  347. sessionConfig = append(sessionConfig, secretsProvider)
  348. }
  349. tags := []string{api.GetImageNameOrDefault(service, project.Name)}
  350. if len(service.Build.Tags) > 0 {
  351. tags = append(tags, service.Build.Tags...)
  352. }
  353. var allow []entitlements.Entitlement
  354. if service.Build.Privileged {
  355. allow = append(allow, entitlements.EntitlementSecurityInsecure)
  356. }
  357. imageLabels := getImageBuildLabels(project, service)
  358. push := options.Push && service.Image != ""
  359. exports := []bclient.ExportEntry{{
  360. Type: "docker",
  361. Attrs: map[string]string{
  362. "load": "true",
  363. "push": fmt.Sprint(push),
  364. },
  365. }}
  366. if len(service.Build.Platforms) > 1 {
  367. exports = []bclient.ExportEntry{{
  368. Type: "image",
  369. Attrs: map[string]string{
  370. "push": fmt.Sprint(push),
  371. },
  372. }}
  373. }
  374. return build.Options{
  375. Inputs: build.Inputs{
  376. ContextPath: service.Build.Context,
  377. DockerfileInline: service.Build.DockerfileInline,
  378. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  379. NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
  380. },
  381. CacheFrom: pb.CreateCaches(cacheFrom),
  382. CacheTo: pb.CreateCaches(cacheTo),
  383. NoCache: service.Build.NoCache,
  384. Pull: service.Build.Pull,
  385. BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
  386. Tags: tags,
  387. Target: service.Build.Target,
  388. Exports: exports,
  389. Platforms: plats,
  390. Labels: imageLabels,
  391. NetworkMode: service.Build.Network,
  392. ExtraHosts: service.Build.ExtraHosts.AsList(),
  393. Session: sessionConfig,
  394. Allow: allow,
  395. }, nil
  396. }
  397. func flatten(in types.MappingWithEquals) types.Mapping {
  398. out := types.Mapping{}
  399. if len(in) == 0 {
  400. return out
  401. }
  402. for k, v := range in {
  403. if v == nil {
  404. continue
  405. }
  406. out[k] = *v
  407. }
  408. return out
  409. }
  410. func dockerFilePath(ctxName string, dockerfile string) string {
  411. if dockerfile == "" {
  412. return ""
  413. }
  414. if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
  415. return dockerfile
  416. }
  417. return filepath.Join(ctxName, dockerfile)
  418. }
  419. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  420. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  421. for _, sshKey := range sshKeys {
  422. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  423. ID: sshKey.ID,
  424. Paths: []string{sshKey.Path},
  425. })
  426. }
  427. return sshprovider.NewSSHAgentProvider(sshConfig)
  428. }
  429. func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
  430. var sources []secretsprovider.Source
  431. for _, secret := range service.Build.Secrets {
  432. config := project.Secrets[secret.Source]
  433. id := secret.Source
  434. if secret.Target != "" {
  435. id = secret.Target
  436. }
  437. switch {
  438. case config.File != "":
  439. sources = append(sources, secretsprovider.Source{
  440. ID: id,
  441. FilePath: config.File,
  442. })
  443. case config.Environment != "":
  444. sources = append(sources, secretsprovider.Source{
  445. ID: id,
  446. Env: config.Environment,
  447. })
  448. default:
  449. return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
  450. }
  451. if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
  452. logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
  453. }
  454. }
  455. store, err := secretsprovider.NewStore(sources)
  456. if err != nil {
  457. return nil, err
  458. }
  459. return secretsprovider.NewSecretProvider(store), nil
  460. }
  461. func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
  462. plats, err := useDockerDefaultOrServicePlatform(project, service, false)
  463. if err != nil {
  464. return nil, err
  465. }
  466. for _, buildPlatform := range service.Build.Platforms {
  467. p, err := platforms.Parse(buildPlatform)
  468. if err != nil {
  469. return nil, err
  470. }
  471. if !utils.Contains(plats, p) {
  472. plats = append(plats, p)
  473. }
  474. }
  475. return plats, nil
  476. }
  477. func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
  478. ret := make(types.Labels)
  479. if service.Build != nil {
  480. for k, v := range service.Build.Labels {
  481. ret.Add(k, v)
  482. }
  483. }
  484. ret.Add(api.VersionLabel, api.ComposeVersion)
  485. ret.Add(api.ProjectLabel, project.Name)
  486. ret.Add(api.ServiceLabel, service.Name)
  487. return ret
  488. }
  489. func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
  490. namedContexts := map[string]build.NamedContext{}
  491. for name, context := range additionalContexts {
  492. namedContexts[name] = build.NamedContext{Path: context}
  493. }
  494. return namedContexts
  495. }
  496. func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
  497. var plats []specs.Platform
  498. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  499. if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
  500. return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
  501. }
  502. p, err := platforms.Parse(platform)
  503. if err != nil {
  504. return nil, err
  505. }
  506. plats = append(plats, p)
  507. }
  508. return plats, nil
  509. }
  510. func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
  511. plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
  512. if (len(plats) > 0 && useOnePlatform) || err != nil {
  513. return plats, err
  514. }
  515. if service.Platform != "" {
  516. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
  517. return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
  518. }
  519. // User defined a service platform and no build platforms, so we should keep the one define on the service level
  520. p, err := platforms.Parse(service.Platform)
  521. if !utils.Contains(plats, p) {
  522. plats = append(plats, p)
  523. }
  524. return plats, err
  525. }
  526. return plats, nil
  527. }