build.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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. "path/filepath"
  18. "github.com/compose-spec/compose-go/types"
  19. "github.com/containerd/containerd/platforms"
  20. "github.com/docker/buildx/build"
  21. _ "github.com/docker/buildx/driver/docker" // required to get default driver registered
  22. "github.com/docker/buildx/store/storeutil"
  23. "github.com/docker/buildx/util/buildflags"
  24. xprogress "github.com/docker/buildx/util/progress"
  25. "github.com/docker/docker/builder/remotecontext/urlutil"
  26. bclient "github.com/moby/buildkit/client"
  27. "github.com/moby/buildkit/session"
  28. "github.com/moby/buildkit/session/auth/authprovider"
  29. "github.com/moby/buildkit/session/secrets/secretsprovider"
  30. "github.com/moby/buildkit/session/sshforward/sshprovider"
  31. "github.com/moby/buildkit/util/entitlements"
  32. specs "github.com/opencontainers/image-spec/specs-go/v1"
  33. "github.com/docker/compose/v2/pkg/api"
  34. "github.com/docker/compose/v2/pkg/progress"
  35. "github.com/docker/compose/v2/pkg/utils"
  36. )
  37. func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
  38. return progress.Run(ctx, func(ctx context.Context) error {
  39. _, err := s.build(ctx, project, options)
  40. return err
  41. }, s.stderr())
  42. }
  43. func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) {
  44. args := flatten(options.Args.Resolve(envResolver(project.Environment)))
  45. builtIDs := make([]string, len(project.Services))
  46. err := InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
  47. if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
  48. return nil
  49. }
  50. for i, service := range project.Services {
  51. if service.Name != name {
  52. continue
  53. }
  54. service, err := project.GetService(name)
  55. if err != nil {
  56. return err
  57. }
  58. if service.Build == nil {
  59. return nil
  60. }
  61. imageName := api.GetImageNameOrDefault(service, project.Name)
  62. buildOptions, err := s.toBuildOptions(project, service, imageName, options)
  63. if err != nil {
  64. return err
  65. }
  66. buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, args)
  67. opts := map[string]build.Options{imageName: buildOptions}
  68. ids, err := s.doBuild(ctx, project, opts, options.Progress)
  69. if err != nil {
  70. return err
  71. }
  72. builtIDs[i] = ids[imageName]
  73. }
  74. return nil
  75. }, func(traversal *graphTraversal) {
  76. traversal.maxConcurrency = s.maxConcurrency
  77. })
  78. imageIDs := map[string]string{}
  79. for i, d := range builtIDs {
  80. if d != "" {
  81. imageIDs[project.Services[i].Image] = d
  82. }
  83. }
  84. return imageIDs, err
  85. }
  86. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  87. for _, service := range project.Services {
  88. if service.Image == "" && service.Build == nil {
  89. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  90. }
  91. }
  92. images, err := s.getLocalImagesDigests(ctx, project)
  93. if err != nil {
  94. return err
  95. }
  96. err = s.pullRequiredImages(ctx, project, images, quietPull)
  97. if err != nil {
  98. return err
  99. }
  100. mode := xprogress.PrinterModeAuto
  101. if quietPull {
  102. mode = xprogress.PrinterModeQuiet
  103. }
  104. err = s.prepareProjectForBuild(project, images)
  105. if err != nil {
  106. return err
  107. }
  108. builtImages, err := s.build(ctx, project, api.BuildOptions{
  109. Progress: mode,
  110. })
  111. if err != nil {
  112. return err
  113. }
  114. for name, digest := range builtImages {
  115. images[name] = digest
  116. }
  117. // set digest as com.docker.compose.image label so we can detect outdated containers
  118. for i, service := range project.Services {
  119. image := api.GetImageNameOrDefault(service, project.Name)
  120. digest, ok := images[image]
  121. if ok {
  122. if project.Services[i].Labels == nil {
  123. project.Services[i].Labels = types.Labels{}
  124. }
  125. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  126. }
  127. }
  128. return nil
  129. }
  130. func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) error {
  131. platform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
  132. for i, service := range project.Services {
  133. if service.Image == "" && service.Build == nil {
  134. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  135. }
  136. if service.Build == nil {
  137. continue
  138. }
  139. imageName := api.GetImageNameOrDefault(service, project.Name)
  140. service.Image = imageName
  141. _, localImagePresent := images[imageName]
  142. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  143. service.Build = nil
  144. project.Services[i] = service
  145. continue
  146. }
  147. if platform != "" {
  148. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
  149. return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", service.Name, platform)
  150. }
  151. service.Platform = platform
  152. }
  153. if service.Platform == "" {
  154. // let builder to build for default platform
  155. service.Build.Platforms = nil
  156. } else {
  157. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
  158. return fmt.Errorf("service %q build configuration does not support platform: %s", service.Name, platform)
  159. }
  160. service.Build.Platforms = []string{service.Platform}
  161. }
  162. project.Services[i] = service
  163. }
  164. return nil
  165. }
  166. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  167. var imageNames []string
  168. for _, s := range project.Services {
  169. imgName := api.GetImageNameOrDefault(s, project.Name)
  170. if !utils.StringContains(imageNames, imgName) {
  171. imageNames = append(imageNames, imgName)
  172. }
  173. }
  174. imgs, err := s.getImages(ctx, imageNames)
  175. if err != nil {
  176. return nil, err
  177. }
  178. images := map[string]string{}
  179. for name, info := range imgs {
  180. images[name] = info.ID
  181. }
  182. for i := range project.Services {
  183. imgName := api.GetImageNameOrDefault(project.Services[i], project.Name)
  184. digest, ok := images[imgName]
  185. if ok {
  186. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  187. }
  188. }
  189. return images, nil
  190. }
  191. func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
  192. if len(opts) == 0 {
  193. return nil, nil
  194. }
  195. if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
  196. return s.doBuildClassic(ctx, project, opts)
  197. }
  198. return s.doBuildBuildkit(ctx, opts, mode)
  199. }
  200. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, options api.BuildOptions) (build.Options, error) {
  201. var tags []string
  202. tags = append(tags, imageTag)
  203. buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
  204. for k, v := range storeutil.GetProxyConfig(s.dockerCli) {
  205. if _, ok := buildArgs[k]; !ok {
  206. buildArgs[k] = v
  207. }
  208. }
  209. plats, err := addPlatforms(project, service)
  210. if err != nil {
  211. return build.Options{}, err
  212. }
  213. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  214. if err != nil {
  215. return build.Options{}, err
  216. }
  217. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  218. if err != nil {
  219. return build.Options{}, err
  220. }
  221. sessionConfig := []session.Attachable{
  222. authprovider.NewDockerAuthProvider(s.configFile()),
  223. }
  224. if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
  225. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
  226. if err != nil {
  227. return build.Options{}, err
  228. }
  229. sessionConfig = append(sessionConfig, sshAgentProvider)
  230. }
  231. if len(service.Build.Secrets) > 0 {
  232. secretsProvider, err := addSecretsConfig(project, service)
  233. if err != nil {
  234. return build.Options{}, err
  235. }
  236. sessionConfig = append(sessionConfig, secretsProvider)
  237. }
  238. if len(service.Build.Tags) > 0 {
  239. tags = append(tags, service.Build.Tags...)
  240. }
  241. var allow []entitlements.Entitlement
  242. if service.Build.Privileged {
  243. allow = append(allow, entitlements.EntitlementSecurityInsecure)
  244. }
  245. imageLabels := getImageBuildLabels(project, service)
  246. exports := []bclient.ExportEntry{{
  247. Type: "docker",
  248. Attrs: map[string]string{
  249. "load": "true",
  250. "push": fmt.Sprint(options.Push),
  251. },
  252. }}
  253. if len(service.Build.Platforms) > 1 {
  254. exports = []bclient.ExportEntry{{
  255. Type: "image",
  256. Attrs: map[string]string{
  257. "push": fmt.Sprint(options.Push),
  258. },
  259. }}
  260. }
  261. return build.Options{
  262. Inputs: build.Inputs{
  263. ContextPath: service.Build.Context,
  264. DockerfileInline: service.Build.DockerfileInline,
  265. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  266. NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
  267. },
  268. CacheFrom: cacheFrom,
  269. CacheTo: cacheTo,
  270. NoCache: service.Build.NoCache || options.NoCache,
  271. Pull: service.Build.Pull || options.Pull,
  272. BuildArgs: buildArgs,
  273. Tags: tags,
  274. Target: service.Build.Target,
  275. Exports: exports,
  276. Platforms: plats,
  277. Labels: imageLabels,
  278. NetworkMode: service.Build.Network,
  279. ExtraHosts: service.Build.ExtraHosts.AsList(),
  280. Session: sessionConfig,
  281. Allow: allow,
  282. }, nil
  283. }
  284. func flatten(in types.MappingWithEquals) types.Mapping {
  285. out := types.Mapping{}
  286. if len(in) == 0 {
  287. return out
  288. }
  289. for k, v := range in {
  290. if v == nil {
  291. continue
  292. }
  293. out[k] = *v
  294. }
  295. return out
  296. }
  297. func mergeArgs(m ...types.Mapping) types.Mapping {
  298. merged := types.Mapping{}
  299. for _, mapping := range m {
  300. for key, val := range mapping {
  301. merged[key] = val
  302. }
  303. }
  304. return merged
  305. }
  306. func dockerFilePath(ctxName string, dockerfile string) string {
  307. if dockerfile == "" {
  308. return ""
  309. }
  310. if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
  311. return dockerfile
  312. }
  313. return filepath.Join(ctxName, dockerfile)
  314. }
  315. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  316. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  317. for _, sshKey := range sshKeys {
  318. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  319. ID: sshKey.ID,
  320. Paths: []string{sshKey.Path},
  321. })
  322. }
  323. return sshprovider.NewSSHAgentProvider(sshConfig)
  324. }
  325. func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
  326. var sources []secretsprovider.Source
  327. for _, secret := range service.Build.Secrets {
  328. config := project.Secrets[secret.Source]
  329. switch {
  330. case config.File != "":
  331. sources = append(sources, secretsprovider.Source{
  332. ID: secret.Source,
  333. FilePath: config.File,
  334. })
  335. case config.Environment != "":
  336. sources = append(sources, secretsprovider.Source{
  337. ID: secret.Source,
  338. Env: config.Environment,
  339. })
  340. default:
  341. return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
  342. }
  343. }
  344. store, err := secretsprovider.NewStore(sources)
  345. if err != nil {
  346. return nil, err
  347. }
  348. return secretsprovider.NewSecretProvider(store), nil
  349. }
  350. func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
  351. plats, err := useDockerDefaultOrServicePlatform(project, service, false)
  352. if err != nil {
  353. return nil, err
  354. }
  355. for _, buildPlatform := range service.Build.Platforms {
  356. p, err := platforms.Parse(buildPlatform)
  357. if err != nil {
  358. return nil, err
  359. }
  360. if !utils.Contains(plats, p) {
  361. plats = append(plats, p)
  362. }
  363. }
  364. return plats, nil
  365. }
  366. func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
  367. ret := make(types.Labels)
  368. if service.Build != nil {
  369. for k, v := range service.Build.Labels {
  370. ret.Add(k, v)
  371. }
  372. }
  373. ret.Add(api.VersionLabel, api.ComposeVersion)
  374. ret.Add(api.ProjectLabel, project.Name)
  375. ret.Add(api.ServiceLabel, service.Name)
  376. return ret
  377. }
  378. func toBuildContexts(additionalContexts map[string]*string) map[string]build.NamedContext {
  379. namedContexts := map[string]build.NamedContext{}
  380. for name, buildContext := range additionalContexts {
  381. namedContexts[name] = build.NamedContext{Path: *buildContext}
  382. }
  383. return namedContexts
  384. }
  385. func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
  386. var plats []specs.Platform
  387. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  388. if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
  389. return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
  390. }
  391. p, err := platforms.Parse(platform)
  392. if err != nil {
  393. return nil, err
  394. }
  395. plats = append(plats, p)
  396. }
  397. return plats, nil
  398. }
  399. func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
  400. plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
  401. if (len(plats) > 0 && useOnePlatform) || err != nil {
  402. return plats, err
  403. }
  404. if service.Platform != "" {
  405. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
  406. return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
  407. }
  408. // User defined a service platform and no build platforms, so we should keep the one define on the service level
  409. p, err := platforms.Parse(service.Platform)
  410. if !utils.Contains(plats, p) {
  411. plats = append(plats, p)
  412. }
  413. return plats, err
  414. }
  415. return plats, nil
  416. }