From fc40bfeaac0df04691bebf06aad22c6b3a5bddf0 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Tue, 12 Nov 2024 18:10:58 +0800
Subject: [PATCH] feat(controller-sdk-go): config add ValuesRefs field

---
 api/config.go         |  38 ++++--
 api/pts.go            |   1 +
 api/pts_test.go       |   6 +-
 config/config.go      |  19 +++
 config/config_test.go | 264 +++++++++++++++++++++++++++++++-----------
 hooks/hooks_test.go   |  43 +++++--
 pts/pts.go            |  15 +++
 pts/pts_test.go       |  44 +++++++
 8 files changed, 338 insertions(+), 92 deletions(-)

diff --git a/api/config.go b/api/config.go
index fc129a0..e5322ae 100644
--- a/api/config.go
+++ b/api/config.go
@@ -9,19 +9,39 @@ import (
 // ConfigTags is the key, value for tag
 type ConfigTags map[string]interface{}
 
-// ConfigValues is the key, value for env
-type ConfigValues map[string]interface{}
+// ConfigValues value for env
+type KV struct {
+	Name  string      `json:"name,omitempty"`
+	Value interface{} `json:"value,omitempty"`
+}
+
+type ConfigValue struct {
+	Ptype string `json:"ptype,omitempty"`
+	Group string `json:"group,omitempty"`
+	KV
+}
+
+type PtypeValue struct {
+	Env []KV     `json:"env,omitempty"`
+	Ref []string `json:"ref,omitempty"`
+}
+
+type ConfigInfo struct {
+	Ptype map[string]PtypeValue `json:"ptype,omitempty"`
+	Group map[string][]KV       `json:"group,omitempty"`
+}
+
+// ValuesRefs is the key, value for refs
+type ValuesRefs map[string][]string
 
 // ConfigSet is the definition of POST /v2/apps/<app id>/config/.
 type ConfigSet struct {
-	Values      ConfigValues            `json:"values"`
-	TypedValues map[string]ConfigValues `json:"typed_values"`
+	Values []ConfigValue `json:"values"`
 }
 
 // ConfigUnset is the definition of POST /v2/apps/<app id>/config/.
 type ConfigUnset struct {
-	Values      ConfigValues            `json:"values"`
-	TypedValues map[string]ConfigValues `json:"typed_values"`
+	Values []ConfigValue `json:"values"`
 }
 
 // Config is the structure of an app's config.
@@ -31,9 +51,9 @@ type Config struct {
 	// App is the app name. It cannot be updated at all right now.
 	App string `json:"app,omitempty"`
 	// Values are exposed as environment variables to the app.
-	Values ConfigValues `json:"values,omitempty"`
+	Values []ConfigValue `json:"values,omitempty"`
 	// Typed values are exposed as environment variables to the app.
-	TypedValues map[string]ConfigValues `json:"typed_values,omitempty"`
+	ValuesRefs ValuesRefs `json:"values_refs,omitempty"`
 	// Limits is used to set process resources limits. The key is the process name
 	// and the value is a limit plan. Ex: std1.xlarge.c1m1
 	Limits map[string]interface{} `json:"limits,omitempty"`
@@ -46,7 +66,7 @@ type Config struct {
 	Tags map[string]ConfigTags `json:"tags,omitempty"`
 	// Registry is a key-value pair to provide authentication for container registries.
 	// The key is the username and the value is the password.
-	Registry map[string]interface{} `json:"registry,omitempty"`
+	Registry map[string]map[string]interface{} `json:"registry,omitempty"`
 	// Created is the time that the application was created and cannot be updated.
 	Created string `json:"created,omitempty"`
 	// Updated is the last time the configuration was changed and cannot be updated.
diff --git a/api/pts.go b/api/pts.go
index 9451cdd..2357d8f 100644
--- a/api/pts.go
+++ b/api/pts.go
@@ -8,6 +8,7 @@ type Ptype struct {
 	UpToDate          int    `json:"up_to_date"`
 	AvailableReplicas int    `json:"available_replicas"`
 	Started           string `json:"started"`
+	Garbage           bool   `json:"garbage"`
 }
 
 // Ptypes defines a collection of app Ptypes.
diff --git a/api/pts_test.go b/api/pts_test.go
index 5db2aa0..138d0f5 100644
--- a/api/pts_test.go
+++ b/api/pts_test.go
@@ -7,9 +7,9 @@ import (
 
 func TestPtypesSorted(t *testing.T) {
 	ptypes := Ptypes{
-		{"web", "v1", "1/1", 1, 1, ""},
-		{"cronjob", "v1", "1/1", 1, 1, ""},
-		{"sleep", "v1", "1/1", 1, 1, ""},
+		{"web", "v1", "1/1", 1, 1, "", false},
+		{"cronjob", "v1", "1/1", 1, 1, "", false},
+		{"sleep", "v1", "1/1", 1, 1, "", false},
 	}
 
 	sort.Sort(ptypes)
diff --git a/config/config.go b/config/config.go
index 36c0246..296d6cd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -63,3 +63,22 @@ func Set(c *drycc.Client, app string, config api.Config) (api.Config, error) {
 
 	return newConfig, reqErr
 }
+
+// Detach config groups from app ptype.
+func Detach(c *drycc.Client, app string, config api.Config) error {
+	body, err := json.Marshal(config)
+
+	if err != nil {
+		return err
+	}
+
+	u := fmt.Sprintf("/v2/apps/%s/config/", app)
+
+	res, reqErr := c.Request("DELETE", u, body)
+	if reqErr != nil {
+		return reqErr
+	}
+	defer res.Body.Close()
+
+	return reqErr
+}
diff --git a/config/config_test.go b/config/config_test.go
index 63efd68..62155b0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -16,25 +16,29 @@ const configFixtureV1 string = `
 {
     "owner": "test",
     "app": "example-go",
-    "values": {
-      "TEST": "testing",
-      "FOO": "bar"
+    "values": [{
+        "name": "NEW_URL2", 
+        "value": "http://localhost:8080/", 
+        "group": "global"
+      },
+      {
+        "name": "NEW_URL", 
+        "value": "http://localhost:8080", 
+        "ptype": "web"
+      }
+    ],
+    "limits": {
+      "web": "std1.xlarge.c1m1"
     },
-    "typed_values": {
-		"web": {
-		  "PORT": "9000"
-		}
-	},
-	"limits": {
-	  "web": "std1.xlarge.c1m1"
-	},
     "tags": {
-	  "web": {
+      "web": {
         "test": "tests"
-	  }
+      }
     },
     "registry": {
-      "username": "bob"
+      "web": {
+        "username": "bob"
+      }
     },
     "created": "2014-01-01T00:00:00UTC",
     "updated": "2014-01-01T00:00:00UTC",
@@ -46,16 +50,16 @@ const configFixtureV2 string = `
 {
     "owner": "test",
     "app": "example-go",
-    "values": {
-      "TEST": "testing",
-      "FOO": "bar",
-	  "VERSION": "2"
-    },
-    "typed_values": {
-		"web": {
-		  "PORT": "9000"
-		}
+    "values": [{
+		"name":  "NEW_URL2",
+		"value": "http://localhost:8080/",
+		"group": "global"
 	},
+	{
+		"name":  "NEW_URL",
+		"value": "http://localhost:8080",
+		"ptype": "web"
+	}],
 	"limits": {
 	  "web": "std1.xlarge.c1m1"
 	},
@@ -65,7 +69,9 @@ const configFixtureV2 string = `
 	  }
     },
     "registry": {
-      "username": "bob"
+      "web": {
+        "username": "bob"
+      }
     },
     "created": "2014-01-01T00:00:00UTC",
     "updated": "2014-01-01T00:00:00UTC",
@@ -77,8 +83,7 @@ const configUnsetFixture string = `
 {
     "owner": "test",
     "app": "unset-test",
-    "values": {},
-	"typed_values": {"web":{"PORT":null}},
+    "values": [],
     "limits": {},
     "tags": {},
 	"registry": {},
@@ -87,9 +92,24 @@ const configUnsetFixture string = `
     "uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
 }
 `
+const configSetRefsFixture string = `
+{
+    "owner": "test",
+    "app": "setrefs-test",
+    "values": [],
+	"values_refs":{"web":["myconfig1"]},
+    "limits": {},
+    "tags": {},
+	"registry": {},
+    "created": "2014-01-01T00:00:00UTC",
+    "updated": "2014-01-01T00:00:00UTC",
+    "uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf76"
+}
+`
 
-const configSetExpected string = `{"values":{"FOO":"bar","TEST":"testing"},"typed_values":{"web":{"PORT":"9000"}},"limits":{"web":"std1.xlarge.c1m1"},"tags":{"web":{"test":"tests"}},"registry":{"username":"bob"}}`
-const configUnsetExpected string = `{"values":{"FOO":null,"TEST":null},"typed_values":{"web":{"PORT":null}},"limits":{"web":null},"tags":{"web":{"test":null}},"registry":{"username":null}}`
+const configSetExpected string = `{"values":[{"group":"global","name":"NEW_URL2","value":"http://localhost:8080/"},{"ptype":"web","name":"NEW_URL","value":"http://localhost:8080"}],"limits":{"web":"std1.xlarge.c1m1"},"tags":{"web":{"test":"tests"}},"registry":{"web":{"username":"bob"}}}`
+const configUnsetExpected string = `{"values":[{"group":"global","name":"TEST","value":""}],"limits":{"web":null},"tags":{"web":{"test":null}},"registry":{"web":{"username":null}}}`
+const configSetRefsExpected string = `{"values_refs":{"web":["myconfig1"]}}`
 
 type fakeHTTPServer struct{}
 
@@ -138,6 +158,27 @@ func (f *fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 		return
 	}
 
+	if req.URL.Path == "/v2/apps/setrefs-test/config/" && 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) != configSetRefsExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", configSetRefsExpected, body)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+			return
+		}
+
+		res.WriteHeader(http.StatusCreated)
+		res.Write([]byte(configSetRefsFixture))
+		return
+	}
+
 	if req.URL.Path == "/v2/apps/example-go/config/" && req.Method == "GET" {
 
 		if req.URL.RawQuery == "version=v2" {
@@ -168,12 +209,21 @@ func TestConfigSet(t *testing.T) {
 	expected := api.Config{
 		Owner: "test",
 		App:   "example-go",
-		Values: api.ConfigValues{
-			"TEST": "testing",
-			"FOO":  "bar",
-		},
-		TypedValues: map[string]api.ConfigValues{
-			"web": {"PORT": "9000"},
+		Values: []api.ConfigValue{
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "NEW_URL2",
+					Value: "http://localhost:8080/",
+				},
+			},
+			{
+				Ptype: "web",
+				KV: api.KV{
+					Name:  "NEW_URL",
+					Value: "http://localhost:8080",
+				},
+			},
 		},
 		Limits: map[string]interface{}{
 			"web": "std1.xlarge.c1m1",
@@ -183,8 +233,10 @@ func TestConfigSet(t *testing.T) {
 				"test": "tests",
 			},
 		},
-		Registry: map[string]interface{}{
-			"username": "bob",
+		Registry: map[string]map[string]interface{}{
+			"web": {
+				"username": "bob",
+			},
 		},
 		Created: "2014-01-01T00:00:00UTC",
 		Updated: "2014-01-01T00:00:00UTC",
@@ -192,12 +244,21 @@ func TestConfigSet(t *testing.T) {
 	}
 
 	configVars := api.Config{
-		Values: api.ConfigValues{
-			"TEST": "testing",
-			"FOO":  "bar",
-		},
-		TypedValues: map[string]api.ConfigValues{
-			"web": {"PORT": "9000"},
+		Values: []api.ConfigValue{
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "NEW_URL2",
+					Value: "http://localhost:8080/",
+				},
+			},
+			{
+				Ptype: "web",
+				KV: api.KV{
+					Name:  "NEW_URL",
+					Value: "http://localhost:8080",
+				},
+			},
 		},
 		Limits: map[string]interface{}{
 			"web": "std1.xlarge.c1m1",
@@ -207,8 +268,10 @@ func TestConfigSet(t *testing.T) {
 				"test": "tests",
 			},
 		},
-		Registry: map[string]interface{}{
-			"username": "bob",
+		Registry: map[string]map[string]interface{}{
+			"web": {
+				"username": "bob",
+			},
 		},
 	}
 
@@ -236,27 +299,26 @@ func TestConfigUnset(t *testing.T) {
 	}
 
 	expected := api.Config{
-		Owner:  "test",
-		App:    "unset-test",
-		Values: map[string]interface{}{},
-		TypedValues: map[string]api.ConfigValues{
-			"web": {"PORT": nil},
-		},
+		Owner:    "test",
+		App:      "unset-test",
+		Values:   []api.ConfigValue{},
 		Limits:   map[string]interface{}{},
 		Tags:     map[string]api.ConfigTags{},
-		Registry: map[string]interface{}{},
+		Registry: map[string]map[string]interface{}{},
 		Created:  "2014-01-01T00:00:00UTC",
 		Updated:  "2014-01-01T00:00:00UTC",
 		UUID:     "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75",
 	}
 
 	configVars := api.Config{
-		Values: map[string]interface{}{
-			"TEST": nil,
-			"FOO":  nil,
-		},
-		TypedValues: map[string]api.ConfigValues{
-			"web": {"PORT": nil},
+		Values: []api.ConfigValue{
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "TEST",
+					Value: "",
+				},
+			},
 		},
 		Limits: map[string]interface{}{
 			"web": nil,
@@ -264,8 +326,10 @@ func TestConfigUnset(t *testing.T) {
 		Tags: map[string]api.ConfigTags{
 			"web": {"test": nil},
 		},
-		Registry: map[string]interface{}{
-			"username": nil,
+		Registry: map[string]map[string]interface{}{
+			"web": {
+				"username": nil,
+			},
 		},
 	}
 
@@ -295,12 +359,21 @@ func TestConfigList(t *testing.T) {
 	expected := api.Config{
 		Owner: "test",
 		App:   "example-go",
-		Values: map[string]interface{}{
-			"TEST": "testing",
-			"FOO":  "bar",
-		},
-		TypedValues: map[string]api.ConfigValues{
-			"web": {"PORT": "9000"},
+		Values: []api.ConfigValue{
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "NEW_URL2",
+					Value: "http://localhost:8080/",
+				},
+			},
+			{
+				Ptype: "web",
+				KV: api.KV{
+					Name:  "NEW_URL",
+					Value: "http://localhost:8080",
+				},
+			},
 		},
 		Limits: map[string]interface{}{
 			"web": "std1.xlarge.c1m1",
@@ -308,8 +381,10 @@ func TestConfigList(t *testing.T) {
 		Tags: map[string]api.ConfigTags{
 			"web": {"test": "tests"},
 		},
-		Registry: map[string]interface{}{
-			"username": "bob",
+		Registry: map[string]map[string]interface{}{
+			"web": {
+				"username": "bob",
+			},
 		},
 		Created: "2014-01-01T00:00:00UTC",
 		Updated: "2014-01-01T00:00:00UTC",
@@ -330,11 +405,60 @@ func TestConfigList(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
-	if version, ok := actual.Values["VERSION"]; ok {
-		if !reflect.DeepEqual(version, "2") {
-			t.Errorf("Expected %v, Got %v", "2", version)
+	url := actual.Values[0].Value
+	if url != "" {
+		if !reflect.DeepEqual(url, "http://localhost:8080/") {
+			t.Errorf("Expected %v, Got %v", "http://localhost:8080/", url)
 		}
 	} else {
 		t.Errorf("version not found %v", actual)
 	}
 }
+
+func TestConfigRefs(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.Config{
+		Owner:  "test",
+		App:    "setrefs-test",
+		Values: []api.ConfigValue{},
+		ValuesRefs: map[string][]string{
+			"web": {
+				"myconfig1",
+			},
+		},
+		Limits:   map[string]interface{}{},
+		Tags:     map[string]api.ConfigTags{},
+		Registry: map[string]map[string]interface{}{},
+		Created:  "2014-01-01T00:00:00UTC",
+		Updated:  "2014-01-01T00:00:00UTC",
+		UUID:     "de1bf5b5-4a72-4f94-a10c-d2a3741cdf76",
+	}
+
+	configVars := api.Config{
+		ValuesRefs: map[string][]string{
+			"web": {
+				"myconfig1",
+			},
+		},
+	}
+
+	actual, err := Set(drycc, "setrefs-test", configVars)
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %v, Got %v", expected, actual)
+	}
+}
diff --git a/hooks/hooks_test.go b/hooks/hooks_test.go
index b2b092c..1633ce0 100644
--- a/hooks/hooks_test.go
+++ b/hooks/hooks_test.go
@@ -22,10 +22,17 @@ const configFixture string = `
 {
 	"owner": "test",
 	"app": "example-go",
-	"values": {
-		"TEST": "testing",
-		"FOO": "bar"
-	},
+	"values":[{
+		"group": "global",
+		"name":  "TEST",
+		"value": "testing"
+	  },
+	  {
+		"group": "global",
+		"name":  "FOO",
+		"value": "bar"
+	  }
+	],
 	"Limits": {
 		"web": "std1.xlarge.c1m1"
 	},
@@ -35,7 +42,9 @@ const configFixture string = `
 		}
 	},
 	"registry": {
-		"username": "bob"
+		"web": {
+			"username": "bob"
+		}
 	},
 	"created": "2014-01-01T00:00:00UTC",
 	"updated": "2014-01-01T00:00:00UTC",
@@ -157,9 +166,21 @@ func TestConfigHook(t *testing.T) {
 	expected := api.Config{
 		Owner: "test",
 		App:   "example-go",
-		Values: map[string]interface{}{
-			"TEST": "testing",
-			"FOO":  "bar",
+		Values: []api.ConfigValue{
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "TEST",
+					Value: "testing",
+				},
+			},
+			{
+				Group: "global",
+				KV: api.KV{
+					Name:  "FOO",
+					Value: "bar",
+				},
+			},
 		},
 		Limits: map[string]interface{}{
 			"web": "std1.xlarge.c1m1",
@@ -169,8 +190,10 @@ func TestConfigHook(t *testing.T) {
 				"test": "tests",
 			},
 		},
-		Registry: map[string]interface{}{
-			"username": "bob",
+		Registry: map[string]map[string]interface{}{
+			"web": {
+				"username": "bob",
+			},
 		},
 		Created: "2014-01-01T00:00:00UTC",
 		Updated: "2014-01-01T00:00:00UTC",
diff --git a/pts/pts.go b/pts/pts.go
index 35ea816..16d8432 100644
--- a/pts/pts.go
+++ b/pts/pts.go
@@ -78,6 +78,21 @@ func Restart(c *drycc.Client, appID string, targets map[string]string) error {
 	return err
 }
 
+// Clean clean an app's processes.
+func Clean(c *drycc.Client, appID string, targets map[string]string) error {
+	u := fmt.Sprintf("/v2/apps/%s/ptypes/clean/", 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
diff --git a/pts/pts_test.go b/pts/pts_test.go
index ba73b00..b88daf4 100644
--- a/pts/pts_test.go
+++ b/pts/pts_test.go
@@ -67,6 +67,8 @@ const scaleExpected string = `{"web":2}`
 
 const restartExpected string = `{"types":"web,worker"}`
 
+const cleanExpected string = `{"types":"task,worker"}`
+
 type fakeHTTPServer struct{}
 
 func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
@@ -119,6 +121,25 @@ func (fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
 		res.Write(nil)
 		return
 	}
+
+	if req.URL.Path == "/v2/apps/example-go/ptypes/clean/" && 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) != cleanExpected {
+			fmt.Printf("Expected '%s', Got '%s'\n", cleanExpected, 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)
 	res.Write(nil)
@@ -247,3 +268,26 @@ func TestScale(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+func TestAppsClean(t *testing.T) {
+	t.Parallel()
+
+	types := map[string]string{
+		"types": "task,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 = Clean(drycc, "example-go", types)
+
+	if err != nil {
+		t.Error(err)
+	}
+}
