From 743c94c450df84f393036ca3aae89c9663aceb37 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Thu, 4 Jul 2024 13:52:10 +0800
Subject: [PATCH 1/2] feat(controller-sdk-go): add fetch ptypes and events
 interface

---
 api/events.go         |  12 ++
 api/pts.go            |  40 +++++++
 api/pts_test.go       |  26 +++++
 events/events.go      |  42 +++++++
 events/events_test.go | 109 ++++++++++++++++++
 go.mod                |   1 -
 go.sum                |   2 -
 ps/ps.go              |  36 ------
 ps/ps_test.go         |  85 --------------
 pts/pts.go            |  87 +++++++++++++++
 pts/pts_test.go       | 251 ++++++++++++++++++++++++++++++++++++++++++
 11 files changed, 567 insertions(+), 124 deletions(-)
 create mode 100644 api/events.go
 create mode 100644 api/pts.go
 create mode 100644 api/pts_test.go
 create mode 100644 events/events.go
 create mode 100644 events/events_test.go
 create mode 100644 pts/pts.go
 create mode 100644 pts/pts_test.go

diff --git a/api/events.go b/api/events.go
new file mode 100644
index 0000000..e7c4381
--- /dev/null
+++ b/api/events.go
@@ -0,0 +1,12 @@
+package api
+
+import "github.com/drycc/controller-sdk-go/pkg/time"
+
+// Event defines the structure of event.
+type AppEvent struct {
+	Reason  string    `json:"reason"`
+	Message string    `json:"message"`
+	Created time.Time `json:"created"`
+}
+
+type AppEvents []AppEvent
diff --git a/api/pts.go b/api/pts.go
new file mode 100644
index 0000000..ba3ecbb
--- /dev/null
+++ b/api/pts.go
@@ -0,0 +1,40 @@
+package api
+
+import "github.com/drycc/controller-sdk-go/pkg/time"
+
+// Ptype defines the structure of ptype deployment.
+type Ptype struct {
+	Name              string    `json:"name"`
+	Release           string    `json:"release"`
+	Ready             string    `json:"ready"`
+	UpToDate          int       `json:"up_to_date"`
+	AvailableReplicas int       `json:"available_replicas"`
+	Started           time.Time `json:"started"`
+}
+
+// Ptypes defines a collection of app Ptypes.
+type Ptypes []Ptype
+
+func (d Ptypes) Len() int           { return len(d) }
+func (d Ptypes) Swap(i, j int)      { d[i], d[j] = d[j], d[i] }
+func (d Ptypes) Less(i, j int) bool { return d[i].Name < d[j].Name }
+
+// PtypeState defines a ptype deployment state.
+type PtypeState struct {
+	Container      string            `json:"container"`
+	Image          string            `json:"image"`
+	Command        []string          `json:"command,omitempty"`
+	Args           []string          `json:"args,omitempty"`
+	LivenessProbe  Healthcheck       `json:"liveness_probe,omitempty"`
+	ReadinessProbe Healthcheck       `json:"readiness_probe,omitempty"`
+	Limits         map[string]string `json:"limits,omitempty"`
+	VolumeMounts   []VolumeMount     `json:"volume_mounts,omitempty"`
+}
+
+type VolumeMount struct {
+	Name      string `json:"name"`
+	MountPath string `json:"mountPath"`
+}
+
+// PtypesState defines a collection of container state.
+type PtypeStates []PtypeState
diff --git a/api/pts_test.go b/api/pts_test.go
new file mode 100644
index 0000000..f6c5b1b
--- /dev/null
+++ b/api/pts_test.go
@@ -0,0 +1,26 @@
+package api
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/pkg/time"
+)
+
+func TestPtypesSorted(t *testing.T) {
+	ptypes := Ptypes{
+		{"web", "v1", "1/1", 1, 1, time.Time{}},
+		{"cronjob", "v1", "1/1", 1, 1, time.Time{}},
+		{"sleep", "v1", "1/1", 1, 1, time.Time{}},
+	}
+
+	sort.Sort(ptypes)
+
+	expectedPtypeNames := []string{"cronjob", "sleep", "web"}
+
+	for i, ptype := range ptypes {
+		if expectedPtypeNames[i] != ptype.Name {
+			t.Errorf("Expected ptypes to be sorted %v, Got %v", expectedPtypeNames[i], ptype.Name)
+		}
+	}
+}
diff --git a/events/events.go b/events/events.go
new file mode 100644
index 0000000..dac4a8b
--- /dev/null
+++ b/events/events.go
@@ -0,0 +1,42 @@
+// Package ps provides methods for managing app processes.
+package events
+
+import (
+	"encoding/json"
+	"fmt"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+)
+
+// List events of an app process.
+func ListPodEvents(c *drycc.Client, appID string, podName string, results int) (api.AppEvents, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/pods/%s/events/", appID, podName)
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return []api.AppEvent{}, -1, reqErr
+	}
+
+	var events []api.AppEvent
+	if err := json.Unmarshal([]byte(body), &events); err != nil {
+		return []api.AppEvent{}, -1, err
+	}
+
+	return events, count, reqErr
+}
+
+// List events of an app ptype.
+func ListPtypeEvents(c *drycc.Client, appID string, ptype string, results int) (api.AppEvents, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/%s-%s/events/", appID, appID, ptype)
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return []api.AppEvent{}, -1, reqErr
+	}
+
+	var events []api.AppEvent
+	if err := json.Unmarshal([]byte(body), &events); err != nil {
+		return []api.AppEvent{}, -1, err
+	}
+
+	return events, count, reqErr
+}
diff --git a/events/events_test.go b/events/events_test.go
new file mode 100644
index 0000000..23bccbf
--- /dev/null
+++ b/events/events_test.go
@@ -0,0 +1,109 @@
+package events
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/pkg/time"
+)
+
+const podEventsFixture string = `
+{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [
+        {
+            "reason": "Scheduled",
+            "message": "Successfully assigned example-go/example-go-web-6b44dbd6c8-h89cg to node1",
+            "created": "2024-07-03T16:28:00"
+        }
+    ]
+}`
+
+const ptypeEventsFixture string = `
+{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [
+        {
+            "reason": "ScalingReplicaSet",
+            "message": "Scaled up replica set example-go-web-6b44dbd6c8 to 2 from 1",
+            "created": "2024-07-03T16:28:00"
+        }
+    ]
+}`
+
+type fakeHTTPServer struct{}
+
+func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
+	res.Header().Add("DRYCC_API_VERSION", drycc.APIVersion)
+	if req.URL.Path == "/v2/apps/example-go/ptypes/example-go-web/events/" && req.Method == "GET" {
+		res.Write([]byte(ptypeEventsFixture))
+		return
+	}
+	if req.URL.Path == "/v2/apps/example-go/pods/example-go-web-6b44dbd6c8-h89cg/events/" && req.Method == "GET" {
+		res.Write([]byte(podEventsFixture))
+		return
+	}
+
+	fmt.Printf("Unrecognized URL %s\n", req.URL)
+	res.WriteHeader(http.StatusNotFound)
+	res.Write(nil)
+}
+
+func TestEvents(t *testing.T) {
+	t.Parallel()
+
+	created := time.Time{}
+	created.UnmarshalText([]byte("2024-07-03T16:28:00"))
+	podExpected := api.AppEvents{
+		{
+			Reason:  "Scheduled",
+			Message: "Successfully assigned example-go/example-go-web-6b44dbd6c8-h89cg to node1",
+			Created: created,
+		},
+	}
+	ptypeExpected := api.AppEvents{
+		{
+			Reason:  "ScalingReplicaSet",
+			Message: "Scaled up replica set example-go-web-6b44dbd6c8 to 2 from 1",
+			Created: created,
+		},
+	}
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, _, err := ListPodEvents(drycc, "example-go", "example-go-web-6b44dbd6c8-h89cg", 100)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(podExpected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", podExpected, actual))
+	}
+
+	actual, _, err = ListPtypeEvents(drycc, "example-go", "web", 100)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(ptypeExpected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", ptypeExpected, actual))
+	}
+}
diff --git a/go.mod b/go.mod
index ff3eab2..c84e00f 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/drycc/controller-sdk-go
 go 1.22
 
 require (
-	github.com/google/uuid v1.6.0
 	github.com/stretchr/testify v1.9.0
 	golang.org/x/net v0.23.0
 )
