builder.go 17 KB

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