build.go 8.1 KB

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