2017-05-15 17:14:31 -04:00
|
|
|
package image
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
"github.com/docker/cli/cli/command/image/build"
|
|
|
|
cliconfig "github.com/docker/cli/cli/config"
|
|
|
|
"github.com/docker/docker/api/types/versions"
|
|
|
|
"github.com/docker/docker/pkg/progress"
|
2017-08-07 05:52:40 -04:00
|
|
|
"github.com/moby/buildkit/session"
|
|
|
|
"github.com/moby/buildkit/session/filesync"
|
2017-05-15 17:14:31 -04:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"golang.org/x/time/rate"
|
|
|
|
)
|
|
|
|
|
|
|
|
const clientSessionRemote = "client-session"
|
|
|
|
|
2017-06-27 10:31:38 -04:00
|
|
|
func isSessionSupported(dockerCli command.Cli) bool {
|
2017-05-15 17:14:31 -04:00
|
|
|
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
|
|
|
|
}
|
|
|
|
|
2017-06-27 10:31:38 -04:00
|
|
|
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
|
2017-05-15 17:14:31 -04:00
|
|
|
var s *session.Session
|
|
|
|
if isSessionSupported(dockerCli) {
|
|
|
|
sharedKey, err := getBuildSharedKey(contextDir)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to get build shared key")
|
|
|
|
}
|
|
|
|
s, err = session.NewSession(filepath.Base(contextDir), sharedKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to create session")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func addDirToSession(session *session.Session, contextDir string, progressOutput progress.Output, done chan error) error {
|
|
|
|
excludes, err := build.ReadDockerignore(contextDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
|
|
|
|
|
|
|
|
workdirProvider := filesync.NewFSSyncProvider(contextDir, excludes)
|
|
|
|
session.Allow(workdirProvider)
|
|
|
|
|
|
|
|
// this will be replaced on parallel build jobs. keep the current
|
|
|
|
// progressbar for now
|
|
|
|
if snpc, ok := workdirProvider.(interface {
|
|
|
|
SetNextProgressCallback(func(int, bool), chan error)
|
|
|
|
}); ok {
|
|
|
|
snpc.SetNextProgressCallback(p.update, done)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type sizeProgress struct {
|
|
|
|
out progress.Output
|
|
|
|
action string
|
|
|
|
limiter *rate.Limiter
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sp *sizeProgress) update(size int, last bool) {
|
|
|
|
if sp.limiter == nil {
|
|
|
|
sp.limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
|
|
|
|
}
|
|
|
|
if last || sp.limiter.Allow() {
|
|
|
|
sp.out.WriteProgress(progress.Progress{Action: sp.action, Current: int64(size), LastUpdate: last})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type bufferedWriter struct {
|
|
|
|
done chan error
|
|
|
|
io.Writer
|
|
|
|
buf *bytes.Buffer
|
|
|
|
flushed chan struct{}
|
|
|
|
mu sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func newBufferedWriter(done chan error, w io.Writer) *bufferedWriter {
|
|
|
|
bw := &bufferedWriter{done: done, Writer: w, buf: new(bytes.Buffer), flushed: make(chan struct{})}
|
|
|
|
go func() {
|
|
|
|
<-done
|
|
|
|
bw.flushBuffer()
|
|
|
|
}()
|
|
|
|
return bw
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bw *bufferedWriter) Write(dt []byte) (int, error) {
|
|
|
|
select {
|
|
|
|
case <-bw.done:
|
|
|
|
bw.flushBuffer()
|
|
|
|
return bw.Writer.Write(dt)
|
|
|
|
default:
|
|
|
|
return bw.buf.Write(dt)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bw *bufferedWriter) flushBuffer() {
|
|
|
|
bw.mu.Lock()
|
|
|
|
select {
|
|
|
|
case <-bw.flushed:
|
|
|
|
default:
|
|
|
|
bw.Writer.Write(bw.buf.Bytes())
|
|
|
|
close(bw.flushed)
|
|
|
|
}
|
|
|
|
bw.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
func getBuildSharedKey(dir string) (string, error) {
|
|
|
|
// build session is hash of build dir with node based randomness
|
|
|
|
s := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", tryNodeIdentifier(), dir)))
|
|
|
|
return hex.EncodeToString(s[:]), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func tryNodeIdentifier() (out string) {
|
|
|
|
out = cliconfig.Dir() // return config dir as default on permission error
|
|
|
|
if err := os.MkdirAll(cliconfig.Dir(), 0700); err == nil {
|
|
|
|
sessionFile := filepath.Join(cliconfig.Dir(), ".buildNodeID")
|
|
|
|
if _, err := os.Lstat(sessionFile); err != nil {
|
|
|
|
if os.IsNotExist(err) { // create a new file with stored randomness
|
|
|
|
b := make([]byte, 32)
|
|
|
|
if _, err := rand.Read(b); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := ioutil.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dt, err := ioutil.ReadFile(sessionFile)
|
|
|
|
if err == nil {
|
|
|
|
return string(dt)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|