build.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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"
  23. _ "github.com/docker/buildx/driver/docker" // required to get default driver registered
  24. "github.com/docker/buildx/util/buildflags"
  25. xprogress "github.com/docker/buildx/util/progress"
  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) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
  169. info, err := s.apiClient.Info(ctx)
  170. if err != nil {
  171. return nil, err
  172. }
  173. if info.OSType == "windows" {
  174. // no support yet for Windows container builds in Buildkit
  175. // https://docs.docker.com/develop/develop-images/build_enhancements/#limitations
  176. err := s.windowsBuild(opts, mode)
  177. return nil, WrapCategorisedComposeError(err, BuildFailure)
  178. }
  179. if len(opts) == 0 {
  180. return nil, nil
  181. }
  182. const drivername = "default"
  183. d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, "", nil, nil, project.WorkingDir)
  184. if err != nil {
  185. return nil, err
  186. }
  187. driverInfo := []build.DriverInfo{
  188. {
  189. Name: "default",
  190. Driver: d,
  191. },
  192. }
  193. // Progress needs its own context that lives longer than the
  194. // build one otherwise it won't read all the messages from
  195. // build and will lock
  196. progressCtx, cancel := context.WithCancel(context.Background())
  197. defer cancel()
  198. w := xprogress.NewPrinter(progressCtx, os.Stdout, mode)
  199. // We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
  200. response, err := build.Build(ctx, driverInfo, opts, nil, nil, w)
  201. errW := w.Wait()
  202. if err == nil {
  203. err = errW
  204. }
  205. if err != nil {
  206. return nil, WrapCategorisedComposeError(err, BuildFailure)
  207. }
  208. imagesBuilt := map[string]string{}
  209. for name, img := range response {
  210. if img == nil || len(img.ExporterResponse) == 0 {
  211. continue
  212. }
  213. digest, ok := img.ExporterResponse["containerimage.digest"]
  214. if !ok {
  215. continue
  216. }
  217. imagesBuilt[name] = digest
  218. }
  219. return imagesBuilt, err
  220. }
  221. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) {
  222. var tags []string
  223. tags = append(tags, imageTag)
  224. buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) {
  225. s, ok := project.Environment[s]
  226. return s, ok
  227. }))
  228. var plats []specs.Platform
  229. if service.Platform != "" {
  230. p, err := platforms.Parse(service.Platform)
  231. if err != nil {
  232. return build.Options{}, err
  233. }
  234. plats = append(plats, p)
  235. }
  236. return build.Options{
  237. Inputs: build.Inputs{
  238. ContextPath: service.Build.Context,
  239. DockerfilePath: filepath.Join(service.Build.Context, service.Build.Dockerfile),
  240. },
  241. BuildArgs: buildArgs,
  242. Tags: tags,
  243. Target: service.Build.Target,
  244. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  245. Platforms: plats,
  246. Labels: service.Build.Labels,
  247. NetworkMode: service.Build.Network,
  248. ExtraHosts: service.Build.ExtraHosts,
  249. Session: []session.Attachable{
  250. authprovider.NewDockerAuthProvider(os.Stderr),
  251. },
  252. }, nil
  253. }
  254. func flatten(in types.MappingWithEquals) types.Mapping {
  255. if len(in) == 0 {
  256. return nil
  257. }
  258. out := types.Mapping{}
  259. for k, v := range in {
  260. if v == nil {
  261. continue
  262. }
  263. out[k] = *v
  264. }
  265. return out
  266. }
  267. func mergeArgs(m ...types.Mapping) types.Mapping {
  268. merged := types.Mapping{}
  269. for _, mapping := range m {
  270. for key, val := range mapping {
  271. merged[key] = val
  272. }
  273. }
  274. return merged
  275. }