exporter: add tar exporter
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>docker-19.03
parent
33bb70c810
commit
c1a1d7033d
|
@ -184,6 +184,14 @@ The local client will copy the files directly to the client. This is useful if B
|
||||||
buildctl build ... --output type=local,dest=path/to/output-dir
|
buildctl build ... --output type=local,dest=path/to/output-dir
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tar exporter is similar to local exporter but transfers the files through a tarball.
|
||||||
|
|
||||||
|
```
|
||||||
|
buildctl build ... --output type=tar,dest=out.tar
|
||||||
|
buildctl build ... --output type=tar > out.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
##### Exporting built image to Docker
|
##### Exporting built image to Docker
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -3,6 +3,7 @@ package client
|
||||||
const (
|
const (
|
||||||
ExporterImage = "image"
|
ExporterImage = "image"
|
||||||
ExporterLocal = "local"
|
ExporterLocal = "local"
|
||||||
|
ExporterTar = "tar"
|
||||||
ExporterOCI = "oci"
|
ExporterOCI = "oci"
|
||||||
ExporterDocker = "docker"
|
ExporterDocker = "docker"
|
||||||
)
|
)
|
||||||
|
|
|
@ -124,7 +124,7 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
|
||||||
return nil, errors.New("output directory is required for local exporter")
|
return nil, errors.New("output directory is required for local exporter")
|
||||||
}
|
}
|
||||||
s.Allow(filesync.NewFSSyncTargetDir(ex.OutputDir))
|
s.Allow(filesync.NewFSSyncTargetDir(ex.OutputDir))
|
||||||
case ExporterOCI, ExporterDocker:
|
case ExporterOCI, ExporterDocker, ExporterTar:
|
||||||
if ex.OutputDir != "" {
|
if ex.OutputDir != "" {
|
||||||
return nil, errors.Errorf("output directory %s is not supported by %s exporter", ex.OutputDir, ex.Type)
|
return nil, errors.Errorf("output directory %s is not supported by %s exporter", ex.OutputDir, ex.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,8 +95,8 @@ func resolveExporterDest(exporter, dest string) (io.WriteCloser, string, error)
|
||||||
return nil, "", errors.New("output directory is required for local exporter")
|
return nil, "", errors.New("output directory is required for local exporter")
|
||||||
}
|
}
|
||||||
return nil, dest, nil
|
return nil, dest, nil
|
||||||
case client.ExporterOCI, client.ExporterDocker:
|
case client.ExporterOCI, client.ExporterDocker, client.ExporterTar:
|
||||||
if dest != "" {
|
if dest != "" && dest != "-" {
|
||||||
fi, err := os.Stat(dest)
|
fi, err := os.Stat(dest)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return nil, "", errors.Wrapf(err, "invalid destination file: %s", dest)
|
return nil, "", errors.Wrapf(err, "invalid destination file: %s", dest)
|
||||||
|
|
|
@ -93,10 +93,13 @@ func (e *localExporterInstance) Export(ctx context.Context, inp exporter.Source)
|
||||||
lbl := "copying files"
|
lbl := "copying files"
|
||||||
if isMap {
|
if isMap {
|
||||||
lbl += " " + k
|
lbl += " " + k
|
||||||
fs = fsutil.SubDirFS(fs, fstypes.Stat{
|
fs, err = fsutil.SubDirFS([]fsutil.Dir{{FS: fs, Stat: fstypes.Stat{
|
||||||
Mode: uint32(os.ModeDir | 0755),
|
Mode: uint32(os.ModeDir | 0755),
|
||||||
Path: strings.Replace(k, "/", "_", -1),
|
Path: strings.Replace(k, "/", "_", -1),
|
||||||
})
|
}}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress := newProgressHandler(ctx, lbl)
|
progress := newProgressHandler(ctx, lbl)
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/moby/buildkit/cache"
|
||||||
|
"github.com/moby/buildkit/exporter"
|
||||||
|
"github.com/moby/buildkit/session"
|
||||||
|
"github.com/moby/buildkit/session/filesync"
|
||||||
|
"github.com/moby/buildkit/snapshot"
|
||||||
|
"github.com/moby/buildkit/util/progress"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tonistiigi/fsutil"
|
||||||
|
fstypes "github.com/tonistiigi/fsutil/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Opt struct {
|
||||||
|
SessionManager *session.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
type localExporter struct {
|
||||||
|
opt Opt
|
||||||
|
// session manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opt Opt) (exporter.Exporter, error) {
|
||||||
|
le := &localExporter{opt: opt}
|
||||||
|
return le, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) {
|
||||||
|
id := session.FromContext(ctx)
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.New("could not access local files without session")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
caller, err := e.opt.SessionManager.Get(timeoutCtx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
li := &localExporterInstance{localExporter: e, caller: caller}
|
||||||
|
return li, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type localExporterInstance struct {
|
||||||
|
*localExporter
|
||||||
|
caller session.Caller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *localExporterInstance) Name() string {
|
||||||
|
return "exporting to client"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *localExporterInstance) Export(ctx context.Context, inp exporter.Source) (map[string]string, error) {
|
||||||
|
var defers []func()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for i := len(defers) - 1; i >= 0; i-- {
|
||||||
|
defers[i]()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
getDir := func(ctx context.Context, k string, ref cache.ImmutableRef) (*fsutil.Dir, error) {
|
||||||
|
var src string
|
||||||
|
var err error
|
||||||
|
if ref == nil {
|
||||||
|
src, err = ioutil.TempDir("", "buildkit")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defers = append(defers, func() { os.RemoveAll(src) })
|
||||||
|
} else {
|
||||||
|
mount, err := ref.Mount(ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lm := snapshot.LocalMounter(mount)
|
||||||
|
|
||||||
|
src, err = lm.Mount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defers = append(defers, func() { lm.Unmount() })
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fsutil.Dir{
|
||||||
|
FS: fsutil.NewFS(src, nil),
|
||||||
|
Stat: fstypes.Stat{
|
||||||
|
Mode: uint32(os.ModeDir | 0755),
|
||||||
|
Path: strings.Replace(k, "/", "_", -1),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fs fsutil.FS
|
||||||
|
|
||||||
|
if len(inp.Refs) > 0 {
|
||||||
|
dirs := make([]fsutil.Dir, 0, len(inp.Refs))
|
||||||
|
for k, ref := range inp.Refs {
|
||||||
|
d, err := getDir(ctx, k, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dirs = append(dirs, *d)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
fs, err = fsutil.SubDirFS(dirs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d, err := getDir(ctx, "", inp.Ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fs = d.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := filesync.CopyFileWriter(ctx, e.caller)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
report := oneOffProgress(ctx, "sending tarball")
|
||||||
|
if err := fsutil.WriteTar(ctx, fs, w); err != nil {
|
||||||
|
w.Close()
|
||||||
|
return nil, report(err)
|
||||||
|
}
|
||||||
|
return nil, report(w.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func oneOffProgress(ctx context.Context, id string) func(err error) error {
|
||||||
|
pw, _, _ := progress.FromContext(ctx)
|
||||||
|
now := time.Now()
|
||||||
|
st := progress.Status{
|
||||||
|
Started: &now,
|
||||||
|
}
|
||||||
|
pw.Write(id, st)
|
||||||
|
return func(err error) error {
|
||||||
|
// TODO: set error on status
|
||||||
|
now := time.Now()
|
||||||
|
st.Completed = &now
|
||||||
|
pw.Write(id, st)
|
||||||
|
pw.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -84,6 +85,7 @@ var allTests = []integration.Test{
|
||||||
testCopyChownExistingDir,
|
testCopyChownExistingDir,
|
||||||
testCopyWildcardCache,
|
testCopyWildcardCache,
|
||||||
testDockerignoreOverride,
|
testDockerignoreOverride,
|
||||||
|
testTarExporter,
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileOpTests = []integration.Test{
|
var fileOpTests = []integration.Test{
|
||||||
|
@ -235,6 +237,84 @@ RUN [ "$(cat testfile)" == "contents0" ]
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTarExporter(t *testing.T, sb integration.Sandbox) {
|
||||||
|
f := getFrontend(t, sb)
|
||||||
|
|
||||||
|
dockerfile := []byte(`
|
||||||
|
FROM scratch AS stage-linux
|
||||||
|
COPY foo forlinux
|
||||||
|
|
||||||
|
FROM scratch AS stage-darwin
|
||||||
|
COPY bar fordarwin
|
||||||
|
|
||||||
|
FROM stage-$TARGETOS
|
||||||
|
`)
|
||||||
|
|
||||||
|
dir, err := tmpdir(
|
||||||
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||||
|
fstest.CreateFile("foo", []byte("data"), 0600),
|
||||||
|
fstest.CreateFile("bar", []byte("data2"), 0600),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
c, err := client.New(context.TODO(), sb.Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
_, err = f.Solve(context.TODO(), c, client.SolveOpt{
|
||||||
|
Exports: []client.ExportEntry{
|
||||||
|
{
|
||||||
|
Type: client.ExporterTar,
|
||||||
|
Output: &nopWriteCloser{buf},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LocalDirs: map[string]string{
|
||||||
|
builder.DefaultLocalNameDockerfile: dir,
|
||||||
|
builder.DefaultLocalNameContext: dir,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m, err := testutil.ReadTarToMap(buf.Bytes(), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mi, ok := m["forlinux"]
|
||||||
|
require.Equal(t, true, ok)
|
||||||
|
require.Equal(t, "data", string(mi.Data))
|
||||||
|
|
||||||
|
// repeat multi-platform
|
||||||
|
buf = &bytes.Buffer{}
|
||||||
|
_, err = f.Solve(context.TODO(), c, client.SolveOpt{
|
||||||
|
Exports: []client.ExportEntry{
|
||||||
|
{
|
||||||
|
Type: client.ExporterTar,
|
||||||
|
Output: &nopWriteCloser{buf},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FrontendAttrs: map[string]string{
|
||||||
|
"platform": "linux/amd64,darwin/amd64",
|
||||||
|
},
|
||||||
|
LocalDirs: map[string]string{
|
||||||
|
builder.DefaultLocalNameDockerfile: dir,
|
||||||
|
builder.DefaultLocalNameContext: dir,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m, err = testutil.ReadTarToMap(buf.Bytes(), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mi, ok = m["linux_amd64/forlinux"]
|
||||||
|
require.Equal(t, true, ok)
|
||||||
|
require.Equal(t, "data", string(mi.Data))
|
||||||
|
|
||||||
|
mi, ok = m["darwin_amd64/fordarwin"]
|
||||||
|
require.Equal(t, true, ok)
|
||||||
|
require.Equal(t, "data2", string(mi.Data))
|
||||||
|
}
|
||||||
|
|
||||||
func testWorkdirCreatesDir(t *testing.T, sb integration.Sandbox) {
|
func testWorkdirCreatesDir(t *testing.T, sb integration.Sandbox) {
|
||||||
f := getFrontend(t, sb)
|
f := getFrontend(t, sb)
|
||||||
|
|
||||||
|
@ -3824,3 +3904,9 @@ func getFileOp(t *testing.T, sb integration.Sandbox) bool {
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
return vv
|
return vv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nopWriteCloser struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nopWriteCloser) Close() error { return nil }
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -49,7 +49,7 @@ require (
|
||||||
github.com/sirupsen/logrus v1.3.0
|
github.com/sirupsen/logrus v1.3.0
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8 // indirect
|
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8 // indirect
|
||||||
github.com/tonistiigi/fsutil v0.0.0-20190319020005-1bdbf124ad49
|
github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76
|
||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea
|
||||||
github.com/uber/jaeger-client-go v0.0.0-20180103221425-e02c85f9069e
|
github.com/uber/jaeger-client-go v0.0.0-20180103221425-e02c85f9069e
|
||||||
github.com/uber/jaeger-lib v1.2.1 // indirect
|
github.com/uber/jaeger-lib v1.2.1 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -128,8 +128,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8 h1:zLV6q4e8Jv9EHjNg/iHfzwDkCve6Ua5jCygptrtXHvI=
|
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8 h1:zLV6q4e8Jv9EHjNg/iHfzwDkCve6Ua5jCygptrtXHvI=
|
||||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||||
github.com/tonistiigi/fsutil v0.0.0-20190319020005-1bdbf124ad49 h1:UFQ7uDVXIH4fFfOb+fISgTl8Ukk0CkGQudHQh980l+0=
|
github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76 h1:eGfgYrNUSD448sa4mxH6nQpyZfN39QH0mLB7QaKIjus=
|
||||||
github.com/tonistiigi/fsutil v0.0.0-20190319020005-1bdbf124ad49/go.mod h1:pzh7kdwkDRh+Bx8J30uqaKJ1M4QrSH/um8fcIXeM8rc=
|
github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76/go.mod h1:pzh7kdwkDRh+Bx8J30uqaKJ1M4QrSH/um8fcIXeM8rc=
|
||||||
github.com/tonistiigi/go-immutable-radix v0.0.0-20170803185627-826af9ccf0fe h1:pd7hrFSqUPxYS9IB+UMG1AB/8EXGXo17ssx0bSQ5L6Y=
|
github.com/tonistiigi/go-immutable-radix v0.0.0-20170803185627-826af9ccf0fe h1:pd7hrFSqUPxYS9IB+UMG1AB/8EXGXo17ssx0bSQ5L6Y=
|
||||||
github.com/tonistiigi/go-immutable-radix v0.0.0-20170803185627-826af9ccf0fe/go.mod h1:/+MCh11CJf2oz0BXmlmqyopK/ad1rKkcOXPoYuPCJYU=
|
github.com/tonistiigi/go-immutable-radix v0.0.0-20170803185627-826af9ccf0fe/go.mod h1:/+MCh11CJf2oz0BXmlmqyopK/ad1rKkcOXPoYuPCJYU=
|
||||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
||||||
|
|
|
@ -3,9 +3,11 @@ package fsutil
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -37,36 +39,80 @@ func (fs *fs) Open(p string) (io.ReadCloser, error) {
|
||||||
return os.Open(filepath.Join(fs.root, p))
|
return os.Open(filepath.Join(fs.root, p))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubDirFS(fs FS, stat types.Stat) FS {
|
type Dir struct {
|
||||||
return &subDirFS{fs: fs, stat: stat}
|
Stat types.Stat
|
||||||
|
FS FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubDirFS(dirs []Dir) (FS, error) {
|
||||||
|
sort.Slice(dirs, func(i, j int) bool {
|
||||||
|
return dirs[i].Stat.Path < dirs[j].Stat.Path
|
||||||
|
})
|
||||||
|
m := map[string]Dir{}
|
||||||
|
for _, d := range dirs {
|
||||||
|
if path.Base(d.Stat.Path) != d.Stat.Path {
|
||||||
|
return nil, errors.Errorf("subdir %s must be single file", d.Stat.Path)
|
||||||
|
}
|
||||||
|
if _, ok := m[d.Stat.Path]; ok {
|
||||||
|
return nil, errors.Errorf("invalid path %s", d.Stat.Path)
|
||||||
|
}
|
||||||
|
m[d.Stat.Path] = d
|
||||||
|
}
|
||||||
|
return &subDirFS{m: m, dirs: dirs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type subDirFS struct {
|
type subDirFS struct {
|
||||||
fs FS
|
m map[string]Dir
|
||||||
stat types.Stat
|
dirs []Dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *subDirFS) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
func (fs *subDirFS) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
main := &StatInfo{Stat: &fs.stat}
|
for _, d := range fs.dirs {
|
||||||
if !main.IsDir() {
|
fi := &StatInfo{Stat: &d.Stat}
|
||||||
return errors.Errorf("fs subdir not mode directory")
|
if !fi.IsDir() {
|
||||||
}
|
return errors.Errorf("fs subdir %s not mode directory", d.Stat.Path)
|
||||||
if main.Name() != fs.stat.Path {
|
|
||||||
return errors.Errorf("subdir path must be single file")
|
|
||||||
}
|
|
||||||
if err := fn(fs.stat.Path, main, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fs.fs.Walk(ctx, func(p string, fi os.FileInfo, err error) error {
|
|
||||||
stat, ok := fi.Sys().(*types.Stat)
|
|
||||||
if !ok {
|
|
||||||
return errors.Wrapf(err, "invalid fileinfo without stat info: %s", p)
|
|
||||||
}
|
}
|
||||||
stat.Path = path.Join(fs.stat.Path, stat.Path)
|
if err := fn(d.Stat.Path, fi, nil); err != nil {
|
||||||
return fn(filepath.Join(fs.stat.Path, p), &StatInfo{stat}, nil)
|
return err
|
||||||
})
|
}
|
||||||
|
if err := d.FS.Walk(ctx, func(p string, fi os.FileInfo, err error) error {
|
||||||
|
stat, ok := fi.Sys().(*types.Stat)
|
||||||
|
if !ok {
|
||||||
|
return errors.Wrapf(err, "invalid fileinfo without stat info: %s", p)
|
||||||
|
}
|
||||||
|
stat.Path = path.Join(d.Stat.Path, stat.Path)
|
||||||
|
if stat.Linkname != "" {
|
||||||
|
if fi.Mode()&os.ModeSymlink != 0 {
|
||||||
|
if strings.HasPrefix(stat.Linkname, "/") {
|
||||||
|
stat.Linkname = path.Join("/"+d.Stat.Path, stat.Linkname)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stat.Linkname = path.Join(d.Stat.Path, stat.Linkname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fn(filepath.Join(d.Stat.Path, p), &StatInfo{stat}, nil)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *subDirFS) Open(p string) (io.ReadCloser, error) {
|
func (fs *subDirFS) Open(p string) (io.ReadCloser, error) {
|
||||||
return fs.fs.Open(strings.TrimPrefix(p, fs.stat.Path+"/"))
|
parts := strings.SplitN(filepath.Clean(p), string(filepath.Separator), 2)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ioutil.NopCloser(&emptyReader{}), nil
|
||||||
|
}
|
||||||
|
d, ok := fs.m[parts[0]]
|
||||||
|
if !ok {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return d.FS.Open(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyReader struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*emptyReader) Read([]byte) (int, error) {
|
||||||
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package fsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tonistiigi/fsutil/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteTar(ctx context.Context, fs FS, w io.Writer) error {
|
||||||
|
tw := tar.NewWriter(w)
|
||||||
|
err := fs.Walk(ctx, func(path string, fi os.FileInfo, err error) error {
|
||||||
|
stat, ok := fi.Sys().(*types.Stat)
|
||||||
|
if !ok {
|
||||||
|
return errors.Wrapf(err, "invalid fileinfo without stat info: %s", path)
|
||||||
|
}
|
||||||
|
hdr, err := tar.FileInfoHeader(fi, stat.Linkname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := filepath.ToSlash(path)
|
||||||
|
if fi.IsDir() && !strings.HasSuffix(name, "/") {
|
||||||
|
name += "/"
|
||||||
|
}
|
||||||
|
hdr.Name = name
|
||||||
|
|
||||||
|
hdr.Uid = int(stat.Uid)
|
||||||
|
hdr.Gid = int(stat.Gid)
|
||||||
|
hdr.Devmajor = stat.Devmajor
|
||||||
|
hdr.Devminor = stat.Devminor
|
||||||
|
hdr.Linkname = stat.Linkname
|
||||||
|
if hdr.Linkname != "" {
|
||||||
|
hdr.Size = 0
|
||||||
|
hdr.Typeflag = tar.TypeLink
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stat.Xattrs) > 0 {
|
||||||
|
hdr.PAXRecords = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range stat.Xattrs {
|
||||||
|
hdr.PAXRecords["SCHILY.xattr."+k] = string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write file header")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 && hdr.Linkname == "" {
|
||||||
|
rc, err := fs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(tw, rc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tw.Close()
|
||||||
|
}
|
|
@ -215,7 +215,7 @@ github.com/stretchr/testify/require
|
||||||
github.com/stretchr/testify/assert
|
github.com/stretchr/testify/assert
|
||||||
# github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8
|
# github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8
|
||||||
github.com/syndtr/gocapability/capability
|
github.com/syndtr/gocapability/capability
|
||||||
# github.com/tonistiigi/fsutil v0.0.0-20190319020005-1bdbf124ad49
|
# github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76
|
||||||
github.com/tonistiigi/fsutil
|
github.com/tonistiigi/fsutil
|
||||||
github.com/tonistiigi/fsutil/types
|
github.com/tonistiigi/fsutil/types
|
||||||
github.com/tonistiigi/fsutil/copy
|
github.com/tonistiigi/fsutil/copy
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
imageexporter "github.com/moby/buildkit/exporter/containerimage"
|
imageexporter "github.com/moby/buildkit/exporter/containerimage"
|
||||||
localexporter "github.com/moby/buildkit/exporter/local"
|
localexporter "github.com/moby/buildkit/exporter/local"
|
||||||
ociexporter "github.com/moby/buildkit/exporter/oci"
|
ociexporter "github.com/moby/buildkit/exporter/oci"
|
||||||
|
tarexporter "github.com/moby/buildkit/exporter/tar"
|
||||||
"github.com/moby/buildkit/frontend"
|
"github.com/moby/buildkit/frontend"
|
||||||
gw "github.com/moby/buildkit/frontend/gateway/client"
|
gw "github.com/moby/buildkit/frontend/gateway/client"
|
||||||
"github.com/moby/buildkit/identity"
|
"github.com/moby/buildkit/identity"
|
||||||
|
@ -252,6 +253,10 @@ func (w *Worker) Exporter(name string, sm *session.Manager) (exporter.Exporter,
|
||||||
return localexporter.New(localexporter.Opt{
|
return localexporter.New(localexporter.Opt{
|
||||||
SessionManager: sm,
|
SessionManager: sm,
|
||||||
})
|
})
|
||||||
|
case client.ExporterTar:
|
||||||
|
return tarexporter.New(tarexporter.Opt{
|
||||||
|
SessionManager: sm,
|
||||||
|
})
|
||||||
case client.ExporterOCI:
|
case client.ExporterOCI:
|
||||||
return ociexporter.New(ociexporter.Opt{
|
return ociexporter.New(ociexporter.Opt{
|
||||||
SessionManager: sm,
|
SessionManager: sm,
|
||||||
|
|
Loading…
Reference in New Issue