build.go 8.1 KB

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