diff --git a/go.sum b/go.sum
index daddf38..8ef8d2d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,5 @@
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
diff --git a/ps/ps.go b/ps/ps.go
index b63a368..b3ca852 100644
--- a/ps/ps.go
+++ b/ps/ps.go
@@ -83,42 +83,6 @@ func Logs(c *drycc.Client, appID, podID string, request api.PodLogsRequest) (*we
 	return conn, nil
 }
 
-// Scale increases or decreases an app's processes. The processes are specified in the target argument,
-// a key-value map, where the key is the process name and the value is the number of replicas
-func Scale(c *drycc.Client, appID string, targets map[string]int) error {
-	u := fmt.Sprintf("/v2/apps/%s/scale/", appID)
-
-	body, err := json.Marshal(targets)
-
-	if err != nil {
-		return err
-	}
-
-	res, err := c.Request("POST", u, body)
-	if err != nil && !drycc.IsErrAPIMismatch(err) {
-		return err
-	}
-	defer 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.
-func Restart(c *drycc.Client, appID string, targets map[string]string) error {
-	u := fmt.Sprintf("/v2/apps/%s/pods/restart/", appID)
-	body, err := json.Marshal(targets)
-	if err != nil {
-		return err
-	}
-	res, err := c.Request("POST", u, body)
-	if err != nil && !drycc.IsErrAPIMismatch(err) {
-		return err
-	}
-	defer res.Body.Close()
-	return err
-}
-
 // Describe pod state
 func Describe(c *drycc.Client, appID string, podID string, results int) (api.PodState, int, error) {
 	u := fmt.Sprintf("/v2/apps/%s/pods/%s/describe/", appID, podID)
diff --git a/ps/ps_test.go b/ps/ps_test.go
index 2ad9d14..163426d 100644
--- a/ps/ps_test.go
+++ b/ps/ps_test.go
@@ -65,10 +65,6 @@ const podStateFixture string = `
 	}]
 }`
 
-const scaleExpected string = `{"web":2}`
-
-const restartExpected string = `{"types":"web,worker"}`
-
 type fakeHTTPServer struct{}
 
 func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
@@ -84,45 +80,6 @@ func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	if req.URL.Path == "/v2/apps/example-go/pods/restart/" && req.Method == "POST" {
-		body, err := io.ReadAll(req.Body)
-
-		if err != nil {
-			fmt.Println(err)
-			res.WriteHeader(http.StatusInternalServerError)
-			res.Write(nil)
-		}
-		if string(body) != restartExpected {
-			fmt.Printf("Expected '%s', Got '%s'\n", restartExpected, body)
-			res.WriteHeader(http.StatusInternalServerError)
-			res.Write(nil)
-			return
-		}
-		res.WriteHeader(http.StatusNoContent)
-		return
-	}
-
-	if req.URL.Path == "/v2/apps/example-go/scale/" && req.Method == "POST" {
-		body, err := io.ReadAll(req.Body)
-
-		if err != nil {
-			fmt.Println(err)
-			res.WriteHeader(http.StatusInternalServerError)
-			res.Write(nil)
-		}
-
-		if string(body) != scaleExpected {
-			fmt.Printf("Expected '%s', Got '%s'\n", scaleExpected, 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)
@@ -245,31 +202,6 @@ func TestPodLogs(t *testing.T) {
 	}
 }
 
-func TestAppsRestart(t *testing.T) {
-	t.Parallel()
-
-	started := time.Time{}
-	started.UnmarshalText([]byte("2016-02-13T00:47:52"))
-	types := map[string]string{
-		"types": "web,worker",
-	}
-
-	handler := fakeHTTPServer{}
-	server := httptest.NewServer(&handler)
-	defer server.Close()
-
-	drycc, err := drycc.New(false, server.URL, "abc")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = Restart(drycc, "example-go", types)
-
-	if err != nil {
-		t.Error(err)
-	}
-}
-
 func TestDescribe(t *testing.T) {
 	t.Parallel()
 
@@ -318,23 +250,6 @@ func TestDescribe(t *testing.T) {
 	}
 }
 
-func TestScale(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 = Scale(drycc, "example-go", map[string]int{"web": 2}); err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestByType(t *testing.T) {
 	t.Parallel()
 
diff --git a/pts/pts.go b/pts/pts.go
new file mode 100644
index 0000000..35ea816
--- /dev/null
+++ b/pts/pts.go
@@ -0,0 +1,87 @@
+// Package ps provides methods for managing app processes.
+package pts
+
+import (
+	"encoding/json"
+	"fmt"
+	"sort"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+)
+
+// List lists an app's processes.
+func List(c *drycc.Client, appID string, results int) (api.Ptypes, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/", appID)
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return []api.Ptype{}, -1, reqErr
+	}
+
+	var ptypes []api.Ptype
+	if err := json.Unmarshal([]byte(body), &ptypes); err != nil {
+		return []api.Ptype{}, -1, err
+	}
+
+	return ptypes, count, reqErr
+}
+
+// Describe Ptype state
+func Describe(c *drycc.Client, appID string, ptype string, results int) (api.PtypeStates, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/%s-%s/describe/", appID, appID, ptype)
+
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return api.PtypeStates{}, -1, reqErr
+	}
+
+	var PtypeStates api.PtypeStates
+	if err := json.Unmarshal([]byte(body), &PtypeStates); err != nil {
+		return api.PtypeStates{}, -1, err
+	}
+	return PtypeStates, count, reqErr
+}
+
+// Scale increases or decreases an app's processes. The processes are specified in the target argument,
+// a key-value map, where the key is the process name and the value is the number of replicas
+func Scale(c *drycc.Client, appID string, targets map[string]int) error {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/scale/", appID)
+
+	body, err := json.Marshal(targets)
+
+	if err != nil {
+		return err
+	}
+
+	res, err := c.Request("POST", u, body)
+	if err != nil && !drycc.IsErrAPIMismatch(err) {
+		return err
+	}
+	defer 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.
+func Restart(c *drycc.Client, appID string, targets map[string]string) error {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/restart/", appID)
+	body, err := json.Marshal(targets)
+	if err != nil {
+		return err
+	}
+	res, err := c.Request("POST", u, body)
+	if err != nil && !drycc.IsErrAPIMismatch(err) {
+		return err
+	}
+	defer res.Body.Close()
+	return err
+}
+
+// ByType organizes process types of an app by process type.
+func ByType(ptypes api.Ptypes) api.Ptypes {
+	// Sort ProcessTypes alphabetically by process types name
+	sort.Sort(ptypes)
+
+	return ptypes
+}
diff --git a/pts/pts_test.go b/pts/pts_test.go
new file mode 100644
index 0000000..07a99d9
--- /dev/null
+++ b/pts/pts_test.go
@@ -0,0 +1,251 @@
+package pts
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/pkg/time"
+)
+
+const ptypesFixture string = `
+{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [
+        {
+			"name": "example-go-web",
+			"release": "v1",
+			"ready": "1/1",
+			"up_to_date": 1,
+            "available_replicas": 1,
+            "started": "2024-07-03T16:28:00"
+        }
+    ]
+}`
+
+const ptypeStateFixture string = `
+{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [{
+        "container": "example-go-web",
+        "image": "registry.drycc.cc/base/base",
+        "command": ["bash", "-c"],
+        "args": ["sleep", "3600s"],
+        "readiness_probe": {
+            "exec": {
+                "command": ["ls", "-la"]
+            },
+            "failureThreshold": 3,
+            "initialDelaySeconds": 50,
+            "periodSeconds": 10,
+            "successThreshold": 1,
+            "timeoutSeconds": 50
+        },
+        "limits": {
+            "cpu": "1",
+            "memory": "2Gi"
+        },
+        "volume_mounts": [
+            {
+                "mountPath": "/data",
+                "name": "myvolume"
+            }
+        ]
+    }]
+}`
+
+const scaleExpected string = `{"web":2}`
+
+const restartExpected string = `{"types":"web,worker"}`
+
+type fakeHTTPServer struct{}
+
+func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
+	res.Header().Add("DRYCC_API_VERSION", drycc.APIVersion)
+
+	if req.URL.Path == "/v2/apps/example-go/ptypes/" && req.Method == "GET" {
+		res.Write([]byte(ptypesFixture))
+		return
+	}
+
+	if req.URL.Path == "/v2/apps/example-go/ptypes/example-go-web/describe/" && req.Method == "GET" {
+		res.Write([]byte(ptypeStateFixture))
+		return
+	}
+	if req.URL.Path == "/v2/apps/example-go/ptypes/restart/" && req.Method == "POST" {
+		body, err := io.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+		if string(body) != restartExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", restartExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusNoContent)
+		return
+	}
+
+	if req.URL.Path == "/v2/apps/example-go/ptypes/scale/" && req.Method == "POST" {
+		body, err := io.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != scaleExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", scaleExpected, 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)
+}
+
+func TestPtype(t *testing.T) {
+	t.Parallel()
+
+	started := time.Time{}
+	started.UnmarshalText([]byte("2024-07-03T16:28:00"))
+	expected := api.Ptypes{
+		{
+			Name:              "example-go-web",
+			Release:           "v1",
+			Ready:             "1/1",
+			UpToDate:          1,
+			AvailableReplicas: 1,
+			Started:           started,
+		},
+	}
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, _, err := List(drycc, "example-go", 100)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", expected, actual))
+	}
+}
+
+func TestDescribe(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)
+	}
+	actual, _, err := Describe(drycc, "example-go", "web", 100)
+	if err != nil {
+		t.Error(err)
+	}
+	expected := api.PtypeStates{
+		{
+			Container: "example-go-web",
+			Image:     "registry.drycc.cc/base/base",
+			Command:   []string{"bash", "-c"},
+			Args:      []string{"sleep", "3600s"},
+			ReadinessProbe: api.Healthcheck{
+				Exec: &api.ExecProbe{
+					Command: []string{"ls", "-la"},
+				},
+				FailureThreshold:    3,
+				InitialDelaySeconds: 50,
+				PeriodSeconds:       10,
+				SuccessThreshold:    1,
+				TimeoutSeconds:      50,
+			},
+			Limits: map[string]string{
+				"cpu":    "1",
+				"memory": "2Gi",
+			},
+			VolumeMounts: []api.VolumeMount{
+				{
+					Name:      "myvolume",
+					MountPath: "/data",
+				},
+			},
+		},
+	}
+	if !reflect.DeepEqual(actual, expected) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", actual, expected))
+	}
+}
+
+func TestAppsRestart(t *testing.T) {
+	t.Parallel()
+
+	started := time.Time{}
+	started.UnmarshalText([]byte("2016-02-13T00:47:52"))
+	types := map[string]string{
+		"types": "web,worker",
+	}
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(&handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = Restart(drycc, "example-go", types)
+
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestScale(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 = Scale(drycc, "example-go", map[string]int{"web": 2}); err != nil {
+		t.Fatal(err)
+	}
+}

From d4664e3513e48ec5ed3a6df97f26b340b5177334 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Fri, 5 Jul 2024 18:45:11 +0800
Subject: [PATCH 2/2] feat(ps): support delete pods

---
 api/ps.go     |  4 ++++
 ps/ps.go      | 17 +++++++++++++++++
 ps/ps_test.go | 36 ++++++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+)

diff --git a/api/ps.go b/api/ps.go
index 533a9cd..b4f1afa 100644
--- a/api/ps.go
+++ b/api/ps.go
@@ -72,3 +72,7 @@ type ContainerState struct {
 
 // PodState defines a collection of container state.
 type PodState []ContainerState
+
+type PodIDs struct {
+	PodIDs string `json:"pod_ids"`
+}
diff --git a/ps/ps.go b/ps/ps.go
index b3ca852..29e520f 100644
--- a/ps/ps.go
+++ b/ps/ps.go
@@ -99,6 +99,23 @@ func Describe(c *drycc.Client, appID string, podID string, results int) (api.Pod
 	return podState, count, reqErr
 }
 
+// delete a pod
+func Delete(c *drycc.Client, appID string, podIDs string) error {
+	u := fmt.Sprintf("/v2/apps/%s/pods/", appID)
+
+	req := api.PodIDs{PodIDs: podIDs}
+
+	body, err := json.Marshal(req)
+	if err != nil {
+		return err
+	}
+	res, err := c.Request("DELETE", u, body)
+	if err == nil {
+		res.Body.Close()
+	}
+	return err
+}
+
 // ByType organizes processes of an app by process type.
 func ByType(processes api.PodsList) api.PodTypes {
 	var pts api.PodTypes
diff --git a/ps/ps_test.go b/ps/ps_test.go
index 163426d..5e3eb49 100644
--- a/ps/ps_test.go
+++ b/ps/ps_test.go
@@ -65,6 +65,8 @@ const podStateFixture string = `
 	}]
 }`
 
+const podDeleteExpected string = `{"pod_ids":"test-pod-web"}`
+
 type fakeHTTPServer struct{}
 
 func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
@@ -79,6 +81,23 @@ func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 		res.Write([]byte(podStateFixture))
 		return
 	}
+	if req.URL.Path == "/v2/apps/example-go/pods/" && req.Method == "DELETE" {
+		body, err := io.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+		if string(body) != podDeleteExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", podDeleteExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusNoContent)
+		return
+	}
 
 	fmt.Printf("Unrecognized URL %s\n", req.URL)
 	res.WriteHeader(http.StatusNotFound)
@@ -301,3 +320,20 @@ func TestByType(t *testing.T) {
 		t.Errorf("Expected: %v, Got %v", expected, actual)
 	}
 }
+
+func TestPodsDelete(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 = Delete(drycc, "example-go", "test-pod-web"); err != nil {
+		t.Fatal(err)
+	}
+}
