build.go 13 KB

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