build.go 12 KB

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