build.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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/docker/pkg/urlutil"
  25. bclient "github.com/moby/buildkit/client"
  26. "github.com/moby/buildkit/session"
  27. "github.com/moby/buildkit/session/auth/authprovider"
  28. "github.com/moby/buildkit/session/sshforward/sshprovider"
  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. if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
  72. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
  73. if err != nil {
  74. return err
  75. }
  76. buildOptions.Session = append(buildOptions.Session, sshAgentProvider)
  77. }
  78. opts[imageName] = buildOptions
  79. }
  80. }
  81. _, err = s.doBuild(ctx, project, opts, options.Progress)
  82. if err == nil {
  83. if len(imagesToBuild) > 0 && !options.Quiet {
  84. utils.DisplayScanSuggestMsg()
  85. }
  86. }
  87. return err
  88. }
  89. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  90. for _, service := range project.Services {
  91. if service.Image == "" && service.Build == nil {
  92. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  93. }
  94. }
  95. images, err := s.getLocalImagesDigests(ctx, project)
  96. if err != nil {
  97. return err
  98. }
  99. err = s.pullRequiredImages(ctx, project, images, quietPull)
  100. if err != nil {
  101. return err
  102. }
  103. mode := xprogress.PrinterModeAuto
  104. if quietPull {
  105. mode = xprogress.PrinterModeQuiet
  106. }
  107. opts, err := s.getBuildOptions(project, images)
  108. if err != nil {
  109. return err
  110. }
  111. builtImages, err := s.doBuild(ctx, project, opts, mode)
  112. if err != nil {
  113. return err
  114. }
  115. if len(builtImages) > 0 {
  116. utils.DisplayScanSuggestMsg()
  117. }
  118. for name, digest := range builtImages {
  119. images[name] = digest
  120. }
  121. // set digest as com.docker.compose.image label so we can detect outdated containers
  122. for i, service := range project.Services {
  123. image := getImageName(service, project.Name)
  124. digest, ok := images[image]
  125. if ok {
  126. if project.Services[i].Labels == nil {
  127. project.Services[i].Labels = types.Labels{}
  128. }
  129. project.Services[i].CustomLabels[api.ImageDigestLabel] = digest
  130. project.Services[i].Image = image
  131. }
  132. }
  133. return nil
  134. }
  135. func (s *composeService) getBuildOptions(project *types.Project, images map[string]string) (map[string]build.Options, error) {
  136. opts := map[string]build.Options{}
  137. for _, service := range project.Services {
  138. if service.Image == "" && service.Build == nil {
  139. return nil, fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  140. }
  141. imageName := getImageName(service, project.Name)
  142. _, localImagePresent := images[imageName]
  143. if service.Build != nil {
  144. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  145. continue
  146. }
  147. opt, err := s.toBuildOptions(project, service, imageName)
  148. if err != nil {
  149. return nil, err
  150. }
  151. opts[imageName] = opt
  152. continue
  153. }
  154. }
  155. return opts, nil
  156. }
  157. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  158. imageNames := []string{}
  159. for _, s := range project.Services {
  160. imgName := getImageName(s, project.Name)
  161. if !utils.StringContains(imageNames, imgName) {
  162. imageNames = append(imageNames, imgName)
  163. }
  164. }
  165. imgs, err := s.getImages(ctx, imageNames)
  166. if err != nil {
  167. return nil, err
  168. }
  169. images := map[string]string{}
  170. for name, info := range imgs {
  171. images[name] = info.ID
  172. }
  173. for _, s := range project.Services {
  174. imgName := getImageName(s, project.Name)
  175. digest, ok := images[imgName]
  176. if ok {
  177. s.CustomLabels[api.ImageDigestLabel] = digest
  178. }
  179. }
  180. return images, nil
  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. if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
  187. return s.doBuildClassic(ctx, opts)
  188. }
  189. return s.doBuildBuildkit(ctx, project, opts, mode)
  190. }
  191. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) {
  192. var tags []string
  193. tags = append(tags, imageTag)
  194. buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) {
  195. s, ok := project.Environment[s]
  196. return s, ok
  197. }))
  198. var plats []specs.Platform
  199. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  200. p, err := platforms.Parse(platform)
  201. if err != nil {
  202. return build.Options{}, err
  203. }
  204. plats = append(plats, p)
  205. }
  206. if service.Platform != "" {
  207. p, err := platforms.Parse(service.Platform)
  208. if err != nil {
  209. return build.Options{}, err
  210. }
  211. plats = append(plats, p)
  212. }
  213. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  214. if err != nil {
  215. return build.Options{}, err
  216. }
  217. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  218. if err != nil {
  219. return build.Options{}, err
  220. }
  221. return build.Options{
  222. Inputs: build.Inputs{
  223. ContextPath: service.Build.Context,
  224. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  225. },
  226. CacheFrom: cacheFrom,
  227. CacheTo: cacheTo,
  228. NoCache: service.Build.NoCache,
  229. Pull: service.Build.Pull,
  230. BuildArgs: buildArgs,
  231. Tags: tags,
  232. Target: service.Build.Target,
  233. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  234. Platforms: plats,
  235. Labels: service.Build.Labels,
  236. NetworkMode: service.Build.Network,
  237. ExtraHosts: service.Build.ExtraHosts,
  238. Session: []session.Attachable{
  239. authprovider.NewDockerAuthProvider(s.stderr()),
  240. },
  241. }, nil
  242. }
  243. func flatten(in types.MappingWithEquals) types.Mapping {
  244. if len(in) == 0 {
  245. return nil
  246. }
  247. out := types.Mapping{}
  248. for k, v := range in {
  249. if v == nil {
  250. continue
  251. }
  252. out[k] = *v
  253. }
  254. return out
  255. }
  256. func mergeArgs(m ...types.Mapping) types.Mapping {
  257. merged := types.Mapping{}
  258. for _, mapping := range m {
  259. for key, val := range mapping {
  260. merged[key] = val
  261. }
  262. }
  263. return merged
  264. }
  265. func dockerFilePath(context string, dockerfile string) string {
  266. if urlutil.IsGitURL(context) || filepath.IsAbs(dockerfile) {
  267. return dockerfile
  268. }
  269. return filepath.Join(context, dockerfile)
  270. }
  271. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  272. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  273. for _, sshKey := range sshKeys {
  274. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  275. ID: sshKey.ID,
  276. Paths: []string{sshKey.Path},
  277. })
  278. }
  279. return sshprovider.NewSSHAgentProvider(sshConfig)
  280. }