builder.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. package buildkit
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/containerd/containerd/content"
  12. "github.com/containerd/containerd/platforms"
  13. "github.com/docker/docker/api/types"
  14. "github.com/docker/docker/api/types/backend"
  15. "github.com/docker/docker/builder"
  16. "github.com/docker/docker/daemon/config"
  17. "github.com/docker/docker/daemon/images"
  18. "github.com/docker/docker/pkg/streamformatter"
  19. "github.com/docker/docker/pkg/system"
  20. "github.com/docker/libnetwork"
  21. controlapi "github.com/moby/buildkit/api/services/control"
  22. "github.com/moby/buildkit/client"
  23. "github.com/moby/buildkit/control"
  24. "github.com/moby/buildkit/identity"
  25. "github.com/moby/buildkit/session"
  26. "github.com/moby/buildkit/util/entitlements"
  27. "github.com/moby/buildkit/util/resolver"
  28. "github.com/moby/buildkit/util/tracing"
  29. "github.com/pkg/errors"
  30. "golang.org/x/sync/errgroup"
  31. "google.golang.org/grpc"
  32. grpcmetadata "google.golang.org/grpc/metadata"
  33. )
  34. type errMultipleFilterValues struct{}
  35. func (errMultipleFilterValues) Error() string { return "filters expect only one value" }
  36. func (errMultipleFilterValues) InvalidParameter() {}
  37. type errConflictFilter struct {
  38. a, b string
  39. }
  40. func (e errConflictFilter) Error() string {
  41. return fmt.Sprintf("conflicting filters: %q and %q", e.a, e.b)
  42. }
  43. func (errConflictFilter) InvalidParameter() {}
  44. var cacheFields = map[string]bool{
  45. "id": true,
  46. "parent": true,
  47. "type": true,
  48. "description": true,
  49. "inuse": true,
  50. "shared": true,
  51. "private": true,
  52. // fields from buildkit that are not exposed
  53. "mutable": false,
  54. "immutable": false,
  55. }
  56. // Opt is option struct required for creating the builder
  57. type Opt struct {
  58. SessionManager *session.Manager
  59. Root string
  60. Dist images.DistributionServices
  61. NetworkController libnetwork.NetworkController
  62. DefaultCgroupParent string
  63. ResolverOpt resolver.ResolveOptionsFunc
  64. BuilderConfig config.BuilderConfig
  65. Rootless bool
  66. }
  67. // Builder can build using BuildKit backend
  68. type Builder struct {
  69. controller *control.Controller
  70. reqBodyHandler *reqBodyHandler
  71. mu sync.Mutex
  72. jobs map[string]*buildJob
  73. }
  74. // New creates a new builder
  75. func New(opt Opt) (*Builder, error) {
  76. reqHandler := newReqBodyHandler(tracing.DefaultTransport)
  77. c, err := newController(reqHandler, opt)
  78. if err != nil {
  79. return nil, err
  80. }
  81. b := &Builder{
  82. controller: c,
  83. reqBodyHandler: reqHandler,
  84. jobs: map[string]*buildJob{},
  85. }
  86. return b, nil
  87. }
  88. // RegisterGRPC registers controller to the grpc server.
  89. func (b *Builder) RegisterGRPC(s *grpc.Server) {
  90. b.controller.Register(s)
  91. }
  92. // Cancel cancels a build using ID
  93. func (b *Builder) Cancel(ctx context.Context, id string) error {
  94. b.mu.Lock()
  95. if j, ok := b.jobs[id]; ok && j.cancel != nil {
  96. j.cancel()
  97. }
  98. b.mu.Unlock()
  99. return nil
  100. }
  101. // DiskUsage returns a report about space used by build cache
  102. func (b *Builder) DiskUsage(ctx context.Context) ([]*types.BuildCache, error) {
  103. duResp, err := b.controller.DiskUsage(ctx, &controlapi.DiskUsageRequest{})
  104. if err != nil {
  105. return nil, err
  106. }
  107. var items []*types.BuildCache
  108. for _, r := range duResp.Record {
  109. items = append(items, &types.BuildCache{
  110. ID: r.ID,
  111. Parent: r.Parent,
  112. Type: r.RecordType,
  113. Description: r.Description,
  114. InUse: r.InUse,
  115. Shared: r.Shared,
  116. Size: r.Size_,
  117. CreatedAt: r.CreatedAt,
  118. LastUsedAt: r.LastUsedAt,
  119. UsageCount: int(r.UsageCount),
  120. })
  121. }
  122. return items, nil
  123. }
  124. // Prune clears all reclaimable build cache
  125. func (b *Builder) Prune(ctx context.Context, opts types.BuildCachePruneOptions) (int64, []string, error) {
  126. ch := make(chan *controlapi.UsageRecord)
  127. eg, ctx := errgroup.WithContext(ctx)
  128. validFilters := make(map[string]bool, 1+len(cacheFields))
  129. validFilters["unused-for"] = true
  130. validFilters["until"] = true
  131. validFilters["label"] = true // TODO(tiborvass): handle label
  132. validFilters["label!"] = true // TODO(tiborvass): handle label!
  133. for k, v := range cacheFields {
  134. validFilters[k] = v
  135. }
  136. if err := opts.Filters.Validate(validFilters); err != nil {
  137. return 0, nil, err
  138. }
  139. pi, err := toBuildkitPruneInfo(opts)
  140. if err != nil {
  141. return 0, nil, err
  142. }
  143. eg.Go(func() error {
  144. defer close(ch)
  145. return b.controller.Prune(&controlapi.PruneRequest{
  146. All: pi.All,
  147. KeepDuration: int64(pi.KeepDuration),
  148. KeepBytes: pi.KeepBytes,
  149. Filter: pi.Filter,
  150. }, &pruneProxy{
  151. streamProxy: streamProxy{ctx: ctx},
  152. ch: ch,
  153. })
  154. })
  155. var size int64
  156. var cacheIDs []string
  157. eg.Go(func() error {
  158. for r := range ch {
  159. size += r.Size_
  160. cacheIDs = append(cacheIDs, r.ID)
  161. }
  162. return nil
  163. })
  164. if err := eg.Wait(); err != nil {
  165. return 0, nil, err
  166. }
  167. return size, cacheIDs, nil
  168. }
  169. // Build executes a build request
  170. func (b *Builder) Build(ctx context.Context, opt backend.BuildConfig) (*builder.Result, error) {
  171. var rc = opt.Source
  172. if buildID := opt.Options.BuildID; buildID != "" {
  173. b.mu.Lock()
  174. upload := false
  175. if strings.HasPrefix(buildID, "upload-request:") {
  176. upload = true
  177. buildID = strings.TrimPrefix(buildID, "upload-request:")
  178. }
  179. if _, ok := b.jobs[buildID]; !ok {
  180. b.jobs[buildID] = newBuildJob()
  181. }
  182. j := b.jobs[buildID]
  183. var cancel func()
  184. ctx, cancel = context.WithCancel(ctx)
  185. j.cancel = cancel
  186. b.mu.Unlock()
  187. if upload {
  188. ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
  189. defer cancel()
  190. err := j.SetUpload(ctx2, rc)
  191. return nil, err
  192. }
  193. if remoteContext := opt.Options.RemoteContext; remoteContext == "upload-request" {
  194. ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
  195. defer cancel()
  196. var err error
  197. rc, err = j.WaitUpload(ctx2)
  198. if err != nil {
  199. return nil, err
  200. }
  201. opt.Options.RemoteContext = ""
  202. }
  203. defer func() {
  204. delete(b.jobs, buildID)
  205. }()
  206. }
  207. var out builder.Result
  208. id := identity.NewID()
  209. frontendAttrs := map[string]string{}
  210. if opt.Options.Target != "" {
  211. frontendAttrs["target"] = opt.Options.Target
  212. }
  213. if opt.Options.Dockerfile != "" && opt.Options.Dockerfile != "." {
  214. frontendAttrs["filename"] = opt.Options.Dockerfile
  215. }
  216. if opt.Options.RemoteContext != "" {
  217. if opt.Options.RemoteContext != "client-session" {
  218. frontendAttrs["context"] = opt.Options.RemoteContext
  219. }
  220. } else {
  221. url, cancel := b.reqBodyHandler.newRequest(rc)
  222. defer cancel()
  223. frontendAttrs["context"] = url
  224. }
  225. cacheFrom := append([]string{}, opt.Options.CacheFrom...)
  226. frontendAttrs["cache-from"] = strings.Join(cacheFrom, ",")
  227. for k, v := range opt.Options.BuildArgs {
  228. if v == nil {
  229. continue
  230. }
  231. frontendAttrs["build-arg:"+k] = *v
  232. }
  233. for k, v := range opt.Options.Labels {
  234. frontendAttrs["label:"+k] = v
  235. }
  236. if opt.Options.NoCache {
  237. frontendAttrs["no-cache"] = ""
  238. }
  239. if opt.Options.PullParent {
  240. frontendAttrs["image-resolve-mode"] = "pull"
  241. } else {
  242. frontendAttrs["image-resolve-mode"] = "default"
  243. }
  244. if opt.Options.Platform != "" {
  245. // same as in newBuilder in builder/dockerfile.builder.go
  246. // TODO: remove once opt.Options.Platform is of type specs.Platform
  247. sp, err := platforms.Parse(opt.Options.Platform)
  248. if err != nil {
  249. return nil, err
  250. }
  251. if err := system.ValidatePlatform(sp); err != nil {
  252. return nil, err
  253. }
  254. frontendAttrs["platform"] = opt.Options.Platform
  255. }
  256. switch opt.Options.NetworkMode {
  257. case "host", "none":
  258. frontendAttrs["force-network-mode"] = opt.Options.NetworkMode
  259. case "", "default":
  260. default:
  261. return nil, errors.Errorf("network mode %q not supported by buildkit", opt.Options.NetworkMode)
  262. }
  263. extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts)
  264. if err != nil {
  265. return nil, err
  266. }
  267. frontendAttrs["add-hosts"] = extraHosts
  268. exporterName := ""
  269. exporterAttrs := map[string]string{}
  270. if len(opt.Options.Outputs) > 1 {
  271. return nil, errors.Errorf("multiple outputs not supported")
  272. } else if len(opt.Options.Outputs) == 0 {
  273. exporterName = "moby"
  274. } else {
  275. // cacheonly is a special type for triggering skipping all exporters
  276. if opt.Options.Outputs[0].Type != "cacheonly" {
  277. exporterName = opt.Options.Outputs[0].Type
  278. exporterAttrs = opt.Options.Outputs[0].Attrs
  279. }
  280. }
  281. if exporterName == "moby" {
  282. if len(opt.Options.Tags) > 0 {
  283. exporterAttrs["name"] = strings.Join(opt.Options.Tags, ",")
  284. }
  285. }
  286. cache := controlapi.CacheOptions{}
  287. if inlineCache := opt.Options.BuildArgs["BUILDKIT_INLINE_CACHE"]; inlineCache != nil {
  288. if b, err := strconv.ParseBool(*inlineCache); err == nil && b {
  289. cache.Exports = append(cache.Exports, &controlapi.CacheOptionsEntry{
  290. Type: "inline",
  291. })
  292. }
  293. }
  294. req := &controlapi.SolveRequest{
  295. Ref: id,
  296. Exporter: exporterName,
  297. ExporterAttrs: exporterAttrs,
  298. Frontend: "dockerfile.v0",
  299. FrontendAttrs: frontendAttrs,
  300. Session: opt.Options.SessionID,
  301. Cache: cache,
  302. }
  303. if opt.Options.NetworkMode == "host" {
  304. req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost)
  305. }
  306. aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output}
  307. eg, ctx := errgroup.WithContext(ctx)
  308. eg.Go(func() error {
  309. resp, err := b.controller.Solve(ctx, req)
  310. if err != nil {
  311. return err
  312. }
  313. if exporterName != "moby" {
  314. return nil
  315. }
  316. id, ok := resp.ExporterResponse["containerimage.digest"]
  317. if !ok {
  318. return errors.Errorf("missing image id")
  319. }
  320. out.ImageID = id
  321. return aux.Emit("moby.image.id", types.BuildResult{ID: id})
  322. })
  323. ch := make(chan *controlapi.StatusResponse)
  324. eg.Go(func() error {
  325. defer close(ch)
  326. // streamProxy.ctx is not set to ctx because when request is cancelled,
  327. // only the build request has to be cancelled, not the status request.
  328. stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch}
  329. return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream)
  330. })
  331. eg.Go(func() error {
  332. for sr := range ch {
  333. dt, err := sr.Marshal()
  334. if err != nil {
  335. return err
  336. }
  337. if err := aux.Emit("moby.buildkit.trace", dt); err != nil {
  338. return err
  339. }
  340. }
  341. return nil
  342. })
  343. if err := eg.Wait(); err != nil {
  344. return nil, err
  345. }
  346. return &out, nil
  347. }
  348. type streamProxy struct {
  349. ctx context.Context
  350. }
  351. func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error {
  352. return nil
  353. }
  354. func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error {
  355. return nil
  356. }
  357. func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) {
  358. }
  359. func (sp *streamProxy) Context() context.Context {
  360. return sp.ctx
  361. }
  362. func (sp *streamProxy) RecvMsg(m interface{}) error {
  363. return io.EOF
  364. }
  365. type statusProxy struct {
  366. streamProxy
  367. ch chan *controlapi.StatusResponse
  368. }
  369. func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error {
  370. return sp.SendMsg(resp)
  371. }
  372. func (sp *statusProxy) SendMsg(m interface{}) error {
  373. if sr, ok := m.(*controlapi.StatusResponse); ok {
  374. sp.ch <- sr
  375. }
  376. return nil
  377. }
  378. type pruneProxy struct {
  379. streamProxy
  380. ch chan *controlapi.UsageRecord
  381. }
  382. func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error {
  383. return sp.SendMsg(resp)
  384. }
  385. func (sp *pruneProxy) SendMsg(m interface{}) error {
  386. if sr, ok := m.(*controlapi.UsageRecord); ok {
  387. sp.ch <- sr
  388. }
  389. return nil
  390. }
  391. type contentStoreNoLabels struct {
  392. content.Store
  393. }
  394. func (c *contentStoreNoLabels) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
  395. return content.Info{}, nil
  396. }
  397. type wrapRC struct {
  398. io.ReadCloser
  399. once sync.Once
  400. err error
  401. waitCh chan struct{}
  402. }
  403. func (w *wrapRC) Read(b []byte) (int, error) {
  404. n, err := w.ReadCloser.Read(b)
  405. if err != nil {
  406. e := err
  407. if e == io.EOF {
  408. e = nil
  409. }
  410. w.close(e)
  411. }
  412. return n, err
  413. }
  414. func (w *wrapRC) Close() error {
  415. err := w.ReadCloser.Close()
  416. w.close(err)
  417. return err
  418. }
  419. func (w *wrapRC) close(err error) {
  420. w.once.Do(func() {
  421. w.err = err
  422. close(w.waitCh)
  423. })
  424. }
  425. func (w *wrapRC) wait() error {
  426. <-w.waitCh
  427. return w.err
  428. }
  429. type buildJob struct {
  430. cancel func()
  431. waitCh chan func(io.ReadCloser) error
  432. }
  433. func newBuildJob() *buildJob {
  434. return &buildJob{waitCh: make(chan func(io.ReadCloser) error)}
  435. }
  436. func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) {
  437. done := make(chan struct{})
  438. var upload io.ReadCloser
  439. fn := func(rc io.ReadCloser) error {
  440. w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})}
  441. upload = w
  442. close(done)
  443. return w.wait()
  444. }
  445. select {
  446. case <-ctx.Done():
  447. return nil, ctx.Err()
  448. case j.waitCh <- fn:
  449. <-done
  450. return upload, nil
  451. }
  452. }
  453. func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error {
  454. select {
  455. case <-ctx.Done():
  456. return ctx.Err()
  457. case fn := <-j.waitCh:
  458. return fn(rc)
  459. }
  460. }
  461. // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format
  462. func toBuildkitExtraHosts(inp []string) (string, error) {
  463. if len(inp) == 0 {
  464. return "", nil
  465. }
  466. hosts := make([]string, 0, len(inp))
  467. for _, h := range inp {
  468. parts := strings.Split(h, ":")
  469. if len(parts) != 2 || parts[0] == "" || net.ParseIP(parts[1]) == nil {
  470. return "", errors.Errorf("invalid host %s", h)
  471. }
  472. hosts = append(hosts, parts[0]+"="+parts[1])
  473. }
  474. return strings.Join(hosts, ","), nil
  475. }
  476. func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) {
  477. var until time.Duration
  478. untilValues := opts.Filters.Get("until") // canonical
  479. unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter
  480. if len(untilValues) > 0 && len(unusedForValues) > 0 {
  481. return client.PruneInfo{}, errConflictFilter{"until", "unused-for"}
  482. }
  483. filterKey := "until"
  484. if len(unusedForValues) > 0 {
  485. filterKey = "unused-for"
  486. }
  487. untilValues = append(untilValues, unusedForValues...)
  488. switch len(untilValues) {
  489. case 0:
  490. // nothing to do
  491. case 1:
  492. var err error
  493. until, err = time.ParseDuration(untilValues[0])
  494. if err != nil {
  495. return client.PruneInfo{}, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", filterKey)
  496. }
  497. default:
  498. return client.PruneInfo{}, errMultipleFilterValues{}
  499. }
  500. bkFilter := make([]string, 0, opts.Filters.Len())
  501. for cacheField := range cacheFields {
  502. if opts.Filters.Include(cacheField) {
  503. values := opts.Filters.Get(cacheField)
  504. switch len(values) {
  505. case 0:
  506. bkFilter = append(bkFilter, cacheField)
  507. case 1:
  508. if cacheField == "id" {
  509. bkFilter = append(bkFilter, cacheField+"~="+values[0])
  510. } else {
  511. bkFilter = append(bkFilter, cacheField+"=="+values[0])
  512. }
  513. default:
  514. return client.PruneInfo{}, errMultipleFilterValues{}
  515. }
  516. }
  517. }
  518. return client.PruneInfo{
  519. All: opts.All,
  520. KeepDuration: until,
  521. KeepBytes: opts.KeepStorage,
  522. Filter: []string{strings.Join(bkFilter, ",")},
  523. }, nil
  524. }