diff --git a/tsuru/client/archiver.go b/tsuru/client/archiver.go deleted file mode 100644 index 5bf1eedb0..000000000 --- a/tsuru/client/archiver.go +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2023 tsuru-client authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package client - -import ( - "archive/tar" - "compress/gzip" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - - gitignore "github.com/sabhiram/go-gitignore" -) - -var ErrMissingFilesToArchive = errors.New("missing files to archive") - -type ArchiveOptions struct { - CompressionLevel *int // defaults to default compression "-1" - IgnoreFiles []string // default to none - Stderr io.Writer // defaults to io.Discard -} - -func DefaultArchiveOptions(w io.Writer) ArchiveOptions { - return ArchiveOptions{ - CompressionLevel: func(lvl int) *int { return &lvl }(gzip.BestCompression), - IgnoreFiles: []string{".tsuruignore"}, - Stderr: w, - } -} - -func Archive(dst io.Writer, filesOnly bool, paths []string, opts ArchiveOptions) error { - if dst == nil { - return fmt.Errorf("destination cannot be nil") - } - - if len(paths) == 0 { - return fmt.Errorf("paths cannot be empty") - } - - if opts.Stderr == nil { - opts.Stderr = io.Discard - } - - var ignoreLines []string - for _, ignoreFile := range opts.IgnoreFiles { - data, err := os.ReadFile(ignoreFile) - if errors.Is(err, os.ErrNotExist) { - continue - } - - if err != nil { - return fmt.Errorf("failed to read ignore file %q: %w", ignoreFile, err) - } - - fmt.Fprintf(opts.Stderr, "Using pattern(s) from %q to include/exclude files...\n", ignoreFile) - - ignoreLines = append(ignoreLines, strings.Split(string(data), "\n")...) - } - - ignore, err := gitignore.CompileIgnoreLines(ignoreLines...) - if err != nil { - return fmt.Errorf("failed to compile all ignore patterns: %w", err) - } - - if opts.CompressionLevel == nil { - opts.CompressionLevel = func(n int) *int { return &n }(gzip.DefaultCompression) - } - - zw, err := gzip.NewWriterLevel(dst, *opts.CompressionLevel) - if err != nil { - return err - } - defer zw.Close() - - tw := tar.NewWriter(zw) - defer tw.Close() - - a := &archiver{ - ignore: *ignore, - stderr: opts.Stderr, - files: map[string]struct{}{}, - } - - return a.archive(tw, filesOnly, paths) -} - -type archiver struct { - ignore gitignore.GitIgnore - stderr io.Writer - files map[string]struct{} -} - -func (a *archiver) archive(tw *tar.Writer, filesOnly bool, paths []string) error { - workingDir, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get the current directory: %w", err) - } - - var added int - - for _, path := range paths { - abs, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("failed to get the absolute filename of %q: %w", path, err) - } - - if !strings.HasPrefix((abs + string(os.PathSeparator)), (workingDir + string(os.PathSeparator))) { - fmt.Fprintf(a.stderr, "WARNING: skipping file %q since you cannot add files outside the current directory\n", path) - continue - } - - fi, err := os.Lstat(path) - if err != nil { - return err - } - - var n int - if fi.IsDir() { - n, err = a.addDir(tw, filesOnly, path, fi) - if err != nil { - return err - } - - added += n - continue - } - - n, err = a.addFile(tw, filesOnly, path, fi) - if err != nil { - return err - } - - added += n - } - - if added == 0 { - return ErrMissingFilesToArchive - } - - return nil -} - -func (a *archiver) addFile(tw *tar.Writer, filesOnly bool, filename string, fi os.FileInfo) (int, error) { - isDir, isRegular, isSymlink := fi.IsDir(), fi.Mode().IsRegular(), fi.Mode()&os.ModeSymlink == os.ModeSymlink - - if !isDir && !isRegular && !isSymlink { // neither dir, regular nor symlink - fmt.Fprintf(a.stderr, "WARNING: Skipping file %q due to unsupported file type.\n", filename) - return 0, nil - } - - if isDir && filesOnly { // there's no need to create dirs in files only - return 0, nil - } - - if a.ignore.MatchesPath(filename) { - fmt.Fprintf(a.stderr, "File %q matches with some pattern provided in the ignore file... skipping it.\n", filename) - return 0, nil - } - - var linkname string - if isSymlink { - target, err := os.Readlink(filename) - if err != nil { - return 0, err - } - - linkname = target - } - - h, err := tar.FileInfoHeader(fi, linkname) - if err != nil { - return 0, err - } - - if !filesOnly { // should preserve the directory tree - h.Name = filename - } - - if _, found := a.files[h.Name]; found { - fmt.Fprintf(a.stderr, "Skipping file %q as it already exists in the current directory.\n", filename) - return 0, nil - } - - a.files[h.Name] = struct{}{} - - if strings.TrimRight(h.Name, string(os.PathSeparator)) == "." { // skipping root dir - return 0, nil - } - - if err = tw.WriteHeader(h); err != nil { - return 0, err - } - - if isDir || isSymlink { // there's no data to copy from dir or symlink - return 1, nil - } - - f, err := os.Open(filename) - if err != nil { - return 0, err - } - defer f.Close() - - written, err := io.CopyN(tw, f, h.Size) - if err != nil { - return 0, err - } - - if written < h.Size { - return 0, io.ErrShortWrite - } - - return 1, nil -} - -func (a *archiver) addDir(tw *tar.Writer, filesOnly bool, path string, fi os.FileInfo) (int, error) { - var added int - return added, filepath.WalkDir(path, fs.WalkDirFunc(func(path string, dentry fs.DirEntry, err error) error { - if err != nil { // fail fast - return err - } - - fi, err := dentry.Info() - if err != nil { - return err - } - - var n int - n, err = a.addFile(tw, filesOnly, path, fi) - if err != nil { - return err - } - - added += n - - return nil - })) -} diff --git a/tsuru/client/archiver_test.go b/tsuru/client/archiver_test.go deleted file mode 100644 index 387574497..000000000 --- a/tsuru/client/archiver_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023 tsuru-client authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package client - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "errors" - "io" - "testing" - - check "gopkg.in/check.v1" -) - -func extractFiles(t *testing.T, c *check.C, r io.Reader) (m []miniFile) { - t.Helper() - - gzr, err := gzip.NewReader(r) - c.Assert(err, check.IsNil) - - tr := tar.NewReader(gzr) - - for { - h, err := tr.Next() - if errors.Is(err, io.EOF) { - break - } - - c.Assert(err, check.IsNil) - - var data []byte - - if h.Typeflag == tar.TypeReg || h.Typeflag == tar.TypeRegA { - var b bytes.Buffer - written, err := io.CopyN(&b, tr, h.Size) - c.Assert(err, check.IsNil) - c.Assert(written, check.Equals, h.Size) - data = b.Bytes() - } - - m = append(m, miniFile{ - Name: h.Name, - Linkname: h.Linkname, - Type: h.Typeflag, - Data: data, - }) - } - - return m -} - -type miniFile struct { - Name string - Linkname string - Type byte - Data []byte -} diff --git a/tsuru/client/archiver_unix_test.go b/tsuru/client/archiver_unix_test.go deleted file mode 100644 index 236b35bf3..000000000 --- a/tsuru/client/archiver_unix_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023 tsuru-client authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux -// +build linux - -package client - -import ( - "archive/tar" - "bytes" - "io" - "net" - "os" - "path/filepath" - - check "gopkg.in/check.v1" -) - -func (s *S) TestArchive_NoDestination(c *check.C) { - err := Archive(nil, false, nil, ArchiveOptions{}) - c.Assert(err, check.ErrorMatches, "destination cannot be nil") -} - -func (s *S) TestArchive_NoPaths(c *check.C) { - err := Archive(io.Discard, false, nil, ArchiveOptions{}) - c.Assert(err, check.ErrorMatches, "paths cannot be empty") - - err = Archive(io.Discard, false, []string{}, ArchiveOptions{}) - c.Assert(err, check.ErrorMatches, "paths cannot be empty") -} - -func (s *S) TestArchive_FileOutsideOfCurrentDir(c *check.C) { - var stderr bytes.Buffer - err := Archive(io.Discard, false, []string{"../../../../var/www/html"}, ArchiveOptions{Stderr: &stderr}) - c.Assert(err, check.ErrorMatches, "missing files to archive") - c.Assert(stderr.String(), check.Matches, `(?s).*WARNING: skipping file "\.\.\/\.\.\/\.\.\/\.\.\/var\/www\/html" since you cannot add files outside the current directory.*`) -} - -func (s *S) TestArchive_PassingWholeDir(c *check.C) { - workingDir, err := os.Getwd() - c.Assert(err, check.IsNil) - - defer func() { os.Chdir(workingDir) }() - - err = os.Chdir(filepath.Join(workingDir, "./testdata/deploy/")) - c.Assert(err, check.IsNil) - - var b bytes.Buffer - - err = Archive(&b, false, []string{"."}, ArchiveOptions{}) - c.Assert(err, check.IsNil) - - got := extractFiles(s.t, c, &b) - expected := []miniFile{ - {Name: "directory", Type: tar.TypeDir}, - {Name: "directory/file.txt", Type: tar.TypeReg, Data: []byte("wat\n")}, - {Name: "file1.txt", Type: tar.TypeReg, Data: []byte("something happened\n")}, - {Name: "file2.txt", Type: tar.TypeReg, Data: []byte("twice\n")}, - } - c.Assert(got, check.DeepEquals, expected) -} - -func (s *S) TestArchive_PassingWholeDir_WithTsuruIgnore(c *check.C) { - workingDir, err := os.Getwd() - c.Assert(err, check.IsNil) - - defer func() { os.Chdir(workingDir) }() - - err = os.Chdir(filepath.Join(workingDir, "./testdata/deploy2/")) - c.Assert(err, check.IsNil) - - var b, stderr bytes.Buffer - - err = Archive(&b, false, []string{"."}, ArchiveOptions{IgnoreFiles: []string{".tsuruignore"}, Stderr: &stderr}) - c.Assert(err, check.IsNil) - - got := extractFiles(s.t, c, &b) - expected := []miniFile{ - {Name: ".tsuruignore", Type: tar.TypeReg, Data: []byte("*.txt")}, - {Name: "directory", Type: tar.TypeDir}, - {Name: "directory/dir2", Type: tar.TypeDir}, - } - c.Assert(got, check.DeepEquals, expected) - - c.Assert(stderr.String(), check.Matches, `(?s)(.*)Using pattern\(s\) from "\.tsuruignore" to include/exclude files\.\.\.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)File "directory/dir2/file\.txt" matches with some pattern provided in the ignore file\.\.\. skipping it\.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)File "directory/file\.txt" matches with some pattern provided in the ignore file\.\.\. skipping it\.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)File "file1\.txt" matches with some pattern provided in the ignore file\.\.\. skipping it\.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)File "file2\.txt" matches with some pattern provided in the ignore file\.\.\. skipping it\.(.*)`) -} - -func (s *S) TestArchive_FilesOnly(c *check.C) { - var b bytes.Buffer - err := Archive(&b, true, []string{"./testdata/deploy/directory/file.txt", "./testdata/deploy2/file1.txt"}, ArchiveOptions{}) - c.Assert(err, check.IsNil) - - got := extractFiles(s.t, c, &b) - expected := []miniFile{ - {Name: "file.txt", Type: tar.TypeReg, Data: []byte("wat\n")}, - {Name: "file1.txt", Type: tar.TypeReg, Data: []byte("something happened\n")}, - } - c.Assert(got, check.DeepEquals, expected) -} - -func (s *S) TestArchive_WithSymlink(c *check.C) { - workingDir, err := os.Getwd() - c.Assert(err, check.IsNil) - - defer func() { os.Chdir(workingDir) }() - - err = os.Chdir(filepath.Join(workingDir, "./testdata-symlink/")) - c.Assert(err, check.IsNil) - - var b bytes.Buffer - err = Archive(&b, false, []string{"."}, ArchiveOptions{}) - c.Assert(err, check.IsNil) - - got := extractFiles(s.t, c, &b) - expected := []miniFile{ - {Name: "link", Linkname: "test", Type: tar.TypeSymlink}, - {Name: "test", Type: tar.TypeDir}, - {Name: "test/index.html", Type: tar.TypeReg, Data: []byte{}}, - } - c.Assert(got, check.DeepEquals, expected) -} - -func (s *S) TestArchive_UnsupportedFileType(c *check.C) { - workingDir, err := os.Getwd() - c.Assert(err, check.IsNil) - - defer func() { os.Chdir(workingDir) }() - - err = os.Chdir(c.MkDir()) - c.Assert(err, check.IsNil) - - l, err := net.Listen("unix", "./server.sock") - c.Assert(err, check.IsNil) - defer l.Close() - - var stderr bytes.Buffer - err = Archive(io.Discard, false, []string{"."}, ArchiveOptions{Stderr: &stderr}) - c.Assert(err, check.ErrorMatches, "missing files to archive") - c.Assert(stderr.String(), check.Matches, `(?s)(.*)WARNING: Skipping file "server.sock" due to unsupported file type.(.*)`) -} - -func (s *S) TestArchive_FilesOnly_MultipleDirs(c *check.C) { - var b, stderr bytes.Buffer - err := Archive(&b, true, []string{"./testdata/deploy", "./testdata/deploy2"}, ArchiveOptions{Stderr: &stderr}) - c.Assert(err, check.IsNil) - - got := extractFiles(s.t, c, &b) - expected := []miniFile{ - {Name: "file.txt", Type: tar.TypeReg, Data: []byte("wat\n")}, - {Name: "file1.txt", Type: tar.TypeReg, Data: []byte("something happened\n")}, - {Name: "file2.txt", Type: tar.TypeReg, Data: []byte("twice\n")}, - {Name: ".tsuruignore", Type: tar.TypeReg, Data: []byte("*.txt")}, - } - c.Assert(got, check.DeepEquals, expected) - - c.Assert(stderr.String(), check.Matches, `(?s)(.*)Skipping file "testdata/deploy2/directory/dir2/file.txt" as it already exists in the current directory.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)Skipping file "testdata/deploy2/directory/file.txt" as it already exists in the current directory.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)Skipping file "testdata/deploy2/file1.txt" as it already exists in the current directory.(.*)`) - c.Assert(stderr.String(), check.Matches, `(?s)(.*)Skipping file "testdata/deploy2/file2.txt" as it already exists in the current directory.(.*)`) -} diff --git a/tsuru/client/build.go b/tsuru/client/build.go index 0d6765342..439cd3304 100644 --- a/tsuru/client/build.go +++ b/tsuru/client/build.go @@ -1,15 +1,12 @@ package client import ( - "bytes" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" - "os" - "path/filepath" "strings" "time" @@ -39,24 +36,13 @@ func (c *AppBuild) Flags() *gnuflag.FlagSet { } func (c *AppBuild) Info() *cmd.Info { - desc := `Build a container image following the app deploy's workflow - but do not change anything on the running application on Tsuru. -You can deploy this container image to the app later. + desc := `Builds a tsuru app image respecting .tsuruignore file. Some examples of calls are: -Files specified in the ".tsuruignore" file are skipped - similar to ".gitignore". +:: -Examples: - To build using app's platform build process (just sending source code or configurations): - Uploading all files within the current directory - $ tsuru app build -a . - - Uploading all files within a specific directory - $ tsuru app build -a mysite/ - - Uploading specific files - $ tsuru app build -a ./myfile.jar ./Procfile - - Uploading specific files but ignoring their directory trees - $ tsuru app build -a --files-only ./my-code/main.go ./tsuru_stuff/Procfile + $ tsuru app build -a myapp -t mytag . + $ tsuru app build -a myapp -t latest myfile.jar Procfile + $ tsuru app build -a myapp -t mytag -f directory/main.go directory/Procfile ` return &cmd.Info{ Name: "app-build", @@ -74,14 +60,6 @@ func (c *AppBuild) Run(context *cmd.Context, client *cmd.Client) error { if len(context.Args) == 0 { return errors.New("You should provide at least one file to build the image.\n") } - - debugWriter := io.Discard - - debug := client != nil && client.Verbosity > 0 // e.g. --verbosity 2 - if debug { - debugWriter = context.Stderr - } - appName, err := c.AppName() if err != nil { return err @@ -111,14 +89,7 @@ func (c *AppBuild) Run(context *cmd.Context, client *cmd.Client) error { } buf := safe.NewBuffer(nil) respBody := prepareUploadStreams(context, buf) - - var archive bytes.Buffer - err = Archive(&archive, c.filesOnly, context.Args, DefaultArchiveOptions(debugWriter)) - if err != nil { - return err - } - - if err = uploadFiles(context, request, buf, body, values, &archive); err != nil { + if err = uploadFiles(context, c.filesOnly, request, buf, body, values); err != nil { return err } resp, err := client.Do(request) @@ -139,33 +110,22 @@ func (c *AppBuild) Run(context *cmd.Context, client *cmd.Client) error { return cmd.ErrAbortCommand } -func uploadFiles(context *cmd.Context, request *http.Request, buf *safe.Buffer, body *safe.Buffer, values url.Values, archive io.Reader) error { - if archive == nil { - request.Header.Set("Content-Type", "application/x-www-form-urlencoded") - _, err := body.WriteString(values.Encode()) - return err - } - +func uploadFiles(context *cmd.Context, filesOnly bool, request *http.Request, buf *safe.Buffer, body *safe.Buffer, values url.Values) error { writer := multipart.NewWriter(body) for k := range values { writer.WriteField(k, values.Get(k)) } - - request.Header.Set("Content-Type", writer.FormDataContentType()) - - f, err := writer.CreateFormFile("file", "archive.tar.gz") + file, err := writer.CreateFormFile("file", "archive.tar.gz") if err != nil { return err } - - if _, err = io.Copy(f, archive); err != nil { - return err - } - - if err = writer.Close(); err != nil { + tm := newTarMaker(context) + err = tm.targz(file, filesOnly, context.Args...) + if err != nil { return err } - + writer.Close() + request.Header.Set("Content-Type", "multipart/form-data; boundary="+writer.Boundary()) fullSize := float64(body.Len()) megabyte := 1024.0 * 1024.0 fmt.Fprintf(context.Stdout, "Uploading files (%0.2fMB)... ", fullSize/megabyte) @@ -188,78 +148,8 @@ func uploadFiles(context *cmd.Context, request *http.Request, buf *safe.Buffer, fmt.Fprintf(context.Stdout, " Processing%s", strings.Repeat(".", count)) count++ } - time.Sleep(2 * time.Second) + time.Sleep(2e9) } }() return nil } - -func buildWithContainerFile(appName, path string, filesOnly bool, files []string, stderr io.Writer) (string, io.Reader, error) { - fi, err := os.Stat(path) - if err != nil { - return "", nil, fmt.Errorf("failed to stat the file %s: %w", path, err) - } - - var containerfile []byte - - switch { - case fi.IsDir(): - path, err = guessingContainerFile(appName, path) - if err != nil { - return "", nil, fmt.Errorf("failed to guess the container file (can you specify the container file passing --dockerfile ./path/to/Dockerfile?): %w", err) - } - - fallthrough - - case fi.Mode().IsRegular(): - containerfile, err = os.ReadFile(path) - if err != nil { - return "", nil, fmt.Errorf("failed to read %s: %w", path, err) - } - - default: - return "", nil, fmt.Errorf("invalid file type") - } - - if len(files) == 0 { // no additional files set, using the dockerfile dir - files = []string{filepath.Dir(path)} - } - - var buildContext bytes.Buffer - err = Archive(&buildContext, filesOnly, files, DefaultArchiveOptions(stderr)) - if err != nil { - return "", nil, err - } - - return string(containerfile), &buildContext, nil -} - -func guessingContainerFile(app, dir string) (string, error) { - validNames := []string{ - fmt.Sprintf("Dockerfile.%s", app), - fmt.Sprintf("Containerfile.%s", app), - "Dockerfile.tsuru", - "Containerfile.tsuru", - "Dockerfile", - "Containerfile", - } - - for _, name := range validNames { - path := filepath.Join(dir, name) - - fi, err := os.Stat(path) - if errors.Is(err, os.ErrNotExist) { - continue - } - - if err != nil { - return "", err - } - - if fi.Mode().IsRegular() { - return path, nil - } - } - - return "", errors.New("container file not found") -} diff --git a/tsuru/client/build_test.go b/tsuru/client/build_test.go index 2fd9d9186..05a739e30 100644 --- a/tsuru/client/build_test.go +++ b/tsuru/client/build_test.go @@ -6,11 +6,8 @@ package client import ( "bytes" - "io" "io/ioutil" "net/http" - "os" - "path/filepath" "strings" "github.com/tsuru/tsuru/cmd" @@ -26,7 +23,9 @@ func (s *S) TestBuildInfo(c *check.C) { func (s *S) TestBuildRun(c *check.C) { calledTimes := 0 var buf bytes.Buffer - err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) + ctx := cmd.Context{Stderr: bytes.NewBufferString(""), Stdout: bytes.NewBufferString("")} + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "testdata", "..") c.Assert(err, check.IsNil) trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "\nOK\n", Status: http.StatusOK}, @@ -63,7 +62,9 @@ func (s *S) TestBuildRun(c *check.C) { func (s *S) TestBuildFail(c *check.C) { var buf bytes.Buffer - err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) + ctx := cmd.Context{Stderr: bytes.NewBufferString(""), Stdout: bytes.NewBufferString("")} + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "testdata", "..") c.Assert(err, check.IsNil) trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "Failed", Status: http.StatusOK}, @@ -127,64 +128,3 @@ func (s *S) TestBuildRunWithoutTag(c *check.C) { c.Assert(err, check.NotNil) c.Assert(err.Error(), check.Equals, "You should provide one tag to build the image.\n") } - -func (s *S) TestGuessingContainerFile(c *check.C) { - cases := []struct { - files []string - app string - expected func(d string) string - expectedError string - }{ - { - expectedError: "container file not found", - }, - { - app: "my-app", - files: []string{"Containerfile"}, - expected: func(root string) string { return filepath.Join(root, "Containerfile") }, - }, - { - app: "my-app", - files: []string{"Containerfile", "Dockerfile"}, - expected: func(root string) string { return filepath.Join(root, "Dockerfile") }, - }, - { - app: "my-app", - files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru"}, - expected: func(root string) string { return filepath.Join(root, "Containerfile.tsuru") }, - }, - { - app: "my-app", - files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru"}, - expected: func(root string) string { return filepath.Join(root, "Dockerfile.tsuru") }, - }, - { - app: "my-app", - files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru", "Containerfile.my-app"}, - expected: func(root string) string { return filepath.Join(root, "Containerfile.my-app") }, - }, - { - app: "my-app", - files: []string{"Containerfile", "Dockerfile", "Containerfile.tsuru", "Dockerfile.tsuru", "Containerfile.my-app", "Dockerfile.my-app"}, - expected: func(root string) string { return filepath.Join(root, "Dockerfile.my-app") }, - }, - } - - for _, tt := range cases { - dir := c.MkDir() - - for _, name := range tt.files { - f, err := os.Create(filepath.Join(dir, name)) - c.Check(err, check.IsNil) - c.Check(f.Close(), check.IsNil) - } - - got, err := guessingContainerFile(tt.app, dir) - if tt.expectedError != "" { - c.Check(err, check.ErrorMatches, tt.expectedError) - } else { - c.Check(err, check.IsNil) - c.Check(got, check.DeepEquals, tt.expected(dir)) - } - } -} diff --git a/tsuru/client/deploy.go b/tsuru/client/deploy.go index 02044dbcd..f56d3a69f 100644 --- a/tsuru/client/deploy.go +++ b/tsuru/client/deploy.go @@ -5,7 +5,8 @@ package client import ( - "bytes" + "archive/tar" + "compress/gzip" "context" "encoding/json" "errors" @@ -14,11 +15,15 @@ import ( "io/ioutil" "net/http" "net/url" + "os" + "path" + "path/filepath" "sort" "strconv" "strings" "sync" + ignore "github.com/sabhiram/go-gitignore" "github.com/tsuru/gnuflag" "github.com/tsuru/go-tsuruclient/pkg/client" "github.com/tsuru/go-tsuruclient/pkg/tsuru" @@ -28,9 +33,14 @@ import ( "github.com/tsuru/tsuru/cmd" tsuruIo "github.com/tsuru/tsuru/io" "github.com/tsuru/tsuru/safe" + terminal "golang.org/x/term" ) -const deployOutputBufferSize = 4096 +const ( + deployOutputBufferSize = 4096 + + clearLineEscape = "\033[2K\r" +) type deployList []tsuruapp.DeployData @@ -138,12 +148,11 @@ var _ cmd.Cancelable = &AppDeploy{} type AppDeploy struct { cmd.AppNameMixIn - image string - message string - dockerfile string - eventID string - fs *gnuflag.FlagSet - m sync.Mutex + image string + message string + eventID string + fs *gnuflag.FlagSet + m sync.Mutex deployVersionArgs filesOnly bool } @@ -161,43 +170,26 @@ func (c *AppDeploy) Flags() *gnuflag.FlagSet { c.fs.BoolVar(&c.filesOnly, "f", false, filesOnly) c.fs.BoolVar(&c.filesOnly, "files-only", false, filesOnly) c.deployVersionArgs.flags(c.fs) - c.fs.StringVar(&c.dockerfile, "dockerfile", "", "Container file") } return c.fs } func (c *AppDeploy) Info() *cmd.Info { - return &cmd.Info{ - Name: "app-deploy", - Usage: "app deploy [--app ] [--image ] [--dockerfile ] [--message ] [--files-only] [--new-version] [--override-old-versions] [file-or-dir ...]", - Desc: `Deploy the source code and/or configurations to the application on Tsuru. - -Files specified in the ".tsuruignore" file are skipped - similar to ".gitignore". It also honors ".dockerignore" file if deploying with container file (--dockerfile). - -Examples: - To deploy using app's platform build process (just sending source code or configurations): - Uploading all files within the current directory - $ tsuru app deploy -a . - - Uploading all files within a specific directory - $ tsuru app deploy -a mysite/ - - Uploading specific files - $ tsuru app deploy -a ./myfile.jar ./Procfile - - Uploading specific files but ignoring their directory trees - $ tsuru app deploy -a --files-only ./my-code/main.go ./tsuru_stuff/Procfile + desc := `Deploys set of files and/or directories to tsuru server. Some examples of +calls are: - To deploy using a container image: - $ tsuru app deploy -a --image registry.example.com/my-company/app:v42 - - To deploy using container file ("docker build" mode): - Sending the the current directory as container build context - uses Dockerfile file as container image instructions: - $ tsuru app deploy -a --dockerfile . +:: - Sending a specific container file and specific directory as container build context: - $ tsuru app deploy -a --dockerfile ./Dockerfile.other ./other/ -`, + $ tsuru app deploy . + $ tsuru app deploy myfile.jar Procfile + $ tsuru app deploy -f directory/main.go directory/Procfile + $ tsuru app deploy mysite + $ tsuru app deploy -i http://registry.mysite.com:5000/image-name +` + return &cmd.Info{ + Name: "app-deploy", + Usage: "app deploy [-a/--app ] [-i/--image ] [-m/--message ] [--new-version] [--override-old-versions] [-f/--files-only] [file-or-dir-2] ... [file-or-dir-n]", + Desc: desc, MinArgs: 0, } } @@ -223,97 +215,64 @@ func prepareUploadStreams(context *cmd.Context, buf *safe.Buffer) io.Writer { func (c *AppDeploy) Run(context *cmd.Context, client *cmd.Client) error { context.RawOutput() - - if c.image == "" && c.dockerfile == "" && len(context.Args) == 0 { - return errors.New("You should provide at least one file, Docker image name or Dockerfile to deploy.\n") + if c.image == "" && len(context.Args) == 0 { + return errors.New("You should provide at least one file or a docker image to deploy.\n") } - if c.image != "" && len(context.Args) > 0 { return errors.New("You can't deploy files and docker image at the same time.\n") } - - if c.image != "" && c.dockerfile != "" { - return errors.New("You can't deploy container image and container file at same time.\n") + appName, err := c.AppName() + if err != nil { + return err } - - debugWriter := io.Discard - - debug := client != nil && client.Verbosity > 0 // e.g. --verbosity 2 - if debug { - debugWriter = context.Stderr + u, err := cmd.GetURL("/apps/" + appName) + if err != nil { + return err } - - appName, err := c.AppName() + request, err := http.NewRequest("GET", u, nil) + if err != nil { + return err + } + _, err = client.Do(request) if err != nil { return err } - - values := url.Values{} - origin := "app-deploy" if c.image != "" { origin = "image" } + values := url.Values{} values.Set("origin", origin) - + c.deployVersionArgs.values(values) if c.message != "" { values.Set("message", c.message) } - - c.deployVersionArgs.values(values) - - u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/deploy", appName)) + u, err = cmd.GetURL(fmt.Sprintf("/apps/%s/deploy", appName)) if err != nil { return err } - body := safe.NewBuffer(nil) - request, err := http.NewRequest("POST", u, body) + request, err = http.NewRequest("POST", u, body) if err != nil { return err } - buf := safe.NewBuffer(nil) - c.m.Lock() respBody := prepareUploadStreams(context, buf) c.m.Unlock() - - var archive io.Reader - if c.image != "" { - fmt.Fprintln(context.Stdout, "Deploying container image...") + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") values.Set("image", c.image) - } - - if c.dockerfile != "" { - fmt.Fprintln(context.Stdout, "Deploying with Dockerfile...") - - var dockerfile string - dockerfile, archive, err = buildWithContainerFile(appName, c.dockerfile, c.filesOnly, context.Args, debugWriter) + _, err = body.WriteString(values.Encode()) if err != nil { return err } - - values.Set("dockerfile", dockerfile) - } - - if c.image == "" && c.dockerfile == "" { - fmt.Fprintln(context.Stdout, "Deploying using app's platform...") - - var buffer bytes.Buffer - err = Archive(&buffer, c.filesOnly, context.Args, DefaultArchiveOptions(debugWriter)) - if err != nil { + fmt.Fprint(context.Stdout, "Deploying image...") + } else { + if err = uploadFiles(context, c.filesOnly, request, buf, body, values); err != nil { return err } - - archive = &buffer } - - if err = uploadFiles(context, request, buf, body, values, archive); err != nil { - return err - } - c.m.Lock() resp, err := client.Do(request) if err != nil { @@ -323,7 +282,6 @@ func (c *AppDeploy) Run(context *cmd.Context, client *cmd.Client) error { defer resp.Body.Close() c.eventID = resp.Header.Get("X-Tsuru-Eventid") c.m.Unlock() - var readBuffer [deployOutputBufferSize]byte var readErr error for readErr == nil { @@ -374,6 +332,188 @@ func (c *AppDeploy) Cancel(ctx cmd.Context, cli *cmd.Client) error { return err } +type tarMaker struct { + ctx *cmd.Context + isTerm bool +} + +func newTarMaker(ctx *cmd.Context) tarMaker { + isTerm := false + if desc, ok := ctx.Stdin.(interface { + Fd() uintptr + }); ok { + fd := int(desc.Fd()) + isTerm = terminal.IsTerminal(fd) + } + return tarMaker{ + ctx: ctx, + isTerm: isTerm, + } +} + +func (m tarMaker) targz(destination io.Writer, filesOnly bool, filepaths ...string) error { + fmt.Fprint(m.ctx.Stdout, "Generating tar.gz...") + defer fmt.Fprintf(m.ctx.Stdout, "%sGenerating tar.gz. Done!\n", clearLineEscape) + ign, err := ignore.CompileIgnoreFile(".tsuruignore") + if err != nil && !os.IsNotExist(err) { + return err + } + gzipWriter := gzip.NewWriter(destination) + tarWriter := tar.NewWriter(gzipWriter) + for _, path := range filepaths { + if path == ".." { + fmt.Fprintf(m.ctx.Stderr, "Warning: skipping %q", path) + continue + } + var fi os.FileInfo + fi, err = os.Lstat(path) + if err != nil { + return err + } + var wd string + wd, err = os.Getwd() + if err != nil { + return err + } + fiName := filepath.Join(wd, fi.Name()) + if ign != nil && ign.MatchesPath(fiName) { + continue + } + if fi.IsDir() { + dir := wd + dirFilesOnly := filesOnly || len(filepaths) == 1 + if dirFilesOnly { + dir = path + path = "." + } + err = inDir(func() error { + return m.addDir(tarWriter, path, ign, dirFilesOnly) + }, dir) + } else { + err = m.addFile(tarWriter, path, filesOnly) + } + if err != nil { + return err + } + } + err = tarWriter.Close() + if err != nil { + return err + } + return gzipWriter.Close() +} + +func inDir(fn func() error, path string) error { + old, err := os.Getwd() + if err != nil { + return err + } + err = os.Chdir(path) + if err != nil { + return err + } + defer os.Chdir(old) + return fn() +} + +func (m tarMaker) addDir(writer *tar.Writer, dirpath string, ign *ignore.GitIgnore, filesOnly bool) error { + dir, err := os.Open(dirpath) + if err != nil { + return err + } + defer dir.Close() + if !filesOnly { + var fi os.FileInfo + fi, err = dir.Stat() + if err != nil { + return err + } + var header *tar.Header + header, err = tar.FileInfoHeader(fi, "") + if err != nil { + return err + } + header.Name = dirpath + err = writer.WriteHeader(header) + if err != nil { + return err + } + } + fis, err := dir.Readdir(0) + if err != nil { + return err + } + wd, err := os.Getwd() + if err != nil { + return err + } + for _, fi := range fis { + fiName := filepath.Join(wd, fi.Name()) + if dirpath != "." { + fiName = filepath.Join(wd, dirpath, fi.Name()) + } + if ign != nil && ign.MatchesPath(fiName) { + continue + } + if fi.IsDir() { + err = m.addDir(writer, path.Join(dirpath, fi.Name()), ign, false) + } else { + err = m.addFile(writer, path.Join(dirpath, fi.Name()), filesOnly) + } + if err != nil { + return err + } + } + return nil +} + +func (m tarMaker) addFile(writer *tar.Writer, filepath string, filesOnly bool) error { + fi, err := os.Lstat(filepath) + if err != nil { + return err + } + var linkName string + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + var target string + target, err = os.Readlink(filepath) + if err != nil { + return err + } + linkName = target + } + header, err := tar.FileInfoHeader(fi, linkName) + if err != nil { + return err + } + header.Name = filepath + if filesOnly { + header.Name = path.Base(filepath) + } + if m.isTerm { + fmt.Fprintf(m.ctx.Stdout, "%sGenerating tar.gz... adding %s", clearLineEscape, header.Name) + } + err = writer.WriteHeader(header) + if err != nil { + return err + } + if linkName != "" { + return nil + } + f, err := os.Open(filepath) + if err != nil { + return err + } + defer f.Close() + n, err := io.Copy(writer, f) + if err != nil { + return err + } + if n != fi.Size() { + return io.ErrShortWrite + } + return nil +} + type firstWriter struct { io.Writer once sync.Once diff --git a/tsuru/client/deploy_test.go b/tsuru/client/deploy_test.go index 178ac1136..d711de18a 100644 --- a/tsuru/client/deploy_test.go +++ b/tsuru/client/deploy_test.go @@ -7,11 +7,16 @@ package client import ( "archive/tar" "bytes" + "compress/gzip" "encoding/json" "io" "io/ioutil" "net/http" + "os" + "path" "path/filepath" + "runtime" + "sort" "strings" "time" @@ -28,16 +33,22 @@ func (s *S) TestDeployInfo(c *check.C) { } func (s *S) TestDeployRun(c *check.C) { + calledTimes := 0 var buf bytes.Buffer - err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) + ctx := cmd.Context{Stderr: bytes.NewBufferString(""), Stdout: bytes.NewBufferString("")} + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "testdata", "..") c.Assert(err, check.IsNil) - trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "deploy worked\nOK\n", Status: http.StatusOK}, CondFunc: func(req *http.Request) bool { + calledTimes++ if req.Body != nil { defer req.Body.Close() } + if calledTimes == 1 { + return req.Method == "GET" && strings.HasSuffix(req.URL.Path, "/apps/secret") + } file, _, transErr := req.FormFile("file") c.Assert(transErr, check.IsNil) content, transErr := ioutil.ReadAll(file) @@ -58,8 +69,10 @@ func (s *S) TestDeployRun(c *check.C) { err = cmd.Flags().Parse(true, []string{"testdata", "..", "-a", "secret"}) c.Assert(err, check.IsNil) context.Args = cmd.Flags().Args() + err = cmd.Run(&context, client) c.Assert(err, check.IsNil) + c.Assert(calledTimes, check.Equals, 2) } type slowReader struct { @@ -77,12 +90,24 @@ func (s *slowReader) Close() error { } func (s *S) TestDeployRunCancel(c *check.C) { + calledTimes := 0 var buf bytes.Buffer - err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) + ctx := cmd.Context{Stderr: bytes.NewBufferString(""), Stdout: bytes.NewBufferString("")} + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "testdata", "..") c.Assert(err, check.IsNil) deploy := make(chan struct{}, 1) trans := cmdtest.MultiConditionalTransport{ ConditionalTransports: []cmdtest.ConditionalTransport{ + { + Transport: cmdtest.Transport{Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + calledTimes++ + c.Assert(req.Method, check.Equals, "GET") + c.Assert(req.URL.Path, check.Equals, "/1.0/apps/secret") + return true + }, + }, { Transport: &cmdtest.BodyTransport{ Status: http.StatusOK, @@ -91,6 +116,7 @@ func (s *S) TestDeployRunCancel(c *check.C) { }, CondFunc: func(req *http.Request) bool { deploy <- struct{}{} + calledTimes++ if req.Body != nil { defer req.Body.Close() } @@ -107,6 +133,7 @@ func (s *S) TestDeployRunCancel(c *check.C) { { Transport: cmdtest.Transport{Status: http.StatusOK}, CondFunc: func(req *http.Request) bool { + calledTimes++ c.Assert(req.Method, check.Equals, "POST") c.Assert(req.URL.Path, check.Equals, "/1.1/events/5aec54d93195b20001194951/cancel") return true @@ -135,17 +162,24 @@ func (s *S) TestDeployRunCancel(c *check.C) { <-deploy err = cmd.Cancel(context, client) c.Assert(err, check.IsNil) + c.Assert(calledTimes, check.Equals, 3) } func (s *S) TestDeployImage(c *check.C) { + calledTimes := 0 trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "deploy worked\nOK\n", Status: http.StatusOK}, CondFunc: func(req *http.Request) bool { + calledTimes++ if req.Body != nil { defer req.Body.Close() } - c.Assert(req.Header.Get("Content-Type"), check.Matches, "application/x-www-form-urlencoded") - c.Assert(req.FormValue("image"), check.Equals, "registr.com/image-to-deploy") + if calledTimes == 1 { + return req.Method == "GET" && strings.HasSuffix(req.URL.Path, "/apps/secret") + } + image := req.FormValue("image") + c.Assert(image, check.Equals, "registr.com/image-to-deploy") + c.Assert(req.Header.Get("Content-Type"), check.Equals, "application/x-www-form-urlencoded") c.Assert(req.FormValue("origin"), check.Equals, "image") return req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/apps/secret/deploy") }, @@ -162,18 +196,26 @@ func (s *S) TestDeployImage(c *check.C) { context.Args = cmd.Flags().Args() err = cmd.Run(&context, client) c.Assert(err, check.IsNil) + c.Assert(calledTimes, check.Equals, 2) } func (s *S) TestDeployRunWithMessage(c *check.C) { + calledTimes := 0 var buf bytes.Buffer - err := Archive(&buf, false, []string{"testdata", ".."}, DefaultArchiveOptions(io.Discard)) + ctx := cmd.Context{Stderr: bytes.NewBufferString(""), Stdout: bytes.NewBufferString("")} + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "testdata", "..") c.Assert(err, check.IsNil) trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "deploy worked\nOK\n", Status: http.StatusOK}, CondFunc: func(req *http.Request) bool { + calledTimes++ if req.Body != nil { defer req.Body.Close() } + if calledTimes == 1 { + return req.Method == "GET" && strings.HasSuffix(req.URL.Path, "/apps/secret") + } file, _, transErr := req.FormFile("file") c.Assert(transErr, check.IsNil) content, transErr := ioutil.ReadAll(file) @@ -197,13 +239,16 @@ func (s *S) TestDeployRunWithMessage(c *check.C) { c.Assert(err, check.IsNil) err = cmd.Run(&context, client) c.Assert(err, check.IsNil) + c.Assert(calledTimes, check.Equals, 2) } func (s *S) TestDeployAuthNotOK(c *check.C) { + calledTimes := 0 trans := cmdtest.ConditionalTransport{ Transport: cmdtest.Transport{Message: "Forbidden", Status: http.StatusForbidden}, CondFunc: func(req *http.Request) bool { - return req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/apps/secret/deploy") + calledTimes++ + return req.Method == "GET" && strings.HasSuffix(req.URL.Path, "/apps/secret") }, } client := cmd.NewClient(&http.Client{Transport: &trans}, nil, manager) @@ -219,6 +264,7 @@ func (s *S) TestDeployAuthNotOK(c *check.C) { context.Args = command.Flags().Args() err = command.Run(&context, client) c.Assert(err, check.ErrorMatches, "Forbidden") + c.Assert(calledTimes, check.Equals, 1) } func (s *S) TestDeployRunNotOK(c *check.C) { @@ -256,79 +302,90 @@ func (s *S) TestDeployRunFileNotFound(c *check.C) { } func (s *S) TestDeployRunWithoutArgsAndImage(c *check.C) { + var stdout, stderr bytes.Buffer + ctx := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + Args: []string{"-a", "secret"}, + } + trans := cmdtest.Transport{Message: "OK\n", Status: http.StatusOK} + client := cmd.NewClient(&http.Client{Transport: &trans}, nil, manager) command := AppDeploy{} - err := command.Flags().Parse(true, []string{"-a", "secret"}) + err := command.Flags().Parse(true, ctx.Args) c.Assert(err, check.IsNil) - ctx := &cmd.Context{Stdout: io.Discard, Stderr: io.Discard, Args: command.Flags().Args()} - client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Status: http.StatusInternalServerError}}, nil, manager) - err = command.Run(ctx, client) + ctx.Args = command.Flags().Args() + err = command.Run(&ctx, client) c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "You should provide at least one file, Docker image name or Dockerfile to deploy.\n") + c.Assert(err.Error(), check.Equals, "You should provide at least one file or a docker image to deploy.\n") } func (s *S) TestDeployRunWithArgsAndImage(c *check.C) { + var stdout, stderr bytes.Buffer + ctx := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + Args: []string{"testdata", "..", "-a", "secret"}, + } + trans := cmdtest.Transport{Message: "OK\n", Status: http.StatusOK} + client := cmd.NewClient(&http.Client{Transport: &trans}, nil, manager) command := AppDeploy{} - err := command.Flags().Parse(true, []string{"-i", "registr.com/image-to-deploy", "./path/to/dir"}) - c.Assert(err, check.IsNil) - ctx := &cmd.Context{Stdout: io.Discard, Stderr: io.Discard, Args: command.Flags().Args()} - client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Status: http.StatusInternalServerError}}, nil, manager) - err = command.Run(ctx, client) - c.Assert(err, check.ErrorMatches, "You can't deploy files and docker image at the same time.\n") + command.Flags().Parse(true, []string{"-i", "registr.com/image-to-deploy"}) + err := command.Run(&ctx, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "You can't deploy files and docker image at the same time.\n") } func (s *S) TestDeployRunRequestFailure(c *check.C) { trans := cmdtest.Transport{Message: "app not found\n", Status: http.StatusNotFound} client := cmd.NewClient(&http.Client{Transport: &trans}, nil, manager) + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + Args: []string{"testdata", "..", "-a", "secret"}, + } command := AppDeploy{} - err := command.Flags().Parse(true, []string{"testdata", "..", "-a", "secret"}) + err := command.Flags().Parse(true, context.Args) c.Assert(err, check.IsNil) - ctx := &cmd.Context{Stdout: io.Discard, Stderr: io.Discard, Args: command.Flags().Args()} - err = command.Run(ctx, client) - c.Assert(err, check.ErrorMatches, "app not found\n") + context.Args = command.Flags().Args() + err = command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "app not found\n") } -func (s *S) TestDeploy_Run_DockerfileAndDockerImage(c *check.C) { - command := AppDeploy{} - err := command.Flags().Parse(true, []string{"-i", "registry.example.com/my-team/my-app:v42", "--dockerfile", "."}) +func (s *S) TestTargzSymlink(c *check.C) { + if runtime.GOOS == "windows" { + c.Skip("no symlink support on windows") + } + var buf, outBuf bytes.Buffer + ctx := cmd.Context{Stderr: &buf, Stdout: &outBuf} + var gzipBuf, tarBuf bytes.Buffer + tm := newTarMaker(&ctx) + err := tm.targz(&gzipBuf, false, "testdata-symlink", "..") c.Assert(err, check.IsNil) - ctx := &cmd.Context{Stdout: io.Discard, Stderr: io.Discard, Args: command.Flags().Args()} - client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Status: http.StatusInternalServerError}}, nil, manager) - err = command.Run(ctx, client) - c.Assert(err, check.ErrorMatches, "You can't deploy container image and container file at same time.\n") -} - -func (s *S) TestDeploy_Run_UsingDockerfile(c *check.C) { - command := AppDeploy{} - err := command.Flags().Parse(true, []string{"-a", "my-app", "--dockerfile", "./testdata/deploy4/"}) + gzipReader, err := gzip.NewReader(&gzipBuf) c.Assert(err, check.IsNil) - - ctx := &cmd.Context{Stdout: io.Discard, Stderr: io.Discard, Args: command.Flags().Args()} - - trans := &cmdtest.ConditionalTransport{ - Transport: cmdtest.Transport{Message: "deployed\nOK\n", Status: http.StatusOK}, - CondFunc: func(req *http.Request) bool { - if req.Body != nil { - defer req.Body.Close() - } - c.Assert(req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") - c.Assert(req.FormValue("dockerfile"), check.Equals, "FROM busybox:latest\n\nCOPY ./app.sh /usr/local/bin/\n") - - file, _, nerr := req.FormFile("file") - c.Assert(nerr, check.IsNil) - defer file.Close() - files := extractFiles(s.t, c, file) - c.Assert(files, check.DeepEquals, []miniFile{ - {Name: filepath.Join("testdata", "deploy4"), Type: tar.TypeDir}, - {Name: filepath.Join("testdata", "deploy4", "Dockerfile"), Type: tar.TypeReg, Data: []byte("FROM busybox:latest\n\nCOPY ./app.sh /usr/local/bin/\n")}, - {Name: filepath.Join("testdata", "deploy4", "app.sh"), Type: tar.TypeReg, Data: []byte("echo \"Starting my application :P\"\n")}, - }) - - return req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/apps/my-app/deploy") - }, + _, err = io.Copy(&tarBuf, gzipReader) + c.Assert(err, check.IsNil) + tarReader := tar.NewReader(&tarBuf) + var headers [][]string + for header, err := tarReader.Next(); err == nil; header, err = tarReader.Next() { + if header.Linkname != "" { + headers = append(headers, []string{header.Name, header.Linkname}) + } } + expected := [][]string{{"testdata-symlink/link", "test"}} + c.Assert(headers, check.DeepEquals, expected) +} - err = command.Run(ctx, cmd.NewClient(&http.Client{Transport: trans}, nil, manager)) - c.Assert(err, check.IsNil) +func (s *S) TestTargzFailure(c *check.C) { + var stderr, stdout bytes.Buffer + ctx := cmd.Context{Stderr: &stderr, Stdout: &stdout} + var buf bytes.Buffer + tm := newTarMaker(&ctx) + err := tm.targz(&buf, false, "/tmp/something/that/definitely/doesn't/exist/right", "testdata") + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Matches, ".*(no such file or directory|cannot find the path specified).*") } func (s *S) TestDeployListInfo(c *check.C) { @@ -562,3 +619,186 @@ func (s *S) TestAppDeployRebuild(c *check.C) { c.Assert(called, check.Equals, true) c.Assert(stdout.String(), check.Equals, expectedOut) } + +func (s *S) TestDeployRunTarGeneration(c *check.C) { + var foundFiles []string + trans := cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: "deploy worked\nOK\n", Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + if req.Method == "GET" { + return true + } + defer req.Body.Close() + file, _, transErr := req.FormFile("file") + c.Assert(transErr, check.IsNil) + gzReader, transErr := gzip.NewReader(file) + c.Assert(transErr, check.IsNil) + tarReader := tar.NewReader(gzReader) + foundFiles = nil + for { + header, transErr := tarReader.Next() + if transErr == io.EOF { + break + } + c.Assert(transErr, check.IsNil) + foundFiles = append(foundFiles, header.Name) + } + return true + }, + } + client := cmd.NewClient(&http.Client{Transport: &trans}, nil, manager) + tests := []struct { + files []string + ignored []string + deployArgs []string + flags []string + expected []string + expectedStderr string + absPath bool + }{ + { + files: []string{"f1", "f2", "d1/f3", "d1/d2/f4"}, + deployArgs: []string{"."}, + expected: []string{"f1", "f2", "d1", "d1/f3", "d1/d2", "d1/d2/f4"}, + flags: []string{"-a", "secret"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy2/file2.txt"}, + deployArgs: []string{"testdata/deploy/file1.txt", "testdata/deploy2/file2.txt"}, + expected: []string{"testdata/deploy/file1.txt", "testdata/deploy2/file2.txt"}, + flags: []string{"-a", "secret"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy2/file2.txt"}, + deployArgs: []string{"testdata/deploy/file1.txt", "testdata/deploy2/file2.txt"}, + flags: []string{"-a", "secret", "-f"}, + expected: []string{"file1.txt", "file2.txt"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy2/file3.txt", "testdata/deploy2/directory/file4.txt"}, + deployArgs: []string{"testdata/deploy", "testdata/deploy2"}, + flags: []string{"-a", "secret"}, + expected: []string{"testdata/deploy", "testdata/deploy2", "testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy2/file3.txt", "testdata/deploy2/directory", "testdata/deploy2/directory/file4.txt"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy2/file3.txt", "testdata/deploy2/directory/file4.txt"}, + deployArgs: []string{"testdata/deploy", "testdata/deploy2"}, + flags: []string{"-a", "secret", "-f"}, + expected: []string{"file1.txt", "file2.txt", "file3.txt", "directory", "directory/file4.txt"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy/directory/file.txt"}, + deployArgs: []string{"testdata/deploy", ".."}, + flags: []string{"-a", "secret"}, + expected: []string{"testdata/deploy", "testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy/directory", "testdata/deploy/directory/file.txt"}, + expectedStderr: `Warning: skipping "\.\."`, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy/directory/file.txt"}, + deployArgs: []string{"testdata/deploy"}, + flags: []string{"-a", "secret"}, + expected: []string{"file1.txt", "file2.txt", "directory", "directory/file.txt"}, + }, + { + files: []string{"testdata/deploy2/file1.txt", "testdata/deploy2/file2.txt", "testdata/deploy2/directory/file.txt", "testdata/deploy2/directory/dir2/file.txt"}, + ignored: []string{"*.txt"}, + flags: []string{"-a", "secret"}, + deployArgs: []string{"testdata/deploy2"}, + expected: []string{"directory", "directory/dir2"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy2/file3.txt", "testdata/deploy2/directory/file4.txt"}, + deployArgs: []string{"testdata/deploy", "testdata/deploy2"}, + ignored: []string{"*.txt"}, + flags: []string{"-a", "secret"}, + expected: []string{"testdata/deploy", "testdata/deploy2", "testdata/deploy2/directory"}, + }, + { + files: []string{"testdata/deploy/file1.txt", "testdata/deploy/file2.txt", "testdata/deploy2/file3.txt", "testdata/deploy2/directory/file4.txt"}, + deployArgs: []string{"testdata/deploy", "testdata/deploy2"}, + ignored: []string{"*.txt"}, + flags: []string{"-a", "secret", "-f"}, + expected: []string{"directory"}, + }, + { + files: []string{"testdata/deploy2/file1.txt", "testdata/deploy2/file2.txt", "testdata/deploy2/directory/file.txt", "testdata/deploy2/directory/dir2/file.txt"}, + ignored: []string{"*.txt"}, + deployArgs: []string{"testdata/deploy2"}, + flags: []string{"-a", "secret"}, + expected: []string{"directory", "directory/dir2"}, + absPath: true, + }, + { + files: []string{"file1.txt", "file2.txt", "directory/file.txt", "directory/dir2/file.txt"}, + ignored: []string{"*.txt"}, + deployArgs: []string{"."}, + flags: []string{"-a", "secret"}, + expected: []string{".tsuruignore", "directory", "directory/dir2"}, + }, + { + files: []string{"testdata/deploy2/file1.txt", "testdata/deploy2/file2.txt", "testdata/deploy2/directory/file.txt", "testdata/deploy2/directory/dir2/file.txt"}, + ignored: []string{"directory"}, + deployArgs: []string{"testdata/deploy2"}, + flags: []string{"-a", "secret"}, + expected: []string{"file1.txt", "file2.txt"}, + }, + { + files: []string{"testdata/deploy2/file1.txt", "testdata/deploy2/file2.txt", "testdata/deploy2/directory/file.txt", "testdata/deploy2/directory/dir2/file.txt"}, + ignored: []string{"*/dir2"}, + deployArgs: []string{"testdata/deploy2"}, + flags: []string{"-a", "secret"}, + expected: []string{"directory", "directory/file.txt", "file1.txt", "file2.txt"}, + }, + { + files: []string{"testdata/deploy2/file1.txt", "testdata/deploy2/file2.txt", "testdata/deploy2/directory/file.txt", "testdata/deploy2/directory/dir2/file.txt"}, + ignored: []string{"directory/dir2/*"}, + deployArgs: []string{"testdata/deploy2"}, + flags: []string{"-a", "secret"}, + expected: []string{"directory", "directory/dir2", "directory/file.txt", "file1.txt", "file2.txt"}, + }, + } + origDir, err := os.Getwd() + c.Assert(err, check.IsNil) + defer os.Chdir(origDir) + for i, tt := range tests { + comment := check.Commentf("test %d", i) + + tmpDir, err := ioutil.TempDir("", "integration") + c.Assert(err, check.IsNil) + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + c.Assert(err, check.IsNil) + for _, f := range tt.files { + err = os.MkdirAll(path.Dir(f), 0700) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(f, []byte{}, 0600) + c.Assert(err, check.IsNil) + } + if len(tt.ignored) > 0 { + err = ioutil.WriteFile(".tsuruignore", []byte(strings.Join(tt.ignored, "\n")), 0600) + c.Assert(err, check.IsNil) + } + + var stdout, stderr bytes.Buffer + if tt.absPath { + for i, f := range tt.deployArgs { + tt.deployArgs[i], err = filepath.Abs(f) + c.Assert(err, check.IsNil) + } + } + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + Args: tt.deployArgs, + } + cmd := AppDeploy{} + err = cmd.Flags().Parse(true, append([]string{"-a", "secret"}, tt.flags...)) + c.Assert(err, check.IsNil) + err = cmd.Run(&context, client) + c.Assert(err, check.IsNil, comment) + sort.Strings(foundFiles) + sort.Strings(tt.expected) + c.Assert(foundFiles, check.DeepEquals, tt.expected, comment) + c.Assert(stderr.String(), check.Matches, tt.expectedStderr, comment) + } +} diff --git a/tsuru/client/suite_test.go b/tsuru/client/suite_test.go index e689e2b1c..4a5c59122 100644 --- a/tsuru/client/suite_test.go +++ b/tsuru/client/suite_test.go @@ -18,7 +18,6 @@ import ( type S struct { defaultLocation time.Location - t *testing.T } func (s *S) SetUpSuite(c *check.C) { @@ -48,11 +47,7 @@ func (s *S) TearDownTest(c *check.C) { formatter.LocalTZ = &s.defaultLocation } -var suite = &S{} -var _ = check.Suite(suite) +var _ = check.Suite(&S{}) var manager *cmd.Manager -func Test(t *testing.T) { - suite.t = t - check.TestingT(t) -} +func Test(t *testing.T) { check.TestingT(t) } diff --git a/tsuru/client/testdata/deploy4/Dockerfile b/tsuru/client/testdata/deploy4/Dockerfile deleted file mode 100644 index d84437e43..000000000 --- a/tsuru/client/testdata/deploy4/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM busybox:latest - -COPY ./app.sh /usr/local/bin/ diff --git a/tsuru/client/testdata/deploy4/app.sh b/tsuru/client/testdata/deploy4/app.sh deleted file mode 100644 index 37c150b41..000000000 --- a/tsuru/client/testdata/deploy4/app.sh +++ /dev/null @@ -1 +0,0 @@ -echo "Starting my application :P"