build.go 16 KB

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