build.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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/docker/buildx/controller/pb"
  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/store/storeutil"
  25. "github.com/docker/buildx/util/buildflags"
  26. xprogress "github.com/docker/buildx/util/progress"
  27. "github.com/docker/docker/builder/remotecontext/urlutil"
  28. bclient "github.com/moby/buildkit/client"
  29. "github.com/moby/buildkit/session"
  30. "github.com/moby/buildkit/session/auth/authprovider"
  31. "github.com/moby/buildkit/session/secrets/secretsprovider"
  32. "github.com/moby/buildkit/session/sshforward/sshprovider"
  33. "github.com/moby/buildkit/util/entitlements"
  34. specs "github.com/opencontainers/image-spec/specs-go/v1"
  35. "github.com/pkg/errors"
  36. "github.com/sirupsen/logrus"
  37. "github.com/docker/compose/v2/pkg/api"
  38. "github.com/docker/compose/v2/pkg/progress"
  39. "github.com/docker/compose/v2/pkg/utils"
  40. )
  41. func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
  42. err := options.Apply(project)
  43. if err != nil {
  44. return err
  45. }
  46. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  47. _, err := s.build(ctx, project, options)
  48. return err
  49. }, s.stdinfo(), "Building")
  50. }
  51. func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
  52. args := options.Args.Resolve(envResolver(project.Environment))
  53. buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
  54. if err != nil {
  55. return nil, err
  56. }
  57. // Progress needs its own context that lives longer than the
  58. // build one otherwise it won't read all the messages from
  59. // build and will lock
  60. progressCtx, cancel := context.WithCancel(context.Background())
  61. defer cancel()
  62. w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress)
  63. if err != nil {
  64. return nil, err
  65. }
  66. builtDigests := make([]string, len(project.Services))
  67. err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
  68. if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
  69. return nil
  70. }
  71. service, idx := getServiceIndex(project, name)
  72. if service.Build == nil {
  73. return nil
  74. }
  75. if !buildkitEnabled {
  76. if service.Build.Args == nil {
  77. service.Build.Args = args
  78. } else {
  79. service.Build.Args = service.Build.Args.OverrideBy(args)
  80. }
  81. id, err := s.doBuildClassic(ctx, project.Name, service, options)
  82. if err != nil {
  83. return err
  84. }
  85. builtDigests[idx] = id
  86. if options.Push {
  87. return s.push(ctx, project, api.PushOptions{})
  88. }
  89. return nil
  90. }
  91. if options.Memory != 0 {
  92. fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored.")
  93. }
  94. buildOptions, err := s.toBuildOptions(project, service, options)
  95. if err != nil {
  96. return err
  97. }
  98. buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args))
  99. digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, options.Builder)
  100. if err != nil {
  101. return err
  102. }
  103. builtDigests[idx] = digest
  104. return nil
  105. }, func(traversal *graphTraversal) {
  106. traversal.maxConcurrency = s.maxConcurrency
  107. })
  108. // enforce all build event get consumed
  109. if errw := w.Wait(); errw != nil {
  110. return nil, errw
  111. }
  112. if err != nil {
  113. return nil, err
  114. }
  115. imageIDs := map[string]string{}
  116. for i, imageDigest := range builtDigests {
  117. if imageDigest != "" {
  118. imageRef := api.GetImageNameOrDefault(project.Services[i], project.Name)
  119. imageIDs[imageRef] = imageDigest
  120. }
  121. }
  122. return imageIDs, err
  123. }
  124. func getServiceIndex(project *types.Project, name string) (types.ServiceConfig, int) {
  125. var service types.ServiceConfig
  126. var idx int
  127. for i, s := range project.Services {
  128. if s.Name == name {
  129. idx, service = i, s
  130. break
  131. }
  132. }
  133. return service, idx
  134. }
  135. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  136. for _, service := range project.Services {
  137. if service.Image == "" && service.Build == nil {
  138. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  139. }
  140. }
  141. images, err := s.getLocalImagesDigests(ctx, project)
  142. if err != nil {
  143. return err
  144. }
  145. err = s.pullRequiredImages(ctx, project, images, quietPull)
  146. if err != nil {
  147. return err
  148. }
  149. mode := xprogress.PrinterModeAuto
  150. if quietPull {
  151. mode = xprogress.PrinterModeQuiet
  152. }
  153. buildRequired, err := s.prepareProjectForBuild(project, images)
  154. if err != nil {
  155. return err
  156. }
  157. if buildRequired {
  158. builtImages, err := s.build(ctx, project, api.BuildOptions{
  159. Progress: mode,
  160. })
  161. if err != nil {
  162. return err
  163. }
  164. for name, digest := range builtImages {
  165. images[name] = digest
  166. }
  167. }
  168. // set digest as com.docker.compose.image label so we can detect outdated containers
  169. for i, service := range project.Services {
  170. image := api.GetImageNameOrDefault(service, project.Name)
  171. digest, ok := images[image]
  172. if ok {
  173. if project.Services[i].Labels == nil {
  174. project.Services[i].Labels = types.Labels{}
  175. }
  176. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  177. }
  178. }
  179. return nil
  180. }
  181. func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
  182. buildRequired := false
  183. err := api.BuildOptions{}.Apply(project)
  184. if err != nil {
  185. return false, err
  186. }
  187. for i, service := range project.Services {
  188. if service.Build == nil {
  189. continue
  190. }
  191. image := api.GetImageNameOrDefault(service, project.Name)
  192. _, localImagePresent := images[image]
  193. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  194. service.Build = nil
  195. project.Services[i] = service
  196. continue
  197. }
  198. if service.Platform == "" {
  199. // let builder to build for default platform
  200. service.Build.Platforms = nil
  201. } else {
  202. service.Build.Platforms = []string{service.Platform}
  203. }
  204. project.Services[i] = service
  205. buildRequired = true
  206. }
  207. return buildRequired, nil
  208. }
  209. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  210. var imageNames []string
  211. for _, s := range project.Services {
  212. imgName := api.GetImageNameOrDefault(s, project.Name)
  213. if !utils.StringContains(imageNames, imgName) {
  214. imageNames = append(imageNames, imgName)
  215. }
  216. }
  217. imgs, err := s.getImages(ctx, imageNames)
  218. if err != nil {
  219. return nil, err
  220. }
  221. images := map[string]string{}
  222. for name, info := range imgs {
  223. images[name] = info.ID
  224. }
  225. for i, service := range project.Services {
  226. imgName := api.GetImageNameOrDefault(service, project.Name)
  227. digest, ok := images[imgName]
  228. if !ok {
  229. continue
  230. }
  231. if service.Platform != "" {
  232. platform, err := platforms.Parse(service.Platform)
  233. if err != nil {
  234. return nil, err
  235. }
  236. inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, digest)
  237. if err != nil {
  238. return nil, err
  239. }
  240. actual := specs.Platform{
  241. Architecture: inspect.Architecture,
  242. OS: inspect.Os,
  243. Variant: inspect.Variant,
  244. }
  245. if !platforms.NewMatcher(platform).Match(actual) {
  246. return nil, errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s",
  247. imgName, platforms.Format(platform), platforms.Format(actual))
  248. }
  249. }
  250. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  251. }
  252. return images, nil
  253. }
  254. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
  255. buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
  256. for k, v := range storeutil.GetProxyConfig(s.dockerCli) {
  257. if _, ok := buildArgs[k]; !ok {
  258. buildArgs[k] = v
  259. }
  260. }
  261. plats, err := addPlatforms(project, service)
  262. if err != nil {
  263. return build.Options{}, err
  264. }
  265. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  266. if err != nil {
  267. return build.Options{}, err
  268. }
  269. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  270. if err != nil {
  271. return build.Options{}, err
  272. }
  273. sessionConfig := []session.Attachable{
  274. authprovider.NewDockerAuthProvider(s.configFile()),
  275. }
  276. if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
  277. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
  278. if err != nil {
  279. return build.Options{}, err
  280. }
  281. sessionConfig = append(sessionConfig, sshAgentProvider)
  282. }
  283. if len(service.Build.Secrets) > 0 {
  284. secretsProvider, err := addSecretsConfig(project, service)
  285. if err != nil {
  286. return build.Options{}, err
  287. }
  288. sessionConfig = append(sessionConfig, secretsProvider)
  289. }
  290. tags := []string{api.GetImageNameOrDefault(service, project.Name)}
  291. if len(service.Build.Tags) > 0 {
  292. tags = append(tags, service.Build.Tags...)
  293. }
  294. var allow []entitlements.Entitlement
  295. if service.Build.Privileged {
  296. allow = append(allow, entitlements.EntitlementSecurityInsecure)
  297. }
  298. imageLabels := getImageBuildLabels(project, service)
  299. push := options.Push && service.Image != ""
  300. exports := []bclient.ExportEntry{{
  301. Type: "docker",
  302. Attrs: map[string]string{
  303. "load": "true",
  304. "push": fmt.Sprint(push),
  305. },
  306. }}
  307. if len(service.Build.Platforms) > 1 {
  308. exports = []bclient.ExportEntry{{
  309. Type: "image",
  310. Attrs: map[string]string{
  311. "push": fmt.Sprint(push),
  312. },
  313. }}
  314. }
  315. return build.Options{
  316. Inputs: build.Inputs{
  317. ContextPath: service.Build.Context,
  318. DockerfileInline: service.Build.DockerfileInline,
  319. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  320. NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
  321. },
  322. CacheFrom: pb.CreateCaches(cacheFrom),
  323. CacheTo: pb.CreateCaches(cacheTo),
  324. NoCache: service.Build.NoCache,
  325. Pull: service.Build.Pull,
  326. BuildArgs: buildArgs,
  327. Tags: tags,
  328. Target: service.Build.Target,
  329. Exports: exports,
  330. Platforms: plats,
  331. Labels: imageLabels,
  332. NetworkMode: service.Build.Network,
  333. ExtraHosts: service.Build.ExtraHosts.AsList(),
  334. Session: sessionConfig,
  335. Allow: allow,
  336. }, nil
  337. }
  338. func flatten(in types.MappingWithEquals) types.Mapping {
  339. out := types.Mapping{}
  340. if len(in) == 0 {
  341. return out
  342. }
  343. for k, v := range in {
  344. if v == nil {
  345. continue
  346. }
  347. out[k] = *v
  348. }
  349. return out
  350. }
  351. func mergeArgs(m ...types.Mapping) types.Mapping {
  352. merged := types.Mapping{}
  353. for _, mapping := range m {
  354. for key, val := range mapping {
  355. merged[key] = val
  356. }
  357. }
  358. return merged
  359. }
  360. func dockerFilePath(ctxName string, dockerfile string) string {
  361. if dockerfile == "" {
  362. return ""
  363. }
  364. if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
  365. return dockerfile
  366. }
  367. return filepath.Join(ctxName, dockerfile)
  368. }
  369. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  370. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  371. for _, sshKey := range sshKeys {
  372. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  373. ID: sshKey.ID,
  374. Paths: []string{sshKey.Path},
  375. })
  376. }
  377. return sshprovider.NewSSHAgentProvider(sshConfig)
  378. }
  379. func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
  380. var sources []secretsprovider.Source
  381. for _, secret := range service.Build.Secrets {
  382. config := project.Secrets[secret.Source]
  383. id := secret.Source
  384. if secret.Target != "" {
  385. id = secret.Target
  386. }
  387. switch {
  388. case config.File != "":
  389. sources = append(sources, secretsprovider.Source{
  390. ID: id,
  391. FilePath: config.File,
  392. })
  393. case config.Environment != "":
  394. sources = append(sources, secretsprovider.Source{
  395. ID: id,
  396. Env: config.Environment,
  397. })
  398. default:
  399. return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
  400. }
  401. if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
  402. logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
  403. }
  404. }
  405. store, err := secretsprovider.NewStore(sources)
  406. if err != nil {
  407. return nil, err
  408. }
  409. return secretsprovider.NewSecretProvider(store), nil
  410. }
  411. func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
  412. plats, err := useDockerDefaultOrServicePlatform(project, service, false)
  413. if err != nil {
  414. return nil, err
  415. }
  416. for _, buildPlatform := range service.Build.Platforms {
  417. p, err := platforms.Parse(buildPlatform)
  418. if err != nil {
  419. return nil, err
  420. }
  421. if !utils.Contains(plats, p) {
  422. plats = append(plats, p)
  423. }
  424. }
  425. return plats, nil
  426. }
  427. func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
  428. ret := make(types.Labels)
  429. if service.Build != nil {
  430. for k, v := range service.Build.Labels {
  431. ret.Add(k, v)
  432. }
  433. }
  434. ret.Add(api.VersionLabel, api.ComposeVersion)
  435. ret.Add(api.ProjectLabel, project.Name)
  436. ret.Add(api.ServiceLabel, service.Name)
  437. return ret
  438. }
  439. func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
  440. namedContexts := map[string]build.NamedContext{}
  441. for name, context := range additionalContexts {
  442. namedContexts[name] = build.NamedContext{Path: context}
  443. }
  444. return namedContexts
  445. }
  446. func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
  447. var plats []specs.Platform
  448. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  449. if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
  450. return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
  451. }
  452. p, err := platforms.Parse(platform)
  453. if err != nil {
  454. return nil, err
  455. }
  456. plats = append(plats, p)
  457. }
  458. return plats, nil
  459. }
  460. func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
  461. plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
  462. if (len(plats) > 0 && useOnePlatform) || err != nil {
  463. return plats, err
  464. }
  465. if service.Platform != "" {
  466. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
  467. return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
  468. }
  469. // User defined a service platform and no build platforms, so we should keep the one define on the service level
  470. p, err := platforms.Parse(service.Platform)
  471. if !utils.Contains(plats, p) {
  472. plats = append(plats, p)
  473. }
  474. return plats, err
  475. }
  476. return plats, nil
  477. }