From ad9b4162aa378913d1736cd5ab738d9cae6f941e Mon Sep 17 00:00:00 2001 From: Javier Romero Date: Wed, 10 Jul 2019 16:27:21 -0500 Subject: [PATCH] Add support for zip-formatted files for the app path Also, disable redundant travis builds for PRs. Resolves #106 Signed-off-by: Javier Romero Signed-off-by: Andrew Meyer --- .travis.yml | 6 +- acceptance/acceptance_test.go | 28 +++-- acceptance/testdata/mock_app.zip | Bin 0 -> 310 bytes build.go | 58 ++++++---- build/lifecycle.go | 6 +- build/phase.go | 43 ++++++-- build/phase_test.go | 8 +- build_test.go | 74 ++++++++----- commands/build.go | 6 +- commands/run.go | 2 +- inspect_builder_test.go | 1 + internal/archive/archive.go | 131 +++++++++++++++++++---- internal/archive/archive_test.go | 63 ++++++++++- internal/archive/testdata/zip-to-tar.zip | Bin 0 -> 510 bytes run.go | 10 +- testdata/empty-file | 0 testdata/jar-file.jar | Bin 0 -> 739 bytes testdata/non-zip-file | 1 + testdata/zip-file.zip | Bin 0 -> 510 bytes 19 files changed, 332 insertions(+), 105 deletions(-) create mode 100644 acceptance/testdata/mock_app.zip create mode 100644 internal/archive/testdata/zip-to-tar.zip create mode 100644 testdata/empty-file create mode 100644 testdata/jar-file.jar create mode 100644 testdata/non-zip-file create mode 100644 testdata/zip-file.zip diff --git a/.travis.yml b/.travis.yml index ae15cf0ee..3c1784312 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,4 +27,8 @@ jobs: - NO_DOCKER=true - GO111MODULE=on script: go test -mod=vendor -count=1 -parallel=1 -v ./... - after_success: go build -mod=vendor -o pack ./cmd/pack \ No newline at end of file + after_success: go build -mod=vendor -o pack ./cmd/pack + +branches: + only: + - master \ No newline at end of file diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 6b774e0fd..f2e1317dd 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -149,11 +149,11 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.Run(t, packCmd("set-default-builder", builder)) }) - it("creates image on the daemon", func() { - t.Log("no previous image exists") + it("creates a runnable, rebuildable image on daemon from app dir", func() { + appPath := filepath.Join("testdata", "mock_app") cmd := packCmd( "build", repoName, - "-p", filepath.Join("testdata", "mock_app"), + "-p", appPath, ) output := h.Run(t, cmd) h.AssertContains(t, output, fmt.Sprintf("Successfully built image '%s'", repoName)) @@ -200,7 +200,7 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.Run(t, cmd) t.Log("rebuild") - cmd = packCmd("build", repoName, "-p", filepath.Join("testdata", "mock_app")) + cmd = packCmd("build", repoName, "-p", appPath) output = h.Run(t, cmd) h.AssertContains(t, output, fmt.Sprintf("Successfully built image '%s'", repoName)) imgId, err = imgIdFromOutput(output, repoName) @@ -225,7 +225,7 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.AssertContainsMatch(t, output, `(?i)\[cacher] reusing layer 'simple/layers:cached-launch-layer'`) t.Log("rebuild with --clear-cache") - cmd = packCmd("build", repoName, "-p", "testdata/mock_app/.", "--clear-cache") + cmd = packCmd("build", repoName, "-p", appPath, "--clear-cache") output = h.Run(t, cmd) h.AssertContains(t, output, fmt.Sprintf("Successfully built image '%s'", repoName)) @@ -244,6 +244,22 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.AssertContainsMatch(t, output, `(?i)\[cacher] (Caching|adding) layer 'simple/layers:cached-launch-layer'`) }) + it("supports building app from a zip file", func() { + appPath := filepath.Join("testdata", "mock_app.zip") + cmd := packCmd( + "build", repoName, + "-p", appPath, + ) + output := h.Run(t, cmd) + h.AssertContains(t, output, fmt.Sprintf("Successfully built image '%s'", repoName)) + imgId, err := imgIdFromOutput(output, repoName) + if err != nil { + t.Log(output) + t.Fatal("Could not determine image id for built image") + } + defer h.DockerRmi(dockerCli, imgId) + }) + when("--buildpack", func() { when("the argument is a tgz or id", func() { var notBuilderTgz string @@ -834,7 +850,7 @@ func createStack(t *testing.T, dockerCli *client.Client) { func createStackImage(t *testing.T, dockerCli *client.Client, repoName string, dir string) { ctx := context.Background() - buildContext, _ := archive.CreateTarReader(dir, "/", 0, 0, -1) + buildContext := archive.ReadDirAsTar(dir, "/", 0, 0, -1) res, err := dockerCli.ImageBuild(ctx, buildContext, dockertypes.ImageBuildOptions{ Tags: []string{repoName}, diff --git a/acceptance/testdata/mock_app.zip b/acceptance/testdata/mock_app.zip new file mode 100644 index 0000000000000000000000000000000000000000..1f3c15d8f60be8d47c292c09541b170e2cdabc62 GIT binary patch literal 310 zcmWIWW@Zs#U|`^2U@70{*Ona5I}6C$4#dn1G7Lqfd7&Yk49wxCA7Z4AsKl05a5FHn zd}U-{0Bce`dyuQiLB#Fh-`cMIvt2kZM`$bF+R732#3|$PvSZ#{MecJp*8Dm3t^K`@ zco|P~?UpRp_=-Eu&LS#@d|Vz$@xQwz;2K?fNW0437?V8WOt|obGFYJ=8GS5}-IIO{bWgbJRfvk_ce}YWK zH*LSV-CZ-Y)_-CQ@MdI^W5(q%3842F7=XTDc-shKfxXEJ@g|xV1H4(;KuQ>a&=W{s H2XPnxDcfRi literal 0 HcmV?d00001 diff --git a/build.go b/build.go index 96552958c..0870a8e5c 100644 --- a/build.go +++ b/build.go @@ -9,15 +9,15 @@ import ( "runtime" "strings" - "github.com/docker/docker/api/types" - "github.com/buildpack/imgutil" + "github.com/docker/docker/api/types" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "github.com/buildpack/pack/build" "github.com/buildpack/pack/builder" "github.com/buildpack/pack/buildpack" + "github.com/buildpack/pack/internal/archive" "github.com/buildpack/pack/style" ) @@ -28,7 +28,7 @@ type Lifecycle interface { type BuildOptions struct { Image string // required Builder string // required - AppDir string // defaults to current working directory + AppPath string // defaults to current working directory RunImage string // defaults to the best mirror from the builder metadata or AdditionalMirrors AdditionalMirrors map[string][]string // only considered if RunImage is not provided Env map[string]string @@ -51,9 +51,9 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return errors.Wrapf(err, "invalid image name '%s'", opts.Image) } - appDir, err := c.processAppDir(opts.AppDir) + appPath, err := c.processAppPath(opts.AppPath) if err != nil { - return errors.Wrapf(err, "invalid app dir '%s'", opts.AppDir) + return errors.Wrapf(err, "invalid app path '%s'", opts.AppPath) } proxyConfig := c.processProxyConfig(opts.ProxyConfig) @@ -91,7 +91,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.ImageRemoveOptions{Force: true}) return c.lifecycle.Execute(ctx, build.LifecycleOptions{ - AppDir: appDir, + AppPath: appPath, Image: imageRef, Builder: ephemeralBuilder, RunImage: runImage, @@ -139,33 +139,49 @@ func (c *Client) validateRunImage(context context.Context, name string, noPull b return img, nil } -func (c *Client) processAppDir(appDir string) (string, error) { +func (c *Client) processAppPath(appPath string) (string, error) { var ( - resolvedAppDir = appDir - err error + resolvedAppPath = appPath + err error ) - if appDir == "" { - if appDir, err = os.Getwd(); err != nil { - return "", err + if appPath == "" { + if appPath, err = os.Getwd(); err != nil { + return "", errors.Wrap(err, "get working dir") } } - if resolvedAppDir, err = filepath.EvalSymlinks(appDir); err != nil { - return "", err + if resolvedAppPath, err = filepath.EvalSymlinks(appPath); err != nil { + return "", errors.Wrap(err, "evaluate symlink") } - if resolvedAppDir, err = filepath.Abs(resolvedAppDir); err != nil { - return "", err + if resolvedAppPath, err = filepath.Abs(resolvedAppPath); err != nil { + return "", errors.Wrap(err, "resolve absolute path") + } + + fi, err := os.Stat(resolvedAppPath) + if err != nil { + return "", errors.Wrap(err, "stat file") } - if fi, err := os.Stat(resolvedAppDir); err != nil { - return "", err - } else if !fi.IsDir() { - return "", fmt.Errorf("%s is not a directory", appDir) + if !fi.IsDir() { + fh, err := os.Open(resolvedAppPath) + if err != nil { + return "", errors.Wrap(err, "read file") + } + defer fh.Close() + + isZip, err := archive.IsZip(fh) + if err != nil { + return "", errors.Wrap(err, "check zip") + } + + if !isZip { + return "", errors.New("app path must be a directory or zip") + } } - return resolvedAppDir, nil + return resolvedAppPath, nil } func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig { diff --git a/build/lifecycle.go b/build/lifecycle.go index 0fab28613..e5c362552 100644 --- a/build/lifecycle.go +++ b/build/lifecycle.go @@ -22,7 +22,7 @@ type Lifecycle struct { builder *builder.Builder logger logging.Logger docker *client.Client - appDir string + appPath string appOnce *sync.Once httpProxy string httpsProxy string @@ -45,7 +45,7 @@ func NewLifecycle(docker *client.Client, logger logging.Logger) *Lifecycle { } type LifecycleOptions struct { - AppDir string + AppPath string Image name.Reference Builder *builder.Builder RunImage string @@ -132,7 +132,7 @@ func (l *Lifecycle) Execute(ctx context.Context, opts LifecycleOptions) error { func (l *Lifecycle) Setup(opts LifecycleOptions) { l.LayersVolume = "pack-layers-" + randString(10) l.AppVolume = "pack-app-" + randString(10) - l.appDir = opts.AppDir + l.appPath = opts.AppPath l.appOnce = &sync.Once{} l.builder = opts.Builder l.httpProxy = opts.HTTPProxy diff --git a/build/phase.go b/build/phase.go index 15078e80d..eb41bb855 100644 --- a/build/phase.go +++ b/build/phase.go @@ -3,6 +3,8 @@ package build import ( "context" "fmt" + "io" + "os" "runtime" "sync" @@ -26,7 +28,7 @@ type Phase struct { hostConf *dcontainer.HostConfig ctr dcontainer.ContainerCreateCreatedBody uid, gid int - appDir string + appPath string appOnce *sync.Once } @@ -50,7 +52,7 @@ func (l *Lifecycle) NewPhase(name string, ops ...func(*Phase) (*Phase, error)) ( logger: l.logger, uid: l.builder.UID, gid: l.builder.GID, - appDir: l.appDir, + appPath: l.appPath, appOnce: l.appOnce, } @@ -113,29 +115,30 @@ func WithRegistryAccess(repos ...string) func(*Phase) (*Phase, error) { func (p *Phase) Run(context context.Context) error { var err error + p.ctr, err = p.docker.ContainerCreate(context, p.ctrConf, p.hostConf, nil, "") if err != nil { return errors.Wrapf(err, "failed to create '%s' container", p.name) } + p.appOnce.Do(func() { - var mode int64 = -1 - if runtime.GOOS == "windows" { - mode = 0777 + var appReader io.ReadCloser + appReader, err = p.createAppReader() + if err != nil { + err = errors.Wrapf(err, "create tar archive from '%s'", p.appPath) + return } + defer appReader.Close() - appReader, errChan := archive.CreateTarReader(p.appDir, appDir, p.uid, p.gid, mode) if err = p.docker.CopyToContainer(context, p.ctr.ID, "/", appReader, types.CopyToContainerOptions{}); err != nil { err = errors.Wrapf(err, "failed to copy files to '%s' container", p.name) - } - - err = <-errChan - if err != nil { - err = errors.Wrapf(err, "create tar archive from '%s'", p.appDir) + return } }) if err != nil { return errors.Wrapf(err, "run %s container", p.name) } + return container.Run( context, p.docker, @@ -148,3 +151,21 @@ func (p *Phase) Run(context context.Context) error { func (p *Phase) Cleanup() error { return p.docker.ContainerRemove(context.Background(), p.ctr.ID, types.ContainerRemoveOptions{Force: true}) } + +func (p *Phase) createAppReader() (io.ReadCloser, error) { + fi, err := os.Stat(p.appPath) + if err != nil { + return nil, err + } + + if fi.IsDir() { + var mode int64 = -1 + if runtime.GOOS == "windows" { + mode = 0777 + } + + return archive.ReadDirAsTar(p.appPath, appDir, p.uid, p.gid, mode), nil + } + + return archive.ReadZipAsTar(p.appPath, appDir, p.uid, p.gid, -1), nil +} diff --git a/build/phase_test.go b/build/phase_test.go index 1b2ff6825..d7201f938 100644 --- a/build/phase_test.go +++ b/build/phase_test.go @@ -45,11 +45,11 @@ func TestPhase(t *testing.T) { dockerCli, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) - repoName = "lifecycle.test." + h.RandString(10) + repoName = "phase.test." + h.RandString(10) CreateFakeLifecycleImage(t, dockerCli, repoName) defer h.DockerRmi(dockerCli, repoName) - spec.Run(t, "lifecycle", testPhase, spec.Report(report.Terminal{}), spec.Parallel()) + spec.Run(t, "phase", testPhase, spec.Report(report.Terminal{}), spec.Parallel()) } func testPhase(t *testing.T, when spec.G, it spec.S) { @@ -308,7 +308,7 @@ func CreateFakeLifecycleImage(t *testing.T, dockerCli *client.Client, repoName s wd, err := os.Getwd() h.AssertNil(t, err) - buildContext, _ := archive.CreateTarReader(filepath.Join(wd, "testdata", "fake-lifecycle"), "/", 0, 0, -1) + buildContext := archive.ReadDirAsTar(filepath.Join(wd, "testdata", "fake-lifecycle"), "/", 0, 0, -1) res, err := dockerCli.ImageBuild(ctx, buildContext, dockertypes.ImageBuildOptions{ Tags: []string{repoName}, @@ -334,7 +334,7 @@ func CreateFakeLifecycle(appDir string, docker *client.Client, logger logging.Lo } subject.Setup(build.LifecycleOptions{ - AppDir: appDir, + AppPath: appDir, Builder: bldr, HTTPProxy: "some-http-proxy", HTTPSProxy: "some-https-proxy", diff --git a/build_test.go b/build_test.go index 646a5a0f2..79f77863c 100644 --- a/build_test.go +++ b/build_test.go @@ -156,38 +156,54 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) resolvedWd, err := filepath.EvalSymlinks(wd) h.AssertNil(t, err) - h.AssertEq(t, fakeLifecycle.Opts.AppDir, resolvedWd) - }) - - it("path must exist", func() { - h.AssertError(t, subject.Build(context.TODO(), BuildOptions{ - Image: "some/app", - Builder: builderName, - AppDir: "not/exist/path", - }), - "invalid app dir 'not/exist/path'", - ) + h.AssertEq(t, fakeLifecycle.Opts.AppPath, resolvedWd) }) + for fileDesc, appPath := range map[string]string{ + "zip": filepath.Join("testdata", "zip-file.zip"), + "jar": filepath.Join("testdata", "jar-file.jar"), + } { + fileDesc := fileDesc + appPath := appPath + + it(fmt.Sprintf("supports %s files", fileDesc), func() { + err := subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: builderName, + AppPath: appPath, + }) + h.AssertNil(t, err) + }) + } + + for fileDesc, testData := range map[string][]string{ + "non-existent": {"not/exist/path", "does not exist"}, + "empty": {filepath.Join("testdata", "empty-file"), "app path must be a directory or zip"}, + "non-zip": {filepath.Join("testdata", "non-zip-file"), "app path must be a directory or zip"}, + } { + fileDesc := fileDesc + appPath := testData[0] + errMessage := testData[0] + + it(fmt.Sprintf("does NOT support %s files", fileDesc), func() { + err := subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: builderName, + AppPath: appPath, + }) - it("path must be a dir", func() { - h.AssertError(t, subject.Build(context.TODO(), BuildOptions{ - Image: "some/app", - Builder: builderName, - AppDir: filepath.Join("testdata", "just-a-file.txt"), - }), - fmt.Sprintf("invalid app dir '%s'", filepath.Join("testdata", "just-a-file.txt")), - ) - }) + h.AssertError(t, err, errMessage) + }) + } it("resolves the absolute path", func() { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ Image: "some/app", Builder: builderName, - AppDir: filepath.Join("testdata", "some-app"), + AppPath: filepath.Join("testdata", "some-app"), })) absPath, err := filepath.Abs(filepath.Join("testdata", "some-app")) h.AssertNil(t, err) - h.AssertEq(t, fakeLifecycle.Opts.AppDir, absPath) + h.AssertEq(t, fakeLifecycle.Opts.AppPath, absPath) }) when("appDir is a symlink", func() { @@ -223,10 +239,10 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ Image: "some/app", Builder: builderName, - AppDir: relLink, + AppPath: relLink, })) - h.AssertEq(t, fakeLifecycle.Opts.AppDir, absoluteAppDir) + h.AssertEq(t, fakeLifecycle.Opts.AppPath, absoluteAppDir) }) it("resolves absolute symbolic links", func() { @@ -236,10 +252,10 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ Image: "some/app", Builder: builderName, - AppDir: relLink, + AppPath: relLink, })) - h.AssertEq(t, fakeLifecycle.Opts.AppDir, absoluteAppDir) + h.AssertEq(t, fakeLifecycle.Opts.AppPath, absoluteAppDir) }) it("resolves symbolic links recursively", func() { @@ -255,10 +271,10 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ Image: "some/app", Builder: builderName, - AppDir: symbolicLink, + AppPath: symbolicLink, })) - h.AssertEq(t, fakeLifecycle.Opts.AppDir, absoluteAppDir) + h.AssertEq(t, fakeLifecycle.Opts.AppPath, absoluteAppDir) }) }) }) @@ -561,7 +577,7 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { }) }) - when("is *nix", func() { + when("is posix", func() { it.Before(func() { h.SkipIf(t, runtime.GOOS == "windows", "Skipped on windows") }) diff --git a/commands/build.go b/commands/build.go index 2db6bb1de..c1de1d229 100644 --- a/commands/build.go +++ b/commands/build.go @@ -15,7 +15,7 @@ import ( ) type BuildFlags struct { - AppDir string + AppPath string Builder string RunImage string Env []string @@ -45,7 +45,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient *pack.Client) *c return err } if err := packClient.Build(ctx, pack.BuildOptions{ - AppDir: flags.AppDir, + AppPath: flags.AppPath, Builder: flags.Builder, AdditionalMirrors: getMirrors(cfg), RunImage: flags.RunImage, @@ -69,7 +69,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient *pack.Client) *c } func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Config) { - cmd.Flags().StringVarP(&buildFlags.AppDir, "path", "p", "", "Path to app dir (defaults to current working directory)") + cmd.Flags().StringVarP(&buildFlags.AppPath, "path", "p", "", "Path to app dir or zip-formatted file (defaults to current working directory)") cmd.Flags().StringVar(&buildFlags.Builder, "builder", cfg.DefaultBuilder, "Builder (defaults to builder configured by 'set-default-builder')") cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)") cmd.Flags().StringArrayVarP(&buildFlags.Env, "env", "e", []string{}, "Build-time environment variable, in the form 'VAR=VALUE' or 'VAR'.\nWhen using latter value-less form, value will be taken from current\n environment at the time this command is executed.\nThis flag may be specified multiple times and will override\n individual values defined by --env-file.") diff --git a/commands/run.go b/commands/run.go index 1cb25f245..218fe8eda 100644 --- a/commands/run.go +++ b/commands/run.go @@ -27,7 +27,7 @@ func Run(logger logging.Logger, cfg config.Config, packClient *pack.Client) *cob return err } return packClient.Run(ctx, pack.RunOptions{ - AppDir: flags.AppDir, + AppPath: flags.AppPath, Builder: flags.Builder, RunImage: flags.RunImage, Env: env, diff --git a/inspect_builder_test.go b/inspect_builder_test.go index 1d8f3888a..0f0528801 100644 --- a/inspect_builder_test.go +++ b/inspect_builder_test.go @@ -57,6 +57,7 @@ func testInspectBuilder(t *testing.T, when spec.G, it spec.S) { when("the image exists", func() { for _, useDaemon := range []bool{true, false} { + useDaemon := useDaemon when(fmt.Sprintf("daemon is %t", useDaemon), func() { it.Before(func() { if useDaemon { diff --git a/internal/archive/archive.go b/internal/archive/archive.go index aeb3e0619..865c94777 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -2,6 +2,7 @@ package archive import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "fmt" @@ -9,7 +10,6 @@ import ( "io/ioutil" "os" "path/filepath" - "runtime" "time" "github.com/pkg/errors" @@ -21,19 +21,28 @@ func init() { NormalizedDateTime = time.Date(1980, time.January, 1, 0, 0, 1, 0, time.UTC) } -func CreateTarReader(srcDir, tarDir string, uid, gid int, mode int64) (io.Reader, chan error) { +func ReadDirAsTar(srcDir, basePath string, uid, gid int, mode int64) io.ReadCloser { + return readAsTar(srcDir, basePath, uid, gid, mode, WriteDirToTar) +} + +func ReadZipAsTar(srcPath, basePath string, uid, gid int, mode int64) io.ReadCloser { + return readAsTar(srcPath, basePath, uid, gid, mode, WriteZipToTar) +} + +func readAsTar(src, basePath string, uid, gid int, mode int64, writeFn func(tw *tar.Writer, srcDir, basePath string, uid, gid int, mode int64) error) io.ReadCloser { r, w := io.Pipe() - errChan := make(chan error, 1) go func() { - defer w.Close() + var err error + defer func() { + w.CloseWithError(err) + }() tw := tar.NewWriter(w) defer tw.Close() - err := WriteDirToTar(tw, srcDir, tarDir, uid, gid, mode) - errChan <- err + err = writeFn(tw, src, basePath, uid, gid, mode) }() - return r, errChan + return r } func CreateSingleFileTarReader(path, txt string) (io.Reader, error) { @@ -138,7 +147,7 @@ func contains(slice []string, element string) bool { return false } -func WriteDirToTar(tw *tar.Writer, srcDir, tarDir string, uid, gid int, mode int64) error { +func WriteDirToTar(tw *tar.Writer, srcDir, basePath string, uid, gid int, mode int64) error { return filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error { if err != nil { return err @@ -173,18 +182,8 @@ func WriteDirToTar(tw *tar.Writer, srcDir, tarDir string, uid, gid int, mode int return nil } - header.Name = filepath.Join(tarDir, relPath) - if runtime.GOOS == "windows" { - header.Name = filepath.ToSlash(header.Name) - } - if mode != -1 { - header.Mode = mode - } - header.ModTime = NormalizedDateTime - header.Uid = uid - header.Gid = gid - header.Uname = "" - header.Gname = "" + header.Name = filepath.ToSlash(filepath.Join(basePath, relPath)) + finalizeHeader(header, uid, gid, mode) if err := tw.WriteHeader(header); err != nil { return err @@ -205,3 +204,95 @@ func WriteDirToTar(tw *tar.Writer, srcDir, tarDir string, uid, gid int, mode int return nil }) } + +func WriteZipToTar(tw *tar.Writer, srcZip, basePath string, uid, gid int, mode int64) error { + zipReader, err := zip.OpenReader(srcZip) + if err != nil { + return err + } + defer zipReader.Close() + + for _, f := range zipReader.File { + var header *tar.Header + if f.Mode()&os.ModeSymlink != 0 { + target, err := func() (string, error) { + r, err := f.Open() + if err != nil { + return "", nil + } + defer r.Close() + + // contents is the target of the symlink + target, err := ioutil.ReadAll(r) + if err != nil { + return "", err + } + + return string(target), nil + }() + + if err != nil { + return err + } + + header, err = tar.FileInfoHeader(f.FileInfo(), target) + if err != nil { + return err + } + } else { + header, err = tar.FileInfoHeader(f.FileInfo(), f.Name) + if err != nil { + return err + } + } + + header.Name = filepath.ToSlash(filepath.Join(basePath, f.Name)) + finalizeHeader(header, uid, gid, mode) + + if err := tw.WriteHeader(header); err != nil { + return err + } + + if f.Mode().IsRegular() { + err := func() error { + fi, err := f.Open() + if err != nil { + return err + } + defer fi.Close() + + _, err = io.Copy(tw, fi) + return err + }() + + if err != nil { + return err + } + } + } + + return nil +} + +func finalizeHeader(header *tar.Header, uid, gid int, mode int64) { + if mode != -1 { + header.Mode = mode + } + header.ModTime = NormalizedDateTime + header.Uid = uid + header.Gid = gid + header.Uname = "" + header.Gname = "" +} + +func IsZip(file *os.File) (bool, error) { + b := make([]byte, 4) + _, err := file.Read(b) + if err != nil && err != io.EOF { + return false, err + } else if err == io.EOF { + return false, nil + } + + return bytes.Equal(b, []byte("\x50\x4B\x03\x04")), nil +} diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index f25eb3280..3206935f0 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -23,7 +23,7 @@ import ( func TestArchive(t *testing.T) { color.NoColor = true rand.Seed(time.Now().UTC().UnixNano()) - spec.Run(t, "Archive", testArchive, spec.Report(report.Terminal{})) + spec.Run(t, "Archive", testArchive, spec.Sequential(), spec.Report(report.Terminal{})) } func testArchive(t *testing.T, when spec.G, it spec.S) { @@ -197,9 +197,70 @@ func testArchive(t *testing.T, when spec.G, it spec.S) { }) }) + when("#WriteZipToTar", func() { + var src string + it.Before(func() { + src = filepath.Join("testdata", "zip-to-tar.zip") + }) + + when("mode is set to 0777", func() { + it("writes a tar to the dest dir with 0777", func() { + fh, err := os.Create(filepath.Join(tmpDir, "some.tar")) + h.AssertNil(t, err) + + tw := tar.NewWriter(fh) + + err = archive.WriteZipToTar(tw, src, "/nested/dir/dir-in-archive", 1234, 2345, 0777) + h.AssertNil(t, err) + h.AssertNil(t, tw.Close()) + h.AssertNil(t, fh.Close()) + + file, err := os.Open(filepath.Join(tmpDir, "some.tar")) + h.AssertNil(t, err) + defer file.Close() + + tr := tar.NewReader(file) + + verify := tarVerifier{t, tr, 1234, 2345} + verify.nextFile("/nested/dir/dir-in-archive/some-file.txt", "some-content", 0777) + verify.nextDirectory("/nested/dir/dir-in-archive/sub-dir", 0777) + if runtime.GOOS != "windows" { + verify.nextSymLink("/nested/dir/dir-in-archive/sub-dir/link-file", "../some-file.txt") + } + }) + }) + + when("mode is set to -1", func() { + it("writes a tar to the dest dir with preexisting file mode", func() { + fh, err := os.Create(filepath.Join(tmpDir, "some.tar")) + h.AssertNil(t, err) + + tw := tar.NewWriter(fh) + + err = archive.WriteZipToTar(tw, src, "/nested/dir/dir-in-archive", 1234, 2345, -1) + h.AssertNil(t, err) + h.AssertNil(t, tw.Close()) + h.AssertNil(t, fh.Close()) + + file, err := os.Open(filepath.Join(tmpDir, "some.tar")) + h.AssertNil(t, err) + defer file.Close() + + tr := tar.NewReader(file) + + verify := tarVerifier{t, tr, 1234, 2345} + verify.nextFile("/nested/dir/dir-in-archive/some-file.txt", "some-content", 0644) + verify.nextDirectory("/nested/dir/dir-in-archive/sub-dir", 0755) + if runtime.GOOS != "windows" { + verify.nextSymLink("/nested/dir/dir-in-archive/sub-dir/link-file", "../some-file.txt") + } + }) + }) + }) } func fileMode(t *testing.T, path string) int64 { + t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("failed to stat %s", path) diff --git a/internal/archive/testdata/zip-to-tar.zip b/internal/archive/testdata/zip-to-tar.zip new file mode 100644 index 0000000000000000000000000000000000000000..b016d43392fe35d95e1765ab1f70d4f92cbce70a GIT binary patch literal 510 zcmWIWW@h1H0D%daSN-0pNZsH8vO$=aL586?KQ~o3Ei)%oucV?RG=!6ZSwN0AHf54> zY-t5I10%~~0p0C?y-!rFuymj?1@_OrPojY@WbCAIm;|EWR^t^m^Jbf>gu43V0 zaQ)IFY9h1Rt5~E-yW2acxVu!u=JTYlCY3uYc7mN?(7~$U2(-Wnh!IYhh~|E7psYt~ zPENjFa!z7#ac+RG_hAQt+RHVjhPQ6b`W2bM@AM_1Pfjb2h;_vKy zbpBFv6MKDwz1N&xC&67CtT*T1+f%za{=WQthQ5pC35!w}_kLBg;@fp&!-L2df@g$h zp8Par!B6(>EPr+8b2(+}7#Bt^-Kdeb$Rg3jUwNir)xWzHa<*=cLaCb-bhu}pbz60j z^Yf}19mOk8oNM)%^v6RmHXjLk6RsF6+oZEWn7`oO6}&oY%_`$fQTsQ2V+O_S z18Ty6#X2ZqpaOVIgOUllR^-?MB?| zY-t5I10%~