Merge pull request #1111 from tiborvass/experimental-buildkit

Support for experimental BuildKit
Upstream-commit: 2daec78609
Component: cli
This commit is contained in:
Andrew Hsu
2018-06-13 18:21:41 -07:00
committed by GitHub
93 changed files with 16400 additions and 310 deletions

View File

@ -16,6 +16,15 @@ const (
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
defaultBuildCacheVerboseFormat = `
ID: {{.ID}}
Description: {{.Description}}
Mutable: {{.Mutable}}
Size: {{.Size}}
CreatedAt: {{.CreatedAt}}
LastUsedAt: {{.LastUsedAt}}
UsageCount: {{.UsageCount}}
`
typeHeader = "TYPE"
totalHeader = "TOTAL"
@ -34,6 +43,7 @@ type DiskUsageContext struct {
Images []*types.ImageSummary
Containers []*types.Container
Volumes []*types.Volume
BuildCache []*types.BuildCache
BuilderSize int64
}
@ -100,6 +110,7 @@ func (ctx *DiskUsageContext) Write() (err error) {
err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{
builderSize: ctx.BuilderSize,
buildCache: ctx.BuildCache,
})
if err != nil {
return err
@ -184,6 +195,13 @@ func (ctx *DiskUsageContext) verboseWrite() error {
// And build cache
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
t := template.Must(template.New("buildcache").Parse(defaultBuildCacheVerboseFormat))
for _, v := range ctx.BuildCache {
t.Execute(ctx.Output, *v)
}
return nil
}
@ -366,6 +384,7 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
type diskUsageBuilderContext struct {
HeaderContext
builderSize int64
buildCache []*types.BuildCache
}
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
@ -377,11 +396,17 @@ func (c *diskUsageBuilderContext) Type() string {
}
func (c *diskUsageBuilderContext) TotalCount() string {
return ""
return fmt.Sprintf("%d", len(c.buildCache))
}
func (c *diskUsageBuilderContext) Active() string {
return ""
numActive := 0
for _, bc := range c.buildCache {
if bc.InUse {
numActive++
}
}
return fmt.Sprintf("%d", numActive)
}
func (c *diskUsageBuilderContext) Size() string {
@ -389,5 +414,12 @@ func (c *diskUsageBuilderContext) Size() string {
}
func (c *diskUsageBuilderContext) Reclaimable() string {
return c.Size()
var inUseBytes int64
for _, bc := range c.buildCache {
if bc.InUse {
inUseBytes += bc.Size
}
}
return units.HumanSize(float64(c.builderSize - inUseBytes))
}

View File

@ -25,7 +25,7 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0B 0B
Build Cache 0 0 0B 0B
`,
},
{
@ -76,7 +76,7 @@ Build cache usage: 0B
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0B 0B
Build Cache 0 0 0B 0B
`,
},
{

View File

@ -2,4 +2,4 @@ TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache
Build Cache 0

View File

@ -17,8 +17,8 @@ size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
total: 0
active: 0
size: 0B
reclaimable: 0B

View File

@ -35,6 +35,8 @@ import (
"github.com/spf13/cobra"
)
var errStdinConflict = errors.New("invalid argument: can't use stdin for both build context and dockerfile")
type buildOptions struct {
context string
dockerfileName string
@ -55,6 +57,7 @@ type buildOptions struct {
isolation string
quiet bool
noCache bool
console opts.NullableBool
rm bool
forceRm bool
pull bool
@ -149,6 +152,9 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation("stream", "experimental", nil)
flags.SetAnnotation("stream", "version", []string{"1.31"})
flags.Var(&options.console, "console", "Show console output (with buildkit only) (true, false, auto)")
flags.SetAnnotation("console", "experimental", nil)
flags.SetAnnotation("console", "version", []string{"1.38"})
return cmd
}
@ -170,6 +176,10 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
// nolint: gocyclo
func runBuild(dockerCli command.Cli, options buildOptions) error {
if os.Getenv("DOCKER_BUILDKIT") != "" {
return runBuildBuildKit(dockerCli, options)
}
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
@ -188,7 +198,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
if options.dockerfileFromStdin() {
if options.contextFromStdin() {
return errors.New("invalid argument: can't use stdin for both build context and dockerfile")
return errStdinConflict
}
dockerfileCtx = dockerCli.In()
}
@ -362,37 +372,11 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
configFile := dockerCli.ConfigFile()
authConfigs, _ := configFile.GetAllCredentials()
buildOptions := types.ImageBuildOptions{
Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(),
Tags: options.tags.GetAll(),
SuppressOutput: options.quiet,
NoCache: options.noCache,
Remove: options.rm,
ForceRemove: options.forceRm,
PullParent: options.pull,
Isolation: container.Isolation(options.isolation),
CPUSetCPUs: options.cpuSetCpus,
CPUSetMems: options.cpuSetMems,
CPUShares: options.cpuShares,
CPUQuota: options.cpuQuota,
CPUPeriod: options.cpuPeriod,
CgroupParent: options.cgroupParent,
Dockerfile: relDockerfile,
ShmSize: options.shmSize.Value(),
Ulimits: options.ulimits.GetList(),
BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
AuthConfigs: authConfigs,
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,
NetworkMode: options.networkMode,
Squash: options.squash,
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
RemoteContext: remote,
Platform: options.platform,
}
buildOptions := imageBuildOptions(dockerCli, options)
buildOptions.Version = types.BuilderV1
buildOptions.Dockerfile = relDockerfile
buildOptions.AuthConfigs = authConfigs
buildOptions.RemoteContext = remote
if s != nil {
go func() {
@ -416,9 +400,9 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
defer response.Body.Close()
imageID := ""
aux := func(m jsonmessage.JSONMessage) {
aux := func(msg jsonmessage.JSONMessage) {
var result types.BuildResult
if err := json.Unmarshal(*m.Aux, &result); err != nil {
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
} else {
imageID = result.ID
@ -602,3 +586,35 @@ func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.Rea
return pipeReader
}
func imageBuildOptions(dockerCli command.Cli, options buildOptions) types.ImageBuildOptions {
configFile := dockerCli.ConfigFile()
return types.ImageBuildOptions{
Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(),
Tags: options.tags.GetAll(),
SuppressOutput: options.quiet,
NoCache: options.noCache,
Remove: options.rm,
ForceRemove: options.forceRm,
PullParent: options.pull,
Isolation: container.Isolation(options.isolation),
CPUSetCPUs: options.cpuSetCpus,
CPUSetMems: options.cpuSetMems,
CPUShares: options.cpuShares,
CPUQuota: options.cpuQuota,
CPUPeriod: options.cpuPeriod,
CgroupParent: options.cgroupParent,
ShmSize: options.shmSize.Value(),
Ulimits: options.ulimits.GetList(),
BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,
NetworkMode: options.networkMode,
Squash: options.squash,
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
Platform: options.platform,
}
}

View File

@ -81,59 +81,82 @@ func ValidateContextDirectory(srcPath string, excludes []string) error {
})
}
// GetContextFromReader will read the contents of the given reader as either a
// Dockerfile or tar archive. Returns a tar archive used as a context and a
// path to the Dockerfile inside the tar.
func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
buf := bufio.NewReader(r)
// DetectArchiveReader detects whether the input stream is an archive or a
// Dockerfile and returns a buffered version of input, safe to consume in lieu
// of input. If an archive is detected, isArchive is set to true, and to false
// otherwise, in which case it is safe to assume input represents the contents
// of a Dockerfile.
func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, err error) {
buf := bufio.NewReader(input)
magic, err := buf.Peek(archiveHeaderSize)
if err != nil && err != io.EOF {
return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err)
return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err)
}
if IsArchive(magic) {
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil
}
// WriteTempDockerfile writes a Dockerfile stream to a temporary file with a
// name specified by DefaultDockerfileName and returns the path to the
// temporary directory containing the Dockerfile.
func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) {
// err is a named return value, due to the defer call below.
dockerfileDir, err = ioutil.TempDir("", "docker-build-tempdockerfile-")
if err != nil {
return "", errors.Errorf("unable to create temporary context directory: %v", err)
}
defer func() {
if err != nil {
os.RemoveAll(dockerfileDir)
}
}()
f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName))
if err != nil {
return "", err
}
defer f.Close()
if _, err := io.Copy(f, rc); err != nil {
return "", err
}
return dockerfileDir, rc.Close()
}
// GetContextFromReader will read the contents of the given reader as either a
// Dockerfile or tar archive. Returns a tar archive used as a context and a
// path to the Dockerfile inside the tar.
func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
rc, isArchive, err := DetectArchiveReader(rc)
if err != nil {
return nil, "", err
}
if isArchive {
return rc, dockerfileName, nil
}
// Input should be read as a Dockerfile.
if dockerfileName == "-" {
return nil, "", errors.New("build context is not an archive")
}
// Input should be read as a Dockerfile.
tmpDir, err := ioutil.TempDir("", "docker-build-context-")
if err != nil {
return nil, "", errors.Errorf("unable to create temporary context directory: %v", err)
}
f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName))
dockerfileDir, err := WriteTempDockerfile(rc)
if err != nil {
return nil, "", err
}
_, err = io.Copy(f, buf)
if err != nil {
f.Close()
return nil, "", err
}
if err := f.Close(); err != nil {
return nil, "", err
}
if err := r.Close(); err != nil {
return nil, "", err
}
tar, err := archive.Tar(tmpDir, archive.Uncompressed)
tar, err := archive.Tar(dockerfileDir, archive.Uncompressed)
if err != nil {
return nil, "", err
}
return ioutils.NewReadCloserWrapper(tar, func() error {
err := tar.Close()
os.RemoveAll(tmpDir)
os.RemoveAll(dockerfileDir)
return err
}), DefaultDockerfileName, nil
}
// IsArchive checks for the magic bytes of a tar or any supported compression

View File

@ -0,0 +1,300 @@
package image
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/containerd/console"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/urlutil"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil"
"golang.org/x/sync/errgroup"
)
const uploadRequestRemote = "upload-request"
var errDockerfileConflict = errors.New("ambiguous Dockerfile source: both stdin and flag correspond to Dockerfiles")
//nolint: gocyclo
func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
ctx := appcontext.Context()
s, err := trySession(dockerCli, options.context)
if err != nil {
return err
}
if s == nil {
return errors.Errorf("buildkit not supported by daemon")
}
var (
remote string
body io.Reader
dockerfileName = options.dockerfileName
dockerfileReader io.ReadCloser
dockerfileDir string
contextDir string
)
switch {
case options.contextFromStdin():
if options.dockerfileFromStdin() {
return errStdinConflict
}
rc, isArchive, err := build.DetectArchiveReader(os.Stdin)
if err != nil {
return err
}
if isArchive {
body = rc
remote = uploadRequestRemote
} else {
if options.dockerfileName != "" {
return errDockerfileConflict
}
dockerfileReader = rc
remote = clientSessionRemote
// TODO: make fssync handle empty contextdir
contextDir, _ = ioutil.TempDir("", "empty-dir")
defer os.RemoveAll(contextDir)
}
case isLocalDir(options.context):
contextDir = options.context
if options.dockerfileFromStdin() {
dockerfileReader = os.Stdin
} else if options.dockerfileName != "" {
dockerfileName = filepath.Base(options.dockerfileName)
dockerfileDir = filepath.Dir(options.dockerfileName)
} else {
dockerfileDir = options.context
}
remote = clientSessionRemote
case urlutil.IsGitURL(options.context):
remote = options.context
case urlutil.IsURL(options.context):
remote = options.context
default:
return errors.Errorf("unable to prepare context: path %q not found", options.context)
}
if dockerfileReader != nil {
dockerfileName = build.DefaultDockerfileName
dockerfileDir, err = build.WriteTempDockerfile(dockerfileReader)
if err != nil {
return err
}
defer os.RemoveAll(dockerfileDir)
}
if dockerfileDir != "" {
s.Allow(filesync.NewFSSyncProvider([]filesync.SyncedDir{
{
Name: "context",
Dir: contextDir,
Map: resetUIDAndGID,
},
{
Name: "dockerfile",
Dir: dockerfileDir,
},
}))
}
s.Allow(authprovider.NewDockerAuthProvider())
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
return s.Run(context.TODO(), dockerCli.Client().DialSession)
})
buildID := stringid.GenerateRandomID()
if body != nil {
eg.Go(func() error {
buildOptions := types.ImageBuildOptions{
Version: types.BuilderBuildKit,
BuildID: uploadRequestRemote + ":" + buildID,
}
response, err := dockerCli.Client().ImageBuild(context.Background(), body, buildOptions)
if err != nil {
return err
}
defer response.Body.Close()
return nil
})
}
eg.Go(func() error {
defer func() { // make sure the Status ends cleanly on build errors
s.Close()
}()
buildOptions := imageBuildOptions(dockerCli, options)
buildOptions.Version = types.BuilderBuildKit
buildOptions.Dockerfile = dockerfileName
//buildOptions.AuthConfigs = authConfigs // handled by session
buildOptions.RemoteContext = remote
buildOptions.SessionID = s.ID()
buildOptions.BuildID = buildID
return doBuild(ctx, eg, dockerCli, options, buildOptions)
})
return eg.Wait()
}
func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, options buildOptions, buildOptions types.ImageBuildOptions) (finalErr error) {
response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
if err != nil {
return err
}
defer response.Body.Close()
done := make(chan struct{})
defer close(done)
eg.Go(func() error {
select {
case <-ctx.Done():
return dockerCli.Client().BuildCancel(context.TODO(), buildOptions.BuildID)
case <-done:
}
return nil
})
t := newTracer()
ssArr := []*client.SolveStatus{}
displayStatus := func(displayCh chan *client.SolveStatus) {
var c console.Console
out := os.Stderr
// TODO: Handle interactive output in non-interactive environment.
consoleOpt := options.console.Value()
if cons, err := console.ConsoleFromFile(out); err == nil && (consoleOpt == nil || *consoleOpt) {
c = cons
}
// not using shared context to not disrupt display but let is finish reporting errors
eg.Go(func() error {
return progressui.DisplaySolveStatus(context.TODO(), c, out, displayCh)
})
}
if options.quiet {
eg.Go(func() error {
// TODO: make sure t.displayCh closes
for ss := range t.displayCh {
ssArr = append(ssArr, ss)
}
<-done
// TODO: verify that finalErr is indeed set when error occurs
if finalErr != nil {
displayCh := make(chan *client.SolveStatus)
go func() {
for _, ss := range ssArr {
displayCh <- ss
}
close(displayCh)
}()
displayStatus(displayCh)
}
return nil
})
} else {
displayStatus(t.displayCh)
}
defer close(t.displayCh)
err = jsonmessage.DisplayJSONMessagesStream(response.Body, os.Stdout, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), t.write)
if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1
if jerr.Code == 0 {
jerr.Code = 1
}
return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
}
}
return err
}
func resetUIDAndGID(s *fsutil.Stat) bool {
s.Uid = 0
s.Gid = 0
return true
}
type tracer struct {
displayCh chan *client.SolveStatus
}
func newTracer() *tracer {
return &tracer{
displayCh: make(chan *client.SolveStatus),
}
}
func (t *tracer) write(msg jsonmessage.JSONMessage) {
var resp controlapi.StatusResponse
if msg.ID != "moby.buildkit.trace" {
return
}
var dt []byte
// ignoring all messages that are not understood
if err := json.Unmarshal(*msg.Aux, &dt); err != nil {
return
}
if err := (&resp).Unmarshal(dt); err != nil {
return
}
s := client.SolveStatus{}
for _, v := range resp.Vertexes {
s.Vertexes = append(s.Vertexes, &client.Vertex{
Digest: v.Digest,
Inputs: v.Inputs,
Name: v.Name,
Started: v.Started,
Completed: v.Completed,
Error: v.Error,
Cached: v.Cached,
})
}
for _, v := range resp.Statuses {
s.Statuses = append(s.Statuses, &client.VertexStatus{
ID: v.ID,
Vertex: v.Vertex,
Name: v.Name,
Total: v.Total,
Current: v.Current,
Timestamp: v.Timestamp,
Started: v.Started,
Completed: v.Completed,
})
}
for _, v := range resp.Logs {
s.Logs = append(s.Logs, &client.VertexLog{
Vertex: v.Vertex,
Stream: int(v.Stream),
Data: v.Msg,
Timestamp: v.Timestamp,
})
}
t.displayCh <- &s
}

View File

@ -49,7 +49,7 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
// Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push.
cnt := 0
handleTarget := func(m jsonmessage.JSONMessage) {
handleTarget := func(msg jsonmessage.JSONMessage) {
cnt++
if cnt > 1 {
// handleTarget should only be called once. This will be treated as an error.
@ -57,7 +57,7 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
}
var pushResult types.PushResult
err := json.Unmarshal(*m.Aux, &pushResult)
err := json.Unmarshal(*msg.Aux, &pushResult)
if err == nil && pushResult.Tag != "" {
if dgst, err := digest.Parse(pushResult.Digest); err == nil {
h, err := hex.DecodeString(dgst.Hex())

View File

@ -59,6 +59,7 @@ func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error {
},
LayersSize: du.LayersSize,
BuilderSize: du.BuilderSize,
BuildCache: du.BuildCache,
Images: du.Images,
Containers: du.Containers,
Volumes: du.Volumes,