From 44e93770ca218239dd4b1613f44bd1033a3e778b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 1 Nov 2023 17:03:15 -0300 Subject: [PATCH 1/2] Add logic to display metadata by process --- go.mod | 2 +- go.sum | 4 +- tsuru/client/apps.go | 1 + tsuru/client/job_or_app.go | 17 ++++++-- tsuru/client/metadata.go | 86 ++++++++++++++++++++++++++++---------- 5 files changed, 83 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index a1df5aec6..babb702eb 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/pmorie/go-open-service-broker-client v0.0.0-20180330214919-dca737037ce6 github.com/sabhiram/go-gitignore v0.0.0-20171017070213-362f9845770f github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa - github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16 + github.com/tsuru/go-tsuruclient v0.0.0-20231031185823-b0abac462f59 github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c diff --git a/go.sum b/go.sum index a37e8a33a..44d507253 100644 --- a/go.sum +++ b/go.sum @@ -733,8 +733,8 @@ github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560 h1:fniQ/BmYAHdnNmY333 github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560/go.mod h1:mj6t8JKWU51GScTT50XRmDj65T5XhTyNvO5FUNV5zS4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa h1:JlLQP1xa13a994p/Aau2e3K9xXYaHNoNvTDVIMHSUa4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa/go.mod h1:UibOSvkMFKRe/eiwktAPAvQG8L+p8nYsECJvu3Dgw7I= -github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16 h1:gjwhjJTOuPlHhytkBXvfEzIzyYytePVvGeq7REbeBGY= -github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic= +github.com/tsuru/go-tsuruclient v0.0.0-20231031185823-b0abac462f59 h1:RgFlupHEAJvIXH6FgtzGQqG8pS6ck3miqEXZN+a4dLs= +github.com/tsuru/go-tsuruclient v0.0.0-20231031185823-b0abac462f59/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 h1:1XDdWFAjIbCSG1OjN9v9KdWhuM8UtYlFcfHe/Ldkchk= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6/go.mod h1:ztYpOhW+u1k21FEqp7nZNgpWbr0dUKok5lgGCZi+1AQ= github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508 h1:eaMg/uBeTv6B7O+AMTq3OKqNsiA0/kB5Zkgy7ipXgcI= diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index e8e5f24bd..c12f32b6d 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -559,6 +559,7 @@ type app struct { UnitsMetrics []unitMetrics VolumeBinds []volumeTypes.VolumeBind ServiceInstanceBinds []tsuru.AppServiceInstanceBinds + Processes []tsuru.AppProcess } type appInternalAddress struct { diff --git a/tsuru/client/job_or_app.go b/tsuru/client/job_or_app.go index 269eccac4..5f4e4d756 100644 --- a/tsuru/client/job_or_app.go +++ b/tsuru/client/job_or_app.go @@ -11,23 +11,34 @@ import ( ) type JobOrApp struct { - Type string - val string - fs *gnuflag.FlagSet + Type string + val string + appProcess string + fs *gnuflag.FlagSet } func (c *JobOrApp) validate() error { appName := c.fs.Lookup("app").Value.String() jobName := c.fs.Lookup("job").Value.String() + var processName string + + if flag := c.fs.Lookup("process"); flag != nil { + processName = flag.Value.String() + } + if appName == "" && jobName == "" { return errors.New("job name or app name is required") } if appName != "" && jobName != "" { return errors.New("please use only one of the -a/--app and -j/--job flags") } + if processName != "" && jobName != "" { + return errors.New("please specify process just for an app") + } if appName != "" { c.Type = "app" c.val = appName + c.appProcess = processName return nil } c.Type = "job" diff --git a/tsuru/client/metadata.go b/tsuru/client/metadata.go index d27258df6..ac568dc06 100644 --- a/tsuru/client/metadata.go +++ b/tsuru/client/metadata.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "sort" "strings" @@ -26,19 +27,26 @@ Example: var allowedTypes = []string{"label", "annotation"} -func (c *JobOrApp) getMetadata(apiClient *tsuru.APIClient) (tsuru.Metadata, error) { +func (c *JobOrApp) getMetadata(apiClient *tsuru.APIClient) (*tsuru.Metadata, map[string]tsuru.Metadata, error) { if c.Type == "job" { job, _, err := apiClient.JobApi.GetJob(context.Background(), c.val) if err != nil { - return tsuru.Metadata{}, err + return nil, nil, err } - return job.Job.Metadata, nil + return &job.Job.Metadata, nil, nil } app, _, err := apiClient.AppApi.AppGet(context.Background(), c.val) if err != nil { - return tsuru.Metadata{}, err + return nil, nil, err } - return app.Metadata, nil + + processMetadata := map[string]tsuru.Metadata{} + + for _, p := range app.Processes { + processMetadata[p.Name] = p.Metadata + } + + return &app.Metadata, processMetadata, nil } func (c *JobOrApp) setMetadata(apiClient *tsuru.APIClient, metadata tsuru.Metadata, noRestart bool) (*http.Response, error) { @@ -49,16 +57,26 @@ func (c *JobOrApp) setMetadata(apiClient *tsuru.APIClient, metadata tsuru.Metada } return apiClient.JobApi.UpdateJob(context.Background(), c.val, j) } + a := tsuru.UpdateApp{ - Metadata: metadata, NoRestart: noRestart, } + + if c.appProcess == "" { + a.Metadata = metadata + } else { + a.Processes = append(a.Processes, tsuru.AppProcess{ + Name: c.appProcess, + Metadata: metadata, + }) + } return apiClient.AppApi.AppUpdate(context.Background(), c.val, a) } type MetadataGet struct { cmd.AppNameMixIn jobName string + appProcess string flagsApplied bool json bool fs *gnuflag.FlagSet @@ -98,7 +116,7 @@ func (c *MetadataGet) Run(context *cmd.Context, cli *cmd.Client) error { if err != nil { return err } - metadata, err := joa.getMetadata(apiClient) + metadata, metadataByProcess, err := joa.getMetadata(apiClient) if err != nil { return err } @@ -107,27 +125,47 @@ func (c *MetadataGet) Run(context *cmd.Context, cli *cmd.Client) error { return formatter.JSON(context.Stdout, metadata) } - formatted := make([]string, 0, len(metadata.Labels)) - for _, v := range metadata.Labels { - formatted = append(formatted, fmt.Sprintf("\t%s: %s", v.Name, v.Value)) + if len(metadataByProcess) > 0 { + fmt.Fprintln(context.Stdout, "Metadata for app:") } - sort.Strings(formatted) - fmt.Fprintln(context.Stdout, "Labels:") - fmt.Fprintln(context.Stdout, strings.Join(formatted, "\n")) + outputMetadata(context.Stdout, metadata) - formatted = make([]string, 0, len(metadata.Annotations)) - for _, v := range metadata.Annotations { - formatted = append(formatted, fmt.Sprintf("\t%s: %s", v.Name, v.Value)) + for process, processMetadata := range metadataByProcess { + if len(metadataByProcess) > 0 { + fmt.Fprintf(context.Stdout, "\nMetadata for process: %q\n", process) + } + outputMetadata(context.Stdout, &processMetadata) } - sort.Strings(formatted) - fmt.Fprintln(context.Stdout, "Annotations:") - fmt.Fprintln(context.Stdout, strings.Join(formatted, "\n")) + return nil } +func outputMetadata(w io.Writer, metadata *tsuru.Metadata) { + if len(metadata.Labels) > 0 { + formatted := make([]string, 0, len(metadata.Labels)) + for _, v := range metadata.Labels { + formatted = append(formatted, fmt.Sprintf("\t%s: %s", v.Name, v.Value)) + } + sort.Strings(formatted) + fmt.Fprintln(w, "Labels:") + fmt.Fprintln(w, strings.Join(formatted, "\n")) + } + + if len(metadata.Annotations) > 0 { + formatted := make([]string, 0, len(metadata.Annotations)) + for _, v := range metadata.Annotations { + formatted = append(formatted, fmt.Sprintf("\t%s: %s", v.Name, v.Value)) + } + sort.Strings(formatted) + fmt.Fprintln(w, "Annotations:") + fmt.Fprintln(w, strings.Join(formatted, "\n")) + } +} + type MetadataSet struct { cmd.AppNameMixIn job string + processName string fs *gnuflag.FlagSet metadataType string noRestart bool @@ -136,7 +174,7 @@ type MetadataSet struct { func (c *MetadataSet) Info() *cmd.Info { return &cmd.Info{ Name: "metadata-set", - Usage: "metadata set [NAME=value] ... <-a/--app appname | -j/--job jobname> [-t/--type type]", + Usage: "metadata set [NAME=value] ... <-a/--app appname | -j/--job jobname> <-p process> [-t/--type type]", Desc: `Sets metadata such as labels and annotations for an application or job.`, MinArgs: 1, } @@ -147,6 +185,8 @@ func (c *MetadataSet) Flags() *gnuflag.FlagSet { c.fs = c.AppNameMixIn.Flags() c.fs.StringVar(&c.job, "job", "", "The name of the job.") c.fs.StringVar(&c.job, "j", "", "The name of the job.") + c.fs.StringVar(&c.processName, "process", "", "The name of process of app (optional).") + c.fs.StringVar(&c.processName, "p", "", "The name of process of app (optional).") c.fs.StringVar(&c.metadataType, "type", "", "Metadata type: annotation or label") c.fs.StringVar(&c.metadataType, "t", "", "Metadata type: annotation or label") c.fs.BoolVar(&c.noRestart, "no-restart", false, "Sets metadata without restarting the application") @@ -219,6 +259,7 @@ func validateType(t string) error { type MetadataUnset struct { cmd.AppNameMixIn job string + processName string fs *gnuflag.FlagSet metadataType string noRestart bool @@ -238,6 +279,8 @@ func (c *MetadataUnset) Flags() *gnuflag.FlagSet { c.fs = c.AppNameMixIn.Flags() c.fs.StringVar(&c.job, "job", "", "The name of the job.") c.fs.StringVar(&c.job, "j", "", "The name of the job.") + c.fs.StringVar(&c.processName, "process", "", "The name of process of app (optional).") + c.fs.StringVar(&c.processName, "p", "", "The name of process of app (optional).") c.fs.StringVar(&c.metadataType, "type", "", "Metadata type: annotation or label") c.fs.StringVar(&c.metadataType, "t", "", "Metadata type: annotation or label") c.fs.BoolVar(&c.noRestart, "no-restart", false, "Sets metadata without restarting the application") @@ -262,7 +305,8 @@ func (c *MetadataUnset) Run(ctx *cmd.Context, cli *cmd.Client) error { items := make([]tsuru.MetadataItem, len(ctx.Args)) for i := range ctx.Args { - items[i] = tsuru.MetadataItem{Name: ctx.Args[i], Delete: true} + parts := strings.SplitN(ctx.Args[i], "=", 2) + items[i] = tsuru.MetadataItem{Name: parts[0], Delete: true} } var metadata tsuru.Metadata From 79012bc806909ac435032e72a0124892357559be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Mon, 6 Nov 2023 10:06:13 -0300 Subject: [PATCH 2/2] Add tests --- tsuru/client/metadata.go | 1 - tsuru/client/metadata_test.go | 133 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/tsuru/client/metadata.go b/tsuru/client/metadata.go index ac568dc06..7aad8eda4 100644 --- a/tsuru/client/metadata.go +++ b/tsuru/client/metadata.go @@ -76,7 +76,6 @@ func (c *JobOrApp) setMetadata(apiClient *tsuru.APIClient, metadata tsuru.Metada type MetadataGet struct { cmd.AppNameMixIn jobName string - appProcess string flagsApplied bool json bool fs *gnuflag.FlagSet diff --git a/tsuru/client/metadata_test.go b/tsuru/client/metadata_test.go index d133b0c13..bbe16ebb0 100644 --- a/tsuru/client/metadata_test.go +++ b/tsuru/client/metadata_test.go @@ -83,6 +83,52 @@ func (s *S) TestMetadataGetJobRun(c *check.C) { c.Assert(stdout.String(), check.Equals, result) } +func (s *S) TestMetadataGetAppRunWithProcesses(c *check.C) { + var stdout, stderr bytes.Buffer + jsonResult := `{ + "name": "somejob", + "metadata": { + "annotations": [{ + "name": "my-annotation", + "value": "some long value" + }], + "labels": [{ + "name": "logging.globoi.com/backup", + "value": "true" + }] + }, + "processes": [ + { + "name": "web", + "metadata": { + "labels": [{ + "name": "logging.globoi.com/sampling", + "value": "0.1" + }] + } + } + ] + }` + result := "Metadata for app:\n" + + "Labels:\n" + + "\tlogging.globoi.com/backup: true\n" + + "Annotations:\n" + + "\tmy-annotation: some long value\n\n" + + "Metadata for process: \"web\"\n" + + "Labels:\n" + + "\tlogging.globoi.com/sampling: 0.1\n" + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + } + client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Message: jsonResult, Status: http.StatusOK}}, nil, manager) + command := MetadataGet{} + command.Flags().Parse(true, []string{"-a", "someapp"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, result) +} + func (s *S) TestMetadataSetInfo(c *check.C) { c.Assert((&MetadataSet{}).Info(), check.NotNil) } @@ -159,6 +205,49 @@ func (s *S) TestMetadataSetRunAppWithLabel(c *check.C) { c.Assert(stdout.String(), check.Equals, expectedOut) } +func (s *S) TestMetadataSetRunAppWithProcess(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"test.tsuru.io/label=some-value"}, + Stdout: &stdout, + Stderr: &stderr, + } + expectedOut := "app \"someapp\" has been updated!\n" + + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + url := strings.HasSuffix(req.URL.Path, "/apps/someapp") + method := req.Method == "PUT" + data, err := io.ReadAll(req.Body) + c.Assert(err, check.IsNil) + var payload map[string]interface{} + err = json.Unmarshal(data, &payload) + c.Assert(err, check.IsNil) + c.Assert(payload, check.DeepEquals, map[string]interface{}{ + "planoverride": map[string]interface{}{}, + "metadata": map[string]interface{}{}, + "processes": []interface{}{ + map[string]interface{}{ + "name": "web", + "metadata": map[string]interface{}{ + "labels": []interface{}{map[string]interface{}{"name": "test.tsuru.io/label", "value": "some-value"}}, + }, + }, + }, + }) + return url && method + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + + command := MetadataSet{} + command.Flags().Parse(true, []string{"-a", "someapp", "-t", "label", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + func (s *S) TestMetadataSetRunJobWithAnnotations(c *check.C) { var stdout, stderr bytes.Buffer context := cmd.Context{ @@ -342,6 +431,50 @@ func (s *S) TestMetadataUnsetRunAppWithLabel(c *check.C) { c.Assert(stdout.String(), check.Equals, expectedOut) } +func (s *S) TestMetadataUnsetRunAppWithProcess(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"test.tsuru.io/label"}, + Stdout: &stdout, + Stderr: &stderr, + } + expectedOut := "app \"someapp\" has been updated!\n" + + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + url := strings.HasSuffix(req.URL.Path, "/apps/someapp") + method := req.Method == "PUT" + data, err := io.ReadAll(req.Body) + c.Assert(err, check.IsNil) + + var payload map[string]interface{} + err = json.Unmarshal(data, &payload) + c.Assert(err, check.IsNil) + c.Assert(payload, check.DeepEquals, map[string]interface{}{ + "planoverride": map[string]interface{}{}, + "metadata": map[string]interface{}{}, + "processes": []interface{}{ + map[string]interface{}{ + "name": "worker", + "metadata": map[string]interface{}{ + "labels": []interface{}{map[string]interface{}{"name": "test.tsuru.io/label", "delete": true}}, + }, + }, + }, + }) + return url && method + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + + command := MetadataUnset{} + command.Flags().Parse(true, []string{"-a", "someapp", "-t", "label", "-p", "worker"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + func (s *S) TestMetadataUnsetRunJobWithAnnotations(c *check.C) { var stdout, stderr bytes.Buffer context := cmd.Context{