You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by bz...@apache.org on 2022/03/17 06:12:15 UTC

[apisix-dashboard] branch master updated: feat: storage grafana path in to etcd (#2362)

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

bzp2010 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 edeed6b  feat: storage grafana path in to etcd (#2362)
edeed6b is described below

commit edeed6ba5097f8d715dbc2c400c051bdb7de1dea
Author: LetsGO <97...@qq.com>
AuthorDate: Thu Mar 17 14:12:10 2022 +0800

    feat: storage grafana path in to etcd (#2362)
---
 api/go.mod                                         |   1 -
 api/go.sum                                         |   3 -
 api/internal/core/entity/entity.go                 |   9 +
 api/internal/core/store/storehub.go                |  13 ++
 .../handler/system_config/system_config.go         | 132 +++++++++++
 .../handler/system_config/system_config_test.go    | 258 +++++++++++++++++++++
 api/internal/route.go                              |   4 +-
 api/test/e2enew/go.mod                             |  32 ---
 api/test/e2enew/go.sum                             |   2 -
 .../system_config/system_config_suite_test.go      |  28 +++
 .../e2enew/system_config/system_config_test.go     | 126 ++++++++++
 11 files changed, 569 insertions(+), 39 deletions(-)

diff --git a/api/go.mod b/api/go.mod
index 257f9b7..691ecfc 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -15,7 +15,6 @@ require (
 	github.com/evanphx/json-patch/v5 v5.1.0
 	github.com/getkin/kin-openapi v0.33.0
 	github.com/gin-contrib/gzip v0.0.3
-	github.com/gin-contrib/pprof v1.3.0
 	github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
 	github.com/gin-gonic/gin v1.6.3
 	github.com/golang-jwt/jwt v3.2.2+incompatible
diff --git a/api/go.sum b/api/go.sum
index a3be366..19d50c6 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -120,14 +120,11 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
 github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
-github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
-github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e h1:8bZpGwoPxkaivQPrAbWl+7zjjUcbFUnYp7yQcx2r2N0=
 github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
 github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
-github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go
index f7d4857..4161696 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -304,3 +304,12 @@ type StreamRoute struct {
 	UpstreamID interface{}            `json:"upstream_id,omitempty"`
 	Plugins    map[string]interface{} `json:"plugins,omitempty"`
 }
+
+// swagger:model SystemConfig
+type SystemConfig struct {
+	ConfigName string                 `json:"config_name"`
+	Desc       string                 `json:"desc,omitempty"`
+	Payload    map[string]interface{} `json:"payload,omitempty"`
+	CreateTime int64                  `json:"create_time,omitempty"`
+	UpdateTime int64                  `json:"update_time,omitempty"`
+}
diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go
index 1184a3f..76d15fe 100644
--- a/api/internal/core/store/storehub.go
+++ b/api/internal/core/store/storehub.go
@@ -40,6 +40,7 @@ const (
 	HubKeyPluginConfig HubKey = "plugin_config"
 	HubKeyProto        HubKey = "proto"
 	HubKeyStreamRoute  HubKey = "stream_route"
+	HubKeySystemConfig HubKey = "system_config"
 )
 
 var (
@@ -229,5 +230,17 @@ func InitStores() error {
 		return err
 	}
 
+	err = InitStore(HubKeySystemConfig, GenericStoreOption{
+		BasePath: conf.ETCDConfig.Prefix + "/system_config",
+		ObjType:  reflect.TypeOf(entity.SystemConfig{}),
+		KeyFunc: func(obj interface{}) string {
+			r := obj.(*entity.SystemConfig)
+			return r.ConfigName
+		},
+	})
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
diff --git a/api/internal/handler/system_config/system_config.go b/api/internal/handler/system_config/system_config.go
new file mode 100644
index 0000000..b74acb3
--- /dev/null
+++ b/api/internal/handler/system_config/system_config.go
@@ -0,0 +1,132 @@
+/*
+ * 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 system_config
+
+import (
+	"errors"
+	"reflect"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/shiningrush/droplet"
+	"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"
+)
+
+type Handler struct {
+	systemConfig store.Interface
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+	return &Handler{
+		systemConfig: store.GetStore(store.HubKeySystemConfig),
+	}, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+	r.GET("/apisix/admin/system_config/:config_name", wgin.Wraps(h.Get,
+		wrapper.InputType(reflect.TypeOf(GetInput{}))))
+	r.POST("/apisix/admin/system_config", wgin.Wraps(h.Post,
+		wrapper.InputType(reflect.TypeOf(entity.SystemConfig{}))))
+	r.PUT("/apisix/admin/system_config", wgin.Wraps(h.Put,
+		wrapper.InputType(reflect.TypeOf(entity.SystemConfig{}))))
+	r.DELETE("/apisix/admin/system_config/:config_name", wgin.Wraps(h.Delete,
+		wrapper.InputType(reflect.TypeOf(DeleteInput{}))))
+}
+
+type GetInput struct {
+	ConfigName string `auto_read:"config_name,path" validate:"required"`
+}
+
+func (h *Handler) Get(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*GetInput)
+	r, err := h.systemConfig.Get(c.Context(), input.ConfigName)
+
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return r, nil
+}
+
+func (h *Handler) Post(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*entity.SystemConfig)
+	input.CreateTime = time.Now().Unix()
+	input.UpdateTime = time.Now().Unix()
+
+	// TODO use json schema to do it
+	if err := h.checkSystemConfig(input); err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	// create
+	res, err := h.systemConfig.Create(c.Context(), input)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return res, nil
+}
+
+func (h *Handler) Put(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*entity.SystemConfig)
+	input.UpdateTime = time.Now().Unix()
+
+	// TODO use json schema to do it
+	if err := h.checkSystemConfig(input); err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	// update
+	res, err := h.systemConfig.Update(c.Context(), input, false)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return res, nil
+}
+
+type DeleteInput struct {
+	ConfigName string `auto_read:"config_name,path" validate:"required"`
+}
+
+func (h *Handler) Delete(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*DeleteInput)
+	err := h.systemConfig.BatchDelete(c.Context(), []string{input.ConfigName})
+
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return nil, nil
+}
+
+func (h *Handler) checkSystemConfig(input *entity.SystemConfig) error {
+	if len(input.ConfigName) < 1 || len(input.ConfigName) > 100 {
+		return errors.New("invalid params: config_name length must be between 1 and 100")
+	}
+
+	if len(input.Payload) < 1 {
+		return errors.New("invalid params: payload is required")
+	}
+
+	return nil
+}
diff --git a/api/internal/handler/system_config/system_config_test.go b/api/internal/handler/system_config/system_config_test.go
new file mode 100644
index 0000000..ca5d6ac
--- /dev/null
+++ b/api/internal/handler/system_config/system_config_test.go
@@ -0,0 +1,258 @@
+/*
+ * 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 system_config
+
+import (
+	"errors"
+	"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"
+)
+
+func TestSystem_Get(t *testing.T) {
+	t.Parallel()
+	type testCase struct {
+		caseDesc  string
+		giveInput *GetInput
+		wantErr   error
+		wantRet   interface{}
+		mockStore store.Interface
+		mockFunc  func(tc *testCase)
+	}
+
+	cases := []*testCase{
+		{
+			caseDesc:  "system config not found",
+			giveInput: &GetInput{ConfigName: "grafana"},
+			wantErr:   data.ErrNotFound,
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Get", mock.Anything, mock.Anything).Return(nil, tc.wantErr)
+				tc.mockStore = mockStore
+			},
+		},
+		{
+			caseDesc:  "get system config success",
+			giveInput: &GetInput{ConfigName: "grafana"},
+			wantErr:   nil,
+			wantRet: entity.SystemConfig{
+				ConfigName: "grafana",
+				Payload: map[string]interface{}{
+					"url": "http://127.0.0.1:3000",
+				},
+			},
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Get", mock.Anything, mock.Anything).Return(tc.wantRet, nil)
+				tc.mockStore = mockStore
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			tc.mockFunc(tc)
+			h := Handler{tc.mockStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Get(ctx)
+			assert.Equal(t, err, tc.wantErr)
+			if err == nil {
+				assert.Equal(t, ret, tc.wantRet)
+			}
+		})
+	}
+}
+
+func TestSystem_Post(t *testing.T) {
+	t.Parallel()
+	type testCase struct {
+		caseDesc  string
+		giveInput *entity.SystemConfig
+		wantErr   error
+		wantRet   interface{}
+		mockStore store.Interface
+		mockFunc  func(tc *testCase)
+	}
+
+	systemConfig := entity.SystemConfig{
+		ConfigName: "grafana",
+		Payload: map[string]interface{}{
+			"url": "http://127.0.0.1:3000",
+		},
+	}
+
+	cases := []*testCase{
+		{
+			caseDesc:  "create system config error",
+			giveInput: &systemConfig,
+			wantErr:   errors.New("mock error"),
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Create", mock.Anything, mock.Anything).Return(nil, tc.wantErr)
+				tc.mockStore = mockStore
+			},
+		},
+		{
+			caseDesc:  "create system config success",
+			giveInput: &systemConfig,
+			wantErr:   nil,
+			wantRet: entity.SystemConfig{
+				ConfigName: "grafana",
+				Payload: map[string]interface{}{
+					"url": "http://127.0.0.1:3000",
+				},
+			},
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Create", mock.Anything, mock.Anything).Return(tc.wantRet, nil)
+				tc.mockStore = mockStore
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			tc.mockFunc(tc)
+			h := Handler{tc.mockStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Post(ctx)
+			assert.Equal(t, err, tc.wantErr)
+			if err == nil {
+				assert.Equal(t, ret, tc.wantRet)
+			}
+		})
+	}
+}
+
+func TestSystem_Put(t *testing.T) {
+	t.Parallel()
+	type testCase struct {
+		caseDesc  string
+		giveInput *entity.SystemConfig
+		wantErr   error
+		wantRet   interface{}
+		mockStore store.Interface
+		mockFunc  func(tc *testCase)
+	}
+
+	systemConfig := entity.SystemConfig{
+		ConfigName: "grafana",
+		Payload: map[string]interface{}{
+			"url": "http://127.0.0.1:3000",
+		},
+	}
+
+	cases := []*testCase{
+		{
+			caseDesc:  "update system config error",
+			giveInput: &systemConfig,
+			wantErr:   errors.New("mock error"),
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil, tc.wantErr)
+				tc.mockStore = mockStore
+			},
+		},
+		{
+			caseDesc:  "update system config success",
+			giveInput: &systemConfig,
+			wantErr:   nil,
+			wantRet: entity.SystemConfig{
+				ConfigName: "grafana",
+				Payload: map[string]interface{}{
+					"url": "http://127.0.0.1:3000",
+				},
+			},
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(tc.wantRet, nil)
+				tc.mockStore = mockStore
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			tc.mockFunc(tc)
+			h := Handler{tc.mockStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Put(ctx)
+			assert.Equal(t, err, tc.wantErr)
+			if err == nil {
+				assert.Equal(t, ret, tc.wantRet)
+			}
+		})
+	}
+}
+
+func TestSystem_Delete(t *testing.T) {
+	t.Parallel()
+	type testCase struct {
+		caseDesc  string
+		giveInput *DeleteInput
+		wantErr   error
+		wantRet   interface{}
+		mockStore store.Interface
+		mockFunc  func(tc *testCase)
+	}
+
+	cases := []*testCase{
+		{
+			caseDesc:  "delete system config error",
+			giveInput: &DeleteInput{ConfigName: "grafana"},
+			wantErr:   errors.New("mock error"),
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("BatchDelete", mock.Anything, mock.Anything).Return(tc.wantErr)
+				tc.mockStore = mockStore
+			},
+		},
+		{
+			caseDesc:  "delete system config success",
+			giveInput: &DeleteInput{ConfigName: "grafana"},
+			wantErr:   nil,
+			mockFunc: func(tc *testCase) {
+				mockStore := &store.MockInterface{}
+				mockStore.On("BatchDelete", mock.Anything, mock.Anything).Return(tc.wantRet)
+				tc.mockStore = mockStore
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.caseDesc, func(t *testing.T) {
+			tc.mockFunc(tc)
+			h := Handler{tc.mockStore}
+			ctx := droplet.NewContext()
+			ctx.SetInput(tc.giveInput)
+			ret, err := h.Delete(ctx)
+			assert.Equal(t, err, tc.wantErr)
+			if err == nil {
+				assert.Equal(t, ret, tc.wantRet)
+			}
+		})
+	}
+}
diff --git a/api/internal/route.go b/api/internal/route.go
index 5ece4e3..7eddc9c 100644
--- a/api/internal/route.go
+++ b/api/internal/route.go
@@ -21,6 +21,7 @@ import (
 	"path/filepath"
 
 	// "github.com/gin-contrib/pprof"
+	"github.com/gin-contrib/gzip"
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 
@@ -42,10 +43,10 @@ import (
 	"github.com/apisix/manager-api/internal/handler/service"
 	"github.com/apisix/manager-api/internal/handler/ssl"
 	"github.com/apisix/manager-api/internal/handler/stream_route"
+	"github.com/apisix/manager-api/internal/handler/system_config"
 	"github.com/apisix/manager-api/internal/handler/tool"
 	"github.com/apisix/manager-api/internal/handler/upstream"
 	"github.com/apisix/manager-api/internal/log"
-	"github.com/gin-contrib/gzip"
 )
 
 func SetUpRouter() *gin.Engine {
@@ -83,6 +84,7 @@ func SetUpRouter() *gin.Engine {
 		migrate.NewHandler,
 		proto.NewHandler,
 		stream_route.NewHandler,
+		system_config.NewHandler,
 	}
 
 	for i := range factories {
diff --git a/api/test/e2enew/go.mod b/api/test/e2enew/go.mod
index 94fdd65..0d6c7fb 100644
--- a/api/test/e2enew/go.mod
+++ b/api/test/e2enew/go.mod
@@ -9,35 +9,3 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/tidwall/gjson v1.11.0
 )
-
-require (
-	github.com/ajg/form v1.5.1 // indirect
-	github.com/andybalholm/brotli v1.0.2 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/fatih/structs v1.0.0 // indirect
-	github.com/fsnotify/fsnotify v1.4.9 // indirect
-	github.com/google/go-querystring v1.0.0 // indirect
-	github.com/gorilla/websocket v1.4.2 // indirect
-	github.com/imkira/go-interpol v1.0.0 // indirect
-	github.com/klauspost/compress v1.12.2 // indirect
-	github.com/nxadm/tail v1.4.8 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/sergi/go-diff v1.0.0 // indirect
-	github.com/tidwall/match v1.1.1 // indirect
-	github.com/tidwall/pretty v1.2.0 // indirect
-	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.27.0 // indirect
-	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
-	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
-	github.com/xeipuuv/gojsonschema v1.1.0 // indirect
-	github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
-	github.com/yudai/gojsondiff v1.0.0 // indirect
-	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
-	golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
-	golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
-	golang.org/x/text v0.3.6 // indirect
-	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
-	moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e // indirect
-)
diff --git a/api/test/e2enew/go.sum b/api/test/e2enew/go.sum
index b0dbf1b..b6d85c7 100644
--- a/api/test/e2enew/go.sum
+++ b/api/test/e2enew/go.sum
@@ -14,7 +14,6 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/gavv/httpexpect/v2 v2.3.1 h1:sGLlKMn8AuHS9ztK9Sb7AJ7OxIL8v2PcLdyxfKt1Fo4=
 github.com/gavv/httpexpect/v2 v2.3.1/go.mod h1:yOE8m/aqFYQDNrgprMeXgq4YynfN9h1NgcE1+1suV64=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -137,7 +136,6 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/api/test/e2enew/system_config/system_config_suite_test.go b/api/test/e2enew/system_config/system_config_suite_test.go
new file mode 100644
index 0000000..122ca1f
--- /dev/null
+++ b/api/test/e2enew/system_config/system_config_suite_test.go
@@ -0,0 +1,28 @@
+/*
+ * 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 system_config
+
+import (
+	"github.com/onsi/ginkgo"
+	"github.com/onsi/gomega"
+	"testing"
+)
+
+func TestSystemConfig(t *testing.T) {
+	gomega.RegisterFailHandler(ginkgo.Fail)
+	ginkgo.RunSpecs(t, "system config suite")
+}
diff --git a/api/test/e2enew/system_config/system_config_test.go b/api/test/e2enew/system_config/system_config_test.go
new file mode 100644
index 0000000..7fc2166
--- /dev/null
+++ b/api/test/e2enew/system_config/system_config_test.go
@@ -0,0 +1,126 @@
+/*
+ * 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 system_config
+
+import (
+	"net/http"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/apisix/manager-api/test/e2enew/base"
+)
+
+var _ = Describe("system config", func() {
+	DescribeTable("test system config data CURD",
+		func(tc base.HttpTestCase) {
+			base.RunTestCase(tc)
+		},
+
+		Entry("get system config should get not found error", base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/system_config/grafana",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusNotFound,
+		}),
+
+		Entry("create system config should get schema validate failed error", base.HttpTestCase{
+			Object: base.ManagerApiExpect(),
+			Method: http.MethodPost,
+			Path:   "/apisix/admin/system_config",
+			Body: `{
+				"config_name": "",
+				"payload": {"url":"http://127.0.0.1:3000"}
+			}`,
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusBadRequest,
+		}),
+
+		Entry("create system config should success", base.HttpTestCase{
+			Object: base.ManagerApiExpect(),
+			Method: http.MethodPost,
+			Path:   "/apisix/admin/system_config",
+			Body: `{
+				"config_name": "grafana",
+				"payload": {"url":"http://127.0.0.1:3000"}
+			}`,
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+			ExpectBody:   "\"config_name\":\"grafana\",\"payload\":{\"url\":\"http://127.0.0.1:3000\"}",
+		}),
+
+		Entry("after create system config get config should succeed", base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/system_config/grafana",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+			ExpectBody:   "\"config_name\":\"grafana\",\"payload\":{\"url\":\"http://127.0.0.1:3000\"}",
+		}),
+
+		Entry("update system config should get schema validate failed error", base.HttpTestCase{
+			Object: base.ManagerApiExpect(),
+			Method: http.MethodPut,
+			Path:   "/apisix/admin/system_config",
+			Body: `{
+				"config_name": "",
+				"payload": {"url":"http://127.0.0.1:2000"}
+			}`,
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusBadRequest,
+		}),
+
+		Entry("update system config should success", base.HttpTestCase{
+			Object: base.ManagerApiExpect(),
+			Method: http.MethodPut,
+			Path:   "/apisix/admin/system_config",
+			Body: `{
+				"config_name": "grafana",
+				"payload": {"url":"http://127.0.0.1:2000"}
+			}`,
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+			ExpectBody:   "\"config_name\":\"grafana\",\"payload\":{\"url\":\"http://127.0.0.1:2000\"}",
+		}),
+
+		Entry("after update system config get config should succeed", base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/system_config/grafana",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+			ExpectBody:   "\"config_name\":\"grafana\",\"payload\":{\"url\":\"http://127.0.0.1:2000\"}",
+		}),
+
+		Entry("delete system config should success", base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/system_config/grafana",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		}),
+
+		Entry("get system config should get not found error", base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/system_config/grafana",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusNotFound,
+		}),
+	)
+})