build.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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/util/buildflags"
  23. xprogress "github.com/docker/buildx/util/progress"
  24. "github.com/docker/cli/cli/command"
  25. "github.com/docker/docker/pkg/urlutil"
  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].CustomLabels[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. for _, s := range project.Services {
  167. imgName := getImageName(s, project.Name)
  168. digest, ok := images[imgName]
  169. if ok {
  170. s.CustomLabels[api.ImageDigestLabel] = digest
  171. }
  172. }
  173. return images, nil
  174. }
  175. func (s *composeService) serverInfo(ctx context.Context) (command.ServerInfo, error) {
  176. ping, err := s.apiClient().Ping(ctx)
  177. if err != nil {
  178. return command.ServerInfo{}, err
  179. }
  180. serverInfo := command.ServerInfo{
  181. HasExperimental: ping.Experimental,
  182. OSType: ping.OSType,
  183. BuildkitVersion: ping.BuilderVersion,
  184. }
  185. return serverInfo, err
  186. }
  187. func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
  188. if len(opts) == 0 {
  189. return nil, nil
  190. }
  191. serverInfo, err := s.serverInfo(ctx)
  192. if err != nil {
  193. return nil, err
  194. }
  195. if buildkitEnabled, err := command.BuildKitEnabled(serverInfo); err != nil || !buildkitEnabled {
  196. return s.doBuildClassic(ctx, opts)
  197. }
  198. return s.doBuildBuildkit(ctx, project, opts, mode)
  199. }
  200. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) {
  201. var tags []string
  202. tags = append(tags, imageTag)
  203. buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) {
  204. s, ok := project.Environment[s]
  205. return s, ok
  206. }))
  207. var plats []specs.Platform
  208. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  209. p, err := platforms.Parse(platform)
  210. if err != nil {
  211. return build.Options{}, err
  212. }
  213. plats = append(plats, p)
  214. }
  215. if service.Platform != "" {
  216. p, err := platforms.Parse(service.Platform)
  217. if err != nil {
  218. return build.Options{}, err
  219. }
  220. plats = append(plats, p)
  221. }
  222. return build.Options{
  223. Inputs: build.Inputs{
  224. ContextPath: service.Build.Context,
  225. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  226. },
  227. BuildArgs: buildArgs,
  228. Tags: tags,
  229. Target: service.Build.Target,
  230. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  231. Platforms: plats,
  232. Labels: service.Build.Labels,
  233. NetworkMode: service.Build.Network,
  234. ExtraHosts: service.Build.ExtraHosts,
  235. Session: []session.Attachable{
  236. authprovider.NewDockerAuthProvider(s.stderr()),
  237. },
  238. }, nil
  239. }
  240. func flatten(in types.MappingWithEquals) types.Mapping {
  241. if len(in) == 0 {
  242. return nil
  243. }
  244. out := types.Mapping{}
  245. for k, v := range in {
  246. if v == nil {
  247. continue
  248. }
  249. out[k] = *v
  250. }
  251. return out
  252. }
  253. func mergeArgs(m ...types.Mapping) types.Mapping {
  254. merged := types.Mapping{}
  255. for _, mapping := range m {
  256. for key, val := range mapping {
  257. merged[key] = val
  258. }
  259. }
  260. return merged
  261. }
  262. func dockerFilePath(context string, dockerfile string) string {
  263. if urlutil.IsGitURL(context) || filepath.IsAbs(dockerfile) {
  264. return dockerfile
  265. }
  266. return filepath.Join(context, dockerfile)
  267. }