From 14db2562e815f945d60d4ea4cb0de3a4f6a7ef7d Mon Sep 17 00:00:00 2001
From: Tonis Tiigi <tonistiigi@gmail.com>
Date: Wed, 10 Jun 2020 19:40:07 -0700
Subject: [PATCH] exec: pull emulator if no local binary found

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
---
 solver/llbsolver/ops/exec.go        |  42 +++++-----
 solver/llbsolver/ops/exec_binfmt.go | 124 ++++++++++++++++++++++++++--
 util/pull/resolver.go               |   4 +
 worker/base/worker.go               |   2 +-
 4 files changed, 146 insertions(+), 26 deletions(-)

diff --git a/solver/llbsolver/ops/exec.go b/solver/llbsolver/ops/exec.go
index 72f5358b1297a..1ca8cf8b3da96 100644
--- a/solver/llbsolver/ops/exec.go
+++ b/solver/llbsolver/ops/exec.go
@@ -31,6 +31,7 @@ import (
 	"github.com/moby/buildkit/solver"
 	"github.com/moby/buildkit/solver/llbsolver"
 	"github.com/moby/buildkit/solver/pb"
+	"github.com/moby/buildkit/source"
 	"github.com/moby/buildkit/util/grpcerrors"
 	"github.com/moby/buildkit/util/progress/logs"
 	utilsystem "github.com/moby/buildkit/util/system"
@@ -47,33 +48,35 @@ import (
 const execCacheType = "buildkit.exec.v0"
 
 type execOp struct {
-	op        *pb.ExecOp
-	cm        cache.Manager
-	sm        *session.Manager
-	md        *metadata.Store
-	exec      executor.Executor
-	w         worker.Worker
-	platform  *pb.Platform
-	numInputs int
+	op            *pb.ExecOp
+	cm            cache.Manager
+	sm            *session.Manager
+	md            *metadata.Store
+	exec          executor.Executor
+	w             worker.Worker
+	sourceManager *source.Manager
+	platform      *pb.Platform
+	numInputs     int
 
 	cacheMounts   map[string]*cacheRefShare
 	cacheMountsMu sync.Mutex
 }
 
-func NewExecOp(v solver.Vertex, op *pb.Op_Exec, platform *pb.Platform, cm cache.Manager, sm *session.Manager, md *metadata.Store, exec executor.Executor, w worker.Worker) (solver.Op, error) {
+func NewExecOp(v solver.Vertex, op *pb.Op_Exec, platform *pb.Platform, cm cache.Manager, m *source.Manager, sm *session.Manager, md *metadata.Store, exec executor.Executor, w worker.Worker) (solver.Op, error) {
 	if err := llbsolver.ValidateOp(&pb.Op{Op: op}); err != nil {
 		return nil, err
 	}
 	return &execOp{
-		op:          op.Exec,
-		cm:          cm,
-		sm:          sm,
-		md:          md,
-		exec:        exec,
-		numInputs:   len(v.Inputs()),
-		w:           w,
-		platform:    platform,
-		cacheMounts: map[string]*cacheRefShare{},
+		op:            op.Exec,
+		cm:            cm,
+		sm:            sm,
+		md:            md,
+		exec:          exec,
+		numInputs:     len(v.Inputs()),
+		w:             w,
+		platform:      platform,
+		cacheMounts:   map[string]*cacheRefShare{},
+		sourceManager: m,
 	}, nil
 }
 
@@ -708,7 +711,7 @@ func (e *execOp) Exec(ctx context.Context, inputs []solver.Result) ([]solver.Res
 		return nil, err
 	}
 
-	emu, err := getEmulator(e.platform, e.cm.IdentityMapping())
+	emu, err := getEmulator(ctx, e.sourceManager, e.w.ContentStore(), e.sm, e.platform, e.cm.IdentityMapping())
 	if err == nil && emu != nil {
 		e.op.Meta.Args = append([]string{qemuMountName}, e.op.Meta.Args...)
 
@@ -717,6 +720,7 @@ func (e *execOp) Exec(ctx context.Context, inputs []solver.Result) ([]solver.Res
 			Src:      emu,
 			Dest:     qemuMountName,
 		})
+		defer emu.Release(context.TODO())
 	}
 	if err != nil {
 		logrus.Warn(err.Error()) // TODO: remove this with pull support
diff --git a/solver/llbsolver/ops/exec_binfmt.go b/solver/llbsolver/ops/exec_binfmt.go
index 5eea8ff95388b..9a1388a8c8ba9 100644
--- a/solver/llbsolver/ops/exec_binfmt.go
+++ b/solver/llbsolver/ops/exec_binfmt.go
@@ -2,23 +2,33 @@ package ops
 
 import (
 	"context"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"time"
 
+	"github.com/containerd/containerd/content"
 	"github.com/containerd/containerd/mount"
 	"github.com/containerd/containerd/platforms"
+	"github.com/containerd/containerd/reference"
 	"github.com/docker/docker/pkg/idtools"
+	"github.com/moby/buildkit/cache"
+	"github.com/moby/buildkit/client"
+	"github.com/moby/buildkit/session"
 	"github.com/moby/buildkit/snapshot"
 	"github.com/moby/buildkit/solver/pb"
+	"github.com/moby/buildkit/source"
 	"github.com/moby/buildkit/util/binfmt_misc"
+	"github.com/moby/buildkit/util/progress"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	copy "github.com/tonistiigi/fsutil/copy"
 )
 
 const qemuMountName = "/dev/.buildkit_qemu_emulator"
+const emulatorImage = "docker.io/tonistiigi/binfmt:buildkit@sha256:5e7df2cf5373ba557ff2e61994cbc4d16adbd0627db1ed55acde3b7a16a693ba"
 
 var qemuArchMap = map[string]string{
 	"arm64":   "aarch64",
@@ -30,12 +40,50 @@ var qemuArchMap = map[string]string{
 }
 
 type emulator struct {
-	path  string
-	idmap *idtools.IdentityMapping
+	mount   snapshot.Mountable
+	release func(context.Context) error
 }
 
 func (e *emulator) Mount(ctx context.Context, readonly bool) (snapshot.Mountable, error) {
-	return &staticEmulatorMount{path: e.path, idmap: e.idmap}, nil
+	return e.mount, nil
+}
+
+func (e *emulator) Release(ctx context.Context) error {
+	if e.release != nil {
+		return e.release(ctx)
+	}
+	return nil
+}
+
+type refEmulatorMount struct {
+	ref  cache.ImmutableRef
+	name string
+}
+
+func (m *refEmulatorMount) Mount() ([]mount.Mount, func() error, error) {
+	mountable, err := m.ref.Mount(context.TODO(), true)
+	if err != nil {
+		return nil, nil, err
+	}
+	mounter := snapshot.LocalMounter(mountable)
+	release := func() error {
+		return mounter.Unmount()
+	}
+	target, err := mounter.Mount()
+	if err != nil {
+		release()
+		return nil, nil, err
+	}
+
+	return []mount.Mount{{
+		Type:    "bind",
+		Source:  filepath.Join(target, "buildkit-qemu-"+m.name),
+		Options: []string{"ro", "bind"},
+	}}, release, nil
+}
+
+func (m *refEmulatorMount) IdentityMapping() *idtools.IdentityMapping {
+	return m.ref.IdentityMapping()
 }
 
 type staticEmulatorMount struct {
@@ -82,7 +130,7 @@ func (m *staticEmulatorMount) IdentityMapping() *idtools.IdentityMapping {
 	return m.idmap
 }
 
-func getEmulator(p *pb.Platform, idmap *idtools.IdentityMapping) (*emulator, error) {
+func getEmulator(ctx context.Context, src *source.Manager, cs content.Store, sm *session.Manager, p *pb.Platform, idmap *idtools.IdentityMapping) (*emulator, error) {
 	all := binfmt_misc.SupportedPlatforms(false)
 	m := make(map[string]struct{}, len(all))
 
@@ -106,9 +154,73 @@ func getEmulator(p *pb.Platform, idmap *idtools.IdentityMapping) (*emulator, err
 	}
 
 	fn, err := exec.LookPath("buildkit-qemu-" + a)
+	if err == nil {
+		return &emulator{mount: &staticEmulatorMount{path: fn, idmap: idmap}}, nil
+	}
+
+	return pullEmulator(ctx, src, cs, sm, pp, a)
+}
+
+func pullEmulator(ctx context.Context, src *source.Manager, cs content.Store, sm *session.Manager, p specs.Platform, name string) (_ *emulator, err error) {
+	id, err := source.NewImageIdentifier(emulatorImage)
+	if err != nil {
+		return nil, err
+	}
+	s := platforms.DefaultSpec()
+	id.Platform = &s
+	id.RecordType = client.UsageRecordTypeInternal
+
+	spec, err := reference.Parse(emulatorImage)
+	if err != nil {
+		return nil, errors.WithStack(err)
+	}
+	var exists bool
+	if dgst := spec.Digest(); dgst != "" {
+		if _, err := cs.Info(ctx, dgst); err == nil {
+			exists = true
+		}
+	}
+	if !exists {
+		defer oneOffProgress(ctx, fmt.Sprintf("pulling emulator for %s", platforms.Format(p)))(err)
+	}
+
+	ctx = progress.WithProgress(ctx, &discard{})
+
+	inst, err := src.Resolve(ctx, id, sm)
 	if err != nil {
-		return nil, errors.Errorf("no emulator available for %v", pp.OS)
+		return nil, err
 	}
 
-	return &emulator{path: fn}, nil
+	ref, err := inst.Snapshot(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return &emulator{mount: &refEmulatorMount{ref: ref, name: name}, release: ref.Release}, nil
+}
+
+func oneOffProgress(ctx context.Context, id string) func(err error) error {
+	pw, _, _ := progress.FromContext(ctx)
+	now := time.Now()
+	st := progress.Status{
+		Started: &now,
+	}
+	pw.Write(id, st)
+	return func(err error) error {
+		now := time.Now()
+		st.Completed = &now
+		pw.Write(id, st)
+		pw.Close()
+		return err
+	}
+}
+
+type discard struct {
+}
+
+func (d *discard) Write(id string, value interface{}) error {
+	return nil
+}
+func (d *discard) Close() error {
+	return nil
 }
diff --git a/util/pull/resolver.go b/util/pull/resolver.go
index ca425c24a7f26..1afb636368fbe 100644
--- a/util/pull/resolver.go
+++ b/util/pull/resolver.go
@@ -2,6 +2,7 @@ package pull
 
 import (
 	"context"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -34,6 +35,9 @@ func NewResolver(ctx context.Context, hosts docker.RegistryHosts, sm *session.Ma
 }
 
 func EnsureManifestRequested(ctx context.Context, res remotes.Resolver, ref string) {
+	if strings.HasPrefix(ref, "docker.io/") {
+		return
+	}
 	rr := res
 	lr, ok := res.(withLocalResolver)
 	if ok {
diff --git a/worker/base/worker.go b/worker/base/worker.go
index 1cbdd6fa0ebbc..cbb919177840a 100644
--- a/worker/base/worker.go
+++ b/worker/base/worker.go
@@ -235,7 +235,7 @@ func (w *Worker) ResolveOp(v solver.Vertex, s frontend.FrontendLLBBridge, sm *se
 		case *pb.Op_Source:
 			return ops.NewSourceOp(v, op, baseOp.Platform, w.SourceManager, sm, w)
 		case *pb.Op_Exec:
-			return ops.NewExecOp(v, op, baseOp.Platform, w.CacheManager, sm, w.MetadataStore, w.Executor, w)
+			return ops.NewExecOp(v, op, baseOp.Platform, w.CacheManager, w.SourceManager, sm, w.MetadataStore, w.Executor, w)
 		case *pb.Op_File:
 			return ops.NewFileOp(v, op, w.CacheManager, w.MetadataStore, w)
 		case *pb.Op_Build: