build.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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/secrets/secretsprovider"
  29. "github.com/moby/buildkit/session/sshforward/sshprovider"
  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, options.SSHs)
  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, []types.SSHKey{})
  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. for _, s := range project.Services {
  168. imgName := getImageName(s, project.Name)
  169. digest, ok := images[imgName]
  170. if ok {
  171. s.CustomLabels[api.ImageDigestLabel] = digest
  172. }
  173. }
  174. return images, nil
  175. }
  176. func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
  177. if len(opts) == 0 {
  178. return nil, nil
  179. }
  180. if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
  181. return s.doBuildClassic(ctx, opts)
  182. }
  183. return s.doBuildBuildkit(ctx, project, opts, mode)
  184. }
  185. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, sshKeys []types.SSHKey) (build.Options, error) {
  186. var tags []string
  187. tags = append(tags, imageTag)
  188. buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) {
  189. s, ok := project.Environment[s]
  190. return s, ok
  191. }))
  192. var plats []specs.Platform
  193. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  194. p, err := platforms.Parse(platform)
  195. if err != nil {
  196. return build.Options{}, err
  197. }
  198. plats = append(plats, p)
  199. }
  200. if service.Platform != "" {
  201. p, err := platforms.Parse(service.Platform)
  202. if err != nil {
  203. return build.Options{}, err
  204. }
  205. plats = append(plats, p)
  206. }
  207. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  208. if err != nil {
  209. return build.Options{}, err
  210. }
  211. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  212. if err != nil {
  213. return build.Options{}, err
  214. }
  215. sessionConfig := []session.Attachable{
  216. authprovider.NewDockerAuthProvider(s.stderr()),
  217. }
  218. if len(sshKeys) > 0 || len(service.Build.SSH) > 0 {
  219. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, sshKeys...))
  220. if err != nil {
  221. return build.Options{}, err
  222. }
  223. sessionConfig = append(sessionConfig, sshAgentProvider)
  224. }
  225. if len(service.Build.Secrets) > 0 {
  226. var sources []secretsprovider.Source
  227. for _, secret := range service.Build.Secrets {
  228. config := project.Secrets[secret.Source]
  229. if config.File == "" {
  230. return build.Options{}, fmt.Errorf("build.secrets only supports file-based secrets: %q", secret.Source)
  231. }
  232. sources = append(sources, secretsprovider.Source{
  233. ID: secret.Source,
  234. FilePath: config.File,
  235. })
  236. }
  237. store, err := secretsprovider.NewStore(sources)
  238. if err != nil {
  239. return build.Options{}, err
  240. }
  241. p := secretsprovider.NewSecretProvider(store)
  242. sessionConfig = append(sessionConfig, p)
  243. }
  244. return build.Options{
  245. Inputs: build.Inputs{
  246. ContextPath: service.Build.Context,
  247. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  248. },
  249. CacheFrom: cacheFrom,
  250. CacheTo: cacheTo,
  251. NoCache: service.Build.NoCache,
  252. Pull: service.Build.Pull,
  253. BuildArgs: buildArgs,
  254. Tags: tags,
  255. Target: service.Build.Target,
  256. Exports: []bclient.ExportEntry{{Type: "image", Attrs: map[string]string{}}},
  257. Platforms: plats,
  258. Labels: service.Build.Labels,
  259. NetworkMode: service.Build.Network,
  260. ExtraHosts: service.Build.ExtraHosts.AsList(),
  261. Session: sessionConfig,
  262. }, nil
  263. }
  264. func flatten(in types.MappingWithEquals) types.Mapping {
  265. if len(in) == 0 {
  266. return nil
  267. }
  268. out := types.Mapping{}
  269. for k, v := range in {
  270. if v == nil {
  271. continue
  272. }
  273. out[k] = *v
  274. }
  275. return out
  276. }
  277. func mergeArgs(m ...types.Mapping) types.Mapping {
  278. merged := types.Mapping{}
  279. for _, mapping := range m {
  280. for key, val := range mapping {
  281. merged[key] = val
  282. }
  283. }
  284. return merged
  285. }
  286. func dockerFilePath(context string, dockerfile string) string {
  287. if urlutil.IsGitURL(context) || filepath.IsAbs(dockerfile) {
  288. return dockerfile
  289. }
  290. return filepath.Join(context, dockerfile)
  291. }
  292. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  293. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  294. for _, sshKey := range sshKeys {
  295. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  296. ID: sshKey.ID,
  297. Paths: []string{sshKey.Path},
  298. })
  299. }
  300. return sshprovider.NewSSHAgentProvider(sshConfig)
  301. }