build.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. package builder
  2. import (
  3. "archive/tar"
  4. "bytes"
  5. "context"
  6. "encoding/csv"
  7. "encoding/json"
  8. "fmt"
  9. "net"
  10. "regexp"
  11. "strconv"
  12. "strings"
  13. "github.com/containerd/containerd/platforms"
  14. "github.com/docker/docker/builder/dockerignore"
  15. controlapi "github.com/moby/buildkit/api/services/control"
  16. "github.com/moby/buildkit/client/llb"
  17. "github.com/moby/buildkit/exporter/containerimage/exptypes"
  18. "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
  19. "github.com/moby/buildkit/frontend/gateway/client"
  20. "github.com/moby/buildkit/solver/pb"
  21. "github.com/moby/buildkit/util/apicaps"
  22. specs "github.com/opencontainers/image-spec/specs-go/v1"
  23. "github.com/pkg/errors"
  24. "golang.org/x/sync/errgroup"
  25. )
  26. const (
  27. DefaultLocalNameContext = "context"
  28. DefaultLocalNameDockerfile = "dockerfile"
  29. keyTarget = "target"
  30. keyFilename = "filename"
  31. keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
  32. keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
  33. defaultDockerfileName = "Dockerfile"
  34. dockerignoreFilename = ".dockerignore"
  35. buildArgPrefix = "build-arg:"
  36. labelPrefix = "label:"
  37. keyNoCache = "no-cache"
  38. keyTargetPlatform = "platform"
  39. keyMultiPlatform = "multi-platform"
  40. keyImageResolveMode = "image-resolve-mode"
  41. keyGlobalAddHosts = "add-hosts"
  42. keyForceNetwork = "force-network-mode"
  43. keyOverrideCopyImage = "override-copy-image" // remove after CopyOp implemented
  44. keyNameContext = "contextkey"
  45. keyNameDockerfile = "dockerfilekey"
  46. )
  47. var httpPrefix = regexp.MustCompile("^https?://")
  48. var gitUrlPathWithFragmentSuffix = regexp.MustCompile("\\.git(?:#.+)?$")
  49. func Build(ctx context.Context, c client.Client) (*client.Result, error) {
  50. opts := c.BuildOpts().Opts
  51. caps := c.BuildOpts().LLBCaps
  52. marshalOpts := []llb.ConstraintsOpt{llb.WithCaps(caps)}
  53. localNameContext := DefaultLocalNameContext
  54. if v, ok := opts[keyNameContext]; ok {
  55. localNameContext = v
  56. }
  57. forceLocalDockerfile := false
  58. localNameDockerfile := DefaultLocalNameDockerfile
  59. if v, ok := opts[keyNameDockerfile]; ok {
  60. forceLocalDockerfile = true
  61. localNameDockerfile = v
  62. }
  63. defaultBuildPlatform := platforms.DefaultSpec()
  64. if workers := c.BuildOpts().Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 {
  65. defaultBuildPlatform = workers[0].Platforms[0]
  66. }
  67. buildPlatforms := []specs.Platform{defaultBuildPlatform}
  68. targetPlatforms := []*specs.Platform{nil}
  69. if v := opts[keyTargetPlatform]; v != "" {
  70. var err error
  71. targetPlatforms, err = parsePlatforms(v)
  72. if err != nil {
  73. return nil, err
  74. }
  75. }
  76. resolveMode, err := parseResolveMode(opts[keyImageResolveMode])
  77. if err != nil {
  78. return nil, err
  79. }
  80. extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts])
  81. if err != nil {
  82. return nil, errors.Wrap(err, "failed to parse additional hosts")
  83. }
  84. defaultNetMode, err := parseNetMode(opts[keyForceNetwork])
  85. if err != nil {
  86. return nil, err
  87. }
  88. filename := opts[keyFilename]
  89. if filename == "" {
  90. filename = defaultDockerfileName
  91. }
  92. var ignoreCache []string
  93. if v, ok := opts[keyNoCache]; ok {
  94. if v == "" {
  95. ignoreCache = []string{} // means all stages
  96. } else {
  97. ignoreCache = strings.Split(v, ",")
  98. }
  99. }
  100. name := "load build definition from " + filename
  101. src := llb.Local(localNameDockerfile,
  102. llb.FollowPaths([]string{filename, filename + ".dockerignore"}),
  103. llb.SessionID(c.BuildOpts().SessionID),
  104. llb.SharedKeyHint(localNameDockerfile),
  105. dockerfile2llb.WithInternalName(name),
  106. )
  107. var buildContext *llb.State
  108. isScratchContext := false
  109. if st, ok := detectGitContext(opts[localNameContext]); ok {
  110. if !forceLocalDockerfile {
  111. src = *st
  112. }
  113. buildContext = st
  114. } else if httpPrefix.MatchString(opts[localNameContext]) {
  115. httpContext := llb.HTTP(opts[localNameContext], llb.Filename("context"), dockerfile2llb.WithInternalName("load remote build context"))
  116. def, err := httpContext.Marshal(marshalOpts...)
  117. if err != nil {
  118. return nil, errors.Wrapf(err, "failed to marshal httpcontext")
  119. }
  120. res, err := c.Solve(ctx, client.SolveRequest{
  121. Definition: def.ToPB(),
  122. })
  123. if err != nil {
  124. return nil, errors.Wrapf(err, "failed to resolve httpcontext")
  125. }
  126. ref, err := res.SingleRef()
  127. if err != nil {
  128. return nil, err
  129. }
  130. dt, err := ref.ReadFile(ctx, client.ReadRequest{
  131. Filename: "context",
  132. Range: &client.FileRange{
  133. Length: 1024,
  134. },
  135. })
  136. if err != nil {
  137. return nil, errors.Errorf("failed to read downloaded context")
  138. }
  139. if isArchive(dt) {
  140. fileop := useFileOp(opts, &caps)
  141. if fileop {
  142. bc := llb.Scratch().File(llb.Copy(httpContext, "/context", "/", &llb.CopyInfo{
  143. AttemptUnpack: true,
  144. }))
  145. if !forceLocalDockerfile {
  146. src = bc
  147. }
  148. buildContext = &bc
  149. } else {
  150. copyImage := opts[keyOverrideCopyImage]
  151. if copyImage == "" {
  152. copyImage = dockerfile2llb.DefaultCopyImage
  153. }
  154. unpack := llb.Image(copyImage, dockerfile2llb.WithInternalName("helper image for file operations")).
  155. Run(llb.Shlex("copy --unpack /src/context /out/"), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("extracting build context"))
  156. unpack.AddMount("/src", httpContext, llb.Readonly)
  157. bc := unpack.AddMount("/out", llb.Scratch())
  158. if !forceLocalDockerfile {
  159. src = bc
  160. }
  161. buildContext = &bc
  162. }
  163. } else {
  164. filename = "context"
  165. if !forceLocalDockerfile {
  166. src = httpContext
  167. }
  168. buildContext = &httpContext
  169. isScratchContext = true
  170. }
  171. }
  172. def, err := src.Marshal(marshalOpts...)
  173. if err != nil {
  174. return nil, errors.Wrapf(err, "failed to marshal local source")
  175. }
  176. eg, ctx2 := errgroup.WithContext(ctx)
  177. var dtDockerfile []byte
  178. var dtDockerignore []byte
  179. var dtDockerignoreDefault []byte
  180. eg.Go(func() error {
  181. res, err := c.Solve(ctx2, client.SolveRequest{
  182. Definition: def.ToPB(),
  183. })
  184. if err != nil {
  185. return errors.Wrapf(err, "failed to resolve dockerfile")
  186. }
  187. ref, err := res.SingleRef()
  188. if err != nil {
  189. return err
  190. }
  191. dtDockerfile, err = ref.ReadFile(ctx2, client.ReadRequest{
  192. Filename: filename,
  193. })
  194. if err != nil {
  195. return errors.Wrapf(err, "failed to read dockerfile")
  196. }
  197. dt, err := ref.ReadFile(ctx2, client.ReadRequest{
  198. Filename: filename + ".dockerignore",
  199. })
  200. if err == nil {
  201. dtDockerignore = dt
  202. }
  203. return nil
  204. })
  205. var excludes []string
  206. if !isScratchContext {
  207. eg.Go(func() error {
  208. dockerignoreState := buildContext
  209. if dockerignoreState == nil {
  210. st := llb.Local(localNameContext,
  211. llb.SessionID(c.BuildOpts().SessionID),
  212. llb.FollowPaths([]string{dockerignoreFilename}),
  213. llb.SharedKeyHint(localNameContext+"-"+dockerignoreFilename),
  214. dockerfile2llb.WithInternalName("load "+dockerignoreFilename),
  215. )
  216. dockerignoreState = &st
  217. }
  218. def, err := dockerignoreState.Marshal(marshalOpts...)
  219. if err != nil {
  220. return err
  221. }
  222. res, err := c.Solve(ctx2, client.SolveRequest{
  223. Definition: def.ToPB(),
  224. })
  225. if err != nil {
  226. return err
  227. }
  228. ref, err := res.SingleRef()
  229. if err != nil {
  230. return err
  231. }
  232. dtDockerignoreDefault, err = ref.ReadFile(ctx2, client.ReadRequest{
  233. Filename: dockerignoreFilename,
  234. })
  235. if err != nil {
  236. return nil
  237. }
  238. return nil
  239. })
  240. }
  241. if err := eg.Wait(); err != nil {
  242. return nil, err
  243. }
  244. if dtDockerignore == nil {
  245. dtDockerignore = dtDockerignoreDefault
  246. }
  247. if dtDockerignore != nil {
  248. excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dtDockerignore))
  249. if err != nil {
  250. return nil, errors.Wrap(err, "failed to parse dockerignore")
  251. }
  252. }
  253. if _, ok := opts["cmdline"]; !ok {
  254. ref, cmdline, ok := dockerfile2llb.DetectSyntax(bytes.NewBuffer(dtDockerfile))
  255. if ok {
  256. return forwardGateway(ctx, c, ref, cmdline)
  257. }
  258. }
  259. exportMap := len(targetPlatforms) > 1
  260. if v := opts[keyMultiPlatform]; v != "" {
  261. b, err := strconv.ParseBool(v)
  262. if err != nil {
  263. return nil, errors.Errorf("invalid boolean value %s", v)
  264. }
  265. if !b && exportMap {
  266. return nil, errors.Errorf("returning multiple target plaforms is not allowed")
  267. }
  268. exportMap = b
  269. }
  270. expPlatforms := &exptypes.Platforms{
  271. Platforms: make([]exptypes.Platform, len(targetPlatforms)),
  272. }
  273. res := client.NewResult()
  274. eg, ctx = errgroup.WithContext(ctx)
  275. for i, tp := range targetPlatforms {
  276. func(i int, tp *specs.Platform) {
  277. eg.Go(func() error {
  278. st, img, err := dockerfile2llb.Dockerfile2LLB(ctx, dtDockerfile, dockerfile2llb.ConvertOpt{
  279. Target: opts[keyTarget],
  280. MetaResolver: c,
  281. BuildArgs: filter(opts, buildArgPrefix),
  282. Labels: filter(opts, labelPrefix),
  283. SessionID: c.BuildOpts().SessionID,
  284. BuildContext: buildContext,
  285. Excludes: excludes,
  286. IgnoreCache: ignoreCache,
  287. TargetPlatform: tp,
  288. BuildPlatforms: buildPlatforms,
  289. ImageResolveMode: resolveMode,
  290. PrefixPlatform: exportMap,
  291. ExtraHosts: extraHosts,
  292. ForceNetMode: defaultNetMode,
  293. OverrideCopyImage: opts[keyOverrideCopyImage],
  294. LLBCaps: &caps,
  295. })
  296. if err != nil {
  297. return errors.Wrapf(err, "failed to create LLB definition")
  298. }
  299. def, err := st.Marshal()
  300. if err != nil {
  301. return errors.Wrapf(err, "failed to marshal LLB definition")
  302. }
  303. config, err := json.Marshal(img)
  304. if err != nil {
  305. return errors.Wrapf(err, "failed to marshal image config")
  306. }
  307. var cacheImports []client.CacheOptionsEntry
  308. // new API
  309. if cacheImportsStr := opts[keyCacheImports]; cacheImportsStr != "" {
  310. var cacheImportsUM []controlapi.CacheOptionsEntry
  311. if err := json.Unmarshal([]byte(cacheImportsStr), &cacheImportsUM); err != nil {
  312. return errors.Wrapf(err, "failed to unmarshal %s (%q)", keyCacheImports, cacheImportsStr)
  313. }
  314. for _, um := range cacheImportsUM {
  315. cacheImports = append(cacheImports, client.CacheOptionsEntry{Type: um.Type, Attrs: um.Attrs})
  316. }
  317. }
  318. // old API
  319. if cacheFromStr := opts[keyCacheFrom]; cacheFromStr != "" {
  320. cacheFrom := strings.Split(cacheFromStr, ",")
  321. for _, s := range cacheFrom {
  322. im := client.CacheOptionsEntry{
  323. Type: "registry",
  324. Attrs: map[string]string{
  325. "ref": s,
  326. },
  327. }
  328. // FIXME(AkihiroSuda): skip append if already exists
  329. cacheImports = append(cacheImports, im)
  330. }
  331. }
  332. r, err := c.Solve(ctx, client.SolveRequest{
  333. Definition: def.ToPB(),
  334. CacheImports: cacheImports,
  335. })
  336. if err != nil {
  337. return err
  338. }
  339. ref, err := r.SingleRef()
  340. if err != nil {
  341. return err
  342. }
  343. if !exportMap {
  344. res.AddMeta(exptypes.ExporterImageConfigKey, config)
  345. res.SetRef(ref)
  346. } else {
  347. p := platforms.DefaultSpec()
  348. if tp != nil {
  349. p = *tp
  350. }
  351. k := platforms.Format(p)
  352. res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
  353. res.AddRef(k, ref)
  354. expPlatforms.Platforms[i] = exptypes.Platform{
  355. ID: k,
  356. Platform: p,
  357. }
  358. }
  359. return nil
  360. })
  361. }(i, tp)
  362. }
  363. if err := eg.Wait(); err != nil {
  364. return nil, err
  365. }
  366. if exportMap {
  367. dt, err := json.Marshal(expPlatforms)
  368. if err != nil {
  369. return nil, err
  370. }
  371. res.AddMeta(exptypes.ExporterPlatformsKey, dt)
  372. }
  373. return res, nil
  374. }
  375. func forwardGateway(ctx context.Context, c client.Client, ref string, cmdline string) (*client.Result, error) {
  376. opts := c.BuildOpts().Opts
  377. if opts == nil {
  378. opts = map[string]string{}
  379. }
  380. opts["cmdline"] = cmdline
  381. opts["source"] = ref
  382. return c.Solve(ctx, client.SolveRequest{
  383. Frontend: "gateway.v0",
  384. FrontendOpt: opts,
  385. })
  386. }
  387. func filter(opt map[string]string, key string) map[string]string {
  388. m := map[string]string{}
  389. for k, v := range opt {
  390. if strings.HasPrefix(k, key) {
  391. m[strings.TrimPrefix(k, key)] = v
  392. }
  393. }
  394. return m
  395. }
  396. func detectGitContext(ref string) (*llb.State, bool) {
  397. found := false
  398. if httpPrefix.MatchString(ref) && gitUrlPathWithFragmentSuffix.MatchString(ref) {
  399. found = true
  400. }
  401. for _, prefix := range []string{"git://", "github.com/", "git@"} {
  402. if strings.HasPrefix(ref, prefix) {
  403. found = true
  404. break
  405. }
  406. }
  407. if !found {
  408. return nil, false
  409. }
  410. parts := strings.SplitN(ref, "#", 2)
  411. branch := ""
  412. if len(parts) > 1 {
  413. branch = parts[1]
  414. }
  415. st := llb.Git(parts[0], branch, dockerfile2llb.WithInternalName("load git source "+ref))
  416. return &st, true
  417. }
  418. func isArchive(header []byte) bool {
  419. for _, m := range [][]byte{
  420. {0x42, 0x5A, 0x68}, // bzip2
  421. {0x1F, 0x8B, 0x08}, // gzip
  422. {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz
  423. } {
  424. if len(header) < len(m) {
  425. continue
  426. }
  427. if bytes.Equal(m, header[:len(m)]) {
  428. return true
  429. }
  430. }
  431. r := tar.NewReader(bytes.NewBuffer(header))
  432. _, err := r.Next()
  433. return err == nil
  434. }
  435. func parsePlatforms(v string) ([]*specs.Platform, error) {
  436. var pp []*specs.Platform
  437. for _, v := range strings.Split(v, ",") {
  438. p, err := platforms.Parse(v)
  439. if err != nil {
  440. return nil, errors.Wrapf(err, "failed to parse target platform %s", v)
  441. }
  442. p = platforms.Normalize(p)
  443. pp = append(pp, &p)
  444. }
  445. return pp, nil
  446. }
  447. func parseResolveMode(v string) (llb.ResolveMode, error) {
  448. switch v {
  449. case pb.AttrImageResolveModeDefault, "":
  450. return llb.ResolveModeDefault, nil
  451. case pb.AttrImageResolveModeForcePull:
  452. return llb.ResolveModeForcePull, nil
  453. case pb.AttrImageResolveModePreferLocal:
  454. return llb.ResolveModePreferLocal, nil
  455. default:
  456. return 0, errors.Errorf("invalid image-resolve-mode: %s", v)
  457. }
  458. }
  459. func parseExtraHosts(v string) ([]llb.HostIP, error) {
  460. if v == "" {
  461. return nil, nil
  462. }
  463. out := make([]llb.HostIP, 0)
  464. csvReader := csv.NewReader(strings.NewReader(v))
  465. fields, err := csvReader.Read()
  466. if err != nil {
  467. return nil, err
  468. }
  469. for _, field := range fields {
  470. parts := strings.SplitN(field, "=", 2)
  471. if len(parts) != 2 {
  472. return nil, errors.Errorf("invalid key-value pair %s", field)
  473. }
  474. key := strings.ToLower(parts[0])
  475. val := strings.ToLower(parts[1])
  476. ip := net.ParseIP(val)
  477. if ip == nil {
  478. return nil, errors.Errorf("failed to parse IP %s", val)
  479. }
  480. out = append(out, llb.HostIP{Host: key, IP: ip})
  481. }
  482. return out, nil
  483. }
  484. func parseNetMode(v string) (pb.NetMode, error) {
  485. if v == "" {
  486. return llb.NetModeSandbox, nil
  487. }
  488. switch v {
  489. case "none":
  490. return llb.NetModeNone, nil
  491. case "host":
  492. return llb.NetModeHost, nil
  493. case "sandbox":
  494. return llb.NetModeSandbox, nil
  495. default:
  496. return 0, errors.Errorf("invalid netmode %s", v)
  497. }
  498. }
  499. func useFileOp(args map[string]string, caps *apicaps.CapSet) bool {
  500. enabled := true
  501. if v, ok := args["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok {
  502. if b, err := strconv.ParseBool(v); err == nil {
  503. enabled = !b
  504. }
  505. }
  506. return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil
  507. }