builder.go 17 KB

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