You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/10/19 01:52:57 UTC

[apisix-dashboard] branch refactor updated: feat: compatible with PUT method of `admin api` and nodes of upstream (#561)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch refactor
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/refactor by this push:
     new 0bbcc75  feat: compatible with PUT method of `admin api` and nodes of upstream (#561)
0bbcc75 is described below

commit 0bbcc7534e0ab570e7d876291301d79e6b5f93bf
Author: nic-chen <33...@users.noreply.github.com>
AuthorDate: Mon Oct 19 09:52:28 2020 +0800

    feat: compatible with PUT method of `admin api` and nodes of upstream (#561)
    
    * feat: support labels
    
    * feat: compatible with PUT method of `admin api`
    
    * fix mock test fail
    
    * feat: upstream nodes format
    
    * test: add test case
    
    * fix code style
    
    * fix: update schema sync tool
---
 api/build-tools/schema-sync.lua                |  1 +
 api/internal/core/entity/entity.go             | 85 ++++++++++++++++++--------
 api/internal/core/entity/format.go             | 52 ++++++++++++++++
 api/internal/core/entity/format_test.go        | 39 ++++++++++++
 api/internal/core/store/store.go               | 42 ++++++-------
 api/internal/core/store/store_test.go          |  8 +--
 api/internal/handler/consumer/consumer.go      | 10 ++-
 api/internal/handler/consumer/consumer_test.go | 29 +++++++++
 api/internal/handler/route/route.go            | 15 ++++-
 api/internal/handler/route/route_test.go       |  6 +-
 api/internal/handler/service/service.go        | 15 ++++-
 api/internal/handler/ssl/ssl.go                |  6 +-
 api/internal/handler/upstream/upstream.go      | 15 ++++-
 13 files changed, 256 insertions(+), 67 deletions(-)

diff --git a/api/build-tools/schema-sync.lua b/api/build-tools/schema-sync.lua
index 596f6d8..257d220 100644
--- a/api/build-tools/schema-sync.lua
+++ b/api/build-tools/schema-sync.lua
@@ -46,6 +46,7 @@ local fake_module_list = {
     'resty.openidc',
     'resty.random',
     'resty.redis',
+    'resty.rediscluster',
     'resty.signal',
     'resty.string',
 
diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go
index 06f4d31..c15a5d5 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -16,6 +16,12 @@
  */
 package entity
 
+import (
+	"time"
+
+	"github.com/apisix/manager-api/internal/utils"
+)
+
 type BaseInfo struct {
 	ID         string `json:"id"`
 	CreateTime int64  `json:"create_time"`
@@ -26,6 +32,24 @@ func (info *BaseInfo) GetBaseInfo() *BaseInfo {
 	return info
 }
 
+func (info *BaseInfo) Creating() {
+	if info.ID == "" {
+		info.ID = utils.GetFlakeUidStr()
+	}
+	info.CreateTime = time.Now().Unix()
+	info.UpdateTime = time.Now().Unix()
+}
+
+func (info *BaseInfo) Updating(storedInfo *BaseInfo) {
+	info.ID = storedInfo.ID
+	info.CreateTime = storedInfo.CreateTime
+	info.UpdateTime = time.Now().Unix()
+}
+
+type BaseInfoSetter interface {
+	GetBaseInfo() *BaseInfo
+}
+
 type BaseInfoGetter interface {
 	GetBaseInfo() *BaseInfo
 }
@@ -46,10 +70,11 @@ type Route struct {
 	FilterFunc      string                 `json:"filter_func,omitempty"`
 	Script          interface{}            `json:"script,omitempty"`
 	Plugins         map[string]interface{} `json:"plugins,omitempty"`
-	Upstream        interface{}            `json:"upstream,omitempty"`
+	Upstream        *UpstreamDef           `json:"upstream,omitempty"`
 	ServiceID       string                 `json:"service_id,omitempty"`
 	UpstreamID      string                 `json:"upstream_id,omitempty"`
 	ServiceProtocol string                 `json:"service_protocol,omitempty"`
+	Labels          map[string]string      `json:"labels,omitempty"`
 }
 
 // --- structures for upstream start  ---
@@ -112,22 +137,27 @@ type HealthChecker struct {
 	Passive Passive `json:"passive,omitempty"`
 }
 
+type UpstreamDef struct {
+	Nodes           interface{}       `json:"nodes,omitempty"`
+	Retries         int               `json:"retries,omitempty"`
+	Timeout         interface{}       `json:"timeout,omitempty"`
+	K8sInfo         interface{}       `json:"k8s_deployment_info,omitempty"`
+	Type            string            `json:"type,omitempty"`
+	Checks          interface{}       `json:"checks,omitempty"`
+	HashOn          string            `json:"hash_on,omitempty"`
+	Key             string            `json:"key,omitempty"`
+	EnableWebsocket bool              `json:"enable_websocket,omitempty"`
+	PassHost        string            `json:"pass_host,omitempty"`
+	UpstreamHost    string            `json:"upstream_host,omitempty"`
+	Name            string            `json:"name,omitempty"`
+	Desc            string            `json:"desc,omitempty"`
+	ServiceName     string            `json:"service_name,omitempty"`
+	Labels          map[string]string `json:"labels,omitempty"`
+}
+
 type Upstream struct {
 	BaseInfo
-	Nodes           []interface{} `json:"nodes,omitempty"`
-	Retries         int           `json:"retries,omitempty"`
-	Timeout         interface{}   `json:"timeout,omitempty"`
-	K8sInfo         interface{}   `json:"k8s_deployment_info,omitempty"`
-	Type            string        `json:"type,omitempty"`
-	Checks          interface{}   `json:"checks,omitempty"`
-	HashOn          string        `json:"hash_on,omitempty"`
-	Key             string        `json:"key,omitempty"`
-	EnableWebsocket bool          `json:"enable_websocket,omitempty"`
-	PassHost        string        `json:"pass_host,omitempty"`
-	UpstreamHost    string        `json:"upstream_host,omitempty"`
-	Name            string        `json:"name,omitempty"`
-	Desc            string        `json:"desc,omitempty"`
-	ServiceName     string        `json:"service_name,omitempty"`
+	UpstreamDef
 }
 
 type UpstreamNameResponse struct {
@@ -150,30 +180,33 @@ type Consumer struct {
 	Username string                 `json:"username"`
 	Desc     string                 `json:"desc,omitempty"`
 	Plugins  map[string]interface{} `json:"plugins,omitempty"`
+	Labels   map[string]string      `json:"labels,omitempty"`
 }
 
 type SSL struct {
 	BaseInfo
-	Cert          string   `json:"cert,omitempty"`
-	Key           string   `json:"key,omitempty"`
-	Sni           string   `json:"sni,omitempty"`
-	Snis          []string `json:"snis,omitempty"`
-	Certs         []string `json:"certs,omitempty"`
-	Keys          []string `json:"keys,omitempty"`
-	ExpTime       int64    `json:"exptime,omitempty"`
-	Status        int      `json:"status"`
-	ValidityStart int64    `json:"validity_start,omitempty"`
-	ValidityEnd   int64    `json:"validity_end,omitempty"`
+	Cert          string            `json:"cert,omitempty"`
+	Key           string            `json:"key,omitempty"`
+	Sni           string            `json:"sni,omitempty"`
+	Snis          []string          `json:"snis,omitempty"`
+	Certs         []string          `json:"certs,omitempty"`
+	Keys          []string          `json:"keys,omitempty"`
+	ExpTime       int64             `json:"exptime,omitempty"`
+	Status        int               `json:"status"`
+	ValidityStart int64             `json:"validity_start,omitempty"`
+	ValidityEnd   int64             `json:"validity_end,omitempty"`
+	Labels        map[string]string `json:"labels,omitempty"`
 }
 
 type Service struct {
 	BaseInfo
 	Name       string                 `json:"name,omitempty"`
 	Desc       string                 `json:"desc,omitempty"`
-	Upstream   interface{}            `json:"upstream,omitempty"`
+	Upstream   *UpstreamDef           `json:"upstream,omitempty"`
 	UpstreamID string                 `json:"upstream_id,omitempty"`
 	Plugins    map[string]interface{} `json:"plugins,omitempty"`
 	Script     string                 `json:"script,omitempty"`
+	Labels     map[string]string      `json:"labels,omitempty"`
 }
 
 type Script struct {
diff --git a/api/internal/core/entity/format.go b/api/internal/core/entity/format.go
new file mode 100644
index 0000000..0c5d8d1
--- /dev/null
+++ b/api/internal/core/entity/format.go
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package entity
+
+import (
+	"log"
+	"strconv"
+	"strings"
+)
+
+func NodesFormat(obj interface{}) interface{} {
+	if value, ok := obj.(map[string]float64); ok {
+		var nodes []*Node
+		var strArr []string
+		for key, val := range value {
+			node := &Node{}
+			strArr = strings.Split(key, ":")
+			if len(strArr) != 2 {
+				log.Println("length of string array is not 2")
+				return obj
+			}
+
+			port, err := strconv.Atoi(strArr[1])
+			if err != nil {
+				log.Println("parse int fail:", err)
+				return obj
+			}
+
+			node.Host = strArr[0]
+			node.Port = port
+			node.Weight = int(val)
+			nodes = append(nodes, node)
+		}
+		return nodes
+	}
+
+	return obj
+}
diff --git a/api/internal/core/entity/format_test.go b/api/internal/core/entity/format_test.go
new file mode 100644
index 0000000..cc51bf2
--- /dev/null
+++ b/api/internal/core/entity/format_test.go
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package entity
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConsumer(t *testing.T) {
+	nodesStr := `{
+    "127.0.0.1:8080": 1
+  }`
+	nodesMap := map[string]float64{}
+	json.Unmarshal([]byte(nodesStr), &nodesMap)
+	res := NodesFormat(nodesMap)
+	nodes := res.([]*Node)
+
+	assert.Equal(t, 1, len(nodes))
+	assert.Equal(t, "127.0.0.1", nodes[0].Host)
+	assert.Equal(t, 8080, nodes[0].Port)
+	assert.Equal(t, 1, nodes[0].Weight)
+}
diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go
index 8a4b71c..b285183 100644
--- a/api/internal/core/store/store.go
+++ b/api/internal/core/store/store.go
@@ -31,14 +31,13 @@ import (
 
 	"github.com/apisix/manager-api/internal/core/entity"
 	"github.com/apisix/manager-api/internal/core/storage"
-	"github.com/apisix/manager-api/internal/utils"
 )
 
 type Interface interface {
 	Get(key string) (interface{}, error)
 	List(input ListInput) (*ListOutput, error)
 	Create(ctx context.Context, obj interface{}) error
-	Update(ctx context.Context, obj interface{}) error
+	Update(ctx context.Context, obj interface{}, createOnFail bool) error
 	BatchDelete(ctx context.Context, keys []string) error
 }
 
@@ -92,6 +91,9 @@ func (s *GenericStore) Init() error {
 		return err
 	}
 	for i := range ret {
+		if ret[i] == "init_dir" {
+			continue
+		}
 		objPtr, err := s.StringToObjPtr(ret[i])
 		if err != nil {
 			return err
@@ -136,6 +138,7 @@ func (s *GenericStore) Get(key string) (interface{}, error) {
 
 type ListInput struct {
 	Predicate func(obj interface{}) bool
+	Format    func(obj interface{}) interface{}
 	PageSize  int
 	// start from 1
 	PageNumber int
@@ -165,6 +168,9 @@ func (s *GenericStore) List(input ListInput) (*ListOutput, error) {
 		if input.Predicate != nil && !input.Predicate(value) {
 			return true
 		}
+		if input.Format != nil {
+			value = input.Format(value)
+		}
 		ret = append(ret, value)
 		return true
 	})
@@ -223,13 +229,9 @@ func (s *GenericStore) ingestValidate(obj interface{}) (err error) {
 }
 
 func (s *GenericStore) Create(ctx context.Context, obj interface{}) error {
-	if getter, ok := obj.(entity.BaseInfoGetter); ok {
-		info := getter.GetBaseInfo()
-		if info.ID == "" {
-			info.ID = utils.GetFlakeUidStr()
-		}
-		info.CreateTime = time.Now().Unix()
-		info.UpdateTime = time.Now().Unix()
+	if setter, ok := obj.(entity.BaseInfoSetter); ok {
+		info := setter.GetBaseInfo()
+		info.Creating()
 	}
 
 	if err := s.ingestValidate(obj); err != nil {
@@ -256,7 +258,7 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error {
 	return nil
 }
 
-func (s *GenericStore) Update(ctx context.Context, obj interface{}) error {
+func (s *GenericStore) Update(ctx context.Context, obj interface{}, createIfNotExist bool) error {
 	if err := s.ingestValidate(obj); err != nil {
 		return err
 	}
@@ -265,21 +267,19 @@ func (s *GenericStore) Update(ctx context.Context, obj interface{}) error {
 	if key == "" {
 		return fmt.Errorf("key is required")
 	}
-	oldObj, ok := s.cache.Load(key)
+	storedObj, ok := s.cache.Load(key)
 	if !ok {
+		if createIfNotExist {
+			return s.Create(ctx, obj)
+		}
 		return fmt.Errorf("key: %s is not found", key)
 	}
 
-	createTime := int64(0)
-	if oldGetter, ok := oldObj.(entity.BaseInfoGetter); ok {
-		oldInfo := oldGetter.GetBaseInfo()
-		createTime = oldInfo.CreateTime
-	}
-
-	if getter, ok := obj.(entity.BaseInfoGetter); ok {
-		info := getter.GetBaseInfo()
-		info.CreateTime = createTime
-		info.UpdateTime = time.Now().Unix()
+	if setter, ok := obj.(entity.BaseInfoGetter); ok {
+		storedGetter := storedObj.(entity.BaseInfoGetter)
+		storedInfo := storedGetter.GetBaseInfo()
+		info := setter.GetBaseInfo()
+		info.Updating(storedInfo)
 	}
 
 	bs, err := json.Marshal(obj)
diff --git a/api/internal/core/store/store_test.go b/api/internal/core/store/store_test.go
index f7ad5f4..53f6a85 100644
--- a/api/internal/core/store/store_test.go
+++ b/api/internal/core/store/store_test.go
@@ -624,7 +624,7 @@ func TestGenericStore_Update(t *testing.T) {
 				Field2: "test2",
 			},
 			giveCache: map[string]interface{}{
-				"test1": struct{}{},
+				"test1": &TestStruct{},
 			},
 			giveStore: &GenericStore{
 				opt: GenericStoreOption{
@@ -643,7 +643,7 @@ func TestGenericStore_Update(t *testing.T) {
 				Field2: "test2",
 			},
 			giveCache: map[string]interface{}{
-				"test1": struct{}{},
+				"test1": &TestStruct{},
 			},
 			giveStore: &GenericStore{
 				opt: GenericStoreOption{
@@ -664,7 +664,7 @@ func TestGenericStore_Update(t *testing.T) {
 				Field2: "test2",
 			},
 			giveCache: map[string]interface{}{
-				"test2": struct{}{},
+				"test2": &TestStruct{},
 			},
 			giveStore: &GenericStore{
 				opt: GenericStoreOption{
@@ -704,7 +704,7 @@ func TestGenericStore_Update(t *testing.T) {
 		tc.giveStore.Stg = mStorage
 		tc.giveStore.opt.Validator = mValidator
 
-		err := tc.giveStore.Update(context.TODO(), tc.giveObj)
+		err := tc.giveStore.Update(context.TODO(), tc.giveObj, false)
 		assert.True(t, validateCalled, tc.caseDesc)
 		if err != nil {
 			assert.Equal(t, tc.wantErr, err, tc.caseDesc)
diff --git a/api/internal/handler/consumer/consumer.go b/api/internal/handler/consumer/consumer.go
index 1fdf3b3..f00d1d2 100644
--- a/api/internal/handler/consumer/consumer.go
+++ b/api/internal/handler/consumer/consumer.go
@@ -50,6 +50,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 		wrapper.InputType(reflect.TypeOf(entity.Consumer{}))))
 	r.PUT("/apisix/admin/consumers/:username", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+	r.PUT("/apisix/admin/consumers", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.DELETE("/apisix/admin/consumers/:usernames", wgin.Wraps(h.BatchDelete,
 		wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
 }
@@ -117,10 +119,12 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 	if input.ID != "" && input.ID != input.Username {
 		return nil, fmt.Errorf("consumer's id and username must be a same value")
 	}
-	input.Consumer.Username = input.Username
-	input.Consumer.ID = input.Username
+	if input.Username != "" {
+		input.Consumer.Username = input.Username
+	}
+	input.Consumer.ID = input.Consumer.Username
 
-	if err := h.consumerStore.Update(c.Context(), &input.Consumer); err != nil {
+	if err := h.consumerStore.Update(c.Context(), &input.Consumer, true); err != nil {
 		//if not exists, create
 		if err.Error() == fmt.Sprintf("key: %s is not found", input.Username) {
 			if err := h.consumerStore.Create(c.Context(), &input.Consumer); err != nil {
diff --git a/api/internal/handler/consumer/consumer_test.go b/api/internal/handler/consumer/consumer_test.go
index 567c796..22664fa 100644
--- a/api/internal/handler/consumer/consumer_test.go
+++ b/api/internal/handler/consumer/consumer_test.go
@@ -197,4 +197,33 @@ func TestConsumer(t *testing.T) {
 	_, err = handler.Create(ctx)
 	assert.NotNil(t, err)
 
+	//create consumer using Update
+	consumer6 := &UpdateInput{}
+	reqBody = `{
+      "username": "nnn",
+      "plugins": {
+          "limit-count": {
+              "count": 2,
+              "time_window": 60,
+              "rejected_code": 503,
+              "key": "remote_addr"
+          }
+      },
+    "desc": "test description"
+  }`
+	json.Unmarshal([]byte(reqBody), consumer6)
+	ctx.SetInput(consumer6)
+	_, err = handler.Update(ctx)
+	assert.Nil(t, err)
+
+	//sleep
+	time.Sleep(time.Duration(100) * time.Millisecond)
+
+	//delete consumer
+	reqBody = `{"usernames": "nnn"}`
+	json.Unmarshal([]byte(reqBody), inputDel)
+	ctx.SetInput(inputDel)
+	_, err = handler.BatchDelete(ctx)
+	assert.Nil(t, err)
+
 }
diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go
index 0895a6a..adb1954 100644
--- a/api/internal/handler/route/route.go
+++ b/api/internal/handler/route/route.go
@@ -61,8 +61,11 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 		wrapper.InputType(reflect.TypeOf(ListInput{}))))
 	r.POST("/apisix/admin/routes", wgin.Wraps(h.Create,
 		wrapper.InputType(reflect.TypeOf(entity.Route{}))))
+	r.PUT("/apisix/admin/routes", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PUT("/apisix/admin/routes/:id", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+
 	r.DELETE("/apisix/admin/routes/:ids", wgin.Wraps(h.BatchDelete,
 		wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
 
@@ -88,6 +91,9 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) {
 		route.Script = script.(*entity.Script).Script
 	}
 
+	//format
+	route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes)
+
 	return route, nil
 }
 
@@ -129,6 +135,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) {
 			}
 			return true
 		},
+		Format: func(obj interface{}) interface{} {
+			route := obj.(*entity.Route)
+			route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes)
+			return route
+		},
 		PageSize:   input.PageSize,
 		PageNumber: input.PageNumber,
 	})
@@ -261,7 +272,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 			return nil, err
 		}
 		//save original conf
-		if err = h.scriptStore.Update(c.Context(), script); err != nil {
+		if err = h.scriptStore.Update(c.Context(), script, true); err != nil {
 			//if not exists, create
 			if err.Error() == fmt.Sprintf("key: %s is not found", script.ID) {
 				if err := h.scriptStore.Create(c.Context(), script); err != nil {
@@ -273,7 +284,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 		}
 	}
 
-	if err := h.routeStore.Update(c.Context(), &input.Route); err != nil {
+	if err := h.routeStore.Update(c.Context(), &input.Route, true); err != nil {
 		return nil, err
 	}
 
diff --git a/api/internal/handler/route/route_test.go b/api/internal/handler/route/route_test.go
index 611dccc..438a058 100644
--- a/api/internal/handler/route/route_test.go
+++ b/api/internal/handler/route/route_test.go
@@ -799,11 +799,7 @@ func TestRoute(t *testing.T) {
       "methods": ["PUT", "GET"],
       "upstream": {
           "type": "roundrobin",
-          "nodes": [{
-              "host": "www.a.com",
-              "port": 80,
-              "weight": 1
-          }]
+          "nodes": {"www.a.com:80": 1}
       }
   }`
 	json.Unmarshal([]byte(reqBody), route3)
diff --git a/api/internal/handler/service/service.go b/api/internal/handler/service/service.go
index 7ee3c0f..deee899 100644
--- a/api/internal/handler/service/service.go
+++ b/api/internal/handler/service/service.go
@@ -48,6 +48,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 		wrapper.InputType(reflect.TypeOf(ListInput{}))))
 	r.POST("/apisix/admin/services", wgin.Wraps(h.Create,
 		wrapper.InputType(reflect.TypeOf(entity.Service{}))))
+	r.PUT("/apisix/admin/services", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PUT("/apisix/admin/services/:id", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PATCH("/apisix/admin/services/:id", wgin.Wraps(h.Patch,
@@ -67,6 +69,10 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	service := r.(*entity.Service)
+	service.Upstream.Nodes = entity.NodesFormat(service.Upstream.Nodes)
+
 	return r, nil
 }
 
@@ -85,6 +91,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) {
 			}
 			return true
 		},
+		Format: func(obj interface{}) interface{} {
+			service := obj.(*entity.Service)
+			service.Upstream.Nodes = entity.NodesFormat(service.Upstream.Nodes)
+			return service
+		},
 		PageSize:   input.PageSize,
 		PageNumber: input.PageNumber,
 	})
@@ -114,7 +125,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 	input := c.Input().(*UpdateInput)
 	input.Service.ID = input.ID
 
-	if err := h.serviceStore.Update(c.Context(), &input.Service); err != nil {
+	if err := h.serviceStore.Update(c.Context(), &input.Service, true); err != nil {
 		return nil, err
 	}
 
@@ -167,7 +178,7 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
 		return nil, err
 	}
 
-	if err := h.serviceStore.Update(c.Context(), &stored); err != nil {
+	if err := h.serviceStore.Update(c.Context(), &stored, false); err != nil {
 		return nil, err
 	}
 
diff --git a/api/internal/handler/ssl/ssl.go b/api/internal/handler/ssl/ssl.go
index a7f5701..3291846 100644
--- a/api/internal/handler/ssl/ssl.go
+++ b/api/internal/handler/ssl/ssl.go
@@ -55,6 +55,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 		wrapper.InputType(reflect.TypeOf(ListInput{}))))
 	r.POST("/apisix/admin/ssl", wgin.Wraps(h.Create,
 		wrapper.InputType(reflect.TypeOf(entity.SSL{}))))
+	r.PUT("/apisix/admin/ssl", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PUT("/apisix/admin/ssl/:id", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PATCH("/apisix/admin/ssl/:id", wgin.Wraps(h.Patch,
@@ -163,7 +165,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 	}
 
 	ssl.ID = input.ID
-	if err := h.sslStore.Update(c.Context(), ssl); err != nil {
+	if err := h.sslStore.Update(c.Context(), ssl, true); err != nil {
 		return nil, err
 	}
 
@@ -203,7 +205,7 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
 		panic(err)
 	}
 
-	if err := h.sslStore.Update(c.Context(), &stored); err != nil {
+	if err := h.sslStore.Update(c.Context(), &stored, false); err != nil {
 		return nil, err
 	}
 
diff --git a/api/internal/handler/upstream/upstream.go b/api/internal/handler/upstream/upstream.go
index 5071ff2..d7e3365 100644
--- a/api/internal/handler/upstream/upstream.go
+++ b/api/internal/handler/upstream/upstream.go
@@ -49,6 +49,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
 		wrapper.InputType(reflect.TypeOf(ListInput{}))))
 	r.POST("/apisix/admin/upstreams", wgin.Wraps(h.Create,
 		wrapper.InputType(reflect.TypeOf(entity.Upstream{}))))
+	r.PUT("/apisix/admin/upstreams", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PUT("/apisix/admin/upstreams/:id", wgin.Wraps(h.Update,
 		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
 	r.PATCH("/apisix/admin/upstreams/:id", wgin.Wraps(h.Patch,
@@ -71,6 +73,10 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	upstream := r.(*entity.Upstream)
+	upstream.Nodes = entity.NodesFormat(upstream.Nodes)
+
 	return r, nil
 }
 
@@ -89,6 +95,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) {
 			}
 			return true
 		},
+		Format: func(obj interface{}) interface{} {
+			upstream := obj.(*entity.Upstream)
+			upstream.Nodes = entity.NodesFormat(upstream.Nodes)
+			return upstream
+		},
 		PageSize:   input.PageSize,
 		PageNumber: input.PageNumber,
 	})
@@ -118,7 +129,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) {
 	input := c.Input().(*UpdateInput)
 	input.Upstream.ID = input.ID
 
-	if err := h.upstreamStore.Update(c.Context(), &input.Upstream); err != nil {
+	if err := h.upstreamStore.Update(c.Context(), &input.Upstream, true); err != nil {
 		return nil, err
 	}
 
@@ -171,7 +182,7 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
 		return nil, err
 	}
 
-	if err := h.upstreamStore.Update(c.Context(), &stored); err != nil {
+	if err := h.upstreamStore.Update(c.Context(), &stored, false); err != nil {
 		return nil, err
 	}