build.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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. builtIDs := 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, service, options)
  82. if err != nil {
  83. return err
  84. }
  85. builtIDs[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. builtIDs[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, d := range builtIDs {
  117. if d != "" {
  118. imageIDs[project.Services[i].Image] = d
  119. }
  120. }
  121. return imageIDs, err
  122. }
  123. func getServiceIndex(project *types.Project, name string) (types.ServiceConfig, int) {
  124. var service types.ServiceConfig
  125. var idx int
  126. for i, s := range project.Services {
  127. if s.Name == name {
  128. idx, service = i, s
  129. break
  130. }
  131. }
  132. return service, idx
  133. }
  134. func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
  135. for _, service := range project.Services {
  136. if service.Image == "" && service.Build == nil {
  137. return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
  138. }
  139. }
  140. images, err := s.getLocalImagesDigests(ctx, project)
  141. if err != nil {
  142. return err
  143. }
  144. err = s.pullRequiredImages(ctx, project, images, quietPull)
  145. if err != nil {
  146. return err
  147. }
  148. mode := xprogress.PrinterModeAuto
  149. if quietPull {
  150. mode = xprogress.PrinterModeQuiet
  151. }
  152. buildRequired, err := s.prepareProjectForBuild(project, images)
  153. if err != nil {
  154. return err
  155. }
  156. if buildRequired {
  157. builtImages, err := s.build(ctx, project, api.BuildOptions{
  158. Progress: mode,
  159. })
  160. if err != nil {
  161. return err
  162. }
  163. for name, digest := range builtImages {
  164. images[name] = digest
  165. }
  166. }
  167. // set digest as com.docker.compose.image label so we can detect outdated containers
  168. for i, service := range project.Services {
  169. image := api.GetImageNameOrDefault(service, project.Name)
  170. digest, ok := images[image]
  171. if ok {
  172. if project.Services[i].Labels == nil {
  173. project.Services[i].Labels = types.Labels{}
  174. }
  175. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  176. }
  177. }
  178. return nil
  179. }
  180. func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
  181. buildRequired := false
  182. err := api.BuildOptions{}.Apply(project)
  183. if err != nil {
  184. return false, err
  185. }
  186. for i, service := range project.Services {
  187. if service.Build == nil {
  188. continue
  189. }
  190. _, localImagePresent := images[service.Image]
  191. if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
  192. service.Build = nil
  193. project.Services[i] = service
  194. continue
  195. }
  196. if service.Platform == "" {
  197. // let builder to build for default platform
  198. service.Build.Platforms = nil
  199. } else {
  200. service.Build.Platforms = []string{service.Platform}
  201. }
  202. project.Services[i] = service
  203. buildRequired = true
  204. }
  205. return buildRequired, nil
  206. }
  207. func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
  208. var imageNames []string
  209. for _, s := range project.Services {
  210. imgName := api.GetImageNameOrDefault(s, project.Name)
  211. if !utils.StringContains(imageNames, imgName) {
  212. imageNames = append(imageNames, imgName)
  213. }
  214. }
  215. imgs, err := s.getImages(ctx, imageNames)
  216. if err != nil {
  217. return nil, err
  218. }
  219. images := map[string]string{}
  220. for name, info := range imgs {
  221. images[name] = info.ID
  222. }
  223. for i, service := range project.Services {
  224. imgName := api.GetImageNameOrDefault(service, project.Name)
  225. digest, ok := images[imgName]
  226. if !ok {
  227. continue
  228. }
  229. if service.Platform != "" {
  230. platform, err := platforms.Parse(service.Platform)
  231. if err != nil {
  232. return nil, err
  233. }
  234. inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, digest)
  235. if err != nil {
  236. return nil, err
  237. }
  238. actual := specs.Platform{
  239. Architecture: inspect.Architecture,
  240. OS: inspect.Os,
  241. Variant: inspect.Variant,
  242. }
  243. if !platforms.NewMatcher(platform).Match(actual) {
  244. return nil, errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s",
  245. imgName, platforms.Format(platform), platforms.Format(actual))
  246. }
  247. }
  248. project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
  249. }
  250. return images, nil
  251. }
  252. func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
  253. tags := []string{service.Image}
  254. buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
  255. for k, v := range storeutil.GetProxyConfig(s.dockerCli) {
  256. if _, ok := buildArgs[k]; !ok {
  257. buildArgs[k] = v
  258. }
  259. }
  260. plats, err := addPlatforms(project, service)
  261. if err != nil {
  262. return build.Options{}, err
  263. }
  264. cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
  265. if err != nil {
  266. return build.Options{}, err
  267. }
  268. cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
  269. if err != nil {
  270. return build.Options{}, err
  271. }
  272. sessionConfig := []session.Attachable{
  273. authprovider.NewDockerAuthProvider(s.configFile()),
  274. }
  275. if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 {
  276. sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...))
  277. if err != nil {
  278. return build.Options{}, err
  279. }
  280. sessionConfig = append(sessionConfig, sshAgentProvider)
  281. }
  282. if len(service.Build.Secrets) > 0 {
  283. secretsProvider, err := addSecretsConfig(project, service)
  284. if err != nil {
  285. return build.Options{}, err
  286. }
  287. sessionConfig = append(sessionConfig, secretsProvider)
  288. }
  289. if len(service.Build.Tags) > 0 {
  290. tags = append(tags, service.Build.Tags...)
  291. }
  292. var allow []entitlements.Entitlement
  293. if service.Build.Privileged {
  294. allow = append(allow, entitlements.EntitlementSecurityInsecure)
  295. }
  296. imageLabels := getImageBuildLabels(project, service)
  297. exports := []bclient.ExportEntry{{
  298. Type: "docker",
  299. Attrs: map[string]string{
  300. "load": "true",
  301. "push": fmt.Sprint(options.Push),
  302. },
  303. }}
  304. if len(service.Build.Platforms) > 1 {
  305. exports = []bclient.ExportEntry{{
  306. Type: "image",
  307. Attrs: map[string]string{
  308. "push": fmt.Sprint(options.Push),
  309. },
  310. }}
  311. }
  312. return build.Options{
  313. Inputs: build.Inputs{
  314. ContextPath: service.Build.Context,
  315. DockerfileInline: service.Build.DockerfileInline,
  316. DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
  317. NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
  318. },
  319. CacheFrom: pb.CreateCaches(cacheFrom),
  320. CacheTo: pb.CreateCaches(cacheTo),
  321. NoCache: service.Build.NoCache,
  322. Pull: service.Build.Pull,
  323. BuildArgs: buildArgs,
  324. Tags: tags,
  325. Target: service.Build.Target,
  326. Exports: exports,
  327. Platforms: plats,
  328. Labels: imageLabels,
  329. NetworkMode: service.Build.Network,
  330. ExtraHosts: service.Build.ExtraHosts.AsList(),
  331. Session: sessionConfig,
  332. Allow: allow,
  333. }, nil
  334. }
  335. func flatten(in types.MappingWithEquals) types.Mapping {
  336. out := types.Mapping{}
  337. if len(in) == 0 {
  338. return out
  339. }
  340. for k, v := range in {
  341. if v == nil {
  342. continue
  343. }
  344. out[k] = *v
  345. }
  346. return out
  347. }
  348. func mergeArgs(m ...types.Mapping) types.Mapping {
  349. merged := types.Mapping{}
  350. for _, mapping := range m {
  351. for key, val := range mapping {
  352. merged[key] = val
  353. }
  354. }
  355. return merged
  356. }
  357. func dockerFilePath(ctxName string, dockerfile string) string {
  358. if dockerfile == "" {
  359. return ""
  360. }
  361. if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
  362. return dockerfile
  363. }
  364. return filepath.Join(ctxName, dockerfile)
  365. }
  366. func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
  367. sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
  368. for _, sshKey := range sshKeys {
  369. sshConfig = append(sshConfig, sshprovider.AgentConfig{
  370. ID: sshKey.ID,
  371. Paths: []string{sshKey.Path},
  372. })
  373. }
  374. return sshprovider.NewSSHAgentProvider(sshConfig)
  375. }
  376. func addSecretsConfig(project *types.Project, service types.ServiceConfig) (session.Attachable, error) {
  377. var sources []secretsprovider.Source
  378. for _, secret := range service.Build.Secrets {
  379. config := project.Secrets[secret.Source]
  380. id := secret.Source
  381. if secret.Target != "" {
  382. id = secret.Target
  383. }
  384. switch {
  385. case config.File != "":
  386. sources = append(sources, secretsprovider.Source{
  387. ID: id,
  388. FilePath: config.File,
  389. })
  390. case config.Environment != "":
  391. sources = append(sources, secretsprovider.Source{
  392. ID: id,
  393. Env: config.Environment,
  394. })
  395. default:
  396. return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
  397. }
  398. if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
  399. logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
  400. }
  401. }
  402. store, err := secretsprovider.NewStore(sources)
  403. if err != nil {
  404. return nil, err
  405. }
  406. return secretsprovider.NewSecretProvider(store), nil
  407. }
  408. func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
  409. plats, err := useDockerDefaultOrServicePlatform(project, service, false)
  410. if err != nil {
  411. return nil, err
  412. }
  413. for _, buildPlatform := range service.Build.Platforms {
  414. p, err := platforms.Parse(buildPlatform)
  415. if err != nil {
  416. return nil, err
  417. }
  418. if !utils.Contains(plats, p) {
  419. plats = append(plats, p)
  420. }
  421. }
  422. return plats, nil
  423. }
  424. func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
  425. ret := make(types.Labels)
  426. if service.Build != nil {
  427. for k, v := range service.Build.Labels {
  428. ret.Add(k, v)
  429. }
  430. }
  431. ret.Add(api.VersionLabel, api.ComposeVersion)
  432. ret.Add(api.ProjectLabel, project.Name)
  433. ret.Add(api.ServiceLabel, service.Name)
  434. return ret
  435. }
  436. func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
  437. namedContexts := map[string]build.NamedContext{}
  438. for name, context := range additionalContexts {
  439. namedContexts[name] = build.NamedContext{Path: context}
  440. }
  441. return namedContexts
  442. }
  443. func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
  444. var plats []specs.Platform
  445. if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
  446. if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
  447. return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
  448. }
  449. p, err := platforms.Parse(platform)
  450. if err != nil {
  451. return nil, err
  452. }
  453. plats = append(plats, p)
  454. }
  455. return plats, nil
  456. }
  457. func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
  458. plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
  459. if (len(plats) > 0 && useOnePlatform) || err != nil {
  460. return plats, err
  461. }
  462. if service.Platform != "" {
  463. if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
  464. return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
  465. }
  466. // User defined a service platform and no build platforms, so we should keep the one define on the service level
  467. p, err := platforms.Parse(service.Platform)
  468. if !utils.Contains(plats, p) {
  469. plats = append(plats, p)
  470. }
  471. return plats, err
  472. }
  473. return plats, nil
  474. }