build.go 10 KB

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