From 029b2047808d6a037c7cbcb0baba8800b3f26367 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo@zhumengyuan.com>
Date: Tue, 8 Sep 2020 17:44:24 +0800
Subject: [PATCH] feat(controller):add drycc resource cmd

---
 api/apps.go                 |   2 +-
 api/resource.go             |  35 +++
 resources/resources.go      | 109 ++++++++
 resources/resources_test.go | 481 ++++++++++++++++++++++++++++++++++++
 volumes/volumes_test.go     |   6 +-
 5 files changed, 629 insertions(+), 4 deletions(-)
 create mode 100644 api/resource.go
 create mode 100644 resources/resources.go
 create mode 100644 resources/resources_test.go

diff --git a/api/apps.go b/api/apps.go
index ff9a762..6604f34 100644
--- a/api/apps.go
+++ b/api/apps.go
@@ -28,7 +28,7 @@ type AppUpdateRequest struct {
 
 // AppRunRequest is the definition of POST /v2/apps/<app id>/run.
 type AppRunRequest struct {
-	Command string `json:"command"`
+	Command string                 `json:"command"`
 	Volumes map[string]interface{} `json:"volumes,omitempty"`
 }
 
diff --git a/api/resource.go b/api/resource.go
new file mode 100644
index 0000000..5d0e9ce
--- /dev/null
+++ b/api/resource.go
@@ -0,0 +1,35 @@
+package api
+
+// Binding is the definition of PATCH /v2/apps/<app_id>/resources/<name>/binding/.
+type Binding struct {
+	BindAction string `json:"bind_action,omitempty"`
+}
+
+// Resource is the structure of an app's resource.
+type Resource 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 resource 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 resource in its current state.
+	// It changes every time the resource is changed and cannot be updated.
+	UUID string `json:"uuid,omitempty"`
+	// Resource's name
+	Name string `json:"name,omitempty"`
+	// Resource's Plan
+	Plan string `json:"plan,omitempty"`
+	// Resource connet info
+	Data map[string]interface{} `json:"data,omitempty"`
+	// Resource's status
+	Status string `json:"status,omitempty"`
+	// Resource's binding status
+	Binding string `json:"binding,omitempty"`
+	// Resource Options
+	Options map[string]interface{} `json:"options,omitempty"`
+}
+
+type Resources []Resource
diff --git a/resources/resources.go b/resources/resources.go
new file mode 100644
index 0000000..72da245
--- /dev/null
+++ b/resources/resources.go
@@ -0,0 +1,109 @@
+// Package config provides methods for managing configuration of apps.
+package resources
+
+import (
+	"encoding/json"
+	"fmt"
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+)
+
+// List list an app's resources.
+func List(c *drycc.Client, appID string, results int) (api.Resources, int, error) {
+	u := fmt.Sprintf("/v2/apps/%s/resources/", appID)
+	body, count, reqErr := c.LimitedRequest(u, results)
+	if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
+		return []api.Resource{}, -1, reqErr
+	}
+	var resources []api.Resource
+	if err := json.Unmarshal([]byte(body), &resources); err != nil {
+		return []api.Resource{}, -1, err
+	}
+	return resources, count, reqErr
+}
+
+// Create create an app's resource.
+func Create(c *drycc.Client, appID string, resource api.Resource) (api.Resource, error) {
+	body, err := json.Marshal(resource)
+	if err != nil {
+		return api.Resource{}, err
+	}
+	u := fmt.Sprintf("/v2/apps/%s/resources/", appID)
+	res, reqErr := c.Request("POST", u, body)
+	if reqErr != nil {
+		return api.Resource{}, reqErr
+	}
+	defer res.Body.Close()
+	newResource := api.Resource{}
+	if err = json.NewDecoder(res.Body).Decode(&newResource); err != nil {
+		return api.Resource{}, err
+	}
+	return newResource, reqErr
+}
+
+// Get retrieves information about a resource
+func Get(c *drycc.Client, appID string, name string) (api.Resource, error) {
+	u := fmt.Sprintf("/v2/apps/%s/resources/%s/", appID, name)
+	res, reqErr := c.Request("GET", u, nil)
+	if reqErr != nil {
+		return api.Resource{}, reqErr
+	}
+	defer res.Body.Close()
+	resResource := api.Resource{}
+	if err := json.NewDecoder(res.Body).Decode(&resResource); err != nil {
+		return api.Resource{}, err
+	}
+	return resResource, reqErr
+}
+
+// Delete delete an app's resource.
+func Delete(c *drycc.Client, appID string, name string) error {
+	u := fmt.Sprintf("/v2/apps/%s/resources/%s/", appID, name)
+	res, err := c.Request("DELETE", u, nil)
+	if err == nil {
+		res.Body.Close()
+	}
+	return err
+}
+
+// Put update resource
+func Put(c *drycc.Client, appID string, name string, resource api.Resource) (api.Resource, error) {
+	body, err := json.Marshal(resource)
+	if err != nil {
+		return api.Resource{}, err
+	}
+	u := fmt.Sprintf("/v2/apps/%s/resources/%s/", appID, name)
+	res, reqErr := c.Request("PUT", u, body)
+	if reqErr != nil {
+		return api.Resource{}, reqErr
+	}
+	defer res.Body.Close()
+	newResource := api.Resource{}
+	if err = json.NewDecoder(res.Body).Decode(&newResource); err != nil {
+		return api.Resource{}, err
+	}
+	return newResource, reqErr
+}
+
+// Binding servicebinding binding with a serviceinstance
+func Binding(c *drycc.Client, appID string, name string, resource api.Binding) (api.Resource, error) {
+	body, err := json.Marshal(resource)
+	if err != nil {
+		return api.Resource{}, err
+	}
+	u := fmt.Sprintf("/v2/apps/%s/resources/%s/binding/", appID, name)
+	res, reqErr := c.Request("PATCH", u, body)
+
+	if reqErr != nil {
+		return api.Resource{}, reqErr
+	}
+	defer res.Body.Close()
+
+	//return api.Resource{}, reqErr
+
+	newResource := api.Resource{}
+	if err = json.NewDecoder(res.Body).Decode(&newResource); err != nil {
+		return api.Resource{}, err
+	}
+	return newResource, reqErr
+}
diff --git a/resources/resources_test.go b/resources/resources_test.go
new file mode 100644
index 0000000..457b745
--- /dev/null
+++ b/resources/resources_test.go
@@ -0,0 +1,481 @@
+package resources
+
+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 resourceCreateExpected string = `{"name":"mysql","plan":"mysql:5.6"}`
+
+const resourcePutExpected string = `{"plan":"mysql:5.7"}`
+
+const resourceCreateFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-go",
+	"name": "mysql",
+	"plan": "mysql:5.6",
+	"data": {},
+	"options": {},
+	"status": null,
+	"binding": null,
+	"created": "2020-09-08T00:00:00UTC",
+	"updated": "2020-09-08T00:00:00UTC"
+}
+`
+
+const resourcePutFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-go",
+	"name": "mysql",
+	"plan": "mysql:5.7",
+	"data": {},
+	"options": {},
+	"status": null,
+	"binding": null,
+	"created": "2020-09-08T00:00:00UTC",
+	"updated": "2020-09-08T00:00:00UTC"
+}
+`
+
+const resourcesFixture string = `
+{
+   "count": 1,
+   "next": null,
+   "previous": null,
+   "results": [
+		{
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			"owner": "test",
+			"app": "example-go",
+			"name": "mysql",
+			"plan": "mysql:5.6",
+			"data": {},
+			"options": {},
+			"status": null,
+			"binding": null,
+			"created": "2020-09-08T00:00:00UTC",
+			"updated": "2020-09-08T00:00:00UTC"
+		}
+   ]
+}
+`
+
+const resourceFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-go",
+	"name": "mysql",
+	"plan": "mysql:5.6",
+	"data": {},
+	"options": {},
+	"status": null,
+	"binding": null,
+	"created": "2020-09-08T00:00:00UTC",
+	"updated": "2020-09-08T00:00:00UTC"
+}
+`
+
+//
+const resourceBindingFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-bind",
+	"name": "mysql",
+	"plan": "mysql:5.6",
+	"data": {},
+	"options": {},
+	"status": null,
+	"binding": null,
+	"created": "2020-09-08T00:00:00UTC",
+	"updated": "2020-09-08T00:00:00UTC"
+}
+`
+
+const resourceUnbindFixture string = `
+{
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+	"owner": "test",
+	"app": "example-unbind",
+	"name": "mysql",
+	"plan": "mysql:5.6",
+	"data": {},
+	"options": {},
+	"status": null,
+	"binding": null,
+	"created": "2020-09-08T00:00:00UTC",
+	"updated": "2020-09-08T00:00:00UTC"
+}
+`
+
+const resourceBindExpected string = `{"bind_action":"bind"}`
+const resourceUnbindExpected string = `{"bind_action":"unbind"}`
+
+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/resources/" && 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) != resourceCreateExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", resourceCreateExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusCreated)
+		res.Write([]byte(resourceCreateFixture))
+		return
+	}
+	// List
+	if req.URL.Path == "/v2/apps/example-go/resources/" && req.Method == "GET" {
+		res.Write([]byte(resourcesFixture))
+		return
+	}
+
+	// Delete
+	if req.URL.Path == "/v2/apps/example-go/resources/mysql/" && req.Method == "DELETE" {
+		res.WriteHeader(http.StatusNoContent)
+		return
+	}
+	// Get
+	if req.URL.Path == "/v2/apps/example-go/resources/mysql/" && req.Method == "GET" {
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(resourceFixture))
+		return
+	}
+	// Put
+	if req.URL.Path == "/v2/apps/example-go/resources/mysql/" && req.Method == "PUT" {
+		body, err := ioutil.ReadAll(req.Body)
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		if string(body) != resourcePutExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", resourcePutExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(resourcePutFixture))
+		return
+	}
+	// Patch bind
+	if req.URL.Path == "/v2/apps/example-bind/resources/mysql/binding/" && 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) != resourceBindExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", resourceBindExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(resourceBindingFixture))
+		return
+	}
+	// Patch unbind
+	if req.URL.Path == "/v2/apps/example-unbind/resources/mysql/binding/" && 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) != resourceUnbindExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", resourceUnbindExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+		res.WriteHeader(http.StatusOK)
+		res.Write([]byte(resourceUnbindFixture))
+		return
+	}
+	fmt.Printf("Unrecognized URL %s\n", req.URL)
+	res.WriteHeader(http.StatusNotFound)
+	res.Write(nil)
+}
+
+func TestResourcesCreate(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Resource{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-go",
+		Name:    "mysql",
+		Plan:    "mysql:5.6",
+		Status:  "",
+		Binding: "",
+		Data:    map[string]interface{}{},
+		Options: map[string]interface{}{},
+		Created: "2020-09-08T00:00:00UTC",
+		Updated: "2020-09-08T00: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)
+	}
+	resource := api.Resource{
+		Name: "mysql",
+		Plan: "mysql:5.6",
+	}
+	actual, err := Create(drycc, "example-go", resource)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", expected, actual))
+	}
+}
+
+//
+func TestResourcesList(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Resources{
+		{
+			UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+			Owner:   "test",
+			App:     "example-go",
+			Name:    "mysql",
+			Plan:    "mysql:5.6",
+			Status:  "",
+			Binding: "",
+			Data:    map[string]interface{}{},
+			Options: map[string]interface{}{},
+			Created: "2020-09-08T00:00:00UTC",
+			Updated: "2020-09-08T00: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 TestResourceDelete(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", "mysql"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestResourceGet(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Resource{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-go",
+		Name:    "mysql",
+		Plan:    "mysql:5.6",
+		Status:  "",
+		Binding: "",
+		Data:    map[string]interface{}{},
+		Options: map[string]interface{}{},
+		Created: "2020-09-08T00:00:00UTC",
+		Updated: "2020-09-08T00: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 := Get(drycc, "example-go", "mysql")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", expected, actual))
+	}
+}
+
+func TestResourcePut(t *testing.T) {
+	t.Parallel()
+
+	expected := api.Resource{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-go",
+		Name:    "mysql",
+		Plan:    "mysql:5.7",
+		Status:  "",
+		Binding: "",
+		Data:    map[string]interface{}{},
+		Options: map[string]interface{}{},
+		Created: "2020-09-08T00:00:00UTC",
+		Updated: "2020-09-08T00: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)
+	}
+	resource := api.Resource{
+		Plan: "mysql:5.7",
+	}
+	actual, err := Put(drycc, "example-go", "mysql", resource)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Error(fmt.Errorf("Expected %v, Got %v", expected, actual))
+	}
+}
+
+func TestResourceBind(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.Resource{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-bind",
+		Name:    "mysql",
+		Plan:    "mysql:5.6",
+		Status:  "",
+		Binding: "",
+		Data:    map[string]interface{}{},
+		Options: map[string]interface{}{},
+		Created: "2020-09-08T00:00:00UTC",
+		Updated: "2020-09-08T00:00:00UTC",
+	}
+
+	resourceVars := api.Binding{
+		BindAction: "bind",
+	}
+	actual, err := Binding(drycc, "example-bind", "mysql", resourceVars)
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %v, Got %v", expected, actual)
+	}
+}
+
+func TestResourceUnbind(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.Resource{
+		UUID:    "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
+		Owner:   "test",
+		App:     "example-unbind",
+		Name:    "mysql",
+		Plan:    "mysql:5.6",
+		Status:  "",
+		Binding: "",
+		Data:    map[string]interface{}{},
+		Options: map[string]interface{}{},
+		Created: "2020-09-08T00:00:00UTC",
+		Updated: "2020-09-08T00:00:00UTC",
+	}
+
+	resourceVars := api.Binding{
+		BindAction: "unbind",
+	}
+	actual, err := Binding(drycc, "example-unbind", "mysql", resourceVars)
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %v, Got %v", expected, actual)
+	}
+}
diff --git a/volumes/volumes_test.go b/volumes/volumes_test.go
index b2708a5..a743ffa 100644
--- a/volumes/volumes_test.go
+++ b/volumes/volumes_test.go
@@ -11,7 +11,7 @@ import (
 	"testing"
 )
 
-const domainCreateExpected string = `{"name":"myvolume","size":"500M"}`
+const volumeCreateExpected string = `{"name":"myvolume","size":"500M"}`
 
 const volumeCreateFixture string = `
 {
@@ -93,8 +93,8 @@ func (f *fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 			res.Write(nil)
 		}
 
-		if string(body) != domainCreateExpected {
-			fmt.Printf("Expected '%s', Got '%s'\n", domainCreateExpected, body)
+		if string(body) != volumeCreateExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", volumeCreateExpected, body)
 			res.WriteHeader(http.StatusInternalServerError)
 			res.Write(nil)
 			return
