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,
+ }),
+ )
+})