diff --git a/cmd/plugin/rpaasv2/cmd/app.go b/cmd/plugin/rpaasv2/cmd/app.go index 3c1b2d953..03d13ac3e 100644 --- a/cmd/plugin/rpaasv2/cmd/app.go +++ b/cmd/plugin/rpaasv2/cmd/app.go @@ -37,6 +37,7 @@ func NewApp(o, e io.Writer, client rpaasclient.Client) (app *cli.App) { NewCmdExec(), NewCmdShell(), NewCmdLogs(), + NewCmdExtraFiles(), } app.Flags = []cli.Flag{ &cli.StringFlag{ diff --git a/cmd/plugin/rpaasv2/cmd/extra_files.go b/cmd/plugin/rpaasv2/cmd/extra_files.go new file mode 100644 index 000000000..f8867fe3b --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/extra_files.go @@ -0,0 +1,327 @@ +// Copyright 2022 tsuru 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 cmd + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/olekukonko/tablewriter" + rpaasclient "github.com/tsuru/rpaas-operator/pkg/rpaas/client" + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" + "github.com/urfave/cli/v2" +) + +func NewCmdExtraFiles() *cli.Command { + return &cli.Command{ + Name: "extra-files", + Aliases: []string{"files"}, + Usage: "Manages persistent files in the instance filesystem", + Subcommands: []*cli.Command{ + NewCmdAddExtraFiles(), + NewCmdUpdateExtraFiles(), + NewCmdDeleteExtraFiles(), + NewCmdListExtraFiles(), + NewCmdGetExtraFile(), + }, + } +} + +func NewCmdAddExtraFiles() *cli.Command { + return &cli.Command{ + Name: "add", + Usage: "Uploads new files", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "file", + Usage: "path in the local filesystem to the file", + Required: true, + }, + }, + Before: setupClient, + Action: runAddExtraFiles, + } +} + +func NewCmdUpdateExtraFiles() *cli.Command { + return &cli.Command{ + Name: "update", + Usage: "Uploads existing files", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "file", + Usage: "path in the local filesystem to the file", + Required: true, + }, + }, + Before: setupClient, + Action: runUpdateExtraFiles, + } +} + +func NewCmdDeleteExtraFiles() *cli.Command { + return &cli.Command{ + Name: "delete", + Aliases: []string{"remove"}, + Usage: "Deletes files", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "file", + Usage: "the name of the file", + Required: true, + }, + }, + Before: setupClient, + Action: runDeleteExtraFiles, + } +} + +func NewCmdListExtraFiles() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "Shows all extra-files inside the instance and it's contents", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.BoolFlag{ + Name: "show-content", + Usage: "shows the content of each file on plain text format", + Required: false, + }, + }, + Before: setupClient, + Action: runListExtraFiles, + } +} + +func NewCmdGetExtraFile() *cli.Command { + return &cli.Command{ + Name: "get", + Aliases: []string{"show"}, + Usage: "Displays the content of the specified file in plain text", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "service", + Aliases: []string{"tsuru-service", "s"}, + Usage: "the Tsuru service name", + }, + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"tsuru-service-instance", "i"}, + Usage: "the reverse proxy instance name", + Required: true, + }, + &cli.StringFlag{ + Name: "file", + Usage: "the name of the file", + Required: true, + }, + }, + Before: setupClient, + Action: runGetExtraFile, + } +} + +func prepareFiles(filePathList []string) ([]types.RpaasFile, error) { + files := []types.RpaasFile{} + for _, fp := range filePathList { + fileContent, err := os.ReadFile(fp) + if err != nil { + return nil, err + } + files = append(files, types.RpaasFile{ + Name: fp, + Content: fileContent, + }) + } + + return files, nil +} + +func extraFilesSuccessMessage(c *cli.Context, prefix, suffix, instance string, files []string) { + fmt.Fprintf(c.App.Writer, "%s ", prefix) + fmt.Fprintf(c.App.Writer, "[%s]", strings.Join(files, ", ")) + fmt.Fprintf(c.App.Writer, " %s %s\n", suffix, instance) +} + +func runAddExtraFiles(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + files, err := prepareFiles(c.StringSlice("file")) + if err != nil { + return err + } + + instance := c.String("instance") + err = client.AddExtraFiles(c.Context, rpaasclient.ExtraFilesArgs{ + Instance: instance, + Files: files, + }) + if err != nil { + return err + } + + fNames := []string{} + for _, file := range files { + fNames = append(fNames, file.Name) + } + extraFilesSuccessMessage(c, "Added", "to", instance, fNames) + return nil +} + +func runUpdateExtraFiles(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + files, err := prepareFiles(c.StringSlice("file")) + if err != nil { + return err + } + + instance := c.String("instance") + err = client.UpdateExtraFiles(c.Context, rpaasclient.ExtraFilesArgs{ + Instance: c.String("instance"), + Files: files, + }) + if err != nil { + return err + } + + fNames := []string{} + for _, file := range files { + fNames = append(fNames, file.Name) + } + extraFilesSuccessMessage(c, "Updated", "on", instance, fNames) + return nil +} + +func runDeleteExtraFiles(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + files := c.StringSlice("file") + instance := c.String("instance") + err = client.DeleteExtraFiles(c.Context, rpaasclient.DeleteExtraFilesArgs{ + Instance: instance, + Files: files, + }) + if err != nil { + return err + } + + extraFilesSuccessMessage(c, "Removed", "from", instance, files) + return nil +} + +func writeExtraFilesOnTableFormat(writer io.Writer, files []types.RpaasFile) { + data := [][]string{} + for _, file := range files { + data = append(data, []string{file.Name, string(file.Content)}) + } + + table := tablewriter.NewWriter(writer) + table.SetHeader([]string{"Name", "Content"}) + table.SetAutoWrapText(false) + table.SetRowLine(true) + table.SetAutoFormatHeaders(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.AppendBulk(data) + table.Render() +} + +func runListExtraFiles(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + showContent := c.Bool("show-content") + args := rpaasclient.ListExtraFilesArgs{ + Instance: c.String("instance"), + ShowContent: showContent, + } + files, err := client.ListExtraFiles(c.Context, args) + if err != nil { + return err + } + switch showContent { + default: + for _, file := range files { + fmt.Fprintln(c.App.Writer, file.Name) + } + case true: + writeExtraFilesOnTableFormat(c.App.Writer, files) + } + return nil +} + +func runGetExtraFile(c *cli.Context) error { + client, err := getClient(c) + if err != nil { + return err + } + + file, err := client.GetExtraFile(c.Context, rpaasclient.GetExtraFileArgs{ + Instance: c.String("instance"), + FileName: c.String("file"), + }) + if err != nil { + return err + } + + fmt.Fprintln(c.App.Writer, strings.TrimSuffix(string(file.Content), "\n")) + return nil +} diff --git a/cmd/plugin/rpaasv2/cmd/extra_files_test.go b/cmd/plugin/rpaasv2/cmd/extra_files_test.go new file mode 100644 index 000000000..35d41ad5b --- /dev/null +++ b/cmd/plugin/rpaasv2/cmd/extra_files_test.go @@ -0,0 +1,398 @@ +// Copyright 2022 tsuru 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 cmd + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + rpaasclient "github.com/tsuru/rpaas-operator/pkg/rpaas/client" + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/fake" + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func TestDeleteExtraFiles(t *testing.T) { + tests := []struct { + name string + args []string + assertion func(t *testing.T, stdout, stderr *bytes.Buffer, err error) + expectedError string + client rpaasclient.Client + }{ + { + name: "when DeleteExtraFiles returns an error", + args: []string{"./rpaasv2", "extra-files", "delete", "-i", "my-instance", "--file", "f1"}, + expectedError: "some error", + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "some error") + }, + client: &fake.FakeClient{ + FakeDeleteExtraFiles: func(args rpaasclient.DeleteExtraFilesArgs) error { + expected := rpaasclient.DeleteExtraFilesArgs{ + Instance: "my-instance", + Files: []string{"f1"}, + } + assert.Equal(t, expected, args) + return fmt.Errorf("some error") + }, + }, + }, + { + name: "when DeleteExtraFiles returns no error", + args: []string{"./rpaasv2", "extra-files", "delete", "-i", "my-instance", "--file", "f1", "--file", "f2"}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error) { + require.NoError(t, err) + s1 := "Removed [f1, f2] from my-instance\n" + assert.Equal(t, s1, stdout.String()) + assert.Empty(t, stderr.String()) + }, + client: &fake.FakeClient{ + FakeDeleteExtraFiles: func(args rpaasclient.DeleteExtraFilesArgs) error { + expected := rpaasclient.DeleteExtraFilesArgs{ + Instance: "my-instance", + Files: []string{"f1", "f2"}, + } + assert.Equal(t, expected, args) + return nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := NewApp(stdout, stderr, tt.client) + err := app.Run(tt.args) + tt.assertion(t, stdout, stderr, err) + }) + } +} + +func TestAddExtraFiles(t *testing.T) { + c1 := "content1" + c2 := "content2" + f1, err := ioutil.TempFile("", "f1") + require.NoError(t, err) + f2, err := ioutil.TempFile("", "f2") + require.NoError(t, err) + _, err = f1.Write([]byte(c1)) + require.NoError(t, err) + require.NoError(t, f1.Close()) + _, err = f2.Write([]byte(c2)) + require.NoError(t, err) + require.NoError(t, f2.Close()) + defer func() { + os.Remove(f1.Name()) + os.Remove(f2.Name()) + }() + tests := []struct { + name string + args []string + assertion func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) + client rpaasclient.Client + }{ + { + name: "when AddExtraFiles returns an error", + args: []string{"./rpaasv2", "extra-files", "add", "-i", "my-instance", "--file", f1.Name()}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) { + assert.Error(t, err) + assert.EqualError(t, err, "some error") + }, + client: &fake.FakeClient{ + FakeAddExtraFiles: func(args rpaasclient.ExtraFilesArgs) error { + expected := rpaasclient.ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: f1.Name(), + Content: []byte(c1), + }, + }, + } + assert.Equal(t, expected, args) + return fmt.Errorf("some error") + }, + }, + }, + { + name: "when AddExtraFiles returns no error", + args: []string{"./rpaasv2", "extra-files", "add", "-i", "my-instance", "--file", f1.Name(), "--file", f2.Name()}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) { + require.NoError(t, err) + s1 := fmt.Sprintf("Added [%s, %s] to my-instance\n", f1Name, f2Name) + assert.Equal(t, s1, stdout.String()) + assert.Empty(t, stderr.String()) + }, + client: &fake.FakeClient{ + FakeAddExtraFiles: func(args rpaasclient.ExtraFilesArgs) error { + expected := rpaasclient.ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: f1.Name(), + Content: []byte(c1), + }, + { + Name: f2.Name(), + Content: []byte(c2), + }, + }, + } + assert.Equal(t, expected, args) + return nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := NewApp(stdout, stderr, tt.client) + err := app.Run(tt.args) + tt.assertion(t, stdout, stderr, err, f1.Name(), f2.Name()) + }) + } +} + +func TestUpdateExtraFiles(t *testing.T) { + c1 := "content1" + c2 := "content2" + f1, err := ioutil.TempFile("", "f1") + require.NoError(t, err) + f2, err := ioutil.TempFile("", "f2") + require.NoError(t, err) + _, err = f1.Write([]byte(c1)) + require.NoError(t, err) + require.NoError(t, f1.Close()) + _, err = f2.Write([]byte(c2)) + require.NoError(t, err) + require.NoError(t, f2.Close()) + defer func() { + os.Remove(f1.Name()) + os.Remove(f2.Name()) + }() + tests := []struct { + name string + args []string + assertion func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) + client rpaasclient.Client + }{ + { + name: "when UpdateExtraFiles returns an error", + args: []string{"./rpaasv2", "extra-files", "update", "-i", "my-instance", "--file", f1.Name()}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) { + assert.Error(t, err) + assert.EqualError(t, err, "some error") + }, + client: &fake.FakeClient{ + FakeUpdateExtraFiles: func(args rpaasclient.ExtraFilesArgs) error { + expected := rpaasclient.ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: f1.Name(), + Content: []byte(c1), + }, + }, + } + assert.Equal(t, expected, args) + return fmt.Errorf("some error") + }, + }, + }, + { + name: "when Update returns no error", + args: []string{"./rpaasv2", "extra-files", "update", "-i", "my-instance", "--file", f1.Name(), "--file", f2.Name()}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error, f1Name, f2Name string) { + require.NoError(t, err) + s1 := fmt.Sprintf("Updated [%s, %s] on my-instance\n", f1Name, f2Name) + assert.Equal(t, s1, stdout.String()) + assert.Empty(t, stderr.String()) + }, + client: &fake.FakeClient{ + FakeUpdateExtraFiles: func(args rpaasclient.ExtraFilesArgs) error { + expected := rpaasclient.ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: f1.Name(), + Content: []byte(c1), + }, + { + Name: f2.Name(), + Content: []byte(c2), + }, + }, + } + assert.Equal(t, expected, args) + return nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := NewApp(stdout, stderr, tt.client) + err := app.Run(tt.args) + tt.assertion(t, stdout, stderr, err, f1.Name(), f2.Name()) + }) + } +} + +func TestListExtraFiles(t *testing.T) { + tests := []struct { + name string + args []string + assertion func(t *testing.T, stdout, stderr *bytes.Buffer, err error) + client rpaasclient.Client + }{ + { + name: "when ListExtraFiles returns an error", + args: []string{"./rpaasv2", "extra-files", "list", "-i", "my-instance"}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error) { + assert.Error(t, err) + assert.EqualError(t, err, "some error") + }, + client: &fake.FakeClient{ + FakeListExtraFiles: func(args rpaasclient.ListExtraFilesArgs) ([]types.RpaasFile, error) { + expectedInstance := "my-instance" + assert.Equal(t, expectedInstance, args.Instance) + return nil, fmt.Errorf("some error") + }, + }, + }, + { + name: "when ListExtraFiles returns no error", + args: []string{"./rpaasv2", "extra-files", "list", "-i", "my-instance"}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error) { + require.NoError(t, err) + s1 := "f1\nf2\n" + assert.Equal(t, s1, stdout.String()) + }, + client: &fake.FakeClient{ + FakeListExtraFiles: func(args rpaasclient.ListExtraFilesArgs) ([]types.RpaasFile, error) { + expectedInstance := "my-instance" + assert.Equal(t, expectedInstance, args.Instance) + return []types.RpaasFile{{Name: "f1"}, {Name: "f2"}}, nil + }, + }, + }, + { + name: "when ListExtraFiles returns no error and --show-content", + args: []string{"./rpaasv2", "extra-files", "list", "-i", "my-instance", "--show-content"}, + assertion: func(t *testing.T, stdout, stderr *bytes.Buffer, err error) { + require.NoError(t, err) + s1 := `+------+----------------+ +| Name | Content | ++------+----------------+ +| f1 | some content 1 | ++------+----------------+ +| f2 | some content 2 | ++------+----------------+ +` + assert.Equal(t, s1, stdout.String()) + assert.Empty(t, stderr.String()) + }, + client: &fake.FakeClient{ + FakeListExtraFiles: func(args rpaasclient.ListExtraFilesArgs) ([]types.RpaasFile, error) { + expectedInstance := "my-instance" + assert.Equal(t, expectedInstance, args.Instance) + return []types.RpaasFile{ + { + Name: "f1", + Content: []byte("some content 1"), + }, + { + Name: "f2", + Content: []byte("some content 2"), + }, + }, nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := NewApp(stdout, stderr, tt.client) + err := app.Run(tt.args) + tt.assertion(t, stdout, stderr, err) + }) + } +} + +func TestGetExtraFile(t *testing.T) { + tests := []struct { + name string + args []string + expected string + expectedError string + client rpaasclient.Client + }{ + { + name: "when GetExtraFile returns an error", + args: []string{"./rpaasv2", "extra-files", "get", "-i", "my-instance", "--file", "f1"}, + expectedError: "some error", + client: &fake.FakeClient{ + FakeGetExtraFile: func(args rpaasclient.GetExtraFileArgs) (types.RpaasFile, error) { + expectedFileName := "f1" + expectedInstance := "my-instance" + assert.Equal(t, expectedFileName, args.FileName) + assert.Equal(t, expectedInstance, args.Instance) + return types.RpaasFile{}, fmt.Errorf("some error") + }, + }, + }, + { + name: "when GetExtraFile returns no error", + args: []string{"./rpaasv2", "extra-files", "get", "-i", "my-instance", "--file", "f1"}, + expected: "some content\n", + client: &fake.FakeClient{ + FakeGetExtraFile: func(args rpaasclient.GetExtraFileArgs) (types.RpaasFile, error) { + expectedFileName := "f1" + expectedInstance := "my-instance" + assert.Equal(t, expectedFileName, args.FileName) + assert.Equal(t, expectedInstance, args.Instance) + return types.RpaasFile{ + Name: args.FileName, + Content: []byte("some content"), + }, nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := NewApp(stdout, stderr, tt.client) + err := app.Run(tt.args) + if tt.expectedError != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, stdout.String()) + assert.Empty(t, stderr.String()) + }) + } +} diff --git a/cmd/plugin/rpaasv2/cmd/info.go b/cmd/plugin/rpaasv2/cmd/info.go index 4137b08a1..92076590f 100644 --- a/cmd/plugin/rpaasv2/cmd/info.go +++ b/cmd/plugin/rpaasv2/cmd/info.go @@ -215,7 +215,6 @@ func memoryValue(q string) string { qt, err := resource.ParseQuantity(q) if err == nil { memory = fmt.Sprintf("%vMi", qt.Value()/(1024*1024)) - } return memory } diff --git a/internal/web/api.go b/internal/web/api.go index 344193306..8122bcb17 100644 --- a/internal/web/api.go +++ b/internal/web/api.go @@ -201,7 +201,7 @@ func newEcho(targetFactory target.Factory) *echo.Echo { group.GET("/:instance/files/:name", getExtraFile) group.POST("/:instance/files", addExtraFiles) group.PUT("/:instance/files", updateExtraFiles) - group.DELETE("/:instance/files/:name", deleteExtraFile) + group.DELETE("/:instance/files", deleteExtraFiles) group.DELETE("/:instance/route", deleteRoute) group.GET("/:instance/route", getRoutes) group.POST("/:instance/route", updateRoute) diff --git a/internal/web/extra_files.go b/internal/web/extra_files.go index 090ae8a20..0602bb886 100644 --- a/internal/web/extra_files.go +++ b/internal/web/extra_files.go @@ -10,6 +10,8 @@ import ( "mime/multipart" "net/http" "net/url" + "strconv" + "strings" "github.com/labstack/echo/v4" @@ -26,11 +28,14 @@ func listExtraFiles(c echo.Context) error { if err != nil { return err } - names := make([]string, len(files)) - for i, file := range files { - names[i] = file.Name + + showContent, _ := strconv.ParseBool(c.QueryParam("show-content")) + if !showContent { + for i := range files { + files[i].Content = nil + } } - return c.JSON(http.StatusOK, names) + return c.JSON(http.StatusOK, files) } func getExtraFile(c echo.Context) error { @@ -113,13 +118,17 @@ func updateExtraFiles(c echo.Context) error { return c.String(http.StatusOK, fmt.Sprintf("%d files were successfully updated\n", len(files))) } -func deleteExtraFile(c echo.Context) error { +func deleteExtraFiles(c echo.Context) error { ctx := c.Request().Context() manager, err := getManager(ctx) if err != nil { return err } - filename, err := url.PathUnescape(c.Param("name")) + var files []string + err = c.Bind(&files) + if err != nil { + return err + } if err != nil { return &echo.HTTPError{ Code: http.StatusBadRequest, @@ -127,11 +136,17 @@ func deleteExtraFile(c echo.Context) error { Internal: err, } } - err = manager.DeleteExtraFiles(ctx, c.Param("instance"), filename) + err = manager.DeleteExtraFiles(ctx, c.Param("instance"), files...) if err != nil { return err } - return c.String(http.StatusOK, fmt.Sprintf("file %q was successfully removed\n", filename)) + + filesStrings, err := url.PathUnescape(strings.Join(files, ",")) + if err != nil { + return err + } + + return c.String(http.StatusOK, fmt.Sprintf("file(s) %s removed\n", filesStrings)) } // getFiles retrieves all multipart files with form name "files" and translate diff --git a/internal/web/extra_files_test.go b/internal/web/extra_files_test.go index f149ae9db..1c8b965f4 100644 --- a/internal/web/extra_files_test.go +++ b/internal/web/extra_files_test.go @@ -5,9 +5,11 @@ package web import ( + "bytes" "encoding/json" "fmt" "net/http" + "strconv" "strings" "testing" @@ -22,36 +24,54 @@ import ( func Test_listExtraFiles(t *testing.T) { testCases := []struct { instance string + showContent bool expectedCode int - expected []string + expected []rpaas.File manager rpaas.RpaasManager }{ { instance: "my-instance", expectedCode: http.StatusOK, - expected: []string{}, + expected: nil, manager: &fake.RpaasManager{}, }, { instance: "my-instance", expectedCode: http.StatusOK, - expected: []string{ - "www/index.html", - "waf/ddos-rules.cnf", + expected: []rpaas.File{ + {Name: "www/index.html"}, + {Name: "waf/ddos-rules.cnf"}, }, manager: &fake.RpaasManager{ FakeGetExtraFiles: func(string) ([]rpaas.File, error) { return []rpaas.File{ - {Name: "www/index.html"}, - {Name: "waf/ddos-rules.cnf"}, + {Name: "www/index.html", Content: []byte("c1")}, + {Name: "waf/ddos-rules.cnf", Content: []byte("c1")}, }, nil }, }, }, { instance: "my-instance", + showContent: true, expectedCode: http.StatusOK, - expected: []string{}, + expected: []rpaas.File{ + {Name: "www/index.html", Content: []byte("c1")}, + {Name: "waf/ddos-rules.cnf", Content: []byte("c2")}, + }, + manager: &fake.RpaasManager{ + FakeGetExtraFiles: func(string) ([]rpaas.File, error) { + return []rpaas.File{ + {Name: "www/index.html", Content: []byte("c1")}, + {Name: "waf/ddos-rules.cnf", Content: []byte("c2")}, + }, nil + }, + }, + }, + { + instance: "my-instance", + expectedCode: http.StatusOK, + expected: []rpaas.File{}, manager: &fake.RpaasManager{ FakeGetExtraFiles: func(string) ([]rpaas.File, error) { return []rpaas.File{}, nil @@ -64,13 +84,13 @@ func Test_listExtraFiles(t *testing.T) { t.Run("", func(t *testing.T) { srv := newTestingServer(t, tt.manager) defer srv.Close() - path := fmt.Sprintf("%s/resources/%s/files", srv.URL, tt.instance) + path := fmt.Sprintf("%s/resources/%s/files?show-content=%s", srv.URL, tt.instance, strconv.FormatBool(tt.showContent)) request, err := http.NewRequest(http.MethodGet, path, nil) require.NoError(t, err) rsp, err := srv.Client().Do(request) require.NoError(t, err) assert.Equal(t, tt.expectedCode, rsp.StatusCode) - var gotFiles []string + var gotFiles []rpaas.File err = json.Unmarshal([]byte(bodyContent(rsp)), &gotFiles) assert.NoError(t, err) assert.Equal(t, tt.expected, gotFiles) @@ -265,23 +285,23 @@ func Test_updateExtraFiles(t *testing.T) { func Test_deleteExtraFiles(t *testing.T) { testCases := []struct { instance string - filename string + files []string expectedCode int expectedBody string manager rpaas.RpaasManager }{ { instance: "my-instance", - filename: "waf%2Fsqli-rules.cnf", + files: []string{"waf%2Fsqli-rules.cnf"}, expectedCode: http.StatusOK, - expectedBody: `file "waf/sqli-rules.cnf" was successfully removed`, + expectedBody: `file(s) waf/sqli-rules.cnf removed`, manager: &fake.RpaasManager{}, }, { instance: "my-instance", - filename: "not-found.cnf", + files: []string{"not-found.cnf"}, expectedCode: http.StatusNotFound, - expectedBody: "not found", + expectedBody: "{\"Msg\":\"not found\"}", manager: &fake.RpaasManager{ FakeDeleteExtraFiles: func(string, ...string) error { return &rpaas.NotFoundError{ @@ -296,13 +316,17 @@ func Test_deleteExtraFiles(t *testing.T) { t.Run("", func(t *testing.T) { srv := newTestingServer(t, tt.manager) defer srv.Close() - path := fmt.Sprintf("%s/resources/%s/files/%s", srv.URL, tt.instance, tt.filename) - request, err := http.NewRequest(http.MethodDelete, path, nil) + b, err := json.Marshal(tt.files) + assert.NoError(t, err) + body := bytes.NewReader(b) + path := fmt.Sprintf("%s/resources/%s/files", srv.URL, tt.instance) + request, err := http.NewRequest(http.MethodDelete, path, body) + request.Header.Add("Content-Type", "application/json") require.NoError(t, err) rsp, err := srv.Client().Do(request) require.NoError(t, err) assert.Equal(t, tt.expectedCode, rsp.StatusCode) - assert.Regexp(t, tt.expectedBody, bodyContent(rsp)) + assert.EqualValues(t, tt.expectedBody, bodyContent(rsp)) }) } } diff --git a/pkg/rpaas/client/client.go b/pkg/rpaas/client/client.go index 5e2cdc3d7..c95c9c41b 100644 --- a/pkg/rpaas/client/client.go +++ b/pkg/rpaas/client/client.go @@ -18,6 +18,25 @@ type ScaleArgs struct { Replicas int32 } +type ExtraFilesArgs struct { + Instance string + Files []types.RpaasFile +} + +type DeleteExtraFilesArgs struct { + Instance string + Files []string +} + +type GetExtraFileArgs struct { + Instance string + FileName string +} + +type ListExtraFilesArgs struct { + Instance string + ShowContent bool +} type UpdateCertificateArgs struct { Instance string Name string @@ -132,6 +151,11 @@ type Client interface { RemoveAutoscale(ctx context.Context, args RemoveAutoscaleArgs) error Exec(ctx context.Context, args ExecArgs) (*websocket.Conn, error) Log(ctx context.Context, args LogArgs) error + AddExtraFiles(ctx context.Context, args ExtraFilesArgs) error + UpdateExtraFiles(ctx context.Context, args ExtraFilesArgs) error + DeleteExtraFiles(ctx context.Context, args DeleteExtraFilesArgs) error + ListExtraFiles(ctx context.Context, args ListExtraFilesArgs) ([]types.RpaasFile, error) + GetExtraFile(ctx context.Context, args GetExtraFileArgs) (types.RpaasFile, error) AddAccessControlList(ctx context.Context, instance, host string, port int) error ListAccessControlList(ctx context.Context, instance string) ([]types.AllowedUpstream, error) diff --git a/pkg/rpaas/client/extra_files.go b/pkg/rpaas/client/extra_files.go new file mode 100644 index 000000000..89e39d699 --- /dev/null +++ b/pkg/rpaas/client/extra_files.go @@ -0,0 +1,216 @@ +// Copyright 2022 tsuru 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 ( + "bytes" + "context" + "encoding/json" + "fmt" + "mime/multipart" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func (args ExtraFilesArgs) Validate() error { + if args.Instance == "" { + return ErrMissingInstance + } + + if len(args.Files) == 0 { + return ErrMissingFiles + } + + return nil +} + +func (args DeleteExtraFilesArgs) Validate() error { + if args.Instance == "" { + return ErrMissingInstance + } + if len(args.Files) == 0 { + return ErrMissingFiles + } + + return nil +} + +func (args GetExtraFileArgs) Validate() error { + if args.Instance == "" { + return ErrMissingInstance + } + + if args.FileName == "" { + return ErrMissingFile + } + + return nil +} + +func prepareBodyRequest(files []types.RpaasFile) (*bytes.Buffer, *multipart.Writer, error) { + buffer := &bytes.Buffer{} + writer := multipart.NewWriter(buffer) + for _, file := range files { + partWriter, err := writer.CreateFormFile("files", filepath.Base(file.Name)) + if err != nil { + return nil, nil, err + } + partWriter.Write(file.Content) + } + if err := writer.Close(); err != nil { + return nil, nil, err + } + return buffer, writer, nil +} + +func (c *client) AddExtraFiles(ctx context.Context, args ExtraFilesArgs) error { + if err := args.Validate(); err != nil { + return err + } + + buffer, w, err := prepareBodyRequest(args.Files) + if err != nil { + return err + } + + body := strings.NewReader(buffer.String()) + pathName := fmt.Sprintf("/resources/%s/files", args.Instance) + req, err := c.newRequest(http.MethodPost, pathName, body, args.Instance) + req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", w.Boundary())) + if err != nil { + return err + } + + response, err := c.do(ctx, req) + if err != nil { + return err + } + + if response.StatusCode != http.StatusCreated { + return newErrUnexpectedStatusCodeFromResponse(response) + } + + return nil +} + +func (c *client) UpdateExtraFiles(ctx context.Context, args ExtraFilesArgs) error { + if err := args.Validate(); err != nil { + return err + } + + buffer, w, err := prepareBodyRequest(args.Files) + if err != nil { + return err + } + + body := strings.NewReader(buffer.String()) + pathName := fmt.Sprintf("/resources/%s/files", args.Instance) + req, err := c.newRequest(http.MethodPut, pathName, body, args.Instance) + req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", w.Boundary())) + if err != nil { + return err + } + + response, err := c.do(ctx, req) + if err != nil { + return err + } + + if response.StatusCode != http.StatusOK { + return newErrUnexpectedStatusCodeFromResponse(response) + } + + return nil +} + +func (c *client) DeleteExtraFiles(ctx context.Context, args DeleteExtraFilesArgs) error { + if err := args.Validate(); err != nil { + return err + } + + b, err := json.Marshal(args.Files) + if err != nil { + return err + } + body := bytes.NewReader(b) + + pathName := fmt.Sprintf("/resources/%s/files", args.Instance) + req, err := c.newRequest(http.MethodDelete, pathName, body, args.Instance) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + response, err := c.do(ctx, req) + if err != nil { + return err + } + + if response.StatusCode != http.StatusOK { + return newErrUnexpectedStatusCodeFromResponse(response) + } + + return nil +} + +func (c *client) ListExtraFiles(ctx context.Context, args ListExtraFilesArgs) ([]types.RpaasFile, error) { + if args.Instance == "" { + return nil, ErrMissingInstance + } + + pathName := fmt.Sprintf("/resources/%s/files?show-content=%s", args.Instance, strconv.FormatBool(args.ShowContent)) + req, err := c.newRequest(http.MethodGet, pathName, nil, args.Instance) + if err != nil { + return nil, err + } + + response, err := c.do(ctx, req) + if err != nil { + return nil, err + } + + if response.StatusCode != http.StatusOK { + return nil, newErrUnexpectedStatusCodeFromResponse(response) + } + + var fileList []types.RpaasFile + err = json.NewDecoder(response.Body).Decode(&fileList) + if err != nil { + return nil, err + } + return fileList, nil +} + +func (c *client) GetExtraFile(ctx context.Context, args GetExtraFileArgs) (types.RpaasFile, error) { + if err := args.Validate(); err != nil { + return types.RpaasFile{}, err + } + + pathName := fmt.Sprintf("/resources/%s/files/%s", args.Instance, args.FileName) + req, err := c.newRequest(http.MethodGet, pathName, nil, args.Instance) + if err != nil { + return types.RpaasFile{}, err + } + + response, err := c.do(ctx, req) + if err != nil { + return types.RpaasFile{}, err + } + + if response.StatusCode != http.StatusOK { + return types.RpaasFile{}, newErrUnexpectedStatusCodeFromResponse(response) + } + + var file types.RpaasFile + err = json.NewDecoder(response.Body).Decode(&file) + if err != nil { + return types.RpaasFile{}, err + } + return file, nil +} diff --git a/pkg/rpaas/client/extra_files_test.go b/pkg/rpaas/client/extra_files_test.go new file mode 100644 index 000000000..ebff823f8 --- /dev/null +++ b/pkg/rpaas/client/extra_files_test.go @@ -0,0 +1,428 @@ +// Copyright 2022 tsuru 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 ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tsuru/rpaas-operator/pkg/rpaas/client/types" +) + +func TestClientThroughTsuru_DeleteExtraFiles(t *testing.T) { + tests := []struct { + name string + args DeleteExtraFilesArgs + expectedError string + handler http.HandlerFunc + }{ + { + name: "when instance is empty", + expectedError: "rpaasv2: instance cannot be empty", + }, + { + name: "when files is nil", + args: DeleteExtraFilesArgs{ + Instance: "my-instance", + }, + expectedError: "rpaasv2: file list must not be empty", + }, + { + name: "when the server returns an error", + args: DeleteExtraFilesArgs{ + Instance: "my-instance", + Files: []string{"f1", "f2"}, + }, + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "instance not found") + }, + }, + { + name: "when the server returns the expected response", + args: DeleteExtraFilesArgs{ + Instance: "my-instance", + Files: []string{"f1", "f2"}, + }, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, `["f1","f2"]`, getBody(t, r)) + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + err := client.DeleteExtraFiles(context.TODO(), tt.args) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + assert.NoError(t, err) + }) + } +} + +func TestClientThroughTsuru_AddExtraFiles(t *testing.T) { + tests := []struct { + name string + args ExtraFilesArgs + expectedError string + handler http.HandlerFunc + }{ + { + name: "when instance is empty", + expectedError: "rpaasv2: instance cannot be empty", + }, + { + name: "when files is nil", + args: ExtraFilesArgs{ + Instance: "my-instance", + }, + expectedError: "rpaasv2: file list must not be empty", + }, + { + name: "when the server returns an error", + args: ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: "f1", + Content: []byte("content 1"), + }, + { + Name: "f2", + Content: []byte("content 2"), + }, + }, + }, + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "instance not found") + }, + }, + { + name: "when the server returns the expected response", + args: ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: "f1", + Content: []byte("content 1"), + }, + { + Name: "f2", + Content: []byte("content 2"), + }, + }, + }, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data") + + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + bodyString := string(body) + assert.Contains(t, bodyString, "Content-Disposition: form-data; name=\"files\"; filename=\"f1\"\r\nContent-Type: application/octet-stream\r\n\r\ncontent 1\r\n") + assert.Contains(t, bodyString, "Content-Disposition: form-data; name=\"files\"; filename=\"f2\"\r\nContent-Type: application/octet-stream\r\n\r\ncontent 2\r\n") + w.WriteHeader(http.StatusCreated) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + err := client.AddExtraFiles(context.TODO(), tt.args) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + assert.NoError(t, err) + }) + } +} + +func TestClientThroughTsuru_UpdateExtraFiles(t *testing.T) { + tests := []struct { + name string + args ExtraFilesArgs + expectedError string + handler http.HandlerFunc + }{ + { + name: "when instance is empty", + expectedError: "rpaasv2: instance cannot be empty", + }, + { + name: "when files is nil", + args: ExtraFilesArgs{ + Instance: "my-instance", + }, + expectedError: "rpaasv2: file list must not be empty", + }, + { + name: "when the server returns an error", + args: ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: "f1", + Content: []byte("content 1"), + }, + { + Name: "f2", + Content: []byte("content 2"), + }, + }, + }, + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "instance not found") + }, + }, + { + name: "when the server returns the expected response", + args: ExtraFilesArgs{ + Instance: "my-instance", + Files: []types.RpaasFile{ + { + Name: "f1", + Content: []byte("content 1"), + }, + { + Name: "f2", + Content: []byte("content 2"), + }, + }, + }, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + assert.Contains(t, r.Header.Get("Content-Type"), "multipart/form-data") + + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + bodyString := string(body) + assert.Contains(t, bodyString, "Content-Disposition: form-data; name=\"files\"; filename=\"f1\"\r\nContent-Type: application/octet-stream\r\n\r\ncontent 1\r\n") + assert.Contains(t, bodyString, "Content-Disposition: form-data; name=\"files\"; filename=\"f2\"\r\nContent-Type: application/octet-stream\r\n\r\ncontent 2\r\n") + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + err := client.UpdateExtraFiles(context.TODO(), tt.args) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + assert.NoError(t, err) + }) + } +} + +func TestClientThroughTsuru_GetExtraFiles(t *testing.T) { + tests := []struct { + name string + args GetExtraFileArgs + expectedError string + expectedFile types.RpaasFile + handler http.HandlerFunc + }{ + { + name: "when instance is empty", + expectedError: "rpaasv2: instance cannot be empty", + }, + { + name: "when files is nil", + args: GetExtraFileArgs{ + Instance: "my-instance", + }, + expectedError: "rpaasv2: file must have a name", + }, + { + name: "when the server returns an error", + args: GetExtraFileArgs{ + Instance: "my-instance", + FileName: "some-file", + }, + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "instance not found") + }, + }, + { + name: "when the server returns the expected response", + args: GetExtraFileArgs{ + Instance: "my-instance", + FileName: "some-file", + }, + expectedFile: types.RpaasFile{ + Name: "some-file", + Content: []byte("some content"), + }, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files/some-file"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + w.Header().Set("Content-type", "application/json") + file := types.RpaasFile{ + Name: "some-file", + Content: []byte("some content"), + } + fileBytes, err := json.Marshal(file) + assert.NoError(t, err) + fmt.Fprint(w, string(fileBytes)) + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + file, err := client.GetExtraFile(context.TODO(), tt.args) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + if tt.expectedFile.Name != "" { + assert.EqualValues(t, file, tt.expectedFile) + } + assert.NoError(t, err) + }) + } +} + +func TestClientThroughTsuru_ListExtraFiles(t *testing.T) { + tests := []struct { + name string + args ListExtraFilesArgs + expectedError string + expectedFiles []types.RpaasFile + handler http.HandlerFunc + }{ + { + name: "when instance is empty", + expectedError: "rpaasv2: instance cannot be empty", + }, + { + name: "when the server returns an error", + args: ListExtraFilesArgs{ + Instance: "my-instance", + }, + expectedError: "rpaasv2: unexpected status code: 404 Not Found, detail: instance not found", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "instance not found") + }, + }, + { + name: "when the server returns the expected response", + args: ListExtraFilesArgs{ + Instance: "my-instance", + }, + expectedFiles: []types.RpaasFile{{Name: "f1"}, {Name: "f2"}}, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files?show-content=false"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + w.Header().Set("Content-type", "application/json") + files := []types.RpaasFile{ + { + Name: "f1", + }, + { + Name: "f2", + }, + } + filesBytes, err := json.Marshal(files) + assert.NoError(t, err) + fmt.Fprint(w, string(filesBytes)) + w.WriteHeader(http.StatusOK) + }, + }, + { + name: "when the server returns the expected response with show-content query param", + args: ListExtraFilesArgs{ + Instance: "my-instance", + ShowContent: true, + }, + expectedFiles: []types.RpaasFile{ + { + Name: "f1", + Content: []byte("c1"), + }, + { + Name: "f2", + Content: []byte("c2"), + }, + }, + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET") + assert.Equal(t, fmt.Sprintf("/services/%s/proxy/%s?callback=%s", FakeTsuruService, "my-instance", "/resources/my-instance/files?show-content=true"), r.URL.RequestURI()) + assert.Equal(t, "Bearer f4k3t0k3n", r.Header.Get("Authorization")) + w.Header().Set("Content-type", "application/json") + files := []types.RpaasFile{ + { + Name: "f1", + Content: []byte("c1"), + }, + { + Name: "f2", + Content: []byte("c2"), + }, + } + filesBytes, err := json.Marshal(files) + assert.NoError(t, err) + fmt.Fprint(w, string(filesBytes)) + w.WriteHeader(http.StatusOK) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := newClientThroughTsuru(t, tt.handler) + defer server.Close() + files, err := client.ListExtraFiles(context.TODO(), tt.args) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + if tt.expectedFiles != nil { + assert.EqualValues(t, tt.expectedFiles, files) + } + assert.NoError(t, err) + }) + } +} diff --git a/pkg/rpaas/client/fake/client.go b/pkg/rpaas/client/fake/client.go index bf609cc50..229c223f6 100644 --- a/pkg/rpaas/client/fake/client.go +++ b/pkg/rpaas/client/fake/client.go @@ -37,6 +37,11 @@ type FakeClient struct { FakeUpdateCertManager func(args client.UpdateCertManagerArgs) error FakeDeleteCertManager func(instance, issuer string) error FakeLog func(args client.LogArgs) error + FakeAddExtraFiles func(args client.ExtraFilesArgs) error + FakeUpdateExtraFiles func(args client.ExtraFilesArgs) error + FakeDeleteExtraFiles func(args client.DeleteExtraFilesArgs) error + FakeListExtraFiles func(args client.ListExtraFilesArgs) ([]types.RpaasFile, error) + FakeGetExtraFile func(args client.GetExtraFileArgs) (types.RpaasFile, error) } var _ client.Client = &FakeClient{} @@ -227,3 +232,43 @@ func (f *FakeClient) Log(ctx context.Context, args client.LogArgs) error { return nil } + +func (f *FakeClient) AddExtraFiles(ctx context.Context, args client.ExtraFilesArgs) error { + if f.FakeAddExtraFiles != nil { + return f.FakeAddExtraFiles(args) + } + + return nil +} + +func (f *FakeClient) UpdateExtraFiles(ctx context.Context, args client.ExtraFilesArgs) error { + if f.FakeUpdateExtraFiles != nil { + return f.FakeUpdateExtraFiles(args) + } + + return nil +} + +func (f *FakeClient) DeleteExtraFiles(ctx context.Context, args client.DeleteExtraFilesArgs) error { + if f.FakeDeleteExtraFiles != nil { + return f.FakeDeleteExtraFiles(args) + } + + return nil +} + +func (f *FakeClient) ListExtraFiles(ctx context.Context, args client.ListExtraFilesArgs) ([]types.RpaasFile, error) { + if f.FakeListExtraFiles != nil { + return f.FakeListExtraFiles(args) + } + + return nil, nil +} + +func (f *FakeClient) GetExtraFile(ctx context.Context, args client.GetExtraFileArgs) (types.RpaasFile, error) { + if f.FakeGetExtraFile != nil { + return f.FakeGetExtraFile(args) + } + + return types.RpaasFile{}, nil +} diff --git a/pkg/rpaas/client/internal_client.go b/pkg/rpaas/client/internal_client.go index 0e6802946..70a1eb888 100644 --- a/pkg/rpaas/client/internal_client.go +++ b/pkg/rpaas/client/internal_client.go @@ -27,6 +27,8 @@ var ( ErrMissingTsuruToken = fmt.Errorf("rpaasv2: tsuru token cannot be empty") ErrMissingTsuruService = fmt.Errorf("rpaasv2: tsuru service cannot be empty") ErrMissingInstance = fmt.Errorf("rpaasv2: instance cannot be empty") + ErrMissingFile = fmt.Errorf("rpaasv2: file must have a name") + ErrMissingFiles = fmt.Errorf("rpaasv2: file list must not be empty") ErrMissingBlockName = fmt.Errorf("rpaasv2: block name cannot be empty") ErrMissingPath = fmt.Errorf("rpaasv2: path cannot be empty") ErrInvalidMaxReplicasNumber = fmt.Errorf("rpaasv2: max replicas can't be lower than 1") diff --git a/pkg/rpaas/client/types/types.go b/pkg/rpaas/client/types/types.go index 525fadf1c..e231960d8 100644 --- a/pkg/rpaas/client/types/types.go +++ b/pkg/rpaas/client/types/types.go @@ -13,6 +13,11 @@ import ( "github.com/tsuru/rpaas-operator/api/v1alpha1" ) +type RpaasFile struct { + Name string `json:"name"` + Content []byte `json:"content"` +} + type Block struct { Name string `json:"block_name" form:"block_name"` Content string `json:"content" form:"content"`