diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index 626cd61f..e8e5f24b 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -1641,313 +1641,3 @@ func addCName(cnames []string, g cmd.AppNameMixIn, client *cmd.Client) error { _, err = client.Do(request) return err } - -type UnitAdd struct { - cmd.AppNameMixIn - fs *gnuflag.FlagSet - process string - version string -} - -func (c *UnitAdd) Info() *cmd.Info { - return &cmd.Info{ - Name: "unit-add", - Usage: "unit add <# of units> [-a/--app appname] [-p/--process processname] [--version version]", - Desc: `Adds new units to a process of an application. You need to have access to the -app to be able to add new units to it.`, - MinArgs: 1, - } -} - -func (c *UnitAdd) Flags() *gnuflag.FlagSet { - if c.fs == nil { - c.fs = c.AppNameMixIn.Flags() - c.fs.StringVar(&c.process, "process", "", "Process name") - c.fs.StringVar(&c.process, "p", "", "Process name") - c.fs.StringVar(&c.version, "version", "", "Version number") - } - return c.fs -} - -func (c *UnitAdd) Run(context *cmd.Context, client *cmd.Client) error { - context.RawOutput() - appName, err := c.AppName() - if err != nil { - return err - } - u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units", appName)) - if err != nil { - return err - } - val := url.Values{} - val.Add("units", context.Args[0]) - val.Add("process", c.process) - val.Set("version", c.version) - request, err := http.NewRequest("PUT", u, bytes.NewBufferString(val.Encode())) - if err != nil { - return err - } - request.Header.Add("Content-Type", "application/x-www-form-urlencoded") - response, err := client.Do(request) - if err != nil { - return err - } - defer response.Body.Close() - return cmd.StreamJSONResponse(context.Stdout, response) -} - -type UnitRemove struct { - cmd.AppNameMixIn - fs *gnuflag.FlagSet - process string - version string -} - -func (c *UnitRemove) Info() *cmd.Info { - return &cmd.Info{ - Name: "unit-remove", - Usage: "unit remove <# of units> [-a/--app appname] [-p/-process processname] [--version version]", - Desc: `Removes units from a process of an application. You need to have access to the -app to be able to remove units from it.`, - MinArgs: 1, - } -} - -func (c *UnitRemove) Flags() *gnuflag.FlagSet { - if c.fs == nil { - c.fs = c.AppNameMixIn.Flags() - c.fs.StringVar(&c.process, "process", "", "Process name") - c.fs.StringVar(&c.process, "p", "", "Process name") - c.fs.StringVar(&c.version, "version", "", "Version number") - } - return c.fs -} - -func (c *UnitRemove) Run(context *cmd.Context, client *cmd.Client) error { - context.RawOutput() - appName, err := c.AppName() - if err != nil { - return err - } - val := url.Values{} - val.Add("units", context.Args[0]) - val.Add("process", c.process) - val.Set("version", c.version) - url, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units?%s", appName, val.Encode())) - if err != nil { - return err - } - request, err := http.NewRequest(http.MethodDelete, url, nil) - if err != nil { - return err - } - response, err := client.Do(request) - if err != nil { - return err - } - return cmd.StreamJSONResponse(context.Stdout, response) -} - -type UnitKill struct { - cmd.AppNameMixIn - fs *gnuflag.FlagSet - force bool -} - -func (c *UnitKill) Info() *cmd.Info { - return &cmd.Info{ - Name: "unit-kill", - Usage: "unit kill [-a/--app appname] [-f/--force] ", - Desc: `Kills units from a process of an application. You need to have access to the -app to be able to remove unit from it.`, - MinArgs: 1, - } -} - -func (c *UnitKill) Flags() *gnuflag.FlagSet { - if c.fs == nil { - c.fs = c.AppNameMixIn.Flags() - c.fs.BoolVar(&c.force, "f", false, "Forces the termination of unit.") - } - return c.fs -} - -func (c *UnitKill) Run(context *cmd.Context, client *cmd.Client) error { - context.RawOutput() - appName, err := c.AppName() - if err != nil { - return err - } - unit := context.Args[0] - - v := url.Values{} - if c.force { - v.Set("force", "true") - } - - url, err := cmd.GetURLVersion("1.12", fmt.Sprintf("/apps/%s/units/%s?%s", appName, unit, v.Encode())) - if err != nil { - return err - } - request, err := http.NewRequest(http.MethodDelete, url, nil) - if err != nil { - return err - } - response, err := client.Do(request) - if err != nil { - return err - } - return cmd.StreamJSONResponse(context.Stdout, response) -} - -type UnitSet struct { - cmd.AppNameMixIn - fs *gnuflag.FlagSet - process string - version int -} - -func (c *UnitSet) Info() *cmd.Info { - return &cmd.Info{ - Name: "unit-set", - Usage: "unit set <# of units> [-a/--app appname] [-p/--process processname] [--version version]", - Desc: `Set the number of units for a process of an application, adding or removing units as needed. You need to have access to the -app to be able to set the number of units for it. The process flag is optional if the app has only 1 process.`, - MinArgs: 1, - } -} - -func (c *UnitSet) Flags() *gnuflag.FlagSet { - if c.fs == nil { - c.fs = c.AppNameMixIn.Flags() - processMessage := "Process name" - c.fs.StringVar(&c.process, "process", "", processMessage) - c.fs.StringVar(&c.process, "p", "", processMessage) - c.fs.IntVar(&c.version, "version", 0, "Version number") - } - return c.fs -} - -func (c *UnitSet) Run(context *cmd.Context, client *cmd.Client) error { - context.RawOutput() - appName, err := c.AppName() - if err != nil { - return err - } - u, err := cmd.GetURL(fmt.Sprintf("/apps/%s", appName)) - if err != nil { - return err - } - request, err := http.NewRequest(http.MethodGet, u, nil) - if err != nil { - return err - } - response, err := client.Do(request) - if err != nil { - return err - } - result, err := io.ReadAll(response.Body) - if err != nil { - return err - } - var a app - err = json.Unmarshal(result, &a) - if err != nil { - return err - } - - unitsByProcess := map[string][]unit{} - unitsByVersion := map[int][]unit{} - for _, u := range a.Units { - unitsByProcess[u.ProcessName] = append(unitsByProcess[u.ProcessName], u) - unitsByVersion[u.Version] = append(unitsByVersion[u.Version], u) - } - - if len(unitsByProcess) != 1 && c.process == "" { - return errors.New("Please use the -p/--process flag to specify which process you want to set units for.") - } - - if len(unitsByVersion) != 1 && c.version == 0 { - return errors.New("Please use the --version flag to specify which version you want to set units for.") - } - - if c.process == "" { - for p := range unitsByProcess { - c.process = p - break - } - } - - if c.version == 0 { - for v := range unitsByVersion { - c.version = v - break - } - } - - existingUnits := 0 - for _, unit := range a.Units { - if unit.ProcessName == c.process && unit.Version == c.version { - existingUnits++ - } - } - - desiredUnits, err := strconv.Atoi(context.Args[0]) - if err != nil { - return err - } - - if existingUnits < desiredUnits { - u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units", appName)) - if err != nil { - return err - } - - unitsToAdd := desiredUnits - existingUnits - val := url.Values{} - val.Add("units", strconv.Itoa(unitsToAdd)) - val.Add("process", c.process) - val.Add("version", strconv.Itoa(c.version)) - request, err := http.NewRequest(http.MethodPut, u, bytes.NewBufferString(val.Encode())) - if err != nil { - return err - } - - request.Header.Add("Content-Type", "application/x-www-form-urlencoded") - response, err := client.Do(request) - if err != nil { - return err - } - - defer response.Body.Close() - return cmd.StreamJSONResponse(context.Stdout, response) - } - - if existingUnits > desiredUnits { - unitsToRemove := existingUnits - desiredUnits - val := url.Values{} - val.Add("units", strconv.Itoa(unitsToRemove)) - val.Add("process", c.process) - val.Add("version", strconv.Itoa(c.version)) - u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units?%s", appName, val.Encode())) - if err != nil { - return err - } - - request, err := http.NewRequest(http.MethodDelete, u, nil) - if err != nil { - return err - } - - response, err := client.Do(request) - if err != nil { - return err - } - - defer response.Body.Close() - return cmd.StreamJSONResponse(context.Stdout, response) - } - - fmt.Fprintf(context.Stdout, "The process %s, version %d already has %d units.\n", c.process, c.version, existingUnits) - return nil -} diff --git a/tsuru/client/apps_test.go b/tsuru/client/apps_test.go index 97d061b8..371c05d5 100644 --- a/tsuru/client/apps_test.go +++ b/tsuru/client/apps_test.go @@ -2834,460 +2834,3 @@ func (s *S) TestAppStop(c *check.C) { func (s *S) TestAppStopIsAFlaggedCommand(c *check.C) { var _ cmd.FlaggedCommand = &AppStop{} } - -func (s *S) TestUnitAdd(c *check.C) { - var stdout, stderr bytes.Buffer - var called bool - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - expectedOut := "-- added unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - result, err := json.Marshal(msg) - c.Assert(err, check.IsNil) - trans := &cmdtest.ConditionalTransport{ - Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, - CondFunc: func(req *http.Request) bool { - called = true - c.Assert(req.FormValue("process"), check.Equals, "p1") - c.Assert(req.FormValue("units"), check.Equals, "3") - return strings.HasSuffix(req.URL.Path, "/apps/radio/units") && req.Method == "PUT" - }, - } - client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) - command := UnitAdd{} - command.Flags().Parse(true, []string{"-a", "radio", "-p", "p1"}) - err = command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(called, check.Equals, true) - c.Assert(stdout.String(), check.Equals, expectedOut) -} - -func (s *S) TestUnitAddWithVersion(c *check.C) { - var stdout, stderr bytes.Buffer - var called bool - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - expectedOut := "-- added unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - result, err := json.Marshal(msg) - c.Assert(err, check.IsNil) - trans := &cmdtest.ConditionalTransport{ - Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, - CondFunc: func(req *http.Request) bool { - called = true - c.Assert(req.FormValue("process"), check.Equals, "p1") - c.Assert(req.FormValue("units"), check.Equals, "3") - c.Assert(req.FormValue("version"), check.Equals, "9") - return strings.HasSuffix(req.URL.Path, "/apps/radio/units") && req.Method == "PUT" - }, - } - client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) - command := UnitAdd{} - command.Flags().Parse(true, []string{"-a", "radio", "-p", "p1", "--version", "9"}) - err = command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(called, check.Equals, true) - c.Assert(stdout.String(), check.Equals, expectedOut) -} - -func (s *S) TestUnitAddFailure(c *check.C) { - var stdout, stderr bytes.Buffer - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - msg := tsuruIo.SimpleJsonMessage{Error: "errored msg"} - result, err := json.Marshal(msg) - c.Assert(err, check.IsNil) - client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Message: string(result), Status: 200}}, nil, manager) - command := UnitAdd{} - command.Flags().Parse(true, []string{"-a", "radio"}) - err = command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "errored msg") -} - -func (s *S) TestUnitAddInfo(c *check.C) { - c.Assert((&UnitAdd{}).Info(), check.NotNil) -} - -func (s *S) TestUnitAddIsFlaggedACommand(c *check.C) { - var _ cmd.FlaggedCommand = &UnitAdd{} -} - -func (s *S) TestUnitRemove(c *check.C) { - var stdout, stderr bytes.Buffer - var called bool - context := cmd.Context{ - Args: []string{"2"}, - Stdout: &stdout, - Stderr: &stderr, - } - expectedOut := "-- removed unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - result, err := json.Marshal(msg) - c.Assert(err, check.IsNil) - trans := &cmdtest.ConditionalTransport{ - Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, - CondFunc: func(req *http.Request) bool { - called = true - c.Assert(req.FormValue("process"), check.Equals, "web1") - c.Assert(req.FormValue("units"), check.Equals, "2") - return strings.HasSuffix(req.URL.Path, "/apps/vapor/units") && req.Method == http.MethodDelete - }, - } - client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) - command := UnitRemove{} - command.Flags().Parse(true, []string{"-a", "vapor", "-p", "web1"}) - err = command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(called, check.Equals, true) - c.Assert(stdout.String(), check.Equals, "-- removed unit --") -} - -func (s *S) TestUnitRemoveFailure(c *check.C) { - var stdout, stderr bytes.Buffer - context := cmd.Context{ - Args: []string{"1"}, - Stdout: &stdout, - Stderr: &stderr, - } - client := cmd.NewClient(&http.Client{ - Transport: &cmdtest.Transport{Message: "Failed to remove.", Status: 500}, - }, nil, manager) - command := UnitRemove{} - command.Flags().Parse(true, []string{"-a", "vapor"}) - err := command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "Failed to remove.") -} - -func (s *S) TestUnitRemoveInfo(c *check.C) { - c.Assert((&UnitRemove{}).Info(), check.NotNil) -} - -func (s *S) TestUnitRemoveIsACommand(c *check.C) { - var _ cmd.Command = &UnitRemove{} -} - -func (s *S) TestUnitSetAddUnits(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - var calledPut bool - context := cmd.Context{ - Args: []string{"10"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - - expectedOut := "-- added unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - resultPut, _ := json.Marshal(msg) - - transport := cmdtest.MultiConditionalTransport{ - ConditionalTransports: []cmdtest.ConditionalTransport{ - { - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - }, - { - CondFunc: func(req *http.Request) bool { - calledPut = true - c.Assert(req.FormValue("process"), check.Equals, "web") - c.Assert(req.FormValue("units"), check.Equals, "7") - return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut - }, - Transport: cmdtest.Transport{Message: string(resultPut), Status: http.StatusOK}, - }, - }, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(calledGet, check.Equals, true) - c.Assert(calledPut, check.Equals, true) - c.Assert(stdout.String(), check.Equals, expectedOut) -} - -func (s *S) TestUnitSetAddUnitsFailure(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - var calledPut bool - context := cmd.Context{ - Args: []string{"10"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - - transport := cmdtest.MultiConditionalTransport{ - ConditionalTransports: []cmdtest.ConditionalTransport{ - { - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - }, - { - CondFunc: func(req *http.Request) bool { - calledPut = true - c.Assert(req.FormValue("process"), check.Equals, "web") - c.Assert(req.FormValue("units"), check.Equals, "7") - return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut - }, - Transport: cmdtest.Transport{Message: "Failed to put.", Status: http.StatusInternalServerError}, - }, - }, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "Failed to put.") - c.Assert(calledGet, check.Equals, true) - c.Assert(calledPut, check.Equals, true) -} - -func (s *S) TestUnitSetRemoveUnits(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - var calledDelete bool - context := cmd.Context{ - Args: []string{"1"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - - expectedOut := "-- removed unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - resultDelete, _ := json.Marshal(msg) - - transport := cmdtest.MultiConditionalTransport{ - ConditionalTransports: []cmdtest.ConditionalTransport{ - { - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - }, - { - CondFunc: func(req *http.Request) bool { - calledDelete = true - c.Assert(req.FormValue("process"), check.Equals, "web") - c.Assert(req.FormValue("units"), check.Equals, "2") - return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodDelete - }, - Transport: cmdtest.Transport{Message: string(resultDelete), Status: http.StatusOK}, - }, - }, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(calledGet, check.Equals, true) - c.Assert(calledDelete, check.Equals, true) - c.Assert(stdout.String(), check.Equals, expectedOut) -} - -func (s *S) TestUnitSetRemoveUnitsFailure(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - var calledDelete bool - context := cmd.Context{ - Args: []string{"1"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - - transport := cmdtest.MultiConditionalTransport{ - ConditionalTransports: []cmdtest.ConditionalTransport{ - { - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - }, - { - CondFunc: func(req *http.Request) bool { - calledDelete = true - c.Assert(req.FormValue("process"), check.Equals, "web") - c.Assert(req.FormValue("units"), check.Equals, "2") - return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodDelete - }, - Transport: cmdtest.Transport{Message: "Failed to delete.", Status: http.StatusInternalServerError}, - }, - }, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "Failed to delete.") - c.Assert(calledGet, check.Equals, true) - c.Assert(calledDelete, check.Equals, true) -} - -func (s *S) TestUnitSetNoChanges(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - transport := cmdtest.ConditionalTransport{ - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(calledGet, check.Equals, true) - c.Assert(stdout.String(), check.Equals, "The process web, version 0 already has 3 units.\n") -} - -func (s *S) TestUnitSetFailedGet(c *check.C) { - var stdout, stderr bytes.Buffer - calledTimes := 0 - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - - transport := cmdtest.ConditionalTransport{ - CondFunc: func(req *http.Request) bool { - calledTimes++ - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: "Failed to get.", Status: http.StatusInternalServerError}, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) - err := command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "Failed to get.") - c.Assert(calledTimes, check.Equals, 1) -} - -func (s *S) TestUnitSetNoProcessSpecifiedAndMultipleExist(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - context := cmd.Context{ - Args: []string{"3"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - transport := cmdtest.ConditionalTransport{ - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1"}) - err := command.Run(&context, client) - c.Assert(err, check.NotNil) - c.Assert(err.Error(), check.Equals, "Please use the -p/--process flag to specify which process you want to set units for.") - c.Assert(calledGet, check.Equals, true) -} - -func (s *S) TestUnitSetNoProcessSpecifiedAndSingleExists(c *check.C) { - var stdout, stderr bytes.Buffer - var calledGet bool - var calledPut bool - context := cmd.Context{ - Args: []string{"10"}, - Stdout: &stdout, - Stderr: &stderr, - } - - resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"worker"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` - - expectedOut := "-- added unit --" - msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} - resultPut, _ := json.Marshal(msg) - - transport := cmdtest.MultiConditionalTransport{ - ConditionalTransports: []cmdtest.ConditionalTransport{ - { - CondFunc: func(req *http.Request) bool { - calledGet = true - return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet - }, - Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, - }, - { - CondFunc: func(req *http.Request) bool { - calledPut = true - c.Assert(req.FormValue("process"), check.Equals, "worker") - c.Assert(req.FormValue("units"), check.Equals, "8") - return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut - }, - Transport: cmdtest.Transport{Message: string(resultPut), Status: http.StatusOK}, - }, - }, - } - - client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) - command := UnitSet{} - command.Flags().Parse(true, []string{"-a", "app1"}) - err := command.Run(&context, client) - c.Assert(err, check.IsNil) - c.Assert(calledGet, check.Equals, true) - c.Assert(calledPut, check.Equals, true) - c.Assert(stdout.String(), check.Equals, expectedOut) -} - -func (s *S) TestUnitSetInfo(c *check.C) { - c.Assert((&UnitSet{}).Info(), check.NotNil) -} - -func (s *S) TestUnitSetIsACommand(c *check.C) { - var _ cmd.Command = &UnitSet{} -} diff --git a/tsuru/client/job_or_app.go b/tsuru/client/job_or_app.go new file mode 100644 index 00000000..269eccac --- /dev/null +++ b/tsuru/client/job_or_app.go @@ -0,0 +1,36 @@ +// 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 ( + "errors" + + "github.com/tsuru/gnuflag" +) + +type JobOrApp struct { + Type string + val string + fs *gnuflag.FlagSet +} + +func (c *JobOrApp) validate() error { + appName := c.fs.Lookup("app").Value.String() + jobName := c.fs.Lookup("job").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 appName != "" { + c.Type = "app" + c.val = appName + return nil + } + c.Type = "job" + c.val = jobName + return nil +} diff --git a/tsuru/client/metadata.go b/tsuru/client/metadata.go index ddab7651..d27258df 100644 --- a/tsuru/client/metadata.go +++ b/tsuru/client/metadata.go @@ -26,31 +26,6 @@ Example: var allowedTypes = []string{"label", "annotation"} -type JobOrApp struct { - Type string - val string - fs *gnuflag.FlagSet -} - -func (c *JobOrApp) validate() error { - appName := c.fs.Lookup("app").Value.String() - jobName := c.fs.Lookup("job").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 appName != "" { - c.Type = "app" - c.val = appName - return nil - } - c.Type = "job" - c.val = jobName - return nil -} - func (c *JobOrApp) getMetadata(apiClient *tsuru.APIClient) (tsuru.Metadata, error) { if c.Type == "job" { job, _, err := apiClient.JobApi.GetJob(context.Background(), c.val) diff --git a/tsuru/client/unit.go b/tsuru/client/unit.go new file mode 100644 index 00000000..bd0b8381 --- /dev/null +++ b/tsuru/client/unit.go @@ -0,0 +1,338 @@ +// 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 ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/tsuru/gnuflag" + "github.com/tsuru/tsuru/cmd" +) + +type UnitAdd struct { + cmd.AppNameMixIn + fs *gnuflag.FlagSet + process string + version string +} + +func (c *UnitAdd) Info() *cmd.Info { + return &cmd.Info{ + Name: "unit-add", + Usage: "unit add <# of units> [-a/--app appname] [-p/--process processname] [--version version]", + Desc: `Adds new units to a process of an application. You need to have access to the +app to be able to add new units to it.`, + MinArgs: 1, + } +} + +func (c *UnitAdd) Flags() *gnuflag.FlagSet { + if c.fs == nil { + c.fs = c.AppNameMixIn.Flags() + c.fs.StringVar(&c.process, "process", "", "Process name") + c.fs.StringVar(&c.process, "p", "", "Process name") + c.fs.StringVar(&c.version, "version", "", "Version number") + } + return c.fs +} + +func (c *UnitAdd) Run(context *cmd.Context, client *cmd.Client) error { + context.RawOutput() + appName, err := c.AppName() + if err != nil { + return err + } + u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units", appName)) + if err != nil { + return err + } + val := url.Values{} + val.Add("units", context.Args[0]) + val.Add("process", c.process) + val.Set("version", c.version) + request, err := http.NewRequest("PUT", u, bytes.NewBufferString(val.Encode())) + if err != nil { + return err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + response, err := client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + return cmd.StreamJSONResponse(context.Stdout, response) +} + +type UnitRemove struct { + cmd.AppNameMixIn + fs *gnuflag.FlagSet + process string + version string +} + +func (c *UnitRemove) Info() *cmd.Info { + return &cmd.Info{ + Name: "unit-remove", + Usage: "unit remove <# of units> [-a/--app appname] [-p/-process processname] [--version version]", + Desc: `Removes units from a process of an application. You need to have access to the +app to be able to remove units from it.`, + MinArgs: 1, + } +} + +func (c *UnitRemove) Flags() *gnuflag.FlagSet { + if c.fs == nil { + c.fs = c.AppNameMixIn.Flags() + c.fs.StringVar(&c.process, "process", "", "Process name") + c.fs.StringVar(&c.process, "p", "", "Process name") + c.fs.StringVar(&c.version, "version", "", "Version number") + } + return c.fs +} + +func (c *UnitRemove) Run(context *cmd.Context, client *cmd.Client) error { + context.RawOutput() + appName, err := c.AppName() + if err != nil { + return err + } + val := url.Values{} + val.Add("units", context.Args[0]) + val.Add("process", c.process) + val.Set("version", c.version) + url, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units?%s", appName, val.Encode())) + if err != nil { + return err + } + request, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return err + } + response, err := client.Do(request) + if err != nil { + return err + } + return cmd.StreamJSONResponse(context.Stdout, response) +} + +type UnitKill struct { + cmd.AppNameMixIn + jobName string + fs *gnuflag.FlagSet + force bool +} + +func (c *UnitKill) Info() *cmd.Info { + return &cmd.Info{ + Name: "unit-kill", + Usage: "unit kill <-a/--app appname|-j/--job jobname> [-f/--force] ", + Desc: `Kills units from a process of an application or job. You need to have access to the +app or job to be able to remove unit from it.`, + MinArgs: 1, + } +} + +func (c *UnitKill) Flags() *gnuflag.FlagSet { + if c.fs == nil { + c.fs = c.AppNameMixIn.Flags() + c.fs.StringVar(&c.jobName, "job", "", "The name of the job.") + c.fs.StringVar(&c.jobName, "j", "", "The name of the job.") + c.fs.BoolVar(&c.force, "f", false, "Forces the termination of unit.") + } + return c.fs +} + +func (c *UnitKill) Run(context *cmd.Context, client *cmd.Client) error { + context.RawOutput() + joa := JobOrApp{fs: c.fs} + err := joa.validate() + if err != nil { + return err + } + if len(context.Args) < 1 { + return errors.New("you must provide the unit name.") + } + unit := context.Args[0] + v := url.Values{} + if c.force { + v.Set("force", "true") + } + version := "1.12" + if joa.Type == "job" { + version = "1.13" + } + url, err := cmd.GetURLVersion(version, fmt.Sprintf("/%ss/%s/units/%s?%s", joa.Type, joa.val, unit, v.Encode())) + if err != nil { + return err + } + request, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return err + } + response, err := client.Do(request) + if err != nil { + return err + } + return cmd.StreamJSONResponse(context.Stdout, response) +} + +type UnitSet struct { + cmd.AppNameMixIn + fs *gnuflag.FlagSet + process string + version int +} + +func (c *UnitSet) Info() *cmd.Info { + return &cmd.Info{ + Name: "unit-set", + Usage: "unit set <# of units> [-a/--app appname] [-p/--process processname] [--version version]", + Desc: `Set the number of units for a process of an application, adding or removing units as needed. You need to have access to the +app to be able to set the number of units for it. The process flag is optional if the app has only 1 process.`, + MinArgs: 1, + } +} + +func (c *UnitSet) Flags() *gnuflag.FlagSet { + if c.fs == nil { + c.fs = c.AppNameMixIn.Flags() + processMessage := "Process name" + c.fs.StringVar(&c.process, "process", "", processMessage) + c.fs.StringVar(&c.process, "p", "", processMessage) + c.fs.IntVar(&c.version, "version", 0, "Version number") + } + return c.fs +} + +func (c *UnitSet) Run(context *cmd.Context, client *cmd.Client) error { + context.RawOutput() + appName, err := c.AppName() + if err != nil { + return err + } + u, err := cmd.GetURL(fmt.Sprintf("/apps/%s", appName)) + if err != nil { + return err + } + request, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + response, err := client.Do(request) + if err != nil { + return err + } + result, err := io.ReadAll(response.Body) + if err != nil { + return err + } + var a app + err = json.Unmarshal(result, &a) + if err != nil { + return err + } + + unitsByProcess := map[string][]unit{} + unitsByVersion := map[int][]unit{} + for _, u := range a.Units { + unitsByProcess[u.ProcessName] = append(unitsByProcess[u.ProcessName], u) + unitsByVersion[u.Version] = append(unitsByVersion[u.Version], u) + } + + if len(unitsByProcess) != 1 && c.process == "" { + return errors.New("Please use the -p/--process flag to specify which process you want to set units for.") + } + + if len(unitsByVersion) != 1 && c.version == 0 { + return errors.New("Please use the --version flag to specify which version you want to set units for.") + } + + if c.process == "" { + for p := range unitsByProcess { + c.process = p + break + } + } + + if c.version == 0 { + for v := range unitsByVersion { + c.version = v + break + } + } + + existingUnits := 0 + for _, unit := range a.Units { + if unit.ProcessName == c.process && unit.Version == c.version { + existingUnits++ + } + } + + desiredUnits, err := strconv.Atoi(context.Args[0]) + if err != nil { + return err + } + + if existingUnits < desiredUnits { + u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units", appName)) + if err != nil { + return err + } + + unitsToAdd := desiredUnits - existingUnits + val := url.Values{} + val.Add("units", strconv.Itoa(unitsToAdd)) + val.Add("process", c.process) + val.Add("version", strconv.Itoa(c.version)) + request, err := http.NewRequest(http.MethodPut, u, bytes.NewBufferString(val.Encode())) + if err != nil { + return err + } + + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + response, err := client.Do(request) + if err != nil { + return err + } + + defer response.Body.Close() + return cmd.StreamJSONResponse(context.Stdout, response) + } + + if existingUnits > desiredUnits { + unitsToRemove := existingUnits - desiredUnits + val := url.Values{} + val.Add("units", strconv.Itoa(unitsToRemove)) + val.Add("process", c.process) + val.Add("version", strconv.Itoa(c.version)) + u, err := cmd.GetURL(fmt.Sprintf("/apps/%s/units?%s", appName, val.Encode())) + if err != nil { + return err + } + + request, err := http.NewRequest(http.MethodDelete, u, nil) + if err != nil { + return err + } + + response, err := client.Do(request) + if err != nil { + return err + } + + defer response.Body.Close() + return cmd.StreamJSONResponse(context.Stdout, response) + } + + fmt.Fprintf(context.Stdout, "The process %s, version %d already has %d units.\n", c.process, c.version, existingUnits) + return nil +} diff --git a/tsuru/client/unit_test.go b/tsuru/client/unit_test.go new file mode 100644 index 00000000..82217222 --- /dev/null +++ b/tsuru/client/unit_test.go @@ -0,0 +1,543 @@ +// 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 ( + "bytes" + "encoding/json" + "net/http" + "strings" + + "github.com/tsuru/tsuru/cmd" + "github.com/tsuru/tsuru/cmd/cmdtest" + tsuruIo "github.com/tsuru/tsuru/io" + check "gopkg.in/check.v1" +) + +func (s *S) TestUnitAdd(c *check.C) { + var stdout, stderr bytes.Buffer + var called bool + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + expectedOut := "-- added unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + result, err := json.Marshal(msg) + c.Assert(err, check.IsNil) + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + called = true + c.Assert(req.FormValue("process"), check.Equals, "p1") + c.Assert(req.FormValue("units"), check.Equals, "3") + return strings.HasSuffix(req.URL.Path, "/apps/radio/units") && req.Method == "PUT" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := UnitAdd{} + command.Flags().Parse(true, []string{"-a", "radio", "-p", "p1"}) + err = command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(called, check.Equals, true) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + +func (s *S) TestUnitAddWithVersion(c *check.C) { + var stdout, stderr bytes.Buffer + var called bool + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + expectedOut := "-- added unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + result, err := json.Marshal(msg) + c.Assert(err, check.IsNil) + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + called = true + c.Assert(req.FormValue("process"), check.Equals, "p1") + c.Assert(req.FormValue("units"), check.Equals, "3") + c.Assert(req.FormValue("version"), check.Equals, "9") + return strings.HasSuffix(req.URL.Path, "/apps/radio/units") && req.Method == "PUT" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := UnitAdd{} + command.Flags().Parse(true, []string{"-a", "radio", "-p", "p1", "--version", "9"}) + err = command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(called, check.Equals, true) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + +func (s *S) TestUnitAddFailure(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + msg := tsuruIo.SimpleJsonMessage{Error: "errored msg"} + result, err := json.Marshal(msg) + c.Assert(err, check.IsNil) + client := cmd.NewClient(&http.Client{Transport: &cmdtest.Transport{Message: string(result), Status: 200}}, nil, manager) + command := UnitAdd{} + command.Flags().Parse(true, []string{"-a", "radio"}) + err = command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "errored msg") +} + +func (s *S) TestUnitAddInfo(c *check.C) { + c.Assert((&UnitAdd{}).Info(), check.NotNil) +} + +func (s *S) TestUnitAddIsFlaggedACommand(c *check.C) { + var _ cmd.FlaggedCommand = &UnitAdd{} +} + +func (s *S) TestUnitRemove(c *check.C) { + var stdout, stderr bytes.Buffer + var called bool + context := cmd.Context{ + Args: []string{"2"}, + Stdout: &stdout, + Stderr: &stderr, + } + expectedOut := "-- removed unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + result, err := json.Marshal(msg) + c.Assert(err, check.IsNil) + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + called = true + c.Assert(req.FormValue("process"), check.Equals, "web1") + c.Assert(req.FormValue("units"), check.Equals, "2") + return strings.HasSuffix(req.URL.Path, "/apps/vapor/units") && req.Method == http.MethodDelete + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := UnitRemove{} + command.Flags().Parse(true, []string{"-a", "vapor", "-p", "web1"}) + err = command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(called, check.Equals, true) + c.Assert(stdout.String(), check.Equals, "-- removed unit --") +} + +func (s *S) TestUnitRemoveFailure(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"1"}, + Stdout: &stdout, + Stderr: &stderr, + } + client := cmd.NewClient(&http.Client{ + Transport: &cmdtest.Transport{Message: "Failed to remove.", Status: 500}, + }, nil, manager) + command := UnitRemove{} + command.Flags().Parse(true, []string{"-a", "vapor"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Failed to remove.") +} + +func (s *S) TestUnitRemoveInfo(c *check.C) { + c.Assert((&UnitRemove{}).Info(), check.NotNil) +} + +func (s *S) TestUnitRemoveIsACommand(c *check.C) { + var _ cmd.Command = &UnitRemove{} +} + +func (s *S) TestUnitSetAddUnits(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + var calledPut bool + context := cmd.Context{ + Args: []string{"10"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + + expectedOut := "-- added unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + resultPut, _ := json.Marshal(msg) + + transport := cmdtest.MultiConditionalTransport{ + ConditionalTransports: []cmdtest.ConditionalTransport{ + { + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + }, + { + CondFunc: func(req *http.Request) bool { + calledPut = true + c.Assert(req.FormValue("process"), check.Equals, "web") + c.Assert(req.FormValue("units"), check.Equals, "7") + return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut + }, + Transport: cmdtest.Transport{Message: string(resultPut), Status: http.StatusOK}, + }, + }, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(calledGet, check.Equals, true) + c.Assert(calledPut, check.Equals, true) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + +func (s *S) TestUnitSetAddUnitsFailure(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + var calledPut bool + context := cmd.Context{ + Args: []string{"10"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + + transport := cmdtest.MultiConditionalTransport{ + ConditionalTransports: []cmdtest.ConditionalTransport{ + { + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + }, + { + CondFunc: func(req *http.Request) bool { + calledPut = true + c.Assert(req.FormValue("process"), check.Equals, "web") + c.Assert(req.FormValue("units"), check.Equals, "7") + return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut + }, + Transport: cmdtest.Transport{Message: "Failed to put.", Status: http.StatusInternalServerError}, + }, + }, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Failed to put.") + c.Assert(calledGet, check.Equals, true) + c.Assert(calledPut, check.Equals, true) +} + +func (s *S) TestUnitSetRemoveUnits(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + var calledDelete bool + context := cmd.Context{ + Args: []string{"1"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + + expectedOut := "-- removed unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + resultDelete, _ := json.Marshal(msg) + + transport := cmdtest.MultiConditionalTransport{ + ConditionalTransports: []cmdtest.ConditionalTransport{ + { + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + }, + { + CondFunc: func(req *http.Request) bool { + calledDelete = true + c.Assert(req.FormValue("process"), check.Equals, "web") + c.Assert(req.FormValue("units"), check.Equals, "2") + return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodDelete + }, + Transport: cmdtest.Transport{Message: string(resultDelete), Status: http.StatusOK}, + }, + }, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(calledGet, check.Equals, true) + c.Assert(calledDelete, check.Equals, true) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + +func (s *S) TestUnitSetRemoveUnitsFailure(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + var calledDelete bool + context := cmd.Context{ + Args: []string{"1"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + + transport := cmdtest.MultiConditionalTransport{ + ConditionalTransports: []cmdtest.ConditionalTransport{ + { + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + }, + { + CondFunc: func(req *http.Request) bool { + calledDelete = true + c.Assert(req.FormValue("process"), check.Equals, "web") + c.Assert(req.FormValue("units"), check.Equals, "2") + return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodDelete + }, + Transport: cmdtest.Transport{Message: "Failed to delete.", Status: http.StatusInternalServerError}, + }, + }, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Failed to delete.") + c.Assert(calledGet, check.Equals, true) + c.Assert(calledDelete, check.Equals, true) +} + +func (s *S) TestUnitSetNoChanges(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + transport := cmdtest.ConditionalTransport{ + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(calledGet, check.Equals, true) + c.Assert(stdout.String(), check.Equals, "The process web, version 0 already has 3 units.\n") +} + +func (s *S) TestUnitSetFailedGet(c *check.C) { + var stdout, stderr bytes.Buffer + calledTimes := 0 + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + + transport := cmdtest.ConditionalTransport{ + CondFunc: func(req *http.Request) bool { + calledTimes++ + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: "Failed to get.", Status: http.StatusInternalServerError}, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1", "-p", "web"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Failed to get.") + c.Assert(calledTimes, check.Equals, 1) +} + +func (s *S) TestUnitSetNoProcessSpecifiedAndMultipleExist(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + context := cmd.Context{ + Args: []string{"3"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"10.10.10.10","ID":"app1/0","Status":"started","ProcessName":"web"},{"Ip":"9.9.9.9","ID":"app1/1","Status":"started","ProcessName":"web"},{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"web"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + transport := cmdtest.ConditionalTransport{ + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Please use the -p/--process flag to specify which process you want to set units for.") + c.Assert(calledGet, check.Equals, true) +} + +func (s *S) TestUnitSetNoProcessSpecifiedAndSingleExists(c *check.C) { + var stdout, stderr bytes.Buffer + var calledGet bool + var calledPut bool + context := cmd.Context{ + Args: []string{"10"}, + Stdout: &stdout, + Stderr: &stderr, + } + + resultGet := `{"name":"app1","teamowner":"myteam","cname":[""],"ip":"myapp.tsuru.io","platform":"php","repository":"git@git.com:php.git","state":"dead","units":[{"Ip":"","ID":"app1/2","Status":"pending","ProcessName":"worker"},{"Ip":"8.8.8.8","ID":"app1/3","Status":"started","ProcessName":"worker"}],"teams":["tsuruteam","crane"],"owner":"myapp_owner","deploys":7,"router":"planb"}` + + expectedOut := "-- added unit --" + msg := tsuruIo.SimpleJsonMessage{Message: expectedOut} + resultPut, _ := json.Marshal(msg) + + transport := cmdtest.MultiConditionalTransport{ + ConditionalTransports: []cmdtest.ConditionalTransport{ + { + CondFunc: func(req *http.Request) bool { + calledGet = true + return strings.HasSuffix(req.URL.Path, "/apps/app1") && req.Method == http.MethodGet + }, + Transport: cmdtest.Transport{Message: resultGet, Status: http.StatusOK}, + }, + { + CondFunc: func(req *http.Request) bool { + calledPut = true + c.Assert(req.FormValue("process"), check.Equals, "worker") + c.Assert(req.FormValue("units"), check.Equals, "8") + return strings.HasSuffix(req.URL.Path, "/apps/app1/units") && req.Method == http.MethodPut + }, + Transport: cmdtest.Transport{Message: string(resultPut), Status: http.StatusOK}, + }, + }, + } + + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitSet{} + command.Flags().Parse(true, []string{"-a", "app1"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(calledGet, check.Equals, true) + c.Assert(calledPut, check.Equals, true) + c.Assert(stdout.String(), check.Equals, expectedOut) +} + +func (s *S) TestUnitSetInfo(c *check.C) { + c.Assert((&UnitSet{}).Info(), check.NotNil) +} + +func (s *S) TestUnitSetIsACommand(c *check.C) { + var _ cmd.Command = &UnitSet{} +} + +func (s *S) TestUnitKill(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"unit1"}, + Stdout: &stdout, + Stderr: &stderr, + } + transport := cmdtest.Transport{ + Message: "", + Status: http.StatusOK, + } + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitKill{} + command.Flags().Parse(true, []string{"-a", "app1", "-f"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + + stdout.Reset() + stderr.Reset() + + context = cmd.Context{ + Args: []string{"unit1"}, + Stdout: &stdout, + Stderr: &stderr, + } + command = UnitKill{} + command.Flags().Parse(true, []string{"-j", "job1", "-f"}) + err = command.Run(&context, client) + c.Assert(err, check.IsNil) +} + +func (s *S) TestUnitKillMissingUnit(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + } + transport := cmdtest.Transport{ + Message: "", + Status: http.StatusOK, + } + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitKill{} + command.Flags().Parse(true, []string{"-a", "app1", "-f"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "you must provide the unit name.") +} + +func (s *S) TestUnitKillAppAndJobMutuallyExclusive(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Args: []string{"app1", "job1"}, + Stdout: &stdout, + Stderr: &stderr, + } + + transport := cmdtest.Transport{ + Message: "", + Status: http.StatusOK, + } + client := cmd.NewClient(&http.Client{Transport: &transport}, nil, manager) + command := UnitKill{} + command.Flags().Parse(true, []string{"-a", "app1", "-j", "job1"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "please use only one of the -a/--app and -j/--job flags") +}