diff --git a/pkg/build/helpers.go b/pkg/build/helpers.go index 6e5f27f..e57852f 100644 --- a/pkg/build/helpers.go +++ b/pkg/build/helpers.go @@ -13,22 +13,27 @@ import ( "fmt" "io" "path/filepath" - "strings" "sync" "text/template" pb "github.com/tsuru/deploy-agent/pkg/build/grpc_build_v1" ) +const ( + DefaultTsuruPlatformWorkingDir = "/home/application/current" + ProcfileName = "Procfile" +) + var ( - ProcfileName = "Procfile" TsuruYamlNames = []string{"tsuru.yml", "tsuru.yaml", "app.yml", "app.yaml"} TsuruConfigDirs = []string{"/home/application/current", "/app/user", "/"} ) -func IsTsuruYaml(name string) bool { +func IsTsuruYaml(filename string) bool { + baseName := filepath.Base(filename) + for _, n := range TsuruYamlNames { - if n == name { + if n == baseName { return true } } @@ -39,8 +44,29 @@ func IsTsuruYaml(name string) bool { type TsuruYamlCandidates map[string]string func (c TsuruYamlCandidates) String() string { - for _, n := range TsuruYamlNames { - if s, found := c[n]; found { + for _, dir := range TsuruConfigDirs { + for _, baseName := range TsuruYamlNames { + filename := filepath.Join(dir, baseName) + + if s, found := c[filename]; found { + return s + } + } + } + + return "" +} + +func IsProcfile(filename string) bool { + return filepath.Base(filename) == ProcfileName +} + +type ProcfileCandidates map[string]string + +func (c ProcfileCandidates) String() string { + for _, dir := range TsuruConfigDirs { + filename := filepath.Join(dir, ProcfileName) + if s, found := c[filename]; found { return s } } @@ -61,7 +87,7 @@ func ExtractTsuruAppFilesFromAppSourceContext(ctx context.Context, r io.Reader) t := tar.NewReader(z) - var procfile string + procfile := make(ProcfileCandidates) tsuruYaml := make(TsuruYamlCandidates) for { @@ -78,30 +104,19 @@ func ExtractTsuruAppFilesFromAppSourceContext(ctx context.Context, r io.Reader) continue } - name := strings.TrimPrefix(h.Name, "./") // e.g. "./Procfile" to "Procfile" - if IsTsuruYaml(name) { - data, err := io.ReadAll(t) - if err != nil { - return nil, err - } + filename := filepath.Join(DefaultTsuruPlatformWorkingDir, h.Name) - tsuruYaml[name] = string(data) - continue + if err = copyTsuruYamlToCandidate(filename, t, tsuruYaml); err != nil { + return nil, err } - if name == ProcfileName { - data, err := io.ReadAll(t) - if err != nil { - return nil, err - } - - procfile = string(data) - continue + if err = copyProcfileToCandidate(filename, t, procfile); err != nil { + return nil, err } } return &pb.TsuruConfig{ - Procfile: procfile, + Procfile: procfile.String(), TsuruYaml: tsuruYaml.String(), }, nil } @@ -111,7 +126,7 @@ func ExtractTsuruAppFilesFromContainerImageTarball(ctx context.Context, r io.Rea return nil, err } - var procfile string + procfile := make(ProcfileCandidates) tsuruYaml := make(TsuruYamlCandidates) t := tar.NewReader(r) @@ -129,34 +144,51 @@ func ExtractTsuruAppFilesFromContainerImageTarball(ctx context.Context, r io.Rea continue } - name := filepath.Base(h.Name) - if IsTsuruYaml(name) { - data, err := io.ReadAll(t) - if err != nil { - return nil, err - } + filename := filepath.Join(string(filepath.Separator), h.Name) - tsuruYaml[name] = string(data) - continue + if err = copyTsuruYamlToCandidate(filename, t, tsuruYaml); err != nil { + return nil, err } - if name == ProcfileName { - data, err := io.ReadAll(t) - if err != nil { - return nil, err - } - - procfile = string(data) - continue + if err = copyProcfileToCandidate(filename, t, procfile); err != nil { + return nil, err } } return &pb.TsuruConfig{ - Procfile: procfile, + Procfile: procfile.String(), TsuruYaml: tsuruYaml.String(), }, nil } +func copyTsuruYamlToCandidate(filename string, r io.Reader, dst TsuruYamlCandidates) error { + if !IsTsuruYaml(filename) { + return nil + } + + data, err := io.ReadAll(r) + if err != nil { + return err + } + + dst[filename] = string(data) + return nil +} + +func copyProcfileToCandidate(filename string, r io.Reader, dst ProcfileCandidates) error { + if !IsProcfile(filename) { + return nil + } + + data, err := io.ReadAll(r) + if err != nil { + return err + } + + dst[filename] = string(data) + return nil +} + type BuildContainerfileParams struct { Image string BuildHooks []string diff --git a/pkg/build/helpers_test.go b/pkg/build/helpers_test.go index 59663f4..1b52dc8 100644 --- a/pkg/build/helpers_test.go +++ b/pkg/build/helpers_test.go @@ -9,6 +9,7 @@ import ( "bytes" "compress/gzip" "context" + "fmt" "io" "strings" "testing" @@ -34,6 +35,9 @@ func TestIsTsuruYaml(t *testing.T) { {name: "app.yaml", expected: true}, {name: "other.yaml"}, {name: "not.txt"}, + {name: "./tsuru.yaml", expected: true}, + {name: "/home/application/current/tsuru.yaml", expected: true}, + {name: "/home/application/current/other.yaml"}, } for _, tt := range cases { @@ -53,39 +57,61 @@ func TestTsuruYamlCandidates_String(t *testing.T) { {}, { candidates: TsuruYamlCandidates{ - "other.yaml": "# My other.yaml file", - "example.yaml": "# example.yaml", + "/home/application/current/other.yaml": "# My other.yaml file", + "/home/application/current/example.yaml": "# example.yaml", }, }, { candidates: TsuruYamlCandidates{ - "tsuru.yml": "# Tsuru YAML from tsuru.yml", - "tsuru.yaml": "-------------------------", + "/home/application/current/tsuru.yml": "# Tsuru YAML from tsuru.yml", + "/home/application/current/tsuru.yaml": "-------------------------", }, expected: "# Tsuru YAML from tsuru.yml", }, { candidates: TsuruYamlCandidates{ - "tsuru.yaml": "# Tsuru YAML from tsuru.yaml", - "app.yaml": "----------------", - "app.yml": "----------------", + "/home/application/current/tsuru.yaml": "# Tsuru YAML from tsuru.yaml", + "/home/application/current/app.yaml": "----------------", + "/home/application/current/app.yml": "----------------", }, expected: "# Tsuru YAML from tsuru.yaml", }, { candidates: TsuruYamlCandidates{ - "app.yaml": "----------------", - "app.yml": "# Tsuru YAML from app.yml", + "/home/application/current/app.yaml": "----------------", + "/home/application/current/app.yml": "# Tsuru YAML from app.yml", }, expected: "# Tsuru YAML from app.yml", }, { candidates: TsuruYamlCandidates{ - "app.yaml": "# Tsuru YAML from app.yaml", - "other.yaml": "--------------------", + "/home/application/current/app.yaml": "# Tsuru YAML from app.yaml", + "/home/application/current/other.yaml": "--------------------", }, expected: "# Tsuru YAML from app.yaml", }, + { + candidates: TsuruYamlCandidates{ + "/home/application/current/tsuru.yaml": "# Tsuru YAML from tsuru.yaml", + "/app/user/tsuru.yaml": "--------------------", + "/tsuru.yaml": "--------------------", + }, + expected: "# Tsuru YAML from tsuru.yaml", + }, + { + candidates: TsuruYamlCandidates{ + "/app/user/tsuru.yaml": "# Tsuru YAML from tsuru.yaml", + "/tsuru.yaml": "--------------------", + }, + expected: "# Tsuru YAML from tsuru.yaml", + }, + { + candidates: TsuruYamlCandidates{ + "/tsuru.yaml": "# Tsuru YAML from tsuru.yaml", + "/other.yaml": "--------------------", + }, + expected: "# Tsuru YAML from tsuru.yaml", + }, } for _, tt := range cases { @@ -95,19 +121,85 @@ func TestTsuruYamlCandidates_String(t *testing.T) { } } +func TestProcfileCandidates_String(t *testing.T) { + t.Parallel() + + cases := []struct { + candidates ProcfileCandidates + expected string + }{ + {}, + { + candidates: ProcfileCandidates{ + "/home/application/current/Procfile": "web: ./path/to/server.sh --port ${PORT}", + "/app/user/Procfile": "--------------------", + "/Procfile": "--------------------", + }, + expected: "web: ./path/to/server.sh --port ${PORT}", + }, + { + candidates: ProcfileCandidates{ + "/app/user/Procfile": "web: ./path/to/server.sh --port ${PORT}", + "/Procfile": "--------------------", + }, + expected: "web: ./path/to/server.sh --port ${PORT}", + }, + { + candidates: ProcfileCandidates{ + "/Procfile": "web: ./path/to/server.sh --port ${PORT}", + "/tmp/Procfile": "--------------------", + }, + expected: "web: ./path/to/server.sh --port ${PORT}", + }, + { + candidates: ProcfileCandidates{ + "/tmp/Procfile": "--------------------", + "/app/user/demo/Procfile": "--------------------", + "/home/application/current/demo/Procfile": "--------------------", + }, + }, + } + + for _, tt := range cases { + assert.Equal(t, tt.expected, tt.candidates.String()) + } +} + func TestExtractTsuruAppFilesFromAppSourceContext(t *testing.T) { + t.Parallel() + cases := []struct { file func(t *testing.T) io.Reader expected *pb.TsuruConfig expectedError string }{ + { + file: func(t *testing.T) io.Reader { + return strings.NewReader(`not gzip`) + }, + expectedError: "app source data must be a GZIP compressed file: unexpected EOF", + }, + + { + file: func(t *testing.T) io.Reader { + var b bytes.Buffer + z := gzip.NewWriter(&b) + fmt.Fprintln(z, "gzip but not tarball") + z.Close() + return &b + }, + expectedError: "failed to read next file in the tarball: unexpected EOF", + }, + { file: func(t *testing.T) io.Reader { var buffer bytes.Buffer newTsuruAppSource(t, &buffer, map[string]string{ - "tsuru.yaml": "# Tsuru YAML", - "app.yml": "# Legacy Tsuru YAML", - "Procfile": `web: /path/to/server.sh --address 0.0.0.0:${PORT}`, + "Procfile": `web: /path/to/server.sh --address 0.0.0.0:${PORT}`, + "tsuru.yaml": "# Tsuru YAML", + "app.yml": "# Legacy Tsuru YAML", + "demo/tsuru.yaml": "# Other Tsuru YAML", + "demo/Procfile": "web: ./path/to/other.sh\nworker: ./my/worker.sh\n", }) return &buffer }, @@ -116,19 +208,65 @@ func TestExtractTsuruAppFilesFromAppSourceContext(t *testing.T) { Procfile: `web: /path/to/server.sh --address 0.0.0.0:${PORT}`, }, }, + } + + for _, tt := range cases { + t.Run("", func(t *testing.T) { + require.NotNil(t, tt.file) + tsuruFiles, err := ExtractTsuruAppFilesFromAppSourceContext(context.TODO(), tt.file(t)) + if err != nil { + require.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tsuruFiles, tt.expected) + }) + } +} + +func TestExtractTsuruAppFilesFromContainerImageTarball(t *testing.T) { + t.Parallel() + + cases := []struct { + file func(t *testing.T) io.Reader + expected *pb.TsuruConfig + expectedError string + }{ + { + file: func(t *testing.T) io.Reader { + return strings.NewReader(`not tarball`) + }, + expectedError: "failed to read next file in the tarball: unexpected EOF", + }, { file: func(t *testing.T) io.Reader { - return strings.NewReader(`not gzip`) + var buffer bytes.Buffer + makeTarballFile(t, &buffer, map[string]string{ + "/home/application/current/Procfile": "Awesome Procfile", + "/home/application/current/demo/Procfile": "bad Procfile", + "/app/user/Procfile": "bad Procfile", + "/tmp/Procfile": "bad Procfile", + "/Procfile": "bad Procfile", + + "/home/application/current/tsuru.yml": "Awesome Tsuru YAML", + "/home/application/current/demo/tsuru.yml": "bad Tsuru YAML", + "/app/user/tsuru.yml": "bad Tsuru YAML", + "/tsuru.yml": "bad Tsuru YAML", + }) + return &buffer + }, + expected: &pb.TsuruConfig{ + TsuruYaml: "Awesome Tsuru YAML", + Procfile: `Awesome Procfile`, }, - expectedError: "app source data must be a GZIP compressed file: unexpected EOF", }, } for _, tt := range cases { t.Run("", func(t *testing.T) { require.NotNil(t, tt.file) - tsuruFiles, err := ExtractTsuruAppFilesFromAppSourceContext(context.TODO(), tt.file(t)) + tsuruFiles, err := ExtractTsuruAppFilesFromContainerImageTarball(context.TODO(), tt.file(t)) if err != nil { require.EqualError(t, err, tt.expectedError) return @@ -139,11 +277,20 @@ func TestExtractTsuruAppFilesFromAppSourceContext(t *testing.T) { } } -func newTsuruAppSource(t *testing.T, b *bytes.Buffer, files map[string]string) { - z := gzip.NewWriter(b) +func newTsuruAppSource(t *testing.T, w io.Writer, files map[string]string) { + t.Helper() + + z := gzip.NewWriter(w) defer z.Close() - tr := tar.NewWriter(z) + makeTarballFile(t, z, files) +} + +func makeTarballFile(t *testing.T, w io.Writer, files map[string]string) { + t.Helper() + + tr := tar.NewWriter(w) + defer tr.Close() for name, content := range files { err := tr.WriteHeader(&tar.Header{