build.go 16 KB

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