build.go 13 KB

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