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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2de2c72  feat: support plugin config (#1509)
2de2c72 is described below

commit 2de2c72cfa8899e98045c3059d281b90cf544404
Author: nic-chen <[email protected]>
AuthorDate: Wed Mar 3 16:50:26 2021 +0800

    feat: support plugin config (#1509)
---
 api/conf/schema.json                               |  14 +
 api/internal/core/entity/entity.go                 |   9 +
 api/internal/core/store/storehub.go                |  29 +-
 api/internal/handler/label/label.go                |  28 +-
 api/internal/handler/label/label_test.go           |  32 +-
 .../handler/plugin_config/plugin_config.go         | 267 ++++++++
 .../handler/plugin_config/plugin_config_test.go    | 715 +++++++++++++++++++++
 api/internal/route.go                              |   2 +
 api/test/e2e/label_test.go                         |  70 ++
 .../plugin_config/plugin_config_suite_test.go      |  36 ++
 .../e2enew/plugin_config/plugin_config_test.go     | 243 +++++++
 11 files changed, 1420 insertions(+), 25 deletions(-)

diff --git a/api/conf/schema.json b/api/conf/schema.json
index 200d9f2..e69c57a 100644
--- a/api/conf/schema.json
+++ b/api/conf/schema.json
@@ -99,6 +99,20 @@
                                                "type": "integer"
                                        }]
                                },
+                               "labels": {
+                                       "description": "key/value pairs to 
specify attributes",
+                                       "maxProperties": 16,
+                                       "patternProperties": {
+                                               ".*": {
+                                                       "description": "value 
of label",
+                                                       "maxLength": 64,
+                                                       "minLength": 1,
+                                                       "pattern": "^\\S+$",
+                                                       "type": "string"
+                                               }
+                                       },
+                                       "type": "object"
+                               },
                                "plugins": {
                                        "type": "object"
                                },
