builder.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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. Exporter: exporterName,
  337. ExporterAttrs: exporterAttrs,
  338. Frontend: "dockerfile.v0",
  339. FrontendAttrs: frontendAttrs,
  340. Session: opt.Options.SessionID,
  341. Cache: cache,
  342. }
  343. if opt.Options.NetworkMode == "host" {
  344. req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost)
  345. }
  346. aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output}
  347. eg, ctx := errgroup.WithContext(ctx)
  348. eg.Go(func() error {
  349. resp, err := b.controller.Solve(ctx, req)
  350. if err != nil {
  351. return err
  352. }
  353. if exporterName != exporter.Moby && exporterName != client.ExporterImage {
  354. return nil
  355. }
  356. id, ok := resp.ExporterResponse["containerimage.digest"]
  357. if !ok {
  358. return errors.Errorf("missing image id")
  359. }
  360. out.ImageID = id
  361. return aux.Emit("moby.image.id", types.BuildResult{ID: id})
  362. })
  363. ch := make(chan *controlapi.StatusResponse)
  364. eg.Go(func() error {
  365. defer close(ch)
  366. // streamProxy.ctx is not set to ctx because when request is cancelled,
  367. // only the build request has to be cancelled, not the status request.
  368. stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch}
  369. return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream)
  370. })
  371. eg.Go(func() error {
  372. for sr := range ch {
  373. dt, err := sr.Marshal()
  374. if err != nil {
  375. return err
  376. }
  377. if err := aux.Emit("moby.buildkit.trace", dt); err != nil {
  378. return err
  379. }
  380. }
  381. return nil
  382. })
  383. if err := eg.Wait(); err != nil {
  384. return nil, err
  385. }
  386. return &out, nil
  387. }
  388. type streamProxy struct {
  389. ctx context.Context
  390. }
  391. func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error {
  392. return nil
  393. }
  394. func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error {
  395. return nil
  396. }
  397. func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) {
  398. }
  399. func (sp *streamProxy) Context() context.Context {
  400. return sp.ctx
  401. }
  402. func (sp *streamProxy) RecvMsg(m interface{}) error {
  403. return io.EOF
  404. }
  405. type statusProxy struct {
  406. streamProxy
  407. ch chan *controlapi.StatusResponse
  408. }
  409. func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error {
  410. return sp.SendMsg(resp)
  411. }
  412. func (sp *statusProxy) SendMsg(m interface{}) error {
  413. if sr, ok := m.(*controlapi.StatusResponse); ok {
  414. sp.ch <- sr
  415. }
  416. return nil
  417. }
  418. type pruneProxy struct {
  419. streamProxy
  420. ch chan *controlapi.UsageRecord
  421. }
  422. func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error {
  423. return sp.SendMsg(resp)
  424. }
  425. func (sp *pruneProxy) SendMsg(m interface{}) error {
  426. if sr, ok := m.(*controlapi.UsageRecord); ok {
  427. sp.ch <- sr
  428. }
  429. return nil
  430. }
  431. type wrapRC struct {
  432. io.ReadCloser
  433. once sync.Once
  434. err error
  435. waitCh chan struct{}
  436. }
  437. func (w *wrapRC) Read(b []byte) (int, error) {
  438. n, err := w.ReadCloser.Read(b)
  439. if err != nil {
  440. e := err
  441. if e == io.EOF {
  442. e = nil
  443. }
  444. w.close(e)
  445. }
  446. return n, err
  447. }
  448. func (w *wrapRC) Close() error {
  449. err := w.ReadCloser.Close()
  450. w.close(err)
  451. return err
  452. }
  453. func (w *wrapRC) close(err error) {
  454. w.once.Do(func() {
  455. w.err = err
  456. close(w.waitCh)
  457. })
  458. }
  459. func (w *wrapRC) wait() error {
  460. <-w.waitCh
  461. return w.err
  462. }
  463. type buildJob struct {
  464. cancel func()
  465. waitCh chan func(io.ReadCloser) error
  466. }
  467. func newBuildJob() *buildJob {
  468. return &buildJob{waitCh: make(chan func(io.ReadCloser) error)}
  469. }
  470. func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) {
  471. done := make(chan struct{})
  472. var upload io.ReadCloser
  473. fn := func(rc io.ReadCloser) error {
  474. w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})}
  475. upload = w
  476. close(done)
  477. return w.wait()
  478. }
  479. select {
  480. case <-ctx.Done():
  481. return nil, ctx.Err()
  482. case j.waitCh <- fn:
  483. <-done
  484. return upload, nil
  485. }
  486. }
  487. func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error {
  488. select {
  489. case <-ctx.Done():
  490. return ctx.Err()
  491. case fn := <-j.waitCh:
  492. return fn(rc)
  493. }
  494. }
  495. // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format
  496. func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) {
  497. if len(inp) == 0 {
  498. return "", nil
  499. }
  500. hosts := make([]string, 0, len(inp))
  501. for _, h := range inp {
  502. host, ip, ok := strings.Cut(h, ":")
  503. if !ok || host == "" || ip == "" {
  504. return "", errors.Errorf("invalid host %s", h)
  505. }
  506. // If the IP Address is a "host-gateway", replace this value with the
  507. // IP address stored in the daemon level HostGatewayIP config variable.
  508. if ip == opts.HostGatewayName {
  509. gateway := hostGatewayIP.String()
  510. if gateway == "" {
  511. return "", fmt.Errorf("unable to derive the IP value for host-gateway")
  512. }
  513. ip = gateway
  514. } else if net.ParseIP(ip) == nil {
  515. return "", fmt.Errorf("invalid host %s", h)
  516. }
  517. hosts = append(hosts, host+"="+ip)
  518. }
  519. return strings.Join(hosts, ","), nil
  520. }
  521. // toBuildkitUlimits converts ulimits from docker type=soft:hard format to buildkit's csv format
  522. func toBuildkitUlimits(inp []*units.Ulimit) (string, error) {
  523. if len(inp) == 0 {
  524. return "", nil
  525. }
  526. ulimits := make([]string, 0, len(inp))
  527. for _, ulimit := range inp {
  528. ulimits = append(ulimits, ulimit.String())
  529. }
  530. return strings.Join(ulimits, ","), nil
  531. }
  532. func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) {
  533. var until time.Duration
  534. untilValues := opts.Filters.Get("until") // canonical
  535. unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter
  536. if len(untilValues) > 0 && len(unusedForValues) > 0 {
  537. return client.PruneInfo{}, errConflictFilter{"until", "unused-for"}
  538. }
  539. filterKey := "until"
  540. if len(unusedForValues) > 0 {
  541. filterKey = "unused-for"
  542. }
  543. untilValues = append(untilValues, unusedForValues...)
  544. switch len(untilValues) {
  545. case 0:
  546. // nothing to do
  547. case 1:
  548. ts, err := timetypes.GetTimestamp(untilValues[0], time.Now())
  549. if err != nil {
  550. return client.PruneInfo{}, errInvalidFilterValue{
  551. errors.Wrapf(err, "%q filter expects a duration (e.g., '24h') or a timestamp", filterKey),
  552. }
  553. }
  554. seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0)
  555. if err != nil {
  556. return client.PruneInfo{}, errInvalidFilterValue{
  557. errors.Wrapf(err, "failed to parse timestamp %q", ts),
  558. }
  559. }
  560. until = time.Since(time.Unix(seconds, nanoseconds))
  561. default:
  562. return client.PruneInfo{}, errMultipleFilterValues{}
  563. }
  564. bkFilter := make([]string, 0, opts.Filters.Len())
  565. for cacheField := range cacheFields {
  566. if opts.Filters.Contains(cacheField) {
  567. values := opts.Filters.Get(cacheField)
  568. switch len(values) {
  569. case 0:
  570. bkFilter = append(bkFilter, cacheField)
  571. case 1:
  572. if cacheField == "id" {
  573. bkFilter = append(bkFilter, cacheField+"~="+values[0])
  574. } else {
  575. bkFilter = append(bkFilter, cacheField+"=="+values[0])
  576. }
  577. default:
  578. return client.PruneInfo{}, errMultipleFilterValues{}
  579. }
  580. }
  581. }
  582. return client.PruneInfo{
  583. All: opts.All,
  584. KeepDuration: until,
  585. KeepBytes: opts.KeepStorage,
  586. Filter: []string{strings.Join(bkFilter, ",")},
  587. }, nil
  588. }