Repository: tensorchord/envd (language: python)

Issue:
feat(buildkit): Expose envd as a buildkit frontend
Mocker uses github.com/moby/buildkit/gateway/client instead of github.com/moby/buildkit/client.

It introduces a new abstraction Build func https://github.com/r2d4/mockerfile/blob/140c6a912bbfdae220febe59ab535ef0acba0e1f/pkg/build/build.go#L37

The benefit is that you can do some post-solve logic like this https://github.com/r2d4/mockerfile/blob/140c6a912bbfdae220febe59ab535ef0acba0e1f/pkg/build/build.go#L65


Discussion / hints:
https://github.com/moby/buildkit/pull/2916#issuecomment-1162442862
If we move to the frontend design, we can integrate with buildkit ecosystem seamlessly. For example, we can use buildx action in GitHub:

```
name: Name
on:
  push:
    branches: [dev]
jobs:
  push_to_registry:
    name: Push Docker image to GitHub Packages
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      - name: Push to GitHub Packages
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.CR_PAT }}
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2.0.1
        with:
          context: ./app/
          push: true
          file: ./app/build.envd
          build-args: |
             ...
```
Ref #510 

Fix patch:
diff --git a/pkg/app/build.go b/pkg/app/build.go
index c5a0462cb..0dcda5ed5 100644
--- a/pkg/app/build.go
+++ b/pkg/app/build.go
@@ -120,9 +120,9 @@ func build(clicontext *cli.Context) error {
 		"output": output,
 	}).Debug("starting build command")
 	builder, err := builder.New(clicontext.Context, cfg,
-		manifest, funcname, buildContext, tag, output, debug)
+		manifest, funcname, buildContext, tag, output, debug, clicontext.Path("public-key"))
 	if err != nil {
 		return errors.Wrap(err, "failed to create the builder")
 	}
-	return builder.Build(clicontext.Context, clicontext.Path("public-key"))
+	return builder.Build(clicontext.Context)
 }
diff --git a/pkg/app/up.go b/pkg/app/up.go
index 9d5ff4e3f..7ac761d1e 100644
--- a/pkg/app/up.go
+++ b/pkg/app/up.go
@@ -153,12 +153,12 @@ func up(clicontext *cli.Context) error {
 	debug := clicontext.Bool("debug")
 	output := ""
 	builder, err := builder.New(clicontext.Context, config, manifest, funcname,
-		buildContext, tag, output, debug)
+		buildContext, tag, output, debug, clicontext.Path("public-key"))
 	if err != nil {
 		return errors.Wrap(err, "failed to create the builder")
 	}
 