diff --git a/api/internal/core/entity/entity.go 
b/api/internal/core/entity/entity.go
index a0ac8c8..b53fb4f 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -86,6 +86,7 @@ type Route struct {
        Script          interface{}            `json:"script,omitempty"`
        ScriptID        interface{}            `json:"script_id,omitempty"` // 
For debug and optimization(cache), currently same as Route's ID
        Plugins         map[string]interface{} `json:"plugins,omitempty"`
+       PluginConfigID  interface{}            
`json:"plugin_config_id,omitempty"`
        Upstream        *UpstreamDef           `json:"upstream,omitempty"`
        ServiceID       interface{}            `json:"service_id,omitempty"`
        UpstreamID      interface{}            `json:"upstream_id,omitempty"`
@@ -257,3 +258,11 @@ type ServerInfo struct {
        Hostname       string `json:"hostname,omitempty"`
        Version        string `json:"version,omitempty"`
 }
+
+// swagger:model GlobalPlugins
+type PluginConfig struct {
+       BaseInfo
+       Desc    string                 `json:"desc,omitempty" 
validate:"max=256"`
+       Plugins map[string]interface{} `json:"plugins"`
+       Labels  map[string]string      `json:"labels,omitempty"`
+}
diff --git a/api/internal/core/store/storehub.go 
b/api/internal/core/store/storehub.go
index 2af02da..2a96e50 100644
--- a/api/internal/core/store/storehub.go
+++ b/api/internal/core/store/storehub.go
@@ -29,14 +29,15 @@ import (
 type HubKey string
 
 const (
-       HubKeyConsumer   HubKey = "consumer"
-       HubKeyRoute      HubKey = "route"
-       HubKeyService    HubKey = "service"
-       HubKeySsl        HubKey = "ssl"
-       HubKeyUpstream   HubKey = "upstream"
-       HubKeyScript     HubKey = "script"
-       HubKeyGlobalRule HubKey = "global_rule"
-       HubKeyServerInfo HubKey = "server_info"
+       HubKeyConsumer     HubKey = "consumer"
+       HubKeyRoute        HubKey = "route"
+       HubKeyService      HubKey = "service"
+       HubKeySsl          HubKey = "ssl"
+       HubKeyUpstream     HubKey = "upstream"
+       HubKeyScript       HubKey = "script"
+       HubKeyGlobalRule   HubKey = "global_rule"
+       HubKeyServerInfo   HubKey = "server_info"
+       HubKeyPluginConfig HubKey = "plugin_config"
 )
 
 var (
@@ -178,5 +179,17 @@ func InitStores() error {
                return err
        }
 
+       err = InitStore(HubKeyPluginConfig, GenericStoreOption{
+               BasePath: "/apisix/plugin_configs",
+               ObjType:  reflect.TypeOf(entity.PluginConfig{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.PluginConfig)
+                       return utils.InterfaceToString(r.ID)
+               },
+       })
+       if err != nil {
+               return err
+       }
+
        return nil
 }
diff --git a/api/internal/handler/label/label.go 
b/api/internal/handler/label/label.go
index 77574f6..791b034 100644
--- a/api/internal/handler/label/label.go
+++ b/api/internal/handler/label/label.go
@@ -38,11 +38,12 @@ import (
 )
 
 type Handler struct {
-       routeStore    store.Interface
-       serviceStore  store.Interface
-       upstreamStore store.Interface
-       sslStore      store.Interface
-       consumerStore store.Interface
+       routeStore        store.Interface
+       serviceStore      store.Interface
+       upstreamStore     store.Interface
+       sslStore          store.Interface
+       consumerStore     store.Interface
+       pluginConfigStore store.Interface
 }
 
 var _ json.Marshaler = Pair{}
@@ -59,11 +60,12 @@ func (p Pair) MarshalJSON() ([]byte, error) {
 
 func NewHandler() (handler.RouteRegister, error) {
        return &Handler{
-               routeStore:    store.GetStore(store.HubKeyRoute),
-               serviceStore:  store.GetStore(store.HubKeyService),
-               upstreamStore: store.GetStore(store.HubKeyUpstream),
-               sslStore:      store.GetStore(store.HubKeySsl),
-               consumerStore: store.GetStore(store.HubKeyConsumer),
+               routeStore:        store.GetStore(store.HubKeyRoute),
+               serviceStore:      store.GetStore(store.HubKeyService),
+               upstreamStore:     store.GetStore(store.HubKeyUpstream),
+               sslStore:          store.GetStore(store.HubKeySsl),
+               consumerStore:     store.GetStore(store.HubKeyConsumer),
+               pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
        }, nil
 }
 
@@ -154,9 +156,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, 
error) {
                items = append(items, h.sslStore)
        case "upstream":
                items = append(items, h.upstreamStore)
+       case "plugin_config":
+               items = append(items, h.pluginConfigStore)
        case "all":
                items = append(items, h.routeStore, h.serviceStore, 
h.upstreamStore,
-                       h.sslStore, h.consumerStore)
+                       h.sslStore, h.consumerStore, h.pluginConfigStore)
        }
 
        predicate := func(obj interface{}) bool {
@@ -173,6 +177,8 @@ func (h *Handler) List(c droplet.Context) (interface{}, 
error) {
                        ls = obj.Labels
                case *entity.Upstream:
                        ls = obj.Labels
+               case *entity.PluginConfig:
+                       ls = obj.Labels
                default:
                        return false
                }
diff --git a/api/internal/handler/label/label_test.go 
b/api/internal/handler/label/label_test.go
index 25f9fda..664288d 100644
--- a/api/internal/handler/label/label_test.go
+++ b/api/internal/handler/label/label_test.go
@@ -178,6 +178,18 @@ func genConsumer(labels map[string]string) 
*entity.Consumer {
        return &r
 }
 
+func genPluginConfig(labels map[string]string) *entity.PluginConfig {
+       r := entity.PluginConfig{
+               BaseInfo: entity.BaseInfo{
+                       ID:         rand.Int(),
+                       CreateTime: rand.Int63(),
+               },
+               Labels: labels,
+       }
+
+       return &r
+}
+
 func TestLabel(t *testing.T) {
        m1 := map[string]string{
                "label1": "value1",
@@ -189,7 +201,7 @@ func TestLabel(t *testing.T) {
        }
 
        // TODO: Test SSL after the ssl config bug fixed
-       types := []string{"route", "service", "upstream", "consumer"}
+       types := []string{"route", "service", "upstream", "consumer", 
"plugin_config"}
 
        var giveData []interface{}
        for _, typ := range types {
@@ -219,6 +231,11 @@ func TestLabel(t *testing.T) {
                                genConsumer(m1),
                                genConsumer(m2),
                        }
+               case "plugin_config":
+                       giveData = []interface{}{
+                               genPluginConfig(m1),
+                               genPluginConfig(m2),
+                       }
                }
 
                var testCases []*testCase
@@ -271,6 +288,8 @@ func TestLabel(t *testing.T) {
                                handler.upstreamStore = genMockStore(t, 
tc.giveData)
                        case "consumer":
                                handler.consumerStore = genMockStore(t, 
tc.giveData)
+                       case "plugin_config":
+                               handler.pluginConfigStore = genMockStore(t, 
tc.giveData)
                        }
 
                        ctx := droplet.NewContext()
@@ -296,11 +315,12 @@ func TestLabel(t *testing.T) {
        }
 
        handler := Handler{
-               routeStore:    genMockStore(t, []interface{}{genRoute(m1)}),
-               sslStore:      genMockStore(t, []interface{}{genSSL(m2)}),
-               upstreamStore: genMockStore(t, []interface{}{genUpstream(m3)}),
-               consumerStore: genMockStore(t, []interface{}{genConsumer(m4)}),
-               serviceStore:  genMockStore(t, []interface{}{genService(m5)}),
+               routeStore:        genMockStore(t, []interface{}{genRoute(m1)}),
+               sslStore:          genMockStore(t, []interface{}{genSSL(m2)}),
+               upstreamStore:     genMockStore(t, 
[]interface{}{genUpstream(m3)}),
+               consumerStore:     genMockStore(t, 
[]interface{}{genConsumer(m4)}),
+               serviceStore:      genMockStore(t, 
[]interface{}{genService(m5)}),
+               pluginConfigStore: genMockStore(t, 
[]interface{}{genPluginConfig(m5)}),
        }
 
        var testCases []*testCase
diff --git a/api/internal/handler/plugin_config/plugin_config.go 
b/api/internal/handler/plugin_config/plugin_config.go
new file mode 100644
index 0000000..ec09a7f
--- /dev/null
+++ b/api/internal/handler/plugin_config/plugin_config.go
@@ -0,0 +1,267 @@
+/*
+ * 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 plugin_config
+
+import (
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "reflect"
+       "strings"
+
+       "github.com/gin-gonic/gin"
+       "github.com/shiningrush/droplet"
+       "github.com/shiningrush/droplet/data"
+       "github.com/shiningrush/droplet/wrapper"
+       wgin "github.com/shiningrush/droplet/wrapper/gin"
+
+       "github.com/apisix/manager-api/internal/core/entity"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+       "github.com/apisix/manager-api/internal/log"
+       "github.com/apisix/manager-api/internal/utils"
+)
+
+type Handler struct {
+       pluginConfigStore store.Interface
+       routeStore        store.Interface
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+       return &Handler{
+               pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+               routeStore:        store.GetStore(store.HubKeyRoute),
+       }, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+       r.GET("/apisix/admin/plugin_configs/:id", wgin.Wraps(h.Get,
+               wrapper.InputType(reflect.TypeOf(GetInput{}))))
+       r.GET("/apisix/admin/plugin_configs", wgin.Wraps(h.List,
+               wrapper.InputType(reflect.TypeOf(ListInput{}))))
+       r.POST("/apisix/admin/plugin_configs", wgin.Wraps(h.Create,
+               wrapper.InputType(reflect.TypeOf(entity.PluginConfig{}))))
+       r.PUT("/apisix/admin/plugin_configs", wgin.Wraps(h.Update,
+               wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+       r.PUT("/apisix/admin/plugin_configs/:id", wgin.Wraps(h.Update,
+               wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+       r.PATCH("/apisix/admin/plugin_configs/:id", wgin.Wraps(h.Patch,
+               wrapper.InputType(reflect.TypeOf(PatchInput{}))))
+       r.PATCH("/apisix/admin/plugin_configs/:id/*path", wgin.Wraps(h.Patch,
+               wrapper.InputType(reflect.TypeOf(PatchInput{}))))
+       r.DELETE("/apisix/admin/plugin_configs/:ids", wgin.Wraps(h.BatchDelete,
+               wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
+}
+
+type GetInput struct {
+       ID string `auto_read:"id,path" validate:"required"`
+}
+
+func (h *Handler) Get(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*GetInput)
+
+       pluginConfig, err := h.pluginConfigStore.Get(c.Context(), input.ID)
+       if err != nil {
+               return handler.SpecCodeResponse(err), err
+       }
+
+       return pluginConfig, nil
+}
+
+type ListInput struct {
+       Search string `auto_read:"search,query"`
+       Label  string `auto_read:"label,query"`
+       store.Pagination
+}
+
+// swagger:operation GET /apisix/admin/plugin_configs getPluginConfigList
+//
+// Return the plugin_config list according to the specified page number and 
page size, and support search.
+//
+// ---
+// produces:
+// - application/json
+// parameters:
+// - name: page
+//   in: query
+//   description: page number
+//   required: false
+//   type: integer
+// - name: page_size
+//   in: query
+//   description: page size
+//   required: false
+//   type: integer
+// - name: search
+//   in: query
+//   description: search keyword
+//   required: false
+//   type: string
+// responses:
+//   '0':
+//     description: list response
+//     schema:
+//       type: array
+//       items:
+//         "$ref": "#/definitions/pluginConfig"
+//   default:
+//     description: unexpected error
+//     schema:
+//       "$ref": "#/definitions/ApiError"
+func (h *Handler) List(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*ListInput)
+       labelMap, err := utils.GenLabelMap(input.Label)
+       if err != nil {
+               return &data.SpecCodeResponse{StatusCode: 
http.StatusBadRequest},
+                       fmt.Errorf("%s: \"%s\"", err.Error(), input.Label)
+       }
+
+       ret, err := h.pluginConfigStore.List(c.Context(), store.ListInput{
+               Predicate: func(obj interface{}) bool {
+                       if input.Search != "" {
+                               return 
strings.Contains(obj.(*entity.PluginConfig).Desc, input.Search)
+                       }
+
+                       if input.Label != "" && 
!utils.LabelContains(obj.(*entity.PluginConfig).Labels, labelMap) {
+                               return false
+                       }
+
+                       return true
+               },
+               PageSize:   input.PageSize,
+               PageNumber: input.PageNumber,
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       return ret, nil
+}
+
+func (h *Handler) Create(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*entity.PluginConfig)
+
+       ret, err := h.pluginConfigStore.Create(c.Context(), input)
+       if err != nil {
+               return handler.SpecCodeResponse(err), err
+       }
+
+       return ret, nil
+}
+
+type UpdateInput struct {
+       ID string `auto_read:"id,path"`
+       entity.PluginConfig
+}
+
+func (h *Handler) Update(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*UpdateInput)
+
+       // check if ID in body is equal ID in path
+       if err := handler.IDCompare(input.ID, input.PluginConfig.ID); err != 
nil {
+               return &data.SpecCodeResponse{StatusCode: 
http.StatusBadRequest}, err
+       }
+
+       if input.ID != "" {
+               input.PluginConfig.ID = input.ID
+       }
+
+       ret, err := h.pluginConfigStore.Update(c.Context(), 
&input.PluginConfig, true)
+       if err != nil {
+               return handler.SpecCodeResponse(err), err
+       }
+
+       return ret, nil
+}
+
+type BatchDelete struct {
+       IDs string `auto_read:"ids,path"`
+}
+
+func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*BatchDelete)
+
+       IDs := strings.Split(input.IDs, ",")
+       IDMap := map[string]bool{}
+       for _, id := range IDs {
+               IDMap[id] = true
+       }
+       ret, err := h.routeStore.List(c.Context(), store.ListInput{
+               Predicate: func(obj interface{}) bool {
+                       id := 
utils.InterfaceToString(obj.(*entity.Route).PluginConfigID)
+                       if _, ok := IDMap[id]; ok {
+                               return true
+                       }
+                       return false
+               },
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       if len(ret.Rows) > 0 {
+               return &data.SpecCodeResponse{StatusCode: 
http.StatusBadRequest},
+                       fmt.Errorf("please disconnect the route (ID: %s) with 
this plugin config first",
+                               ret.Rows[0].(*entity.Route).ID)
+       }
+
+       if err := h.pluginConfigStore.BatchDelete(c.Context(), 
strings.Split(input.IDs, ",")); err != nil {
+               return handler.SpecCodeResponse(err), err
+       }
+
+       return nil, nil
+}
+
+type PatchInput struct {
+       ID      string `auto_read:"id,path"`
+       SubPath string `auto_read:"path,path"`
+       Body    []byte `auto_read:"@body"`
+}
+
+func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
+       input := c.Input().(*PatchInput)
+       reqBody := input.Body
+       id := input.ID
+       subPath := input.SubPath
+
+       stored, err := h.pluginConfigStore.Get(c.Context(), id)
+       if err != nil {
+               log.Warnf("get stored data from etcd failed: %s", err)
+               return handler.SpecCodeResponse(err), err
+       }
+
+       res, err := utils.MergePatch(stored, subPath, reqBody)
+       if err != nil {
+               log.Warnf("merge failed: %s", err)
+               return handler.SpecCodeResponse(err), err
+       }
+
+       var pluginConfig entity.PluginConfig
+       if err := json.Unmarshal(res, &pluginConfig); err != nil {
+               log.Warnf("unmarshal to pluginConfig failed: %s", err)
+               return handler.SpecCodeResponse(err), err
+       }
+
+       ret, err := h.pluginConfigStore.Update(c.Context(), &pluginConfig, 
false)
+       if err != nil {
+               log.Warnf("update failed: %s", err)
+               return handler.SpecCodeResponse(err), err
+       }
+
+       return ret, nil
+}
diff --git a/api/internal/handler/plugin_config/plugin_config_test.go 
b/api/internal/handler/plugin_config/plugin_config_test.go
new file mode 100644
index 0000000..1dbdbbe
--- /dev/null
+++ b/api/internal/handler/plugin_config/plugin_config_test.go
@@ -0,0 +1,715 @@
+/*
+ * 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 plugin_config
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "testing"
+
+       "github.com/shiningrush/droplet"
+       "github.com/shiningrush/droplet/data"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+
+       "github.com/apisix/manager-api/internal/core/entity"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+)
+
+func TestPluginConfig_Get(t *testing.T) {
+       tests := []struct {
+               caseDesc   string
+               giveInput  *GetInput
+               giveRet    *entity.PluginConfig
+               giveErr    error
+               wantErr    error
+               wantGetKey string
+               wantRet    interface{}
+       }{
+               {
+                       caseDesc:   "normal",
+                       giveInput:  &GetInput{ID: "1"},
+                       wantGetKey: "1",
+                       giveRet: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID: "1",
+                               },
+                               Plugins: map[string]interface{}{
+                                       "limit-count": map[string]interface{}{
+                                               "count":         2,
+                                               "time_window":   60,
+                                               "rejected_code": 503,
+                                               "key":           "remote_addr",
+                                       },
+                               },
+                       },
+                       wantRet: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID: "1",
+                               },
+                               Plugins: map[string]interface{}{
+                                       "limit-count": map[string]interface{}{
+                                               "count":         2,
+                                               "time_window":   60,
+                                               "rejected_code": 503,
+                                               "key":           "remote_addr",
+                                       },
+                               },
+                       },
+               },
+               {
+                       caseDesc:   "store get failed",
+                       giveInput:  &GetInput{ID: "failed_key"},
+                       wantGetKey: "failed_key",
+                       giveErr:    fmt.Errorf("get failed"),
+                       wantErr:    fmt.Errorf("get failed"),
+                       wantRet: &data.SpecCodeResponse{
+                               StatusCode: http.StatusInternalServerError,
+                       },
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := true
+                       mStore := &store.MockInterface{}
+                       mStore.On("Get", mock.Anything, 
mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               assert.Equal(t, tc.wantGetKey, args.Get(0))
+                       }).Return(tc.giveRet, tc.giveErr)
+
+                       h := Handler{pluginConfigStore: mStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.Get(ctx)
+                       assert.True(t, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
+
+func TestPluginConfig_List(t *testing.T) {
+       tests := []struct {
+               caseDesc  string
+               giveInput *ListInput
+               giveData  []*entity.PluginConfig
+               giveErr   error
+               wantErr   error
+               wantInput store.ListInput
+               wantRet   interface{}
+       }{
+               {
+                       caseDesc: "list all plugin config",
+                       giveInput: &ListInput{
+                               Pagination: store.Pagination{
+                                       PageSize:   10,
+                                       PageNumber: 10,
+                               },
+                       },
+                       wantInput: store.ListInput{
+                               PageSize:   10,
+                               PageNumber: 10,
+                       },
+                       giveData: []*entity.PluginConfig{
+                               {Desc: "1"},
+                               {Desc: "s2"},
+                               {Desc: "test_plugin_config"},
+                               {Desc: "plugin_config_test"},
+                       },
+                       wantRet: &store.ListOutput{
+                               Rows: []interface{}{
+                                       &entity.PluginConfig{Desc: "1"},
+                                       &entity.PluginConfig{Desc: "s2"},
+                                       &entity.PluginConfig{Desc: 
"test_plugin_config"},
+                                       &entity.PluginConfig{Desc: 
"plugin_config_test"},
+                               },
+                               TotalSize: 4,
+                       },
+               },
+               {
+                       caseDesc: "list plugin config with 'plugin_config'",
+                       giveInput: &ListInput{
+                               Search: "plugin_config",
+                               Pagination: store.Pagination{
+                                       PageSize:   10,
+                                       PageNumber: 10,
+                               },
+                       },
+                       wantInput: store.ListInput{
+                               PageSize:   10,
+                               PageNumber: 10,
+                       },
+                       giveData: []*entity.PluginConfig{
+                               {BaseInfo: entity.BaseInfo{CreateTime: 
1609376661}, Desc: "1"},
+                               {BaseInfo: entity.BaseInfo{CreateTime: 
1609376662}, Desc: "s2"},
+                               {BaseInfo: entity.BaseInfo{CreateTime: 
1609376663}, Desc: "test_plugin_config"},
+                               {BaseInfo: entity.BaseInfo{CreateTime: 
1609376664}, Desc: "plugin_config_test"},
+                       },
+                       wantRet: &store.ListOutput{
+                               Rows: []interface{}{
+                                       &entity.PluginConfig{BaseInfo: 
entity.BaseInfo{CreateTime: 1609376663}, Desc: "test_plugin_config"},
+                                       &entity.PluginConfig{BaseInfo: 
entity.BaseInfo{CreateTime: 1609376664}, Desc: "plugin_config_test"},
+                               },
+                               TotalSize: 2,
+                       },
+               },
+               {
+                       caseDesc: "list plugin config with label",
+                       giveInput: &ListInput{
+                               Label: "extra",
+                               Pagination: store.Pagination{
+                                       PageSize:   10,
+                                       PageNumber: 10,
+                               },
+                       },
+                       wantInput: store.ListInput{
+                               PageSize:   10,
+                               PageNumber: 10,
+                       },
+                       giveData: []*entity.PluginConfig{
+                               {
+                                       Desc: "1",
+                                       Labels: map[string]string{
+                                               "version": "v1",
+                                               "extra":   "t",
+                                       },
+                               },
+                               {Desc: "s2"},
+                               {Desc: "test_plugin_config"},
+                               {Desc: "plugin_config_test"},
+                       },
+                       wantRet: &store.ListOutput{
+                               Rows: []interface{}{
+                                       &entity.PluginConfig{
+                                               Desc: "1",
+                                               Labels: map[string]string{
+                                                       "version": "v1",
+                                                       "extra":   "t",
+                                               },
+                                       },
+                               },
+                               TotalSize: 1,
+                       },
+               },
+               {
+                       caseDesc: "list plugin config with label (k:v)",
+                       giveInput: &ListInput{
+                               Label: "version:v1",
+                               Pagination: store.Pagination{
+                                       PageSize:   10,
+                                       PageNumber: 10,
+                               },
+                       },
+                       wantInput: store.ListInput{
+                               PageSize:   10,
+                               PageNumber: 10,
+                       },
+                       giveData: []*entity.PluginConfig{
+                               {
+                                       Desc: "1",
+                                       Labels: map[string]string{
+                                               "version": "v1",
+                                               "build":   "16",
+                                       },
+                               },
+                               {Desc: "s2"},
+                               {Desc: "test_plugin_config"},
+                               {Desc: "plugin_config_test"},
+                       },
+                       wantRet: &store.ListOutput{
+                               Rows: []interface{}{
+                                       &entity.PluginConfig{
+                                               Desc: "1",
+                                               Labels: map[string]string{
+                                                       "version": "v1",
+                                                       "build":   "16",
+                                               },
+                                       },
+                               },
+                               TotalSize: 1,
+                       },
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := true
+                       mStore := &store.MockInterface{}
+                       mStore.On("List", mock.Anything, 
mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               input := args.Get(0).(store.ListInput)
+                               assert.Equal(t, tc.wantInput.PageSize, 
input.PageSize)
+                               assert.Equal(t, tc.wantInput.PageNumber, 
input.PageNumber)
+                       }).Return(func(input store.ListInput) *store.ListOutput 
{
+                               var returnData []interface{}
+                               for _, c := range tc.giveData {
+                                       if input.Predicate(c) {
+                                               if input.Format == nil {
+                                                       returnData = 
append(returnData, c)
+                                                       continue
+                                               }
+
+                                               returnData = append(returnData, 
input.Format(c))
+                                       }
+                               }
+                               return &store.ListOutput{
+                                       Rows:      returnData,
+                                       TotalSize: len(returnData),
+                               }
+                       }, tc.giveErr)
+
+                       h := Handler{pluginConfigStore: mStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.List(ctx)
+                       assert.True(t, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
+
+func TestPluginConfig_Create(t *testing.T) {
+       tests := []struct {
+               caseDesc  string
+               getCalled bool
+               giveInput *entity.PluginConfig
+               giveRet   interface{}
+               giveErr   error
+               wantInput *entity.PluginConfig
+               wantErr   error
+               wantRet   interface{}
+       }{
+               {
+                       caseDesc:  "create success",
+                       getCalled: true,
+                       giveInput: &entity.PluginConfig{
+                               Desc: "test plugin config",
+                       },
+                       wantInput: &entity.PluginConfig{
+                               Desc: "test plugin config",
+                       },
+               },
+               {
+                       caseDesc:  "create failed, create return error",
+                       getCalled: true,
+                       giveInput: &entity.PluginConfig{
+                               Desc: "test plugin config",
+                       },
+                       giveErr: fmt.Errorf("create failed"),
+                       wantInput: &entity.PluginConfig{
+                               Desc: "test plugin config",
+                       },
+                       wantErr: fmt.Errorf("create failed"),
+                       wantRet: handler.SpecCodeResponse(fmt.Errorf("create 
failed")),
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := false
+                       pluginConfigStore := &store.MockInterface{}
+                       pluginConfigStore.On("Create", mock.Anything, 
mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               input := args.Get(1).(*entity.PluginConfig)
+                               assert.Equal(t, tc.wantInput, input)
+                       }).Return(tc.giveRet, tc.giveErr)
+
+                       h := Handler{pluginConfigStore: pluginConfigStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.Create(ctx)
+                       assert.Equal(t, tc.getCalled, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
+
+func TestPluginConfig_Update(t *testing.T) {
+       tests := []struct {
+               caseDesc  string
+               getCalled bool
+               giveInput *UpdateInput
+               giveErr   error
+               giveRet   interface{}
+               wantInput *entity.PluginConfig
+               wantErr   error
+               wantRet   interface{}
+       }{
+               {
+                       caseDesc:  "create success",
+                       getCalled: true,
+                       giveInput: &UpdateInput{
+                               ID: "1",
+                               PluginConfig: entity.PluginConfig{
+                                       Desc: "test plugin config",
+                               },
+                       },
+                       wantInput: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID: "1",
+                               },
+                               Desc: "test plugin config",
+                       },
+               },
+               {
+                       caseDesc: "create failed, different id",
+                       giveInput: &UpdateInput{
+                               ID: "1",
+                               PluginConfig: entity.PluginConfig{
+                                       BaseInfo: entity.BaseInfo{
+                                               ID: "s2",
+                                       },
+                                       Desc: "test plugin config",
+                               },
+                       },
+                       wantRet: &data.SpecCodeResponse{StatusCode: 
http.StatusBadRequest},
+                       wantErr: fmt.Errorf("ID on path (1) doesn't match ID on 
body (s2)"),
+               },
+               {
+                       caseDesc:  "update failed, update return error",
+                       getCalled: true,
+                       giveInput: &UpdateInput{
+                               ID: "1",
+                               PluginConfig: entity.PluginConfig{
+                                       Desc: "test plugin config",
+                               },
+                       },
+                       giveErr: fmt.Errorf("update failed"),
+                       wantInput: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{ID: "1"},
+                               Desc:     "test plugin config",
+                       },
+                       wantErr: fmt.Errorf("update failed"),
+                       wantRet: handler.SpecCodeResponse(fmt.Errorf("update 
failed")),
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := false
+                       pluginConfigStore := &store.MockInterface{}
+                       pluginConfigStore.On("Update", mock.Anything, 
mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               input := args.Get(1).(*entity.PluginConfig)
+                               createIfNotExist := args.Get(2).(bool)
+                               assert.Equal(t, tc.wantInput, input)
+                               assert.True(t, createIfNotExist)
+                       }).Return(tc.giveRet, tc.giveErr)
+
+                       h := Handler{pluginConfigStore: pluginConfigStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.Update(ctx)
+                       assert.Equal(t, tc.getCalled, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
+
+func TestPluginConfig_Patch(t *testing.T) {
+       existPluginConfig := &entity.PluginConfig{
+               BaseInfo: entity.BaseInfo{
+                       ID:         "1",
+                       CreateTime: 1609340491,
+                       UpdateTime: 1609340491,
+               },
+               Plugins: map[string]interface{}{
+                       "limit-count": map[string]interface{}{
+                               "count":         2,
+                               "time_window":   60,
+                               "rejected_code": 503,
+                               "key":           "remote_addr",
+                       },
+               },
+               Labels: map[string]string{
+                       "version": "v1",
+               },
+               Desc: "desc",
+       }
+
+       tests := []struct {
+               caseDesc          string
+               giveInput         *PatchInput
+               giveErr           error
+               giveRet           interface{}
+               wantInput         *entity.PluginConfig
+               wantErr           error
+               wantRet           interface{}
+               pluginConfigInput string
+               pluginConfigRet   *entity.PluginConfig
+               pluginConfigErr   error
+               called            bool
+       }{
+               {
+                       caseDesc: "patch all success",
+                       giveInput: &PatchInput{
+                               ID:      "1",
+                               SubPath: "",
+                               Body: []byte(`{
+                                               "desc":"patched",
+                                               "plugins":{
+                                                       "limit-count":{
+                                                               "count":2,
+                                                               
"time_window":60,
+                                                               
"rejected_code": 504,
+                                                               
"key":"remote_addr"
+                                                       },
+                                                       "key-auth":{
+                                                               "key":"auth-one"
+                                                       }
+                                               },
+                                               "labels":{
+                                                       "version":"v1",
+                                                       "build":"16"
+                                               }
+                                       }`),
+                       },
+                       wantInput: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID:         "1",
+                                       CreateTime: 1609340491,
+                                       UpdateTime: 1609340491,
+                               },
+                               Desc: "patched",
+                               Plugins: map[string]interface{}{
+                                       "limit-count": map[string]interface{}{
+                                               "count":         float64(2),
+                                               "time_window":   float64(60),
+                                               "rejected_code": float64(504),
+                                               "key":           "remote_addr",
+                                       },
+                                       "key-auth": map[string]interface{}{
+                                               "key": "auth-one",
+                                       },
+                               },
+                               Labels: map[string]string{
+                                       "version": "v1",
+                                       "build":   "16",
+                               },
+                       },
+                       pluginConfigInput: "1",
+                       pluginConfigRet:   existPluginConfig,
+                       called:            true,
+               },
+               {
+                       caseDesc: "patch part of plugin config success",
+                       giveInput: &PatchInput{
+                               ID:      "1",
+                               SubPath: "",
+                               Body: []byte(`{
+                                               "desc":"patched"
+                                       }`),
+                       },
+                       wantInput: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID:         "1",
+                                       CreateTime: 1609340491,
+                                       UpdateTime: 1609340491,
+                               },
+                               Desc: "patched",
+                               Plugins: map[string]interface{}{
+                                       "limit-count": map[string]interface{}{
+                                               "count":         float64(2),
+                                               "time_window":   float64(60),
+                                               "rejected_code": float64(503),
+                                               "key":           "remote_addr",
+                                       },
+                               },
+                               Labels: map[string]string{
+                                       "version": "v1",
+                               },
+                       },
+                       pluginConfigInput: "1",
+                       pluginConfigRet:   existPluginConfig,
+                       called:            true,
+               },
+               {
+                       caseDesc: "patch desc success with sub path",
+                       giveInput: &PatchInput{
+                               ID:      "1",
+                               SubPath: "/desc",
+                               Body:    []byte(`"desc_patched"`),
+                       },
+                       wantInput: &entity.PluginConfig{
+                               BaseInfo: entity.BaseInfo{
+                                       ID:         "1",
+                                       CreateTime: 1609340491,
+                                       UpdateTime: 1609340491,
+                               },
+                               Desc: "desc_patched",
+                               Plugins: map[string]interface{}{
+                                       "limit-count": map[string]interface{}{
+                                               "count":         float64(2),
+                                               "time_window":   float64(60),
+                                               "rejected_code": float64(503),
+                                               "key":           "remote_addr",
+                                       },
+                               },
+                               Labels: map[string]string{
+                                       "version": "v1",
+                               },
+                       },
+                       pluginConfigInput: "1",
+                       pluginConfigRet:   existPluginConfig,
+                       called:            true,
+               },
+               {
+                       caseDesc: "patch failed, plugin config store get error",
+                       giveInput: &PatchInput{
+                               ID:   "1",
+                               Body: []byte{},
+                       },
+                       pluginConfigInput: "1",
+                       pluginConfigErr:   fmt.Errorf("get error"),
+                       wantRet:           
handler.SpecCodeResponse(fmt.Errorf("get error")),
+                       wantErr:           fmt.Errorf("get error"),
+                       called:            false,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := false
+                       pluginConfigStore := &store.MockInterface{}
+                       pluginConfigStore.On("Update", mock.Anything, 
mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               input := args.Get(1).(*entity.PluginConfig)
+                               createIfNotExist := args.Get(2).(bool)
+                               assert.Equal(t, tc.wantInput, input)
+                               assert.False(t, createIfNotExist)
+                       }).Return(tc.giveRet, tc.giveErr)
+
+                       pluginConfigStore.On("Get", mock.Anything, 
mock.Anything).Run(func(args mock.Arguments) {
+                               input := args.Get(0).(string)
+                               assert.Equal(t, tc.pluginConfigInput, input)
+                       }).Return(tc.pluginConfigRet, tc.pluginConfigErr)
+
+                       h := Handler{pluginConfigStore: pluginConfigStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.Patch(ctx)
+                       assert.Equal(t, tc.called, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
+
+func TestPluginConfigs_Delete(t *testing.T) {
+       tests := []struct {
+               caseDesc  string
+               giveInput *BatchDelete
+               giveErr   error
+               listRet   *store.ListOutput
+               wantInput []string
+               wantErr   error
+               wantRet   interface{}
+       }{
+               {
+                       caseDesc: "delete success",
+                       giveInput: &BatchDelete{
+                               IDs: "1",
+                       },
+                       listRet: &store.ListOutput{
+                               Rows:      []interface{}{},
+                               TotalSize: 0,
+                       },
+                       wantInput: []string{"1"},
+               },
+               {
+                       caseDesc: "batch delete success",
+                       giveInput: &BatchDelete{
+                               IDs: "1,s2",
+                       },
+                       listRet: &store.ListOutput{
+                               Rows:      []interface{}{},
+                               TotalSize: 0,
+                       },
+                       wantInput: []string{"1", "s2"},
+               },
+
+               {
+                       caseDesc: "delete failed - being used by user",
+                       giveInput: &BatchDelete{
+                               IDs: "001,002",
+                       },
+                       giveErr: fmt.Errorf("delete failed"),
+                       wantInput: []string{
+                               "001",
+                               "002",
+                       },
+                       listRet: &store.ListOutput{
+                               Rows: []interface{}{
+                                       &entity.Route{BaseInfo: 
entity.BaseInfo{ID: "a"}},
+                                       &entity.Route{BaseInfo: 
entity.BaseInfo{ID: "b"}},
+                               },
+                               TotalSize: 2,
+                       },
+                       wantErr: errors.New("please disconnect the route (ID: 
a) with this plugin config first"),
+                       wantRet: &data.SpecCodeResponse{
+                               StatusCode: http.StatusBadRequest,
+                       },
+               },
+
+               {
+                       caseDesc: "delete failed",
+                       giveInput: &BatchDelete{
+                               IDs: "1",
+                       },
+                       listRet: &store.ListOutput{
+                               Rows:      []interface{}{},
+                               TotalSize: 0,
+                       },
+                       giveErr:   fmt.Errorf("delete error"),
+                       wantInput: []string{"1"},
+                       wantRet:   handler.SpecCodeResponse(fmt.Errorf("delete 
error")),
+                       wantErr:   fmt.Errorf("delete error"),
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       getCalled := false
+                       pluginConfigStore := &store.MockInterface{}
+                       pluginConfigStore.On("BatchDelete", mock.Anything, 
mock.Anything).Run(func(args mock.Arguments) {
+                               getCalled = true
+                               input := args.Get(1).([]string)
+                               assert.Equal(t, tc.wantInput, input)
+                       }).Return(tc.giveErr)
+
+                       mockRouteStore := &store.MockInterface{}
+                       mockRouteStore.On("List", mock.Anything).Run(func(args 
mock.Arguments) {
+                               getCalled = true
+                       }).Return(tc.listRet, nil)
+
+                       h := Handler{pluginConfigStore: pluginConfigStore, 
routeStore: mockRouteStore}
+                       ctx := droplet.NewContext()
+                       ctx.SetInput(tc.giveInput)
+                       ret, err := h.BatchDelete(ctx)
+                       assert.True(t, getCalled)
+                       assert.Equal(t, tc.wantRet, ret)
+                       assert.Equal(t, tc.wantErr, err)
+               })
+       }
+}
diff --git a/api/internal/route.go b/api/internal/route.go
index 4d5f9b9..5b7666d 100644
--- a/api/internal/route.go
+++ b/api/internal/route.go
@@ -36,6 +36,7 @@ import (
        "github.com/apisix/manager-api/internal/handler/healthz"
        "github.com/apisix/manager-api/internal/handler/label"
        "github.com/apisix/manager-api/internal/handler/plugin"
+       "github.com/apisix/manager-api/internal/handler/plugin_config"
        "github.com/apisix/manager-api/internal/handler/route"
        "github.com/apisix/manager-api/internal/handler/route_online_debug"
        "github.com/apisix/manager-api/internal/handler/server_info"
@@ -78,6 +79,7 @@ func SetUpRouter() *gin.Engine {
                data_loader.NewHandler,
                data_loader.NewImportHandler,
                tool.NewHandler,
+               plugin_config.NewHandler,
        }
 
        for i := range factories {
diff --git a/api/test/e2e/label_test.go b/api/test/e2e/label_test.go
index 690d913..5dfbd0e 100644
--- a/api/test/e2e/label_test.go
+++ b/api/test/e2e/label_test.go
@@ -125,6 +125,28 @@ func TestLabel(t *testing.T) {
                        ExpectStatus: http.StatusOK,
                },
                {
+                       Desc:   "create plugin_config",
+                       Object: ManagerApiExpect(t),
+                       Method: http.MethodPut,
+                       Path:   "/apisix/admin/plugin_configs/1",
+                       Body: `{
+                               "plugins": {
+                                       "response-rewrite": {
+                                               "headers": {
+                                                       "X-VERSION":"22.0"
+                                               }
+                                       }
+                               },
+                               "labels": {
+                                       "version": "v2",
+                                       "build":   "17",
+                                       "extra":   "test"
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": token},
+                       ExpectStatus: http.StatusOK,
+               },
+               {
                        Desc:         "get route label",
                        Object:       ManagerApiExpect(t),
                        Method:       http.MethodGet,
@@ -162,6 +184,46 @@ func TestLabel(t *testing.T) {
                        ExpectBody:   
"{\"build\":\"16\"},{\"env\":\"production\"},{\"extra\":\"test\"},{\"version\":\"v2\"}",
                },
                {
+                       Desc:         "get plugin_config label",
+                       Object:       ManagerApiExpect(t),
+                       Method:       http.MethodGet,
+                       Headers:      map[string]string{"Authorization": token},
+                       Path:         "/apisix/admin/labels/plugin_config",
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   
"{\"build\":\"17\"},{\"extra\":\"test\"},{\"version\":\"v2\"}",
+               },
+               {
+                       Desc:   "update plugin_config",
+                       Object: ManagerApiExpect(t),
+                       Method: http.MethodPut,
+                       Path:   "/apisix/admin/plugin_configs/1",
+                       Body: `{
+                               "plugins": {
+                                       "response-rewrite": {
+                                               "headers": {
+                                                       "X-VERSION":"22.0"
+                                               }
+                                       }
+                               },
+                               "labels": {
+                                       "version": "v3",
+                                       "build":   "16",
+                                       "extra":   "test"
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": token},
+                       ExpectStatus: http.StatusOK,
+               },
+               {
+                       Desc:         "get plugin_config label again to verify 
update",
+                       Object:       ManagerApiExpect(t),
+                       Method:       http.MethodGet,
+                       Headers:      map[string]string{"Authorization": token},
+                       Path:         "/apisix/admin/labels/plugin_config",
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   
"{\"build\":\"16\"},{\"extra\":\"test\"},{\"version\":\"v3\"}",
+               },
+               {
                        Desc:         "get all label",
                        Object:       ManagerApiExpect(t),
                        Method:       http.MethodGet,
@@ -302,6 +364,14 @@ func TestLabel(t *testing.T) {
                        Headers:      map[string]string{"Authorization": token},
                        ExpectStatus: http.StatusOK,
                },
+               {
+                       Desc:         "delete plugin_config",
+                       Object:       ManagerApiExpect(t),
+                       Method:       http.MethodDelete,
+                       Path:         "/apisix/admin/plugin_configs/1",
+                       Headers:      map[string]string{"Authorization": token},
+                       ExpectStatus: http.StatusOK,
+               },
        }
 
        for _, tc := range tests {
diff --git a/api/test/e2enew/plugin_config/plugin_config_suite_test.go 
b/api/test/e2enew/plugin_config/plugin_config_suite_test.go
new file mode 100644
index 0000000..3bb1fcb
--- /dev/null
+++ b/api/test/e2enew/plugin_config/plugin_config_suite_test.go
@@ -0,0 +1,36 @@
+/*
+ * 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 plugin_config
+
+import (
+       "testing"
+       "time"
+
+       "github.com/onsi/ginkgo"
+
+       "e2enew/base"
+)
+
+func TestPluginConfig(t *testing.T) {
+       ginkgo.RunSpecs(t, "plugin config suite")
+}
+
+var _ = ginkgo.AfterSuite(func() {
+       base.CleanResource("plugin_configs")
+       base.CleanResource("routes")
+       time.Sleep(base.SleepTime)
+})
diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go 
b/api/test/e2enew/plugin_config/plugin_config_test.go
new file mode 100644
index 0000000..3314cab
--- /dev/null
+++ b/api/test/e2enew/plugin_config/plugin_config_test.go
@@ -0,0 +1,243 @@
+/*
+ * 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 plugin_config
+
+import (
+       "net/http"
+
+       "github.com/onsi/ginkgo"
+       "github.com/onsi/ginkgo/extensions/table"
+
+       "e2enew/base"
+)
+
+var _ = ginkgo.Describe("Plugin Config", func() {
+       table.DescribeTable("test plugin config",
+               func(tc base.HttpTestCase) {
+                       base.RunTestCase(tc)
+               },
+               table.Entry("make sure the route doesn't exist", 
base.HttpTestCase{
+                       Object:       base.APISIXExpect(),
+                       Method:       http.MethodGet,
+                       Path:         "/hello",
+                       ExpectStatus: http.StatusNotFound,
+                       ExpectBody:   `{"error_msg":"404 Route Not Found"}`,
+               }),
+               table.Entry("create plugin config", base.HttpTestCase{
+                       Object: base.ManagerApiExpect(),
+                       Path:   "/apisix/admin/plugin_configs/1",
+                       Method: http.MethodPut,
+                       Body: `{
+                               "plugins": {
+                                       "response-rewrite": {
+                                               "headers": {
+                                                       "X-VERSION":"1.0"
+                                               }
+                                       },
+                                       "uri-blocker": {
+                                               "block_rules": 
["select.+(from|limit)", "(?:(union(.*?)select))"]
+                                       }
+                               },
+                               "labels": {
+                                       "version": "v1",
+                                       "build":   "16"
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("create plugin config 2", base.HttpTestCase{
+                       Object: base.ManagerApiExpect(),
+                       Path:   "/apisix/admin/plugin_configs/2",
+                       Method: http.MethodPut,
+                       Body: `{
+                               "plugins": {
+                                       "response-rewrite": {
+                                               "headers": {
+                                                       "X-VERSION":"22.0"
+                                               }
+                                       }
+                               },
+                               "labels": {
+                                       "version": "v2",
+                                       "build":   "17",
+                                       "extra":   "test"
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("get plugin config", base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Path:         "/apisix/admin/plugin_configs/1",
+                       Method:       http.MethodGet,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   
`"plugins":{"response-rewrite":{"headers":{"X-VERSION":"1.0"}},"uri-blocker":{"block_rules":["select.+(from|limit)","(?:(union(.*?)select))"]}}`,
+                       Sleep:        base.SleepTime,
+               }),
+               table.Entry("search plugin_config list by label ", 
base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Path:         "/apisix/admin/plugin_configs",
+                       Query:        "label=build:16",
+                       Method:       http.MethodGet,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   `"labels":{"build":"16","version":"v1"}`,
+                       Sleep:        base.SleepTime,
+               }),
+               table.Entry("search plugin_config list by label (only key)", 
base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Path:         "/apisix/admin/plugin_configs",
+                       Query:        "label=extra",
+                       Method:       http.MethodGet,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   
`"labels":{"build":"17","extra":"test","version":"v2"}`,
+                       Sleep:        base.SleepTime,
+               }),
+               table.Entry("create route with the plugin config created 
before", base.HttpTestCase{
+                       Object: base.ManagerApiExpect(),
+                       Method: http.MethodPut,
+                       Path:   "/apisix/admin/routes/r1",
+                       Body: `{
+                                "uri": "/hello",
+                                "plugin_config_id": "1",
+                                "upstream": {
+                                        "type": "roundrobin",
+                                       "nodes": [{
+                                               "host": "` + base.UpstreamIp + 
`",
+                                               "port": 1981,
+                                               "weight": 1
+                                       }]
+                                }
+                        }`,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("verify route with header", base.HttpTestCase{
+                       Object:        base.APISIXExpect(),
+                       Method:        http.MethodGet,
+                       Path:          "/hello",
+                       ExpectStatus:  http.StatusOK,
+                       ExpectBody:    "hello world",
+                       ExpectHeaders: map[string]string{"X-VERSION": "1.0"},
+                       Sleep:         base.SleepTime,
+               }),
+               table.Entry("verify route that should be blocked", 
base.HttpTestCase{
+                       Object:        base.APISIXExpect(),
+                       Method:        http.MethodGet,
+                       Path:          "/hello",
+                       Query:         "name=;select%20from%20sys",
+                       ExpectStatus:  http.StatusForbidden,
+                       ExpectHeaders: map[string]string{"X-VERSION": "1.0"},
+                       Sleep:         base.SleepTime,
+               }),
+               table.Entry("update plugin config by patch", base.HttpTestCase{
+                       Object: base.ManagerApiExpect(),
+                       Path:   "/apisix/admin/plugin_configs/1",
+                       Method: http.MethodPatch,
+                       Body: `{
+                               "plugins": {
+                                       "response-rewrite": {
+                                               "headers": {
+                                                       "X-VERSION":"2.0"
+                                               }
+                                       },
+                                       "uri-blocker": {
+                                               "block_rules": ["none"]
+                                       }
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("verify patch update", base.HttpTestCase{
+                       Object:        base.APISIXExpect(),
+                       Method:        http.MethodGet,
+                       Path:          "/hello",
+                       ExpectStatus:  http.StatusOK,
+                       ExpectBody:    "hello world",
+                       ExpectHeaders: map[string]string{"X-VERSION": "2.0"},
+                       Sleep:         base.SleepTime,
+               }),
+               table.Entry("verify patch update(should not block)", 
base.HttpTestCase{
+                       Object:        base.APISIXExpect(),
+                       Method:        http.MethodGet,
+                       Path:          "/hello",
+                       Query:         "name=;select%20from%20sys",
+                       ExpectStatus:  http.StatusOK,
+                       ExpectBody:    "hello world",
+                       ExpectHeaders: map[string]string{"X-VERSION": "2.0"},
+               }),
+               table.Entry("update plugin config by sub path patch", 
base.HttpTestCase{
+                       Object: base.ManagerApiExpect(),
+                       Path:   "/apisix/admin/plugin_configs/1/plugins",
+                       Method: http.MethodPatch,
+                       Body: `{
+                               "response-rewrite": {
+                                       "headers": {
+                                               "X-VERSION":"3.0"
+                                       }
+                               }
+                       }`,
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("verify patch (sub path)", base.HttpTestCase{
+                       Object:        base.APISIXExpect(),
+                       Method:        http.MethodGet,
+                       Path:          "/hello",
+                       ExpectStatus:  http.StatusOK,
+                       ExpectBody:    "hello world",
+                       ExpectHeaders: map[string]string{"X-VERSION": "3.0"},
+                       Sleep:         base.SleepTime,
+               }),
+               table.Entry("delete route", base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Method:       http.MethodDelete,
+                       Path:         "/apisix/admin/routes/r1",
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+               }),
+               table.Entry("delete plugin config", base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Method:       http.MethodDelete,
+                       Path:         "/apisix/admin/plugin_configs/1",
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusOK,
+                       Sleep:        base.SleepTime,
+               }),
+               table.Entry("make sure the plugin config has been deleted", 
base.HttpTestCase{
+                       Object:       base.ManagerApiExpect(),
+                       Method:       http.MethodGet,
+                       Path:         "/apisix/admin/plugin_configs/1",
+                       Headers:      map[string]string{"Authorization": 
base.GetToken()},
+                       ExpectStatus: http.StatusNotFound,
+                       ExpectBody:   `{"code":10001,"message":"data not 
found"`,
+                       Sleep:        base.SleepTime,
+               }),
+               table.Entry("make sure the route has been deleted", 
base.HttpTestCase{
+                       Object:       base.APISIXExpect(),
+                       Method:       http.MethodGet,
+                       Path:         "/hello",
+                       ExpectStatus: http.StatusNotFound,
+                       ExpectBody:   `{"error_msg":"404 Route Not Found"}`,
+                       Sleep:        base.SleepTime,
+               }),
+       )
+})

Reply via email to