From 5dfe895f92a3d41fab52b1e688c08880bd36665f Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 24 Feb 2021 01:01:09 +0800 Subject: [PATCH 01/17] chore: sync JSON Schema from APISIX --- api/conf/schema.json | 48 ++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/api/conf/schema.json b/api/conf/schema.json index 345e9a0713..3601cab0a4 100644 --- a/api/conf/schema.json +++ b/api/conf/schema.json @@ -168,6 +168,8 @@ "not": { "anyOf": [{ "required": ["plugins", "script"] + }, { + "required": ["plugin_config_id", "script"] }] }, "properties": { @@ -239,6 +241,17 @@ "minLength": 1, "type": "string" }, + "plugin_config_id": { + "anyOf": [{ + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_.]+$", + "type": "string" + }, { + "minimum": 1, + "type": "integer" + }] + }, "plugins": { "type": "object" }, @@ -368,7 +381,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -435,7 +448,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -633,7 +646,7 @@ "enum": ["grpc", "grpcs", "http", "https"] }, "service_name": { - "maxLength": 100, + "maxLength": 256, "minLength": 1, "type": "string" }, @@ -798,7 +811,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -865,7 +878,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -1063,7 +1076,7 @@ "enum": ["grpc", "grpcs", "http", "https"] }, "service_name": { - "maxLength": 100, + "maxLength": 256, "minLength": 1, "type": "string" }, @@ -1308,7 +1321,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -1375,7 +1388,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -1573,7 +1586,7 @@ "enum": ["grpc", "grpcs", "http", "https"] }, "service_name": { - "maxLength": 100, + "maxLength": 256, "minLength": 1, "type": "string" }, @@ -1660,7 +1673,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -1727,7 +1740,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -1925,7 +1938,7 @@ "enum": ["grpc", "grpcs", "http", "https"] }, "service_name": { - "maxLength": 100, + "maxLength": 256, "minLength": 1, "type": "string" }, @@ -2920,6 +2933,11 @@ "policy": { "enum": ["redis"] }, + "redis_database": { + "default": 0, + "minimum": 0, + "type": "integer" + }, "redis_host": { "minLength": 2, "type": "string" @@ -3834,7 +3852,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -3901,7 +3919,7 @@ "uniqueItems": true }, "interval": { - "default": 0, + "default": 1, "minimum": 1, "type": "integer" }, @@ -4099,7 +4117,7 @@ "enum": ["grpc", "grpcs", "http", "https"] }, "service_name": { - "maxLength": 100, + "maxLength": 256, "minLength": 1, "type": "string" }, From 9ad805ebe12b2e554e97d60fd839f3a1c46c2e90 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 24 Feb 2021 01:11:52 +0800 Subject: [PATCH 02/17] chore: add plugin config struct --- api/conf/schema.json | 33 +++++++++++++++++++++++++++++- api/internal/core/entity/entity.go | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/api/conf/schema.json b/api/conf/schema.json index 3601cab0a4..c7784a6db3 100644 --- a/api/conf/schema.json +++ b/api/conf/schema.json @@ -78,6 +78,37 @@ "required": ["plugins"], "type": "object" }, + "plugin_config": { + "additionalProperties": false, + "properties": { + "create_time": { + "type": "integer" + }, + "desc": { + "maxLength": 256, + "type": "string" + }, + "id": { + "anyOf": [{ + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_.]+$", + "type": "string" + }, { + "minimum": 1, + "type": "integer" + }] + }, + "plugins": { + "type": "object" + }, + "update_time": { + "type": "integer" + } + }, + "required": ["id", "plugins"], + "type": "object" + }, "plugins": { "items": { "properties": { @@ -4324,4 +4355,4 @@ "version": 0.1 } } -} +} \ No newline at end of file diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index a0ac8c84cf..5102cec23c 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,10 @@ 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"` +} From 70a80c91e5288f9e92e2bac6eaf50647e58e87a1 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 24 Feb 2021 22:59:25 +0800 Subject: [PATCH 03/17] feat: add plugin config handler --- api/internal/core/store/storehub.go | 13 + .../handler/plugin_config/plugin_config.go | 228 +++++++ .../plugin_config/plugin_config_test.go | 605 ++++++++++++++++++ 3 files changed, 846 insertions(+) create mode 100644 api/internal/handler/plugin_config/plugin_config.go create mode 100644 api/internal/handler/plugin_config/plugin_config_test.go diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go index f3a9c3016d..0e77c94715 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/core/store/storehub.go @@ -36,6 +36,7 @@ const ( HubKeyScript HubKey = "script" HubKeyGlobalRule HubKey = "global_rule" HubKeyServerInfo HubKey = "server_info" + HubKeyPluginConfig HubKey = "plugin_config" ) var ( @@ -177,5 +178,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/plugin_config/plugin_config.go b/api/internal/handler/plugin_config/plugin_config.go new file mode 100644 index 0000000000..03b1e81364 --- /dev/null +++ b/api/internal/handler/plugin_config/plugin_config.go @@ -0,0 +1,228 @@ +/* + * 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" + "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 +} + +func NewHandler() (handler.RouteRegister, error) { + return &Handler{ + pluginConfigStore: store.GetStore(store.HubKeyPluginConfig), + }, 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"` + 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) + + 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) + } + 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) + + 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("%s", err) + return handler.SpecCodeResponse(err), err + } + + res, err := utils.MergePatch(stored, subPath, reqBody) + if err != nil { + log.Warnf("%s", err) + return handler.SpecCodeResponse(err), err + } + + var pluginConfig entity.PluginConfig + if err := json.Unmarshal(res, &pluginConfig); err != nil { + log.Warnf("%s", err) + return handler.SpecCodeResponse(err), err + } + + ret, err := h.pluginConfigStore.Update(c.Context(), &pluginConfig, false) + if err != nil { + log.Warnf("%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 0000000000..bbaa0e3e21 --- /dev/null +++ b/api/internal/handler/plugin_config/plugin_config_test.go @@ -0,0 +1,605 @@ +/* + * 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 ( + "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 key 1", + giveInput: &ListInput{ + Search: "1", + 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"}, + }, + 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", + }, + }, + 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" + } + } + }`), + }, + 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", + }, + }, + }, + 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", + }, + }, + }, + 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", + }, + }, + }, + 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 + wantInput []string + wantErr error + wantRet interface{} + }{ + { + caseDesc: "delete success", + giveInput: &BatchDelete{ + IDs: "1", + }, + wantInput: []string{"1"}, + }, + { + caseDesc: "batch delete success", + giveInput: &BatchDelete{ + IDs: "1,s2", + }, + wantInput: []string{"1", "s2"}, + }, + { + caseDesc: "delete failed", + giveInput: &BatchDelete{ + IDs: "1", + }, + 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) + + h := Handler{pluginConfigStore: pluginConfigStore} + 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) + }) + } +} From 3845477d11f18ac2508533adf0e49bc1ffa65199 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 24 Feb 2021 23:33:50 +0800 Subject: [PATCH 04/17] test: add e2e test cases --- api/internal/route.go | 2 + .../plugin_config/plugin_config_suite_test.go | 36 ++++ .../plugin_config/plugin_config_test.go | 185 ++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 api/test/e2enew/plugin_config/plugin_config_suite_test.go create mode 100644 api/test/e2enew/plugin_config/plugin_config_test.go diff --git a/api/internal/route.go b/api/internal/route.go index 4d5f9b9795..5b7666dd3f 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/e2enew/plugin_config/plugin_config_suite_test.go b/api/test/e2enew/plugin_config/plugin_config_suite_test.go new file mode 100644 index 0000000000..3bb1fcb827 --- /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 0000000000..e082f89430 --- /dev/null +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -0,0 +1,185 @@ +/* + * 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))"] + } + } + }`, + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }), + 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"}, + }), + 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" + } + } + } + }`, + 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 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, + }), + + 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("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("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, + }), + ) +}) From 129181de93b5580df1d8a569209205932b3e3066 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Thu, 25 Feb 2021 09:28:44 +0800 Subject: [PATCH 05/17] test: fix ci --- api/test/e2enew/plugin_config/plugin_config_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index e082f89430..d9255b471c 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -91,6 +91,7 @@ var _ = ginkgo.Describe("Plugin Config", func() { 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(), From b186a696bafb0ddabd3d689b9a4275aa190bb375 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 1 Mar 2021 15:25:21 +0800 Subject: [PATCH 06/17] fix: review --- api/internal/core/store/storehub.go | 18 ++++---- .../handler/data_loader/route_import.go | 2 - .../handler/plugin_config/plugin_config.go | 36 +++++++++++++-- .../plugin_config/plugin_config_test.go | 45 ++++++++++++++++++- api/internal/handler/service/service.go | 1 - 5 files changed, 85 insertions(+), 17 deletions(-) diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go index 0465a47d06..2a96e5031c 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/core/store/storehub.go @@ -29,15 +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" - HubKeyPluginConfig HubKey = "plugin_config" + 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 ( diff --git a/api/internal/handler/data_loader/route_import.go b/api/internal/handler/data_loader/route_import.go index d1ed42598c..b070d968c9 100644 --- a/api/internal/handler/data_loader/route_import.go +++ b/api/internal/handler/data_loader/route_import.go @@ -58,11 +58,9 @@ func NewImportHandler() (handler.RouteRegister, error) { }, nil } - var regPathVar = regexp.MustCompile(`{[\w.]*}`) var regPathRepeat = regexp.MustCompile(`-APISIX-REPEAT-URI-[\d]*`) - func (h *ImportHandler) ApplyRoute(r *gin.Engine) { r.POST("/apisix/admin/import/routes", wgin.Wraps(h.Import, wrapper.InputType(reflect.TypeOf(ImportInput{})))) diff --git a/api/internal/handler/plugin_config/plugin_config.go b/api/internal/handler/plugin_config/plugin_config.go index 03b1e81364..eb91a07b1c 100644 --- a/api/internal/handler/plugin_config/plugin_config.go +++ b/api/internal/handler/plugin_config/plugin_config.go @@ -18,6 +18,7 @@ package plugin_config import ( "encoding/json" + "fmt" "net/http" "reflect" "strings" @@ -37,11 +38,13 @@ import ( 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 } @@ -181,6 +184,31 @@ type BatchDelete struct { 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 } @@ -202,25 +230,25 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) { stored, err := h.pluginConfigStore.Get(c.Context(), id) if err != nil { - log.Warnf("%s", err) + 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("%s", err) + 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("%s", err) + 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("%s", err) + log.Warnf("update failed: %s", err) return handler.SpecCodeResponse(err), err } diff --git a/api/internal/handler/plugin_config/plugin_config_test.go b/api/internal/handler/plugin_config/plugin_config_test.go index bbaa0e3e21..5bf6c26a7a 100644 --- a/api/internal/handler/plugin_config/plugin_config_test.go +++ b/api/internal/handler/plugin_config/plugin_config_test.go @@ -18,6 +18,7 @@ package plugin_config import ( + "errors" "fmt" "net/http" "testing" @@ -553,6 +554,7 @@ func TestPluginConfigs_Delete(t *testing.T) { caseDesc string giveInput *BatchDelete giveErr error + listRet *store.ListOutput wantInput []string wantErr error wantRet interface{} @@ -562,6 +564,10 @@ func TestPluginConfigs_Delete(t *testing.T) { giveInput: &BatchDelete{ IDs: "1", }, + listRet: &store.ListOutput{ + Rows: []interface{}{}, + TotalSize: 0, + }, wantInput: []string{"1"}, }, { @@ -569,13 +575,45 @@ func TestPluginConfigs_Delete(t *testing.T) { 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")), @@ -593,7 +631,12 @@ func TestPluginConfigs_Delete(t *testing.T) { assert.Equal(t, tc.wantInput, input) }).Return(tc.giveErr) - h := Handler{pluginConfigStore: pluginConfigStore} + 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) diff --git a/api/internal/handler/service/service.go b/api/internal/handler/service/service.go index 15c4a76256..174e9ba15d 100644 --- a/api/internal/handler/service/service.go +++ b/api/internal/handler/service/service.go @@ -261,4 +261,3 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) { return ret, nil } - From 6e664b40c34ac8b9d5daab1d91a6301c1eab4bd9 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 1 Mar 2021 16:55:41 +0800 Subject: [PATCH 07/17] fix e2e test failed. --- .../plugin_config/plugin_config_test.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index d9255b471c..308d85733f 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -102,6 +102,9 @@ var _ = ginkgo.Describe("Plugin Config", func() { "response-rewrite": { "headers": { "X-VERSION":"2.0" + }, + "uri-blocker": { + "block_rules": ["none"] } } } @@ -150,14 +153,21 @@ var _ = ginkgo.Describe("Plugin Config", func() { 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, @@ -167,13 +177,6 @@ var _ = ginkgo.Describe("Plugin Config", func() { ExpectBody: `{"code":10001,"message":"data not found"`, 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("make sure the route has been deleted", base.HttpTestCase{ Object: base.APISIXExpect(), Method: http.MethodGet, From 78a5ce49713c47e1d7d5a5e80503e9fe6706949a Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 1 Mar 2021 17:23:57 +0800 Subject: [PATCH 08/17] fix e2e test failed. --- api/test/e2enew/plugin_config/plugin_config_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 308d85733f..a575d2bdf4 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -102,10 +102,10 @@ var _ = ginkgo.Describe("Plugin Config", func() { "response-rewrite": { "headers": { "X-VERSION":"2.0" - }, - "uri-blocker": { - "block_rules": ["none"] } + }, + "uri-blocker": { + "block_rules": ["none"] } } }`, From 498513e3d1e055b88d4ab0a1a4678b1f0b1759f2 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 1 Mar 2021 23:43:42 +0800 Subject: [PATCH 09/17] test: add test case for get plugin_config --- api/conf/schema.json | 2 +- api/test/e2enew/plugin_config/plugin_config_test.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/conf/schema.json b/api/conf/schema.json index de9fc891fa..200d9f229a 100644 --- a/api/conf/schema.json +++ b/api/conf/schema.json @@ -4572,4 +4572,4 @@ "version": 0.1 } } -} \ No newline at end of file +} diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index a575d2bdf4..10a249361f 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -56,6 +56,14 @@ var _ = ginkgo.Describe("Plugin Config", func() { Headers: map[string]string{"Authorization": base.GetToken()}, ExpectStatus: http.StatusOK, }), + table.Entry("create 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))"]}}`, + }), table.Entry("create route with the plugin config created before", base.HttpTestCase{ Object: base.ManagerApiExpect(), Method: http.MethodPut, From e9b95b3ef5fe1104a8aa62b577614bda9c33474a Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 2 Mar 2021 12:25:13 +0800 Subject: [PATCH 10/17] chore: update schema for labels of plugin config --- api/conf/schema.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/conf/schema.json b/api/conf/schema.json index 200d9f229a..e69c57aa6e 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" }, From a6ae7a45d40e6556231eaadfa46369c9b2283850 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 2 Mar 2021 16:01:26 +0800 Subject: [PATCH 11/17] feat: support labels for plugin config --- api/internal/core/entity/entity.go | 1 + .../handler/plugin_config/plugin_config.go | 11 +++ .../plugin_config/plugin_config_test.go | 75 ++++++++++++++++++- .../plugin_config/plugin_config_test.go | 2 +- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index 5102cec23c..b53fb4f1eb 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -264,4 +264,5 @@ 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/handler/plugin_config/plugin_config.go b/api/internal/handler/plugin_config/plugin_config.go index eb91a07b1c..ec09a7f495 100644 --- a/api/internal/handler/plugin_config/plugin_config.go +++ b/api/internal/handler/plugin_config/plugin_config.go @@ -84,6 +84,7 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) { type ListInput struct { Search string `auto_read:"search,query"` + Label string `auto_read:"label,query"` store.Pagination } @@ -123,12 +124,22 @@ type ListInput struct { // "$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, diff --git a/api/internal/handler/plugin_config/plugin_config_test.go b/api/internal/handler/plugin_config/plugin_config_test.go index 5bf6c26a7a..1dbdbbe651 100644 --- a/api/internal/handler/plugin_config/plugin_config_test.go +++ b/api/internal/handler/plugin_config/plugin_config_test.go @@ -172,9 +172,9 @@ func TestPluginConfig_List(t *testing.T) { }, }, { - caseDesc: "list plugin config with key 1", + caseDesc: "list plugin config with label", giveInput: &ListInput{ - Search: "1", + Label: "extra", Pagination: store.Pagination{ PageSize: 10, PageNumber: 10, @@ -185,14 +185,64 @@ func TestPluginConfig_List(t *testing.T) { PageNumber: 10, }, giveData: []*entity.PluginConfig{ - {Desc: "1"}, + { + 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"}, + &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, }, @@ -392,6 +442,9 @@ func TestPluginConfig_Patch(t *testing.T) { "key": "remote_addr", }, }, + Labels: map[string]string{ + "version": "v1", + }, Desc: "desc", } @@ -425,6 +478,10 @@ func TestPluginConfig_Patch(t *testing.T) { "key-auth":{ "key":"auth-one" } + }, + "labels":{ + "version":"v1", + "build":"16" } }`), }, @@ -446,6 +503,10 @@ func TestPluginConfig_Patch(t *testing.T) { "key": "auth-one", }, }, + Labels: map[string]string{ + "version": "v1", + "build": "16", + }, }, pluginConfigInput: "1", pluginConfigRet: existPluginConfig, @@ -475,6 +536,9 @@ func TestPluginConfig_Patch(t *testing.T) { "key": "remote_addr", }, }, + Labels: map[string]string{ + "version": "v1", + }, }, pluginConfigInput: "1", pluginConfigRet: existPluginConfig, @@ -502,6 +566,9 @@ func TestPluginConfig_Patch(t *testing.T) { "key": "remote_addr", }, }, + Labels: map[string]string{ + "version": "v1", + }, }, pluginConfigInput: "1", pluginConfigRet: existPluginConfig, diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 10a249361f..2a4569597e 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -56,7 +56,7 @@ var _ = ginkgo.Describe("Plugin Config", func() { Headers: map[string]string{"Authorization": base.GetToken()}, ExpectStatus: http.StatusOK, }), - table.Entry("create plugin config", base.HttpTestCase{ + table.Entry("get plugin config", base.HttpTestCase{ Object: base.ManagerApiExpect(), Path: "/apisix/admin/plugin_configs/1", Method: http.MethodGet, From c136d2ed2afe30359bc061bdeaa5c11d59b9cc0e Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 2 Mar 2021 16:18:50 +0800 Subject: [PATCH 12/17] test: add e2e test cases --- .../plugin_config/plugin_config_test.go | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 2a4569597e..739030da8d 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -51,6 +51,31 @@ var _ = ginkgo.Describe("Plugin Config", func() { "uri-blocker": { "block_rules": ["select.+(from|limit)", "(?:(union(.*?)select))"] } + }, + Labels: map[string]string{ + "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: map[string]string{ + "version": "v2", + "build": "17", + "extra": "test", } }`, Headers: map[string]string{"Authorization": base.GetToken()}, @@ -64,6 +89,26 @@ var _ = ginkgo.Describe("Plugin Config", func() { ExpectStatus: http.StatusOK, ExpectBody: `"plugins":{"response-rewrite":{"headers":{"X-VERSION":"1.0"}},"uri-blocker":{"block_rules":["select.+(from|limit)","(?:(union(.*?)select))"]}}`, }), + 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, From 7dc77bf53b902d3ab635ffac6f3ef98a79a44b2c Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 2 Mar 2021 16:23:53 +0800 Subject: [PATCH 13/17] fix: go fmt --- api/test/e2enew/plugin_config/plugin_config_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 739030da8d..1d10e8a7fe 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -82,12 +82,12 @@ var _ = ginkgo.Describe("Plugin Config", func() { ExpectStatus: http.StatusOK, }), table.Entry("get plugin config", base.HttpTestCase{ - Object: base.ManagerApiExpect(), - Path: "/apisix/admin/plugin_configs/1", - Method: http.MethodGet, + 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))"]}}`, + ExpectBody: `"plugins":{"response-rewrite":{"headers":{"X-VERSION":"1.0"}},"uri-blocker":{"block_rules":["select.+(from|limit)","(?:(union(.*?)select))"]}}`, }), table.Entry("search plugin_config list by label ", base.HttpTestCase{ Object: base.ManagerApiExpect(), From 74a8e33c6ae66f16e370a98fa987d55ca58df8b6 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 3 Mar 2021 11:30:05 +0800 Subject: [PATCH 14/17] feat: support plugin_config in label list api --- api/internal/handler/label/label.go | 28 ++++++++++------- api/internal/handler/label/label_test.go | 32 +++++++++++++++---- api/test/e2e/label_test.go | 39 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/api/internal/handler/label/label.go b/api/internal/handler/label/label.go index 77574f6e60..791b034cdc 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 25f9fda355..664288d190 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/test/e2e/label_test.go b/api/test/e2e/label_test.go index 690d913866..8f8f9d5e13 100644 --- a/api/test/e2e/label_test.go +++ b/api/test/e2e/label_test.go @@ -124,6 +124,28 @@ func TestLabel(t *testing.T) { Headers: map[string]string{"Authorization": token}, 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), @@ -161,6 +183,15 @@ func TestLabel(t *testing.T) { ExpectStatus: http.StatusOK, 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: "get all label", Object: ManagerApiExpect(t), @@ -302,6 +333,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 { From b3644c830d423fe6a6a8770d216565ce4be11609 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 3 Mar 2021 11:39:47 +0800 Subject: [PATCH 15/17] fix: e2e test failed --- api/test/e2enew/plugin_config/plugin_config_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 1d10e8a7fe..0678a49366 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -52,9 +52,9 @@ var _ = ginkgo.Describe("Plugin Config", func() { "block_rules": ["select.+(from|limit)", "(?:(union(.*?)select))"] } }, - Labels: map[string]string{ + "labels": { "version": "v1", - "build": "16", + "build": "16" } }`, Headers: map[string]string{"Authorization": base.GetToken()}, @@ -72,10 +72,10 @@ var _ = ginkgo.Describe("Plugin Config", func() { } } }, - Labels: map[string]string{ + "labels"": { "version": "v2", "build": "17", - "extra": "test", + "extra": "test" } }`, Headers: map[string]string{"Authorization": base.GetToken()}, @@ -88,6 +88,7 @@ var _ = ginkgo.Describe("Plugin Config", func() { 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(), From 25ea7543ef6392a44f34cf0aaf245e9e4200d875 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 3 Mar 2021 13:50:09 +0800 Subject: [PATCH 16/17] fix: e2e test failed --- api/test/e2enew/plugin_config/plugin_config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/e2enew/plugin_config/plugin_config_test.go b/api/test/e2enew/plugin_config/plugin_config_test.go index 0678a49366..3314cab87e 100644 --- a/api/test/e2enew/plugin_config/plugin_config_test.go +++ b/api/test/e2enew/plugin_config/plugin_config_test.go @@ -72,7 +72,7 @@ var _ = ginkgo.Describe("Plugin Config", func() { } } }, - "labels"": { + "labels": { "version": "v2", "build": "17", "extra": "test" From 6a13c9259f76d1a2ef08c26a450fcdc72c470c3f Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 3 Mar 2021 15:11:39 +0800 Subject: [PATCH 17/17] fix: review --- api/test/e2e/label_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/api/test/e2e/label_test.go b/api/test/e2e/label_test.go index 8f8f9d5e13..5dfbd0ed0e 100644 --- a/api/test/e2e/label_test.go +++ b/api/test/e2e/label_test.go @@ -192,6 +192,37 @@ func TestLabel(t *testing.T) { 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),