Merge pull request #78 from tonistiigi/multiple-local-sources

source: add multiple dir support for local source
docker-18.09
Akihiro Suda 2017-07-21 15:34:44 +09:00 committed by GitHub
commit f533e5e373
8 changed files with 96 additions and 55 deletions

View File

@ -59,6 +59,7 @@ Different versions of the example scripts show different ways of describing the
- `./examples/buildkit0` - uses only exec operations, defines a full stage per component. - `./examples/buildkit0` - uses only exec operations, defines a full stage per component.
- `./examples/buildkit1` - cloning git repositories has been separated for extra concurrency. - `./examples/buildkit1` - cloning git repositories has been separated for extra concurrency.
- `./examples/buildkit2` - uses git sources directly instead of running `git clone`, allowing better performance and much safer caching. - `./examples/buildkit2` - uses git sources directly instead of running `git clone`, allowing better performance and much safer caching.
- `./examples/buildkit3` - allows using local source files for separate components eg. `./buildkit3 --runc=local | buildctl build --local runc-src=some/local/path`
#### Supported runc version #### Supported runc version

View File

@ -106,6 +106,6 @@ func testBuildMultiMount(t *testing.T, address string) {
err = llb.WriteTo(dt, buf) err = llb.WriteTo(dt, buf)
assert.Nil(t, err) assert.Nil(t, err)
err = c.Solve(context.TODO(), buf, nil, "", nil, "") err = c.Solve(context.TODO(), buf, SolveOpt{}, nil)
assert.Nil(t, err) assert.Nil(t, err)
} }

View File

