github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/command/image/build_session.go (about)

     1  package image
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/rand"
     7  	"crypto/sha256"
     8  	"encoding/hex"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/docker/cli/cli/command"
    18  	"github.com/docker/cli/cli/command/image/build"
    19  	cliconfig "github.com/docker/cli/cli/config"
    20  	"github.com/docker/docker/api/types/versions"
    21  	"github.com/docker/docker/pkg/progress"
    22  	"github.com/moby/buildkit/session"
    23  	"github.com/moby/buildkit/session/filesync"
    24  	"github.com/pkg/errors"
    25  	"golang.org/x/time/rate"
    26  )
    27  
    28  const clientSessionRemote = "client-session"
    29  
    30  func isSessionSupported(dockerCli command.Cli) bool {
    31  	if versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
    32  		return true
    33  	}
    34  	return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
    35  }
    36  
    37  func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
    38  	var s *session.Session
    39  	if isSessionSupported(dockerCli) {
    40  		sharedKey, err := getBuildSharedKey(contextDir)
    41  		if err != nil {
    42  			return nil, errors.Wrap(err, "failed to get build shared key")
    43  		}
    44  		s, err = session.NewSession(context.Background(), filepath.Base(contextDir), sharedKey)
    45  		if err != nil {
    46  			return nil, errors.Wrap(err, "failed to create session")
    47  		}
    48  	}
    49  	return s, nil
    50  }
    51  
    52  func addDirToSession(session *session.Session, contextDir string, progressOutput progress.Output, done chan error) error {
    53  	excludes, err := build.ReadDockerignore(contextDir)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
    59  
    60  	workdirProvider := filesync.NewFSSyncProvider([]filesync.SyncedDir{
    61  		{Dir: contextDir, Excludes: excludes},
    62  	})
    63  	session.Allow(workdirProvider)
    64  
    65  	// this will be replaced on parallel build jobs. keep the current
    66  	// progressbar for now
    67  	if snpc, ok := workdirProvider.(interface {
    68  		SetNextProgressCallback(func(int, bool), chan error)
    69  	}); ok {
    70  		snpc.SetNextProgressCallback(p.update, done)
    71  	}
    72  
    73  	return nil
    74  }
    75  
    76  type sizeProgress struct {
    77  	out     progress.Output
    78  	action  string
    79  	limiter *rate.Limiter
    80  }
    81  
    82  func (sp *sizeProgress) update(size int, last bool) {
    83  	if sp.limiter == nil {
    84  		sp.limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
    85  	}
    86  	if last || sp.limiter.Allow() {
    87  		sp.out.WriteProgress(progress.Progress{Action: sp.action, Current: int64(size), LastUpdate: last})
    88  	}
    89  }
    90  
    91  type bufferedWriter struct {
    92  	done chan error
    93  	io.Writer
    94  	buf     *bytes.Buffer
    95  	flushed chan struct{}
    96  	mu      sync.Mutex
    97  }
    98  
    99  func newBufferedWriter(done chan error, w io.Writer) *bufferedWriter {
   100  	bw := &bufferedWriter{done: done, Writer: w, buf: new(bytes.Buffer), flushed: make(chan struct{})}
   101  	go func() {
   102  		<-done
   103  		bw.flushBuffer()
   104  	}()
   105  	return bw
   106  }
   107  
   108  func (bw *bufferedWriter) Write(dt []byte) (int, error) {
   109  	select {
   110  	case <-bw.done:
   111  		bw.flushBuffer()
   112  		return bw.Writer.Write(dt)
   113  	default:
   114  		return bw.buf.Write(dt)
   115  	}
   116  }
   117  
   118  func (bw *bufferedWriter) flushBuffer() {
   119  	bw.mu.Lock()
   120  	select {
   121  	case <-bw.flushed:
   122  	default:
   123  		bw.Writer.Write(bw.buf.Bytes())
   124  		close(bw.flushed)
   125  	}
   126  	bw.mu.Unlock()
   127  }
   128  
   129  func (bw *bufferedWriter) String() string {
   130  	return fmt.Sprintf("%s", bw.Writer)
   131  }
   132  
   133  func getBuildSharedKey(dir string) (string, error) {
   134  	// build session is hash of build dir with node based randomness
   135  	s := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", tryNodeIdentifier(), dir)))
   136  	return hex.EncodeToString(s[:]), nil
   137  }
   138  
   139  func tryNodeIdentifier() string {
   140  	out := cliconfig.Dir() // return config dir as default on permission error
   141  	if err := os.MkdirAll(cliconfig.Dir(), 0700); err == nil {
   142  		sessionFile := filepath.Join(cliconfig.Dir(), ".buildNodeID")
   143  		if _, err := os.Lstat(sessionFile); err != nil {
   144  			if os.IsNotExist(err) { // create a new file with stored randomness
   145  				b := make([]byte, 32)
   146  				if _, err := rand.Read(b); err != nil {
   147  					return out
   148  				}
   149  				if err := ioutil.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
   150  					return out
   151  				}
   152  			}
   153  		}
   154  
   155  		dt, err := ioutil.ReadFile(sessionFile)
   156  		if err == nil {
   157  			return string(dt)
   158  		}
   159  	}
   160  	return out
   161  }