From 3121f02441c01cac69c0c127672d726fe099d082 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Tue, 13 Oct 2020 20:29:28 +0800
Subject: [PATCH] feat(ps):add ps:stop/start command

---
 api/ps.go      |  18 +++--
 api/ps_test.go |  14 ++--
 ps/ps.go       |  48 +++++++++++-
 ps/ps_test.go  | 196 +++++++++++++++++++++++++++++++++++++------------
 4 files changed, 215 insertions(+), 61 deletions(-)

diff --git a/api/ps.go b/api/ps.go
index b1a23ec..136d921 100644
--- a/api/ps.go
+++ b/api/ps.go
@@ -10,11 +10,12 @@ type ProcessType map[string]string
 
 // Pods defines the structure of a process.
 type Pods struct {
-	Release string    `json:"release"`
-	Type    string    `json:"type"`
-	Name    string    `json:"name"`
-	State   string    `json:"state"`
-	Started time.Time `json:"started"`
+	Release  string    `json:"release,omitempty"`
+	Type     string    `json:"type,omitempty"`
+	Name     string    `json:"name,omitempty"`
+	State    string    `json:"state,omitempty"`
+	Started  time.Time `json:"started,omitempty"`
+	Replicas int       `json:"replicas,omitempty"`
 }
 
 // PodsList defines a collection of app pods.
@@ -28,11 +29,18 @@ func (p PodsList) Less(i, j int) bool { return p[i].Name < p[j].Name }
 type PodType struct {
 	Type     string
 	PodsList PodsList
+	Replicas int
+	Status   string
 }
 
 // PodTypes holds groups of pods organized by type.
 type PodTypes []PodType
 
+// Start is the definition of POST /v2/apps/<app_id>/stop or POST /v2/apps/<app_id>/start.
+type Types struct {
+	Types []string `json:"types,omitempty"`
+}
+
 func (p PodTypes) Len() int           { return len(p) }
 func (p PodTypes) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
 func (p PodTypes) Less(i, j int) bool { return p[i].Type < p[j].Type }
