build.go 7.8 KB

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