build.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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. "net/url"
  18. "os"
  19. "path"
  20. "strings"
  21. "github.com/docker/compose-cli/api/compose"
  22. "github.com/compose-spec/compose-go/types"
  23. "github.com/containerd/containerd/platforms"
  24. "github.com/docker/buildx/build"
  25. "github.com/docker/buildx/driver"
  26. _ "github.com/docker/buildx/driver/docker" // required to get default driver registered
  27. "github.com/docker/buildx/util/progress"
  28. "github.com/docker/docker/errdefs"
  29. bclient "github.com/moby/buildkit/client"
  30. specs "github.com/opencontainers/image-spec/specs-go/v1"
  31. )
  32. func (s *composeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
  33. opts := map[string]build.Options{}
  34. imagesToBuild := []string{}
  35. for _, service := range project.Services {
  36. if service.Build != nil {
  37. imageName := getImageName(service, project.Name)
  38. imagesToBuild = append(imagesToBuild, imageName)
  39. buildOptions, err := s.toBuildOptions(service, project.WorkingDir, imageName)
  40. if err != nil {
  41. return err
  42. }
  43. buildOptions.Pull = options.Pull
  44. buildOptions.BuildArgs = options.Args
  45. buildOptions.NoCache = options.NoCache
  46. opts[imageName] = buildOptions
  47. buildOptions.CacheFrom, err = build.ParseCacheEntry(service.Build.CacheFrom)
  48. if err != nil {
  49. return err
  50. }
  51. for _, image := range service.Build.CacheFrom {
  52. buildOptions.CacheFrom = append(buildOptions.CacheFrom, bclient.CacheOptionsEntry{
  53. Type: "registry",
  54. Attrs: map[string]string{"ref": image},
  55. })
  56. }
  57. }
  58. }
  59. err := s.build(ctx, project, opts, options.Progress)
  60. if err == nil {
  61. displayScanSuggestMsg(imagesToBuild)
  62. }
  63. return err
  64. }
  65. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  66. opts := map[string]build.Options{}
  67. imagesToBuild := []string{}
  68. for _, service := range project.Services {
  69. if service.Image == "" && service.Build == nil {
  70. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  71. }
  72. imageName := getImageName(service, project.Name)
  73. localImagePresent, err := s.localImagePresent(ctx, imageName)
  74. if err != nil {
  75. return err
  76. }
  77. if service.Build != nil {
  78. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  79. continue
  80. }
  81. imagesToBuild = append(imagesToBuild, imageName)
  82. opts[imageName], err = s.toBuildOptions(service, project.WorkingDir, imageName)
  83. if err != nil {
  84. return err
  85. }
  86. continue
  87. }
  88. if service.Image != "" {
  89. if localImagePresent {
  90. continue
  91. }
  92. }
  93. // Buildx has no command to "just pull", see
  94. // so we bake a temporary dockerfile that will just pull and export pulled image
  95. opts[service.Name] = build.Options{
  96. Inputs: build.Inputs{
  97. ContextPath: ".",
  98. DockerfilePath: "-",
  99. InStream: strings.NewReader("FROM " + service.Image),
  100. },
  101. Tags: []string{service.Image},
  102. Pull: true,
  103. }
  104. }
  105. mode := progress.PrinterModeAuto
  106. if quietPull {
  107. mode = progress.PrinterModeQuiet
  108. }
  109. err := s.build(ctx, project, opts, mode)
  110. if err == nil {
  111. displayScanSuggestMsg(imagesToBuild)
  112. }
  113. return err
  114. }
  115. func (s *composeService) localImagePresent(ctx context.Context, imageName string) (bool, error) {
  116. _, _, err := s.apiClient.ImageInspectWithRaw(ctx, imageName)
  117. if err != nil {
  118. if errdefs.IsNotFound(err) {
  119. return false, nil
  120. }
  121. return false, err
  122. }
  123. return true, nil
  124. }
  125. func (s *composeService) build(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) error {
  126. if len(opts) == 0 {
  127. return nil
  128. }
  129. const drivername = "default"
  130. d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, nil, nil, nil, "", nil, nil, project.WorkingDir)
  131. if err != nil {
  132. return err
  133. }
  134. driverInfo := []build.DriverInfo{
  135. {
  136. Name: "default",
  137. Driver: d,
  138. },
  139. }
  140. // Progress needs its own context that lives longer than the
  141. // build one otherwise it won't read all the messages from
  142. // build and will lock
  143. progressCtx, cancel := context.WithCancel(context.Background())
  144. defer cancel()
  145. w := progress.NewPrinter(progressCtx, os.Stdout, mode)
  146. // We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
  147. _, err = build.Build(ctx, driverInfo, opts, nil, nil, w)
  148. errW := w.Wait()
  149. if err == nil {
  150. err = errW
  151. }
  152. return err
  153. }
  154. func (s *composeService) toBuildOptions(service types.ServiceConfig, contextPath string, imageTag string) (build.Options, error) {
  155. var tags []string
  156. tags = append(tags, imageTag)
  157. if service.Build.Dockerfile == "" {
  158. service.Build.Dockerfile = "Dockerfile"
  159. }
  160. var buildArgs map[string]string
  161. var plats []specs.Platform
  162. if service.Platform != "" {
  163. p, err := platforms.Parse(service.Platform)
  164. if err != nil {
  165. return build.Options{}, err
  166. }
  167. plats = append(plats, p)
  168. }
  169. var input build.Inputs
  170. _, err := url.ParseRequestURI(service.Build.Context)
  171. if err == nil {
  172. input = build.Inputs{
  173. ContextPath: service.Build.Context,
  174. DockerfilePath: service.Build.Dockerfile,
  175. }
  176. } else {
  177. input = build.Inputs{
  178. ContextPath: path.Join(contextPath, service.Build.Context),
  179. DockerfilePath: path.Join(contextPath, service.Build.Context, service.Build.Dockerfile),
  180. }
  181. }
  182. return build.Options{
  183. Inputs: input,
  184. BuildArgs: flatten(mergeArgs(service.Build.Args, buildArgs)),
  185. Tags: tags,
  186. Target: service.Build.Target,
  187. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  188. Platforms: plats,
  189. Labels: service.Build.Labels,
  190. }, nil
  191. }
  192. func flatten(in types.MappingWithEquals) map[string]string {
  193. if len(in) == 0 {
  194. return nil
  195. }
  196. out := make(map[string]string)
  197. for k, v := range in {
  198. if v == nil {
  199. continue
  200. }
  201. out[k] = *v
  202. }
  203. return out
  204. }
  205. func mergeArgs(src types.MappingWithEquals, values map[string]string) types.MappingWithEquals {
  206. for key := range src {
  207. if val, ok := values[key]; ok {
  208. if val == "" {
  209. src[key] = nil
  210. } else {
  211. src[key] = &val
  212. }
  213. }
  214. }
  215. return src
  216. }