@ -21,7 +21,15 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func (c *Client) Solve(ctx context.Context, r io.Reader, statusChan chan *SolveStatus, exporter string, exporterAttrs map[string]string, localDir string) error { type SolveOpt struct {
Exporter string
ExporterAttrs map[string]string
LocalDirs map[string]string
SharedKey string
// Session string
}
func (c *Client) Solve(ctx context.Context, r io.Reader, opt SolveOpt, statusChan chan *SolveStatus) error {
defer func() { defer func() {
if statusChan != nil { if statusChan != nil {
close(statusChan) close(statusChan)
@ -37,7 +45,8 @@ func (c *Client) Solve(ctx context.Context, r io.Reader, statusChan chan *SolveS
return errors.New("invalid empty definition") return errors.New("invalid empty definition")
} }
if err := validateLocals(def, localDir); err != nil { syncedDirs, err := prepareSyncedDirs(def, opt.LocalDirs)
if err != nil {
return err return err
} }
@ -47,19 +56,13 @@ func (c *Client) Solve(ctx context.Context, r io.Reader, statusChan chan *SolveS
statusContext, cancelStatus := context.WithCancel(context.Background()) statusContext, cancelStatus := context.WithCancel(context.Background())
defer cancelStatus() defer cancelStatus()
sharedKey, err := getSharedKey(localDir) s, err := session.NewSession(defaultSessionName(), opt.SharedKey)
if err != nil {
return errors.Wrap(err, "failed to get build shared key")
}
s, err := session.NewSession(filepath.Base(localDir), sharedKey)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create session") return errors.Wrap(err, "failed to create session")
} }
if localDir != "" { if len(syncedDirs) > 0 {
_, dir, _ := parseLocalDir(localDir) s.Allow(filesync.NewFSSyncProvider(syncedDirs))
workdirProvider := filesync.NewFSSyncProvider(dir, nil)
s.Allow(workdirProvider)
} }
eg.Go(func() error { eg.Go(func() error {
@ -78,8 +81,8 @@ func (c *Client) Solve(ctx context.Context, r io.Reader, statusChan chan *SolveS
_, err = c.controlClient().Solve(ctx, &controlapi.SolveRequest{ _, err = c.controlClient().Solve(ctx, &controlapi.SolveRequest{
Ref: ref, Ref: ref,
Definition: def, Definition: def,
Exporter: exporter, Exporter: opt.Exporter,
ExporterAttrs: exporterAttrs, ExporterAttrs: opt.ExporterAttrs,
Session: s.ID(), Session: s.ID(),
}) })
if err != nil { if err != nil {
@ -152,43 +155,40 @@ func generateID() string {
return hex.EncodeToString(b) return hex.EncodeToString(b)
} }
func validateLocals(defs [][]byte, localDir string) error { func prepareSyncedDirs(defs [][]byte, localDirs map[string]string) ([]filesync.SyncedDir, error) {
k, _, err := parseLocalDir(localDir) for _, d := range localDirs {
if err != nil { fi, err := os.Stat(d)
return err if err != nil {
return nil, errors.Wrapf(err, "could not find %s", d)
}
if !fi.IsDir() {
return nil, errors.Errorf("%s not a directory", d)
}
} }
dirs := make([]filesync.SyncedDir, 0, len(localDirs))
for _, dt := range defs { for _, dt := range defs {
var op pb.Op var op pb.Op
if err := (&op).Unmarshal(dt); err != nil { if err := (&op).Unmarshal(dt); err != nil {
return errors.Wrap(err, "failed to parse llb proto op") return nil, errors.Wrap(err, "failed to parse llb proto op")
} }
if src := op.GetSource(); src != nil { if src := op.GetSource(); src != nil {
if strings.HasPrefix(src.Identifier, "local://") { // TODO: just make a type property if strings.HasPrefix(src.Identifier, "local://") { // TODO: just make a type property
name := strings.TrimPrefix(src.Identifier, "local://") name := strings.TrimPrefix(src.Identifier, "local://")
if name != k { d, ok := localDirs[name]
return errors.Errorf("local directory %s not enabled", name) if !ok {
return nil, errors.Errorf("local directory %s not enabled", name)
} }
dirs = append(dirs, filesync.SyncedDir{Name: name, Dir: d}) // TODO: excludes
} }
} }
} }
return nil return dirs, nil
} }
func parseLocalDir(str string) (string, string, error) { func defaultSessionName() string {
if str == "" { wd, err := os.Getwd()
return "", "", nil
}
parts := strings.SplitN(str, "=", 2)
if len(parts) != 2 {
return "", "", errors.Errorf("invalid local indentifier %q, need name=dir", str)
}
fi, err := os.Stat(parts[1])
if err != nil { if err != nil {
return "", "", errors.Wrapf(err, "could not find %s", parts[1]) return "unknown"
} }
if !fi.IsDir() { return filepath.Base(wd)
return "", "", errors.Errorf("%s not a directory", parts[1])
}
return parts[0], parts[1], nil
} }

View File

@ -33,7 +33,7 @@ var buildCommand = cli.Command{
Name: "no-progress", Name: "no-progress",
Usage: "Don't show interactive progress", Usage: "Don't show interactive progress",
}, },
cli.StringFlag{ cli.StringSliceFlag{
Name: "local", Name: "local",
Usage: "Allow build access to the local directory", Usage: "Allow build access to the local directory",
}, },
@ -64,8 +64,17 @@ func build(clicontext *cli.Context) error {
return errors.Wrap(err, "invalid exporter-opt") return errors.Wrap(err, "invalid exporter-opt")
} }
localDirs, err := attrMap(clicontext.StringSlice("local"))
if err != nil {
return errors.Wrap(err, "invalid local")
}
eg.Go(func() error { eg.Go(func() error {
return c.Solve(ctx, os.Stdin, ch, clicontext.String("exporter"), exporterAttrs, clicontext.String("local")) return c.Solve(ctx, os.Stdin, client.SolveOpt{
Exporter: clicontext.String("exporter"),
ExporterAttrs: exporterAttrs,
LocalDirs: localDirs,
}, ch)
}) })
eg.Go(func() error { eg.Go(func() error {

View File

@ -12,7 +12,7 @@ type buildOpt struct {
target string target string
containerd string containerd string
runc string runc string
local bool buildkit string
} }
func main() { func main() {
@ -20,7 +20,7 @@ func main() {
flag.StringVar(&opt.target, "target", "containerd", "target (standalone, containerd)") flag.StringVar(&opt.target, "target", "containerd", "target (standalone, containerd)")
flag.StringVar(&opt.containerd, "containerd", "master", "containerd version") flag.StringVar(&opt.containerd, "containerd", "master", "containerd version")
flag.StringVar(&opt.runc, "runc", "v1.0.0-rc3", "runc version") flag.StringVar(&opt.runc, "runc", "v1.0.0-rc3", "runc version")
flag.BoolVar(&opt.local, "local", false, "use local buildkit source") flag.StringVar(&opt.buildkit, "buildkit", "master", "buildkit version")
flag.Parse() flag.Parse()
bk := buildkit(opt) bk := buildkit(opt)
@ -52,17 +52,25 @@ func goRepo(s *llb.State, repo string, src *llb.State) func(ro ...llb.RunOption)
func runc(version string) *llb.State { func runc(version string) *llb.State {
repo := "github.com/opencontainers/runc" repo := "github.com/opencontainers/runc"
return goRepo(goBuildBase(), repo, llb.Git(repo, version))( src := llb.Git(repo, version)
if version == "local" {
src = llb.Local("runc-src")
}
return goRepo(goBuildBase(), repo, src)(
llb.Shlex("go build -o /out/runc ./"), llb.Shlex("go build -o /out/runc ./"),
) )
} }
func containerd(version string) *llb.State { func containerd(version string) *llb.State {
repo := "github.com/containerd/containerd" repo := "github.com/containerd/containerd"
src := llb.Git(repo, version, llb.KeepGitDir())
if version == "local" {
src = llb.Local("containerd-src")
}
return goRepo( return goRepo(
goBuildBase(). goBuildBase().
Run(llb.Shlex("apk add --no-cache btrfs-progs-dev")).Root(), Run(llb.Shlex("apk add --no-cache btrfs-progs-dev")).Root(),
repo, llb.Git(repo, version, llb.KeepGitDir()))( repo, src)(
llb.Shlex("go build -o /out/containerd ./cmd/containerd"), llb.Shlex("go build -o /out/containerd ./cmd/containerd"),
) )
} }
@ -70,7 +78,7 @@ func containerd(version string) *llb.State {
func buildkit(opt buildOpt) *llb.State { func buildkit(opt buildOpt) *llb.State {
repo := "github.com/moby/buildkit" repo := "github.com/moby/buildkit"
src := llb.Git(repo, "master") src := llb.Git(repo, "master")
if opt.local { if opt.buildkit == "local" {
src = llb.Local("buildkit-src") src = llb.Local("buildkit-src")
} }
run := goRepo(goBuildBase(), repo, src) run := goRepo(goBuildBase(), repo, src)
@ -109,6 +117,6 @@ func copyFrom(src *llb.State, srcPath, destPath string) llb.StateOption {
func copy(src *llb.State, srcPath string, dest *llb.State, destPath string) *llb.State { func copy(src *llb.State, srcPath string, dest *llb.State, destPath string) *llb.State {
cpImage := llb.Image("docker.io/library/alpine:latest") cpImage := llb.Image("docker.io/library/alpine:latest")
cp := cpImage.Run(llb.Shlexf("cp -a /src%s /dest%s", srcPath, destPath)) cp := cpImage.Run(llb.Shlexf("cp -a /src%s /dest%s", srcPath, destPath))
cp.AddMount("/src", src) cp.AddMount("/src", src, llb.Readonly)
return cp.AddMount("/dest", dest) return cp.AddMount("/dest", dest)
} }

View File

@ -16,20 +16,28 @@ import (
const ( const (
keyOverrideExcludes = "override-excludes" keyOverrideExcludes = "override-excludes"
keyIncludePatterns = "include-patterns" keyIncludePatterns = "include-patterns"
keyDirName = "dir-name"
) )
type fsSyncProvider struct { type fsSyncProvider struct {
root string dirs map[string]SyncedDir
excludes []string p progressCb
p progressCb doneCh chan error
doneCh chan error }
type SyncedDir struct {
Name string
Dir string
Excludes []string
} }
// NewFSSyncProvider creates a new provider for sending files from client // NewFSSyncProvider creates a new provider for sending files from client
func NewFSSyncProvider(root string, excludes []string) session.Attachable { func NewFSSyncProvider(dirs []SyncedDir) session.Attachable {
p := &fsSyncProvider{ p := &fsSyncProvider{
root: root, dirs: map[string]SyncedDir{},
excludes: excludes, }
for _, d := range dirs {
p.dirs[d.Name] = d
} }
return p return p
} }
@ -59,9 +67,19 @@ func (sp *fsSyncProvider) handle(method string, stream grpc.ServerStream) error
opts, _ := metadata.FromContext(stream.Context()) // if no metadata continue with empty object opts, _ := metadata.FromContext(stream.Context()) // if no metadata continue with empty object
name, ok := opts[keyDirName]
if !ok || len(name) != 1 {
return errors.New("no dir name in request")
}
dir, ok := sp.dirs[name[0]]
if !ok {
return errors.Errorf("no access allowed to dir %q", name[0])
}
var excludes []string var excludes []string
if len(opts[keyOverrideExcludes]) == 0 || opts[keyOverrideExcludes][0] != "true" { if len(opts[keyOverrideExcludes]) == 0 || opts[keyOverrideExcludes][0] != "true" {
excludes = sp.excludes excludes = dir.Excludes
} }
includes := opts[keyIncludePatterns] includes := opts[keyIncludePatterns]
@ -76,7 +94,7 @@ func (sp *fsSyncProvider) handle(method string, stream grpc.ServerStream) error
doneCh = sp.doneCh doneCh = sp.doneCh
sp.doneCh = nil sp.doneCh = nil
} }
err := pr.sendFn(stream, sp.root, includes, excludes, progress) err := pr.sendFn(stream, dir.Dir, includes, excludes, progress)
if doneCh != nil { if doneCh != nil {
if err != nil { if err != nil {
doneCh <- err doneCh <- err
@ -122,6 +140,7 @@ var supportedProtocols = []protocol{
// FSSendRequestOpt defines options for FSSend request // FSSendRequestOpt defines options for FSSend request
type FSSendRequestOpt struct { type FSSendRequestOpt struct {
Name string
IncludePatterns []string IncludePatterns []string
OverrideExcludes bool OverrideExcludes bool
DestDir string DestDir string
@ -156,6 +175,8 @@ func FSSync(ctx context.Context, c session.Caller, opt FSSendRequestOpt) error {
opts[keyIncludePatterns] = opt.IncludePatterns opts[keyIncludePatterns] = opt.IncludePatterns
} }
opts[keyDirName] = []string{opt.Name}
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()

View File

@ -32,7 +32,7 @@ func TestFileSyncIncludePatterns(t *testing.T) {
m, err := session.NewManager() m, err := session.NewManager()
require.NoError(t, err) require.NoError(t, err)
fs := NewFSSyncProvider(tmpDir, nil) fs := NewFSSyncProvider([]SyncedDir{{Name: "test0", Dir: tmpDir}})
s.Allow(fs) s.Allow(fs)
dialer := session.Dialer(testutil.TestStream(testutil.Handler(m.HandleConn))) dialer := session.Dialer(testutil.TestStream(testutil.Handler(m.HandleConn)))
@ -49,6 +49,7 @@ func TestFileSyncIncludePatterns(t *testing.T) {
return err return err
} }
if err := FSSync(ctx, c, FSSendRequestOpt{ if err := FSSync(ctx, c, FSSendRequestOpt{
Name: "test0",
DestDir: destDir, DestDir: destDir,
IncludePatterns: []string{"ba*"}, IncludePatterns: []string{"ba*"},
}); err != nil { }); err != nil {

View File

@ -137,6 +137,7 @@ func (ls *localSourceHandler) Snapshot(ctx context.Context) (out cache.Immutable
}() }()
opt := filesync.FSSendRequestOpt{ opt := filesync.FSSendRequestOpt{
Name: ls.src.Name,
IncludePatterns: nil, IncludePatterns: nil,
OverrideExcludes: false, OverrideExcludes: false,
DestDir: dest, DestDir: dest,