diff --git a/api/ps_test.go b/api/ps_test.go
index 5d1b1a3..3c02993 100644
--- a/api/ps_test.go
+++ b/api/ps_test.go
@@ -9,10 +9,10 @@ import (
 
 func TestPodsListSorted(t *testing.T) {
 	pods := PodsList{
-		{"", "web", "web.fsdfgh4", "up", time.Time{}},
-		{"", "web", "web.asdfgh1", "up", time.Time{}},
-		{"", "web", "web.csdfgh3", "up", time.Time{}},
-		{"", "web", "web.bsdfgh2", "up", time.Time{}},
+		{"", "web", "web.fsdfgh4", "up", time.Time{}, 4},
+		{"", "web", "web.asdfgh1", "up", time.Time{}, 4},
+		{"", "web", "web.csdfgh3", "up", time.Time{}, 4},
+		{"", "web", "web.bsdfgh2", "up", time.Time{}, 4},
 	}
 
 	sort.Sort(pods)
@@ -28,9 +28,9 @@ func TestPodsListSorted(t *testing.T) {
 
 func TestPodTypesSorted(t *testing.T) {
 	podTypes := PodTypes{
-		{"worker", PodsList{}},
-		{"web", PodsList{}},
-		{"clock", PodsList{}},
+		{"worker", PodsList{}, 4, "started"},
+		{"web", PodsList{}, 4, "started"},
+		{"clock", PodsList{}, 4, "started"},
 	}
 
 	sort.Sort(podTypes)
diff --git a/ps/ps.go b/ps/ps.go
index 3ed1eff..faa6424 100644
--- a/ps/ps.go
+++ b/ps/ps.go
@@ -44,6 +44,40 @@ func Scale(c *drycc.Client, appID string, targets map[string]int) error {
 	return err
 }
 
+// Stop decreases an app's processes to 0. The processes are specified in the target argument
+func Stop(c *drycc.Client, appID string, targets map[string][]string) error {
+	u := fmt.Sprintf("/v2/apps/%s/stop/", appID)
+
+	body, err := json.Marshal(targets)
+
+	if err != nil {
+		return err
+	}
+
+	res, err := c.Request("POST", u, body)
+	if err == nil {
+		return res.Body.Close()
+	}
+	return err
+}
+
+// Start increases an app's processes to setting. The processes are specified in the target argument
+func Start(c *drycc.Client, appID string, targets map[string][]string) error {
+	u := fmt.Sprintf("/v2/apps/%s/start/", appID)
+
+	body, err := json.Marshal(targets)
+
+	if err != nil {
+		return err
+	}
+
+	res, err := c.Request("POST", u, body)
+	if err == nil {
+		return res.Body.Close()
+	}
+	return err
+}
+
 // Restart restarts an app's processes. To restart all app processes, pass empty strings for
 // procType and name. To restart an specific process, pass an procType by leave name empty.
 // To restart a specific instance, pass a procType and a name.
@@ -84,16 +118,26 @@ func ByType(processes api.PodsList) api.PodTypes {
 		for i, pt := range pts {
 			if pt.Type == process.Type {
 				exists = true
-				pts[i].PodsList = append(pts[i].PodsList, process)
+				if process.Name != "" {
+					pts[i].PodsList = append(pts[i].PodsList, process)
+				}
 				break
 			}
 		}
 
 		// Is processtype for process doesn't exist, create a new one
 		if !exists {
+			p := api.PodsList{}
+			status := "stopped"
+			if process.Name != "" {
+				p = api.PodsList{process}
+				status = "started"
+			}
 			pts = append(pts, api.PodType{
 				Type:     process.Type,
-				PodsList: api.PodsList{process},
+				PodsList: p,
+				Replicas: process.Replicas,
+				Status:   status,
 			})
 		}
 	}
diff --git a/ps/ps_test.go b/ps/ps_test.go
index be912f0..76c9d02 100644
--- a/ps/ps_test.go
+++ b/ps/ps_test.go
@@ -24,7 +24,8 @@ const processesFixture string = `
             "type": "web",
             "name": "example-go-v2-web-45678",
             "state": "up",
-            "started": "2016-02-13T00:47:52"
+            "started": "2016-02-13T00:47:52",
+            "replicas": 1
         }
     ]
 }`
@@ -35,7 +36,8 @@ const restartAllFixture string = `[
         "type": "web",
         "name": "example-go-v2-web-45678",
         "state": "up",
-        "started": "2016-02-13T00:47:52"
+        "started": "2016-02-13T00:47:52",
+        "replicas": 1
     }
 ]
 `
@@ -46,7 +48,8 @@ const restartWorkerFixture string = `[
         "type": "worker",
         "name": "example-go-v2-worker-45678",
         "state": "up",
-        "started": "2016-02-13T00:47:52"
+        "started": "2016-02-13T00:47:52",
+        "replicas": 1
     }
 ]
 `
@@ -57,13 +60,18 @@ const restartWebTwoFixture string = `[
         "type": "web",
         "name": "example-go-v2-web-45678",
         "state": "up",
-        "started": "2016-02-13T00:47:52"
+        "started": "2016-02-13T00:47:52",
+        "replicas": 1
     }
 ]
 `
 
 const scaleExpected string = `{"web":2}`
 
+const stopExpected string = `{"types":["web"]}`
+
+const startExpected string = `{"types":["web"]}`
+
 type fakeHTTPServer struct{}
 
 func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
@@ -115,6 +123,48 @@ func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 		return
 	}
 
+	if req.URL.Path == "/v2/apps/example-go/stop/" && req.Method == "POST" {
+		body, err := ioutil.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != stopExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", stopExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+
+		res.WriteHeader(http.StatusNoContent)
+		res.Write(nil)
+		return
+	}
+
+	if req.URL.Path == "/v2/apps/example-go/start/" && req.Method == "POST" {
+		body, err := ioutil.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != startExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", startExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+
+		res.WriteHeader(http.StatusNoContent)
+		res.Write(nil)
+		return
+	}
+
 	fmt.Printf("Unrecognized URL %s\n", req.URL)
 	res.WriteHeader(http.StatusNotFound)
 	res.Write(nil)
@@ -127,11 +177,12 @@ func TestProcessesList(t *testing.T) {
 	started.UnmarshalText([]byte("2016-02-13T00:47:52"))
 	expected := api.PodsList{
 		{
-			Release: "v2",
-			Type:    "web",
-			Name:    "example-go-v2-web-45678",
-			State:   "up",
-			Started: started,
+			Release:  "v2",
+			Type:     "web",
+			Name:     "example-go-v2-web-45678",
+			State:    "up",
+			Started:  started,
+			Replicas: 1,
 		},
 	}
 
@@ -172,11 +223,12 @@ func TestAppsRestart(t *testing.T) {
 			Type: "",
 			Expected: []api.Pods{
 				{
-					Release: "v2",
-					Type:    "web",
-					Name:    "example-go-v2-web-45678",
-					State:   "up",
-					Started: started,
+					Release:  "v2",
+					Type:     "web",
+					Name:     "example-go-v2-web-45678",
+					State:    "up",
+					Started:  started,
+					Replicas: 1,
 				},
 			},
 		},
@@ -185,11 +237,12 @@ func TestAppsRestart(t *testing.T) {
 			Type: "worker",
 			Expected: []api.Pods{
 				{
-					Release: "v2",
-					Type:    "worker",
-					Name:    "example-go-v2-worker-45678",
-					State:   "up",
-					Started: started,
+					Release:  "v2",
+					Type:     "worker",
+					Name:     "example-go-v2-worker-45678",
+					State:    "up",
+					Started:  started,
+					Replicas: 1,
 				},
 			},
 		},
@@ -198,11 +251,12 @@ func TestAppsRestart(t *testing.T) {
 			Type: "worker",
 			Expected: []api.Pods{
 				{
-					Release: "v2",
-					Type:    "worker",
-					Name:    "example-go-v2-worker-45678",
-					State:   "up",
-					Started: started,
+					Release:  "v2",
+					Type:     "worker",
+					Name:     "example-go-v2-worker-45678",
+					State:    "up",
+					Started:  started,
+					Replicas: 1,
 				},
 			},
 		},
@@ -211,11 +265,12 @@ func TestAppsRestart(t *testing.T) {
 			Type: "web",
 			Expected: []api.Pods{
 				{
-					Release: "v2",
-					Type:    "web",
-					Name:    "example-go-v2-web-45678",
-					State:   "up",
-					Started: started,
+					Release:  "v2",
+					Type:     "web",
+					Name:     "example-go-v2-web-45678",
+					State:    "up",
+					Started:  started,
+					Replicas: 1,
 				},
 			},
 		},
@@ -260,6 +315,40 @@ func TestScale(t *testing.T) {
 	}
 }
 
+func TestStop(t *testing.T) {
+	t.Parallel()
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(&handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err = Stop(drycc, "example-go", map[string][]string{"types": {"web"}}); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestStart(t *testing.T) {
+	t.Parallel()
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(&handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err = Start(drycc, "example-go", map[string][]string{"types": {"web"}}); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestByType(t *testing.T) {
 	t.Parallel()
 
@@ -270,39 +359,52 @@ func TestByType(t *testing.T) {
 		{
 			Type: "abc",
 			PodsList: api.PodsList{
-				{Type: "abc", Name: "1", Started: started},
-				{Type: "abc", Name: "2", Started: started},
-				{Type: "abc", Name: "3", Started: started},
+				{Type: "abc", Name: "1", Started: started, Replicas: 3},
+				{Type: "abc", Name: "2", Started: started, Replicas: 3},
+				{Type: "abc", Name: "3", Started: started, Replicas: 3},
 			},
+			Replicas: 3,
+			Status:   "started",
+		},
+		{
+			Type:     "job",
+			PodsList: api.PodsList{},
+			Replicas: 3,
+			Status:   "stopped",
 		},
 		{
 			Type: "web",
 			PodsList: api.PodsList{
-				{Type: "web", Name: "test1", Started: started},
-				{Type: "web", Name: "test2", Started: started},
-				{Type: "web", Name: "test3", Started: started},
+				{Type: "web", Name: "test1", Started: started, Replicas: 3},
+				{Type: "web", Name: "test2", Started: started, Replicas: 3},
+				{Type: "web", Name: "test3", Started: started, Replicas: 3},
 			},
+			Replicas: 3,
+			Status:   "started",
 		},
 		{
 			Type: "worker",
 			PodsList: api.PodsList{
-				{Type: "worker", Name: "a", Started: started},
-				{Type: "worker", Name: "b", Started: started},
-				{Type: "worker", Name: "c", Started: started},
+				{Type: "worker", Name: "a", Started: started, Replicas: 3},
+				{Type: "worker", Name: "b", Started: started, Replicas: 3},
+				{Type: "worker", Name: "c", Started: started, Replicas: 3},
 			},
+			Replicas: 3,
+			Status:   "started",
 		},
 	}
 
 	input := api.PodsList{
-		{Type: "worker", Name: "c", Started: started},
-		{Type: "abc", Name: "2", Started: started},
-		{Type: "worker", Name: "b", Started: started},
-		{Type: "web", Name: "test1", Started: started},
-		{Type: "web", Name: "test3", Started: started},
-		{Type: "abc", Name: "1", Started: started},
-		{Type: "worker", Name: "a", Started: started},
-		{Type: "abc", Name: "3", Started: started},
-		{Type: "web", Name: "test2", Started: started},
+		{Type: "worker", Name: "c", Started: started, Replicas: 3},
+		{Type: "abc", Name: "2", Started: started, Replicas: 3},
+		{Type: "worker", Name: "b", Started: started, Replicas: 3},
+		{Type: "web", Name: "test1", Started: started, Replicas: 3},
+		{Type: "web", Name: "test3", Started: started, Replicas: 3},
+		{Type: "abc", Name: "1", Started: started, Replicas: 3},
+		{Type: "worker", Name: "a", Started: started, Replicas: 3},
+		{Type: "abc", Name: "3", Started: started, Replicas: 3},
+		{Type: "web", Name: "test2", Started: started, Replicas: 3},
+		{Type: "job", Replicas: 3},
 	}
 
 	actual := ByType(input)
