build.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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/compose-spec/compose-go/types"
  20. "github.com/containerd/containerd/platforms"
  21. "github.com/docker/buildx/build"
  22. _ "github.com/docker/buildx/driver/docker" // required to get default driver registered
  23. "github.com/docker/buildx/util/buildflags"
  24. xprogress "github.com/docker/buildx/util/progress"
  25. "github.com/docker/cli/cli/command"
  26. "github.com/docker/cli/cli/flags"
  27. "github.com/docker/docker/client"
  28. bclient "github.com/moby/buildkit/client"
  29. "github.com/moby/buildkit/session"
  30. "github.com/moby/buildkit/session/auth/authprovider"
  31. specs "github.com/opencontainers/image-spec/specs-go/v1"
  32. "github.com/docker/compose/v2/pkg/api"
  33. "github.com/docker/compose/v2/pkg/progress"
  34. "github.com/docker/compose/v2/pkg/utils"
  35. )
  36. func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
  37. return progress.Run(ctx, func(ctx context.Context) error {
  38. return s.build(ctx, project, options)
  39. })
  40. }
  41. func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
  42. opts := map[string]build.Options{}
  43. imagesToBuild := []string{}
  44. args := flatten(options.Args.Resolve(func(s string) (string, bool) {
  45. s, ok := project.Environment[s]
  46. return s, ok
  47. }))
  48. services, err := project.GetServices(options.Services...)
  49. if err != nil {
  50. return err
  51. }
  52. for _, service := range services {
  53. if service.Build != nil {
  54. imageName := getImageName(service, project.Name)
  55. imagesToBuild = append(imagesToBuild, imageName)
  56. buildOptions, err := s.toBuildOptions(project, service, imageName)
  57. if err != nil {
  58. return err
  59. }
  60. buildOptions.Pull = options.Pull
  61. buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, args)
  62. buildOptions.NoCache = options.NoCache
  63. buildOptions.CacheFrom, err = buildflags.ParseCacheEntry(service.Build.CacheFrom)
  64. if err != nil {
  65. return err
  66. }
  67. for _, image := range service.Build.CacheFrom {
  68. buildOptions.CacheFrom = append(buildOptions.CacheFrom, bclient.CacheOptionsEntry{
  69. Type: "registry",
  70. Attrs: map[string]string{"ref": image},
  71. })
  72. }
  73. opts[imageName] = buildOptions
  74. }
  75. }
  76. _, err = s.doBuild(ctx, project, opts, options.Progress)
  77. if err == nil {
  78. if len(imagesToBuild) > 0 && !options.Quiet {
  79. utils.DisplayScanSuggestMsg()
  80. }
  81. }
  82. return err
  83. }
  84. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  85. for _, service := range project.Services {
  86. if service.Image == "" && service.Build == nil {
  87. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  88. }
  89. }
  90. images, err := s.getLocalImagesDigests(ctx, project)
  91. if err != nil {
  92. return err
  93. }
  94. err = s.pullRequiredImages(ctx, project, images, quietPull)
  95. if err != nil {
  96. return err
  97. }
  98. mode := xprogress.PrinterModeAuto
  99. if quietPull {
  100. mode = xprogress.PrinterModeQuiet
  101. }
  102. opts, err := s.getBuildOptions(project, images)
  103. if err != nil {
  104. return err
  105. }
  106. builtImages, err := s.doBuild(ctx, project, opts, mode)
  107. if err != nil {
  108. return err
  109. }
  110. if len(builtImages) > 0 {
  111. utils.DisplayScanSuggestMsg()
  112. }
  113. for name, digest := range builtImages {
  114. images[name] = digest
  115. }
  116. // set digest as com.docker.compose.image label so we can detect outdated containers
  117. for i, service := range project.Services {
  118. image := getImageName(service, project.Name)
  119. digest, ok := images[image]
  120. if ok {
  121. if project.Services[i].Labels == nil {
  122. project.Services[i].Labels = types.Labels{}
  123. }
  124. project.Services[i].Labels[api.ImageDigestLabel] = digest
  125. project.Services[i].Image = image
  126. }
  127. }
  128. return nil
  129. }
  130. func (s *composeService) getBuildOptions(project *types.Project, images map[string]string) (map[string]build.Options, error) {
  131. opts := map[string]build.Options{}
  132. for _, service := range project.Services {
  133. if service.Image == "" && service.Build == nil {
  134. return nil, fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  135. }
  136. imageName := getImageName(service, project.Name)
  137. _, localImagePresent := images[imageName]
  138. if service.Build != nil {
  139. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  140. continue
  141. }
  142. opt, err := s.toBuildOptions(project, service, imageName)
  143. if err != nil {
  144. return nil, err
  145. }
  146. opts[imageName] = opt
  147. continue
  148. }
  149. }
  150. return opts, nil
  151. }
  152. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  153. imageNames := []string{}
  154. for _, s := range project.Services {
  155. imgName := getImageName(s, project.Name)
  156. if !utils.StringContains(imageNames, imgName) {
  157. imageNames = append(imageNames, imgName)
  158. }
  159. }
  160. imgs, err := s.getImages(ctx, imageNames)
  161. if err != nil {
  162. return nil, err
  163. }
  164. images := map[string]string{}
  165. for name, info := range imgs {
  166. images[name] = info.ID
  167. }
  168. return images, nil
  169. }
  170. func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
  171. if len(opts) == 0 {
  172. return nil, nil
  173. }
  174. dockerCli, err := command.NewDockerCli()
  175. if err != nil {
  176. return nil, err
  177. }
  178. err = dockerCli.Initialize(flags.NewClientOptions(), command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
  179. return s.apiClient, nil
  180. }))
  181. if err != nil {
  182. return nil, err
  183. }
  184. if buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo()); err != nil || !buildkitEnabled {
  185. return s.doBuildClassic(ctx, dockerCli, opts)
  186. }
  187. return s.doBuildBuildkit(ctx, project, opts, mode)
  188. }
  189. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) {
  190. var tags []string
  191. tags = append(tags, imageTag)
  192. buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) {
  193. s, ok := project.Environment[s]
  194. return s, ok
  195. }))
  196. var plats []specs.Platform
  197. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  198. p, err := platforms.Parse(platform)
  199. if err != nil {
  200. return build.Options{}, err
  201. }
  202. plats = append(plats, p)
  203. }
  204. if service.Platform != "" {
  205. p, err := platforms.Parse(service.Platform)
  206. if err != nil {
  207. return build.Options{}, err
  208. }
  209. plats = append(plats, p)
  210. }
  211. return build.Options{
  212. Inputs: build.Inputs{
  213. ContextPath: service.Build.Context,
  214. DockerfilePath: filepath.Join(service.Build.Context, service.Build.Dockerfile),
  215. },
  216. BuildArgs: buildArgs,
  217. Tags: tags,
  218. Target: service.Build.Target,
  219. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  220. Platforms: plats,
  221. Labels: service.Build.Labels,
  222. NetworkMode: service.Build.Network,
  223. ExtraHosts: service.Build.ExtraHosts,
  224. Session: []session.Attachable{
  225. authprovider.NewDockerAuthProvider(os.Stderr),
  226. },
  227. }, nil
  228. }
  229. func flatten(in types.MappingWithEquals) types.Mapping {
  230. if len(in) == 0 {
  231. return nil
  232. }
  233. out := types.Mapping{}
  234. for k, v := range in {
  235. if v == nil {
  236. continue
  237. }
  238. out[k] = *v
  239. }
  240. return out
  241. }
  242. func mergeArgs(m ...types.Mapping) types.Mapping {
  243. merged := types.Mapping{}
  244. for _, mapping := range m {
  245. for key, val := range mapping {
  246. merged[key] = val
  247. }
  248. }
  249. return merged
  250. }