-	if err := builder.Build(clicontext.Context, clicontext.Path("public-key")); err != nil {
+	if err := builder.Build(clicontext.Context); err != nil {
 		return errors.Wrap(err, "failed to build the image")
 	}
 	// Do not attach GPU if the flag is set.
diff --git a/pkg/builder/build.go b/pkg/builder/build.go
new file mode 100644
index 000000000..506cac80c
--- /dev/null
+++ b/pkg/builder/build.go
@@ -0,0 +1,54 @@
+// Copyright 2022 The envd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package builder
+
+import (
+	"context"
+
+	"github.com/cockroachdb/errors"
+	"github.com/moby/buildkit/frontend/gateway/client"
+)
+
+func (b generalBuilder) BuildFunc() func(ctx context.Context, c client.Client) (*client.Result, error) {
+	return func(ctx context.Context, c client.Client) (*client.Result, error) {
+		depsFiles := []string{
+			b.pubKeyPath,
+			b.configFilePath,
+			b.manifestFilePath,
+		}
+		isUpdated, err := b.CheckDepsFileUpdate(ctx, b.tag, depsFiles)
+		if err != nil {
+			b.logger.Debugf("failed to check manifest update: %s", err)
+		}
+		if !isUpdated {
+			b.logger.Infof("manifest is not updated, skip building")
+			return nil, nil
+		}
+
+		def, err := b.compile(ctx)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to compile")
+		}
+
+		res, err := c.Solve(ctx, client.SolveRequest{
+			Definition: def.ToPB(),
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		return res, nil
+	}
+}
diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index ebd8434c6..359da0c29 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -39,7 +39,7 @@ import (
 )
 
 type Builder interface {
-	Build(ctx context.Context, pub string) error
+	Build(ctx context.Context) error
 	GPUEnabled() bool
 	NumGPUs() int
 }
@@ -51,6 +51,7 @@ type generalBuilder struct {
 	tag              string
 	buildContextDir  string
 	buildfuncname    string
+	pubKeyPath       string
 
 	entries []client.ExportEntry
 
@@ -60,7 +61,8 @@ type generalBuilder struct {
 }
 
 func New(ctx context.Context, configFilePath, manifestFilePath, funcname,
-	buildContextDir, tag string, output string, debug bool) (Builder, error) {
+	buildContextDir, tag string, output string, debug bool,
+	pubKeyPath string) (Builder, error) {
 	entries, err := parseOutput(output)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse output")
@@ -89,6 +91,7 @@ func New(ctx context.Context, configFilePath, manifestFilePath, funcname,
 		configFilePath:   configFilePath,
 		buildContextDir:  buildContextDir,
 		entries:          entries,
+		pubKeyPath:       pubKeyPath,
 		progressMode:     mode,
 		tag:              tag,
 		logger: logrus.WithFields(logrus.Fields{
@@ -150,31 +153,13 @@ func (b generalBuilder) CheckDepsFileUpdate(ctx context.Context, tag string, dep
 	return false, nil
 }
 
-func (b generalBuilder) Build(ctx context.Context, pub string) error {
-	depsFiles := []string{
-		pub,
-		b.configFilePath,
-		b.manifestFilePath,
-	}
-	isUpdated, err := b.CheckDepsFileUpdate(ctx, b.tag, depsFiles)
-	if err != nil {
-		b.logger.Debugf("failed to check manifest update: %s", err)
-	}
-	if !isUpdated {
-		b.logger.Infof("manifest is not updated, skip building")
-		return nil
-	}
-	def, err := b.compile(ctx, pub)
-	if err != nil {
-		return errors.Wrap(err, "failed to compile")
-	}
-
+func (b generalBuilder) Build(ctx context.Context) error {
 	pw, err := progresswriter.NewPrinter(ctx, os.Stdout, b.progressMode)
 	if err != nil {
 		return errors.Wrap(err, "failed to create progress writer")
 	}
 
-	if err = b.build(ctx, def, pw); err != nil {
+	if err = b.build(ctx, pw); err != nil {
 		return errors.Wrap(err, "failed to build")
 	}
 	return nil
@@ -192,11 +177,11 @@ func (b generalBuilder) interpret() error {
 	return nil
 }
 
-func (b generalBuilder) compile(ctx context.Context, pub string) (*llb.Definition, error) {
+func (b generalBuilder) compile(ctx context.Context) (*llb.Definition, error) {
 	if err := b.interpret(); err != nil {
 		return nil, errors.Wrap(err, "failed to interpret")
 	}
-	def, err := ir.Compile(ctx, fileutil.Base(b.buildContextDir), pub)
+	def, err := ir.Compile(ctx, fileutil.Base(b.buildContextDir), b.pubKeyPath)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to compile build.envd")
 	}
@@ -226,7 +211,7 @@ func (b generalBuilder) imageConfig(ctx context.Context) (string, error) {
 	return data, nil
 }
 
-func (b generalBuilder) build(ctx context.Context, def *llb.Definition, pw progresswriter.Writer) error {
+func (b generalBuilder) build(ctx context.Context, pw progresswriter.Writer) error {
 	imageConfig, err := b.imageConfig(ctx)
 	if err != nil {
 		return errors.Wrap(err, "failed to get labels")
@@ -260,22 +245,22 @@ func (b generalBuilder) build(ctx context.Context, def *llb.Definition, pw progr
 					}
 				}
 				defer pipeW.Close()
-				_, err := b.Solve(ctx, def, client.SolveOpt{
+				_, err := b.Client.Build(ctx, client.SolveOpt{
 					Exports: []client.ExportEntry{entry},
 					LocalDirs: map[string]string{
-						flag.FlagContextDir: b.buildContextDir,
-						flag.FlagCacheDir:   home.GetManager().CacheDir(),
+						// TODO(gaocegege): Move it to BuildFunc with the help
+						// of llb.Local
+						flag.FlagCacheDir: home.GetManager().CacheDir(),
 					},
 					Session: attachable,
 					// TODO(gaocegege): Use llb.WithProxy to implement it.
 					FrontendAttrs: map[string]string{
 						"build-arg:HTTPS_PROXY": os.Getenv("HTTPS_PROXY"),
 					},
-				}, pw.Status())
+				}, "envd", b.BuildFunc(), pw.Status())
 
 				if err != nil {
 					err = errors.Wrap(err, "failed to solve LLB")
-					b.logger.Error(err)
 					return err
 				}
 				b.logger.Debug("llb def is solved successfully")
@@ -299,22 +284,20 @@ func (b generalBuilder) build(ctx context.Context, def *llb.Definition, pw progr
 			})
 		default:
 			eg.Go(func() error {
-				_, err := b.Solve(ctx, def, client.SolveOpt{
+				_, err := b.Client.Build(ctx, client.SolveOpt{
 					Exports: []client.ExportEntry{entry},
 					LocalDirs: map[string]string{
-						flag.FlagContextDir: b.buildContextDir,
-						flag.FlagCacheDir:   home.GetManager().CacheDir(),
+						flag.FlagCacheDir: home.GetManager().CacheDir(),
 					},
 					Session: attachable,
 					// TODO(gaocegege): Use llb.WithProxy to implement it.
 					FrontendAttrs: map[string]string{
 						"build-arg:HTTPS_PROXY": os.Getenv("HTTPS_PROXY"),
 					},
-				}, pw.Status())
+				}, "envd", b.BuildFunc(), pw.Status())
 
 				if err != nil {
 					err = errors.Wrap(err, "failed to solve LLB")
-					b.logger.Error(err)
 					return err
 				}
 				b.logger.Debug("llb def is solved successfully")
diff --git a/pkg/buildkitd/buildkitd.go b/pkg/buildkitd/buildkitd.go
index 24266df90..81ca9316c 100644
--- a/pkg/buildkitd/buildkitd.go
+++ b/pkg/buildkitd/buildkitd.go
@@ -24,6 +24,7 @@ import (
 	"github.com/cockroachdb/errors"
 	"github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/client/llb"
+	gateway "github.com/moby/buildkit/frontend/gateway/client"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	"github.com/tonistiigi/units"
@@ -46,6 +47,9 @@ type Client interface {
 	// Solve calls Solve on the controller.
 	Solve(ctx context.Context, def *llb.Definition,
 		opt client.SolveOpt, statusChan chan *client.SolveStatus) (*client.SolveResponse, error)
+	Build(ctx context.Context, opt client.SolveOpt, product string,
+		buildFunc gateway.BuildFunc, statusChan chan *client.SolveStatus,
+	) (*client.SolveResponse, error)
 	Prune(ctx context.Context, keepDuration time.Duration,
 		keepStorage float64, filter []string, verbose, all bool) error
 	Close() error
@@ -144,7 +148,8 @@ func (c *generalClient) maybeStart(ctx context.Context,
 	}
 
 	if err := c.waitUntilConnected(ctx, connectingTimeout); err != nil {
-		return "", errors.Wrap(err, "failed to connect to buildkitd")
+		return "", errors.Wrapf(err,
+			"failed to connect to buildkitd %s", c.BuildkitdAddr())
 	}
 
 	return c.BuildkitdAddr(), nil
diff --git a/pkg/buildkitd/mock/mock.go b/pkg/buildkitd/mock/mock.go
index 1052ee350..5a34c7b95 100644
--- a/pkg/buildkitd/mock/mock.go
+++ b/pkg/buildkitd/mock/mock.go
@@ -12,6 +12,7 @@ import (
 	gomock "github.com/golang/mock/gomock"
 	client "github.com/moby/buildkit/client"
 	llb "github.com/moby/buildkit/client/llb"
+	client0 "github.com/moby/buildkit/frontend/gateway/client"
 )
 
 // MockClient is a mock of Client interface.
@@ -37,6 +38,21 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
 	return m.recorder
 }
 
+// Build mocks base method.
+func (m *MockClient) Build(ctx context.Context, opt client.SolveOpt, product string, buildFunc client0.BuildFunc, statusChan chan *client.SolveStatus) (*client.SolveResponse, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Build", ctx, opt, product, buildFunc, statusChan)
+	ret0, _ := ret[0].(*client.SolveResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Build indicates an expected call of Build.
+func (mr *MockClientMockRecorder) Build(ctx, opt, product, buildFunc, statusChan interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockClient)(nil).Build), ctx, opt, product, buildFunc, statusChan)
+}
+
 // BuildkitdAddr mocks base method.
 func (m *MockClient) BuildkitdAddr() string {
 	m.ctrl.T.Helper()
diff --git a/pkg/flag/consts.go b/pkg/flag/consts.go
index 04c98d0af..617ee845e 100644
--- a/pkg/flag/consts.go
+++ b/pkg/flag/consts.go
@@ -16,7 +16,6 @@ package flag
 
 const (
 	FlagCacheDir       = "cache-dir"
-	FlagContextDir     = "context-dir"
 	FlagBuildkitdImage = "buildkitd-image"
 	FlagDebug          = "debug"
 )


Test patch:
diff --git a/pkg/builder/builder_test.go b/pkg/builder/builder_test.go
index 821f6fd76..43b4d7618 100644
--- a/pkg/builder/builder_test.go
+++ b/pkg/builder/builder_test.go
@@ -20,7 +20,7 @@ import (
 	"os"
 
 	"github.com/golang/mock/gomock"
-	"github.com/moby/buildkit/client/llb"
+	"github.com/moby/buildkit/client"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/sirupsen/logrus"
@@ -50,12 +50,14 @@ var _ = Describe("Builder", func() {
 			BeforeEach(func() {
 				ctrl := gomock.NewController(GinkgoT())
 				ctrlStarlark := gomock.NewController(GinkgoT())
+				pub := sshconfig.GetPublicKey()
 				b = &generalBuilder{
 					manifestFilePath: manifestFilePath,
 					configFilePath:   configFilePath,
-					progressMode:     "auto",
+					progressMode:     "plain",
 					tag:              tag,
 					buildfuncname:    "build",
+					pubKeyPath:       pub,
 					logger: logrus.WithFields(logrus.Fields{
 						"tag": tag,
 					}),
@@ -68,50 +70,46 @@ var _ = Describe("Builder", func() {
 				ir.DefaultGraph.Writer = w
 			})
 
-			When("failed to interpret config", func() {
+			When("build error", func() {
 				It("should get an error", func() {
-					expected := errors.New("failed to interpret config")
-					b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile(
-						gomock.Eq(configFilePath), "",
-					).Return(nil, expected)
-					pub := sshconfig.GetPublicKey()
-					err := b.Build(context.TODO(), pub)
-					Expect(err).To(HaveOccurred())
-				})
-			})
+					b.entries = []client.ExportEntry{
+						{
+							Type: client.ExporterDocker,
+						},
+					}
 
-			When("failed to interpret manifest", func() {
-				It("should get an error", func() {
-					expected := errors.New("failed to interpret manifest")
-					pub := sshconfig.GetPublicKey()
-					b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile(
-						gomock.Eq(configFilePath), gomock.Eq(""),
-					).Return(nil, nil)
-					b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile(
-						gomock.Eq(b.manifestFilePath), gomock.Eq("build"),
-					).Return(nil, expected)
-					err := b.Build(context.TODO(), pub)
+					b.Client.(*mockbuildkitd.MockClient).EXPECT().Build(gomock.Any(),
+						gomock.Any(), gomock.Eq("envd"), gomock.Any(), gomock.Any()).
+						Return(nil, errors.New("build error"))
+
+					pw, err := progresswriter.NewPrinter(context.TODO(), os.Stdout, b.progressMode)
+					Expect(err).NotTo(HaveOccurred())
+
+					close(pw.Status())
+					err = b.build(context.TODO(), pw)
 					Expect(err).To(HaveOccurred())
 				})
 			})
+
 			It("should build successfully", func() {
 				err := home.Initialize()
 				Expect(err).ToNot(HaveOccurred())
 
-				b.Client.(*mockbuildkitd.MockClient).EXPECT().Solve(
-					gomock.Any(),
-					gomock.Any(),
-					gomock.Any(),
-					gomock.Any(),
-				).Return(nil, nil).AnyTimes()
+				b.entries = []client.ExportEntry{
+					{
+						Type: client.ExporterDocker,
+					},
+				}
+
+				b.Client.(*mockbuildkitd.MockClient).EXPECT().Build(gomock.Any(),
+					gomock.Any(), gomock.Eq("envd"), gomock.Any(), gomock.Any()).
+					Return(nil, nil)
 
-				var def *llb.Definition
 				pw, err := progresswriter.NewPrinter(context.TODO(), os.Stdout, b.progressMode)
-				if err != nil {
-					Fail(err.Error())
-				}
+				Expect(err).NotTo(HaveOccurred())
+
 				close(pw.Status())
-				err = b.build(context.TODO(), def, pw)
+				err = b.build(context.TODO(), pw)
 				Expect(err).ToNot(HaveOccurred())
 			})
 		})