From 51eb66f437dc97e356cac883b14f362390e45a55 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Wed, 26 Aug 2020 16:21:30 +0800
Subject: [PATCH] feat(controller-sdk-go): drycc run cmd add --mount para

---
 .gitignore              |   1 +
 api/apps.go             |   1 +
 api/volumes.go          |  34 ++++
 apps/apps.go            |   7 +-
 apps/apps_test.go       |   2 +-
 volumes/volumes.go      |  81 ++++++++++
 volumes/volumes_test.go | 337 ++++++++++++++++++++++++++++++++++++++++
 7 files changed, 460 insertions(+), 3 deletions(-)
 create mode 100644 api/volumes.go
 create mode 100644 volumes/volumes.go
 create mode 100644 volumes/volumes_test.go

diff --git a/.gitignore b/.gitignore
index 935ed30..791d79a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ _testmain.go
 
 vendor
 coverage.txt
+.idea/*
diff --git a/api/apps.go b/api/apps.go
index ec9893f..ff9a762 100644
--- a/api/apps.go
+++ b/api/apps.go
@@ -29,6 +29,7 @@ type AppUpdateRequest struct {
 // AppRunRequest is the definition of POST /v2/apps/<app id>/run.
 type AppRunRequest struct {
 	Command string `json:"command"`
+	Volumes map[string]interface{} `json:"volumes,omitempty"`
 }
 
 // AppRunResponse is the definition of /v2/apps/<app id>/run.
diff --git a/api/volumes.go b/api/volumes.go
new file mode 100644
index 0000000..2d4728d
--- /dev/null
+++ b/api/volumes.go
@@ -0,0 +1,34 @@
+package api
+
+// Mount is the definition of PATCH /v2/apps/<app_id>/volumes/<name>/path/.
+type Mount struct {
+	Values map[string]string `json:"values"`
+}
+
+// Unmount is the definition of PATCH /v2/apps/<app_id>/volumes/<name>/path/.
+type Unmount struct {
+	Values map[string]interface{} `json:"values"`
+}
+
+// Volume is the structure of an app's volume.
+type Volume struct {
+	// Owner is the app owner.
+	Owner string `json:"owner,omitempty"`
+	// App is the app the tls settings apply to and cannot be updated.
+	App string `json:"app,omitempty"`
+	// Created is the time that the volume was created and cannot be updated.
+	Created string `json:"created,omitempty"`
+	// Updated is the last time the TLS settings was changed and cannot be updated.
+	Updated string `json:"updated,omitempty"`
+	// UUID is a unique string reflecting the volume in its current state.
+	// It changes every time the volume is changed and cannot be updated.
+	UUID string `json:"uuid,omitempty"`
+	// Volume's name
+	Name string `json:"name,omitempty"`
+	//Volume's size
+	Size string `json:"size,omitempty"`
+	// mount application's path
+	Path map[string]interface{} `json:"path,omitempty"`
+}
+
+type Volumes []Volume
diff --git a/apps/apps.go b/apps/apps.go
index 972da4b..5343492 100644
--- a/apps/apps.go
+++ b/apps/apps.go
@@ -112,8 +112,11 @@ func Logs(c *drycc.Client, appID string, lines int) (string, error) {
 
 // Run a one-time command in your app. This will start a kubernetes job with the
 // same container image and environment as the rest of the app.
-func Run(c *drycc.Client, appID string, command string) (api.AppRunResponse, error) {
-	req := api.AppRunRequest{Command: command}
+func Run(c *drycc.Client, appID string, command string, volumes map[string]interface{}) (api.AppRunResponse, error) {
+	req := api.AppRunRequest{
+		Command: command,
+		Volumes: volumes,
+	}
 	body, err := json.Marshal(req)
 
 	if err != nil {
diff --git a/apps/apps_test.go b/apps/apps_test.go
index 9d8e6ec..4c037cf 100644
--- a/apps/apps_test.go
+++ b/apps/apps_test.go
@@ -252,7 +252,7 @@ func TestAppsRun(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	actual, err := Run(drycc, "example-go", "echo hi")
+	actual, err := Run(drycc, "example-go", "echo hi", nil)
 
 	if err != nil {
 		t.Fatal(err)
diff --git a/volumes/volumes.go b/volumes/volumes.go
new file mode 100644
index 0000000..6156fa6
--- /dev/null
+++ b/volumes/volumes.go
@@ -0,0 +1,81 @@
+// Package config provides methods for managing configuration of apps.
+package volumes
+
+import (
+	"encoding/json"
+	"fmt"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+)
+
+// List list an app's volumes.
+func List(c *drycc.Client, appID string, results int) (api.Volumes, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/volumes/", appID)
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return []api.Volume{}, -1, reqErr
+	}
+	var volumes []api.Volume
+	if err := json.Unmarshal([]byte(body), &volumes); err != nil {
+		return []api.Volume{}, -1, err
+	}
+	return volumes, count, reqErr
+}
+
+// Create create an app's Volume.
+func Create(c *drycc.Client, appID string, volume api.Volume) (api.Volume, error) {
+	body, err := json.Marshal(volume)
+	if err != nil {
+		return api.Volume{}, err
+	}
+	u := fmt.Sprintf("/v2/apps/%s/volumes/", appID)
+	res, reqErr := c.Request("POST", u, body)
+	if reqErr != nil {
+		return api.Volume{}, reqErr
+	}
+	defer res.Body.Close()
+	newVolume := api.Volume{}
+	if err = json.NewDecoder(res.Body).Decode(&newVolume); err != nil {
+		return api.Volume{}, err
+	}
+	return newVolume, reqErr
+}
+
+// Delete delete an app's Volume.
+func Delete(c *drycc.Client, appID string, name string) error {
+	u := fmt.Sprintf("/v2/apps/%s/volumes/%s/", appID, name)
+	res, err := c.Request("DELETE", u, nil)
+	if err == nil {
+		res.Body.Close()
+	}
+	return err
+}
+
+// Mount mount an app's volume and creates a new release.
+// This is a patching operation, which means when you call Mount() with an api.Volumes:
+//
+//    - If the variable does not exist, it will be set.
+//    - If the variable exists, it will be overwritten.
+//    - If the variable is set to nil, it will be unmount.
+//    - If the variable was ignored in the api.Volumes, it will remain unchanged.
+//
+// Calling Mount() with an empty api.Volume will return a drycc.ErrConflict.
+// Trying to Unmount a key that does not exist returns a drycc.ErrUnprocessable.
+func Mount(c *drycc.Client, appID string, name string, volume api.Volume) (api.Volume, error) {
+	body, err := json.Marshal(volume)
+	if err != nil {
+		return api.Volume{}, err
+	}
+	u := fmt.Sprintf("/v2/apps/%s/volumes/%s/path/", appID, name)
+	res, reqErr := c.Request("PATCH", u, body)
+	if reqErr != nil {
+		return api.Volume{}, reqErr
+	}
+	defer res.Body.Close()
+	newVolume := api.Volume{}
+	if err = json.NewDecoder(res.Body).Decode(&newVolume); err != nil {
+		return api.Volume{}, err
+	}
+	return newVolume, reqErr
+}
diff --git a/volumes/volumes_test.go b/volumes/volumes_test.go
new file mode 100644
index 0000000..b2708a5
--- /dev/null
+++ b/volumes/volumes_test.go
@@ -0,0 +1,337 @@
+package volumes
+
+import (
+	"fmt"
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+)
+
+const domainCreateExpected string = `{"name":"myvolume","size":"500M"}`
+
+const volumeCreateFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-go",
+	"name": "myvolume",
+	"size": "500M",
+	"path": {},
+	"created": "2020-08-26T00:00:00UTC",
+	"updated": "2020-08-26T00:00:00UTC"
+}
+`
+
+const volumesFixture string = `
+{
+    "count": 1,
+    "next": null,
+    "previous": null,
+    "results": [
+		{
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			"owner": "test",
+			"app": "example-go",
+			"name": "myvolume",
+			"size": "500M",
+			"path": {},
+			"created": "2020-08-26T00:00:00UTC",
+			"updated": "2020-08-26T00:00:00UTC"
+		}
+    ]
+}
+`
+
+const volumeMountFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-go",
+	"name": "myvolume",
+	"size": "500M",
+	"path": {
+		"cmd":  "/data/cmd1",
+		"web": "/data/web1"
+	},
+	"created": "2020-08-26T00:00:00UTC",
+	"updated": "2020-08-26T00:00:00UTC"
+}
+`
+
+const volumeUnmountFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "unmount-test",
+	"name": "myvolume",
+	"size": "500M",
+	"path": {},
+	"created": "2020-08-26T00:00:00UTC",
+	"updated": "2020-08-26T00:00:00UTC"
+}
+`
+
+const volumeMountExpected string = `{"path":{"cmd":"/data/cmd1","web":"/data/web1"}}`
+const volumeUnmountExpected string = `{"path":{"cmd":null,"web":null}}`
+
+type fakeHTTPServer struct{}
+
+func (f *fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
+	res.Header().Add("DRYCC_API_VERSION", drycc.APIVersion)
+
+	// Create
+	if req.URL.Path == "/v2/apps/example-go/volumes/" && 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) != domainCreateExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", domainCreateExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+
+		res.WriteHeader(http.StatusCreated)
+		res.Write([]byte(volumeCreateFixture))
+		return
+	}
+
+	// Delete
+	if req.URL.Path == "/v2/apps/example-go/volumes/myvolume/" && req.Method == "DELETE" {
+		res.WriteHeader(http.StatusNoContent)
+		//res.Write([]byte(volumeMountFixture))
+		return
+	}
+
+	// List
+	if req.URL.Path == "/v2/apps/example-go/volumes/" && req.Method == "GET" {
+		res.Write([]byte(volumesFixture))
+		return
+	}
+
+	//　Mount
+	if req.URL.Path == "/v2/apps/example-go/volumes/myvolume/path/" && req.Method == "PATCH" {
+		body, err := ioutil.ReadAll(req.Body)
+
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != volumeMountExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", volumeMountExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(volumeMountFixture))
+		return
+	}
+
+	//　Unmount
+	if req.URL.Path == "/v2/apps/unmount-test/volumes/myvolume/path/" && req.Method == "PATCH" {
+		body, err := ioutil.ReadAll(req.Body)
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != volumeUnmountExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", volumeUnmountExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(volumeUnmountFixture))
+		return
+	}
+	fmt.Printf("Unrecognized URL %s\n", req.URL)
+	res.WriteHeader(http.StatusNotFound)
+	res.Write(nil)
+}
+
+func TestVolumesCreate(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Volume{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-go",
+		Name:    "myvolume",
+		Size:    "500M",
+		Path:    map[string]interface{}{},
+		Created: "2020-08-26T00:00:00UTC",
+		Updated: "2020-08-26T00:00:00UTC",
+	}
+
+	handler := fakeHTTPServer{}
+	server := httptest.NewServer(&handler)
+	defer server.Close()
+
+	drycc, err := drycc.New(false, server.URL, "abc")
+	if err != nil {
+		t.Fatal(err)
+	}
+	volume := api.Volume{
+		Name: "myvolume",
+		Size: "500M",
+	}
+	actual, err := Create(drycc, "example-go", volume)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", expected, actual))
+	}
+}
+
+func TestVolumesDelete(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", "myvolume"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestVolumesList(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Volumes{
+		{
+			UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			App:     "example-go",
+			Owner:   "test",
+			Name:    "myvolume",
+			Path:    map[string]interface{}{},
+			Size:    "500M",
+			Created: "2020-08-26T00:00:00UTC",
+			Updated: "2020-08-26T00:00:00UTC",
+		},
+	}
+
+	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 TestVolumeMount(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)
+	}
+
+	expected := api.Volume{
+		Name:  "myvolume",
+		Owner: "test",
+		App:   "example-go",
+		Path: map[string]interface{}{
+			"cmd": "/data/cmd1",
+			"web": "/data/web1",
+		},
+		Size:    "500M",
+		Created: "2020-08-26T00:00:00UTC",
+		Updated: "2020-08-26T00:00:00UTC",
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	}
+
+	volumeVars := api.Volume{
+		Path: map[string]interface{}{
+			"cmd": "/data/cmd1",
+			"web": "/data/web1",
+		},
+	}
+	actual, err := Mount(drycc, "example-go", "myvolume", volumeVars)
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %v, Got %v", expected, actual)
+	}
+}
+
+func TestVolumeUnmount(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)
+	}
+
+	expected := api.Volume{
+		Name:    "myvolume",
+		Owner:   "test",
+		App:     "unmount-test",
+		Path:    map[string]interface{}{},
+		Size:    "500M",
+		Created: "2020-08-26T00:00:00UTC",
+		Updated: "2020-08-26T00:00:00UTC",
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	}
+
+	volumeVars := api.Volume{
+		Path: map[string]interface{}{
+			"cmd": nil,
+			"web": nil,
+		},
+	}
+	actual, err := Mount(drycc, "unmount-test", "myvolume", volumeVars)
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %v, Got %v", expected, actual)
+	}
+}
