Skip to content

Commit

Permalink
feat(cli): support envd build --output (#402)
Browse files Browse the repository at this point in the history
Signed-off-by: Yuchen Cheng <rudeigerc@gmail.com>
  • Loading branch information
rudeigerc committed Jun 16, 2022
1 parent 8679557 commit 6160899
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 21 deletions.
8 changes: 7 additions & 1 deletion pkg/app/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ var CommandBuild = &cli.Command{
Aliases: []string{"pubk"},
Value: sshconfig.GetPublicKey(),
},
&cli.PathFlag{
Name: "output",
Usage: "Output destination (format: type=tar,dest=path)",
Aliases: []string{"o"},
Value: "",
},
},

Action: build,
Expand Down Expand Up @@ -94,7 +100,7 @@ func build(clicontext *cli.Context) error {
})
logger.Debug("starting build command")

builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag)
builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag, clicontext.Path("output"))
if err != nil {
return errors.Wrap(err, "failed to create the builder")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/app/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func up(clicontext *cli.Context) error {
})
logger.Debug("starting up command")

builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag)
builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag, "")
if err != nil {
return errors.Wrap(err, "failed to create the builder")
}
Expand Down
67 changes: 49 additions & 18 deletions pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,25 @@ type generalBuilder struct {
progressMode string
tag string
buildContextDir string
outputType string
outputDest string

logger *logrus.Entry
starlark.Interpreter
buildkitd.Client
}

func New(ctx context.Context,
configFilePath, manifestFilePath, buildContextDir, tag string) (Builder, error) {
func New(ctx context.Context, configFilePath, manifestFilePath, buildContextDir, tag, output string) (Builder, error) {
outputType, outputDest, err := parseOutput(output)
if err != nil {
return nil, errors.Wrap(err, "failed to parse output")
}

b := &generalBuilder{
manifestFilePath: manifestFilePath,
configFilePath: configFilePath,
outputType: outputType,
outputDest: outputDest,
buildContextDir: buildContextDir,
// TODO(gaocegege): Support other mode?
progressMode: "auto",
Expand Down Expand Up @@ -154,6 +162,7 @@ func (b generalBuilder) build(ctx context.Context, def *llb.Definition, pw progr

// Create a pipe to load the image into the docker host.
pipeR, pipeW := io.Pipe()

eg.Go(func() error {
defer pipeW.Close()
_, err := b.Solve(ctx, def, client.SolveOpt{
Expand Down Expand Up @@ -195,22 +204,44 @@ func (b generalBuilder) build(ctx context.Context, def *llb.Definition, pw progr
return pw.Err()
})

// Load the image to docker host.
eg.Go(func() error {
defer pipeR.Close()
dockerClient, err := docker.NewClient(ctx)
if err != nil {
return errors.Wrap(err, "failed to new docker client")
}
b.logger.Debug("loading image to docker host")
if err := dockerClient.Load(ctx, pipeR, true); err != nil {
err = errors.Wrap(err, "failed to load docker image")
b.logger.Error(err)
return err
}
b.logger.Debug("loaded docker image successfully")
return nil
})
if b.outputDest != "" {
// Save the image to the output file.
eg.Go(func() error {
defer pipeR.Close()
f, err := os.Create(b.outputDest)
if err != nil {
return err
}

defer f.Close()
_, err = io.Copy(f, pipeR)
if err != nil {
return err
}

b.logger.Debug("export the image successfully")
return nil
})
}

if b.outputDest == "" {
// Load the image to docker host.
eg.Go(func() error {
defer pipeR.Close()
dockerClient, err := docker.NewClient(ctx)
if err != nil {
return errors.Wrap(err, "failed to new docker client")
}
b.logger.Debug("loading image to docker host")
if err := dockerClient.Load(ctx, pipeR, true); err != nil {
err = errors.Wrap(err, "failed to load docker image")
b.logger.Error(err)
return err
}
b.logger.Debug("loaded docker image successfully")
return nil
})
}

err = eg.Wait()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var _ = Describe("Builder", func() {
buildkitdSocket = "wrong"
viper.Set(flag.FlagBuildkitdContainer, buildkitdSocket)
It("should return an error", func() {
_, err := New(context.TODO(), configFilePath, manifestFilePath, buildContext, tag)
_, err := New(context.TODO(), configFilePath, manifestFilePath, buildContext, tag, "")
Expect(err).To(HaveOccurred())
})
})
Expand Down
34 changes: 34 additions & 0 deletions pkg/builder/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ package builder

import (
"encoding/json"
"regexp"
"strings"

"github.com/cockroachdb/errors"
"github.com/containerd/containerd/platforms"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
Expand Down Expand Up @@ -58,3 +61,34 @@ func DefaultPathEnv(os string) string {
}
return DefaultPathEnvUnix
}

// parseOutput parses the output string and returns the output type and destination.
func parseOutput(output string) (string, string, error) {
if output == "" {
return "", "", nil
}

// Example: type=tar,dest=path
matched, err := regexp.Match(`^type=[\s\S]+,dest=[\s\S]+$`, []byte(output))
if err != nil {
return "", "", errors.Errorf("failed to match output: %v", err)
}
if !matched {
return "", "", errors.Errorf("unsupported format: %s", output)
}

fields := strings.Split(output, ",")
outputMap := make(map[string]string, len(fields))
for _, field := range fields {
pair := strings.Split(field, "=")
outputMap[pair[0]] = pair[1]
}

outputType := outputMap["type"]
outputDest := outputMap["dest"]
if outputType != "tar" {
return "", "", errors.Errorf("unsupported output type: %s", outputType)
}

return outputType, outputDest, nil
}
78 changes: 78 additions & 0 deletions pkg/builder/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 "testing"

func Test_parseOutput(t *testing.T) {
type args struct {
output string
}

tests := []struct {
name string
args args
outputType string
outputDest string
wantErr bool
}{{
"parsing output successfully",
args{
output: "type=tar,dest=test.tar",
},
"tar",
"test.tar",
false,
}, {
"output without type",
args{
output: "type=,dest=test.tar",
},
"",
"",
true,
}, {
"output without dest",
args{
output: "type=tar,dest=",
},
"",
"",
true,
}, {
"no output",
args{
output: "",
},
"",
"",
false,
}}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := parseOutput(tt.args.output)
if (err != nil) != tt.wantErr {
t.Errorf("parseOutput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.outputType {
t.Errorf("parseOutput() outputType = %v, want %v", got, tt.outputType)
}
if got1 != tt.outputDest {
t.Errorf("parseOutput() outputDest = %v, want %v", got1, tt.outputDest)
}
})
}
}

0 comments on commit 6160899

Please sign in to comment.