This is an automated email from the ASF dual-hosted git repository.
juzhiyuan 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 b9a0227 fix: Support string type for the script field in Route (#1289)
b9a0227 is described below
commit b9a0227f30ac3f2bf843ab01f37e240a8bd30730
Author: Joey <[email protected]>
AuthorDate: Fri Jan 15 12:45:29 2021 +0800
fix: Support string type for the script field in Route (#1289)
* Support string type for script field in Route
Signed-off-by: imjoey <[email protected]>
* Add validating lua code when create/update routes
also improve the test case in unittest and e2e
Signed-off-by: imjoey <[email protected]>
* typo fix and style format
Signed-off-by: imjoey <[email protected]>
* Improve testcases
Signed-off-by: imjoey <[email protected]>
* Addtional check the Script via log in APISIX
Signed-off-by: imjoey <[email protected]>
* ngx.log print log in error.log, instead of access.log
Signed-off-by: imjoey <[email protected]>
* Use ngx.WARN instead of INFO to enable output
Signed-off-by: imjoey <[email protected]>
---
api/internal/handler/route/route.go | 50 +++++---
api/internal/handler/route/route_test.go | 149 ++++++++++++++++++++++-
api/internal/utils/utils.go | 8 ++
api/internal/utils/utils_test.go | 11 ++
api/test/e2e/base.go | 31 +++++
api/test/e2e/route_with_script_luacode_test.go | 156 +++++++++++++++++++++++++
6 files changed, 389 insertions(+), 16 deletions(-)
diff --git a/api/internal/handler/route/route.go
b/api/internal/handler/route/route.go
index 89083ed..73f0fa6 100644
--- a/api/internal/handler/route/route.go
+++ b/api/internal/handler/route/route.go
@@ -30,8 +30,9 @@ import (
"github.com/shiningrush/droplet"
"github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/wrapper"
- "github.com/yuin/gopher-lua"
wgin "github.com/shiningrush/droplet/wrapper/gin"
+ lua "github.com/yuin/gopher-lua"
+
"github.com/apisix/manager-api/internal/conf"
"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/core/store"
@@ -327,12 +328,25 @@ func (h *Handler) Create(c droplet.Context) (interface{},
error) {
script := &entity.Script{}
script.ID = utils.InterfaceToString(input.ID)
script.Script = input.Script
- //to lua
+
var err error
- input.Script, err =
generateLuaCode(input.Script.(map[string]interface{}))
- if err != nil {
- return nil, err
+ // Explicitly to lua if input script is of the map type,
otherwise
+ // it will always represent a piece of lua code of the string
type.
+ if scriptConf, ok := input.Script.(map[string]interface{}); ok {
+ // For lua code of map type, syntax validation is done
by
+ // the generateLuaCode function
+ input.Script, err = generateLuaCode(scriptConf)
+ if err != nil {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
+ } else {
+ // For lua code of string type, use utility func to
syntax validation
+ err = utils.ValidateLuaCode(input.Script.(string))
+ if err != nil {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
}
+
//save original conf
if err = h.scriptStore.Create(c.Context(), script); err != nil {
return nil, err
@@ -392,17 +406,25 @@ func (h *Handler) Update(c droplet.Context) (interface{},
error) {
script := &entity.Script{}
script.ID = input.ID
script.Script = input.Script
- //to lua
+
var err error
- scriptConf, ok := input.Script.(map[string]interface{})
- if !ok {
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
- fmt.Errorf("invalid `script`")
- }
- input.Route.Script, err = generateLuaCode(scriptConf)
- if err != nil {
- return &data.SpecCodeResponse{StatusCode:
http.StatusInternalServerError}, err
+ // Explicitly to lua if input script is of the map type,
otherwise
+ // it will always represent a piece of lua code of the string
type.
+ if scriptConf, ok := input.Script.(map[string]interface{}); ok {
+ // For lua code of map type, syntax validation is done
by
+ // the generateLuaCode function
+ input.Route.Script, err = generateLuaCode(scriptConf)
+ if err != nil {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
+ } else {
+ // For lua code of string type, use utility func to
syntax validation
+ err = utils.ValidateLuaCode(input.Script.(string))
+ if err != nil {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
}
+
//save original conf
if err = h.scriptStore.Update(c.Context(), script, true); err
!= nil {
//if not exists, create
diff --git a/api/internal/handler/route/route_test.go
b/api/internal/handler/route/route_test.go
index 4618755..5630d31 100644
--- a/api/internal/handler/route/route_test.go
+++ b/api/internal/handler/route/route_test.go
@@ -938,7 +938,7 @@ func TestRoute(t *testing.T) {
dataPage = retPage.(*store.ListOutput)
assert.Equal(t, len(dataPage.Rows), 1)
- //sleep
+ //sleep
time.Sleep(time.Duration(100) * time.Millisecond)
// list search and status not match
@@ -1197,7 +1197,7 @@ func TestRoute(t *testing.T) {
assert.Nil(t, err)
}
-func Test_Route_With_Script(t *testing.T) {
+func Test_Route_With_Script_Dag2lua(t *testing.T) {
// init
err := storage.InitETCDClient(conf.ETCDConfig)
assert.Nil(t, err)
@@ -1349,3 +1349,148 @@ func Test_Route_With_Script(t *testing.T) {
_, err = handler.BatchDelete(ctx)
assert.Nil(t, err)
}
+
+func Test_Route_With_Script_Luacode(t *testing.T) {
+ // init
+ err := storage.InitETCDClient(conf.ETCDConfig)
+ assert.Nil(t, err)
+ err = store.InitStores()
+ assert.Nil(t, err)
+
+ handler := &Handler{
+ routeStore: store.GetStore(store.HubKeyRoute),
+ svcStore: store.GetStore(store.HubKeyService),
+ upstreamStore: store.GetStore(store.HubKeyUpstream),
+ scriptStore: store.GetStore(store.HubKeyScript),
+ }
+ assert.NotNil(t, handler)
+
+ // create with script of valid lua syntax
+ ctx := droplet.NewContext()
+ route := &entity.Route{}
+ reqBody := `{
+ "id": "1",
+ "uri": "/index.html",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "www.a.com",
+ "port": 80,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
+ }`
+ err = json.Unmarshal([]byte(reqBody), route)
+ assert.Nil(t, err)
+ ctx.SetInput(route)
+ _, err = handler.Create(ctx)
+ assert.Nil(t, err)
+
+ // sleep
+ time.Sleep(time.Duration(20) * time.Millisecond)
+
+ // get
+ input := &GetInput{}
+ input.ID = "1"
+ ctx.SetInput(input)
+ ret, err := handler.Get(ctx)
+ stored := ret.(*entity.Route)
+ assert.Nil(t, err)
+ assert.Equal(t, stored.ID, route.ID)
+ assert.Equal(t, "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M", stored.Script)
+
+ // update via empty script
+ route2 := &UpdateInput{}
+ route2.ID = "1"
+ reqBody = `{
+ "id": "1",
+ "uri": "/index.html",
+ "enable_websocket": true,
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "www.a.com",
+ "port": 80,
+ "weight": 1
+ }]
+ }
+ }`
+
+ err = json.Unmarshal([]byte(reqBody), route2)
+ assert.Nil(t, err)
+ ctx.SetInput(route2)
+ _, err = handler.Update(ctx)
+ assert.Nil(t, err)
+
+ //sleep
+ time.Sleep(time.Duration(100) * time.Millisecond)
+
+ //get, script should be nil
+ input = &GetInput{}
+ input.ID = "1"
+ ctx.SetInput(input)
+ ret, err = handler.Get(ctx)
+ stored = ret.(*entity.Route)
+ assert.Nil(t, err)
+ assert.Equal(t, stored.ID, route.ID)
+ assert.Nil(t, stored.Script)
+
+ // 2nd update via invalid script
+ input3 := &UpdateInput{}
+ input3.ID = "1"
+ reqBody = `{
+ "id": "1",
+ "uri": "/index.html",
+ "enable_websocket": true,
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "www.a.com",
+ "port": 80,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\")"
+ }`
+
+ err = json.Unmarshal([]byte(reqBody), input3)
+ assert.Nil(t, err)
+ ctx.SetInput(input3)
+ _, err = handler.Update(ctx)
+ // err should NOT be nil
+ assert.NotNil(t, err)
+
+ // delete test data
+ inputDel := &BatchDelete{}
+ reqBody = `{"ids": "1"}`
+ err = json.Unmarshal([]byte(reqBody), inputDel)
+ assert.Nil(t, err)
+ ctx.SetInput(inputDel)
+ _, err = handler.BatchDelete(ctx)
+ assert.Nil(t, err)
+
+ // 2nd create with script of invalid lua syntax
+ ctx = droplet.NewContext()
+ route = &entity.Route{}
+ reqBody = `{
+ "id": "1",
+ "uri": "/index.html",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "www.a.com",
+ "port": 80,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\")"
+ }`
+ err = json.Unmarshal([]byte(reqBody), route)
+ assert.Nil(t, err)
+ ctx.SetInput(route)
+ ret, err = handler.Create(ctx)
+ assert.NotNil(t, err)
+ assert.EqualError(t, err, "<string> at EOF: syntax error\n")
+ assert.Equal(t, http.StatusBadRequest,
ret.(*data.SpecCodeResponse).StatusCode)
+}
diff --git a/api/internal/utils/utils.go b/api/internal/utils/utils.go
index 6b7ea10..0a612c2 100644
--- a/api/internal/utils/utils.go
+++ b/api/internal/utils/utils.go
@@ -26,6 +26,7 @@ import (
"strings"
"github.com/sony/sonyflake"
+ "github.com/yuin/gopher-lua/parse"
)
var _sf *sonyflake.Sonyflake
@@ -161,3 +162,10 @@ func LabelContains(labels map[string]string, reqLabels
map[string]struct{}) bool
return false
}
+
+// ValidateLuaCode validates lua syntax for input code, return nil
+// if passed, otherwise a non-nil error will be returned
+func ValidateLuaCode(code string) error {
+ _, err := parse.Parse(strings.NewReader(code), "<string>")
+ return err
+}
diff --git a/api/internal/utils/utils_test.go b/api/internal/utils/utils_test.go
index 1d7c154..5cec1e8 100644
--- a/api/internal/utils/utils_test.go
+++ b/api/internal/utils/utils_test.go
@@ -117,3 +117,14 @@ func TestLabelContains(t *testing.T) {
}
assert.True(t, LabelContains(mp, reqMap))
}
+
+func TestValidateLuaCode(t *testing.T) {
+ validLuaCode := "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
+ err := ValidateLuaCode(validLuaCode)
+ assert.Nil(t, err)
+
+ invalidLuaCode := "local _M = {} \n function _M.access(api_ctx) \n
ngx.log(ngx.WARN,\"hit access phase\")"
+ err = ValidateLuaCode(invalidLuaCode)
+ assert.NotNil(t, err)
+ assert.Equal(t, "<string> at EOF: syntax error\n", err.Error())
+}
diff --git a/api/test/e2e/base.go b/api/test/e2e/base.go
index 8f7b23a..08fa71e 100644
--- a/api/test/e2e/base.go
+++ b/api/test/e2e/base.go
@@ -307,3 +307,34 @@ func CleanAPISIXErrorLog(t *testing.T) {
}
assert.Nil(t, err)
}
+
+// ReadAPISIXAccessLog reads the access log of APISIX.
+func ReadAPISIXAccessLog(t *testing.T) string {
+ cmd := exec.Command("pwd")
+ pwdByte, err := cmd.CombinedOutput()
+ pwd := string(pwdByte)
+ pwd = strings.Replace(pwd, "\n", "", 1)
+ pwd = pwd[:strings.Index(pwd, "/e2e")]
+ bytes, err := ioutil.ReadFile(pwd + "/docker/apisix_logs/access.log")
+ assert.Nil(t, err)
+ logContent := string(bytes)
+
+ return logContent
+}
+
+// CleanAPISIXAccessLog cleans the access log of APISIX.
+// It's always recommended to call this function before checking
+// its content.
+func CleanAPISIXAccessLog(t *testing.T) {
+ cmd := exec.Command("pwd")
+ pwdByte, err := cmd.CombinedOutput()
+ pwd := string(pwdByte)
+ pwd = strings.Replace(pwd, "\n", "", 1)
+ pwd = pwd[:strings.Index(pwd, "/e2e")]
+ cmd = exec.Command("sudo", "echo", " > ",
pwd+"/docker/apisix_logs/access.log")
+ _, err = cmd.CombinedOutput()
+ if err != nil {
+ fmt.Println("cmd error:", err.Error())
+ }
+ assert.Nil(t, err)
+}
diff --git a/api/test/e2e/route_with_script_luacode_test.go
b/api/test/e2e/route_with_script_luacode_test.go
new file mode 100644
index 0000000..fdfb212
--- /dev/null
+++ b/api/test/e2e/route_with_script_luacode_test.go
@@ -0,0 +1,156 @@
+/*
+ * 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 e2e
+
+import (
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRoute_with_script_lucacode(t *testing.T) {
+
+ // clean error log
+ CleanAPISIXErrorLog(t)
+
+ tests := []HttpTestCase{
+ {
+ Desc: "create route with script of valid lua code",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r1",
+ Body: `{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function
_M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ Desc: "get the route",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodGet,
+ Path: "/apisix/admin/routes/r1",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "hit access phase",
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "hit the route",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/hello",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "hello world\n",
+ },
+ {
+ Desc: "update route with script of valid lua code",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r1",
+ Body: `{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1981,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function
_M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ Desc: "update route with script of invalid lua code",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r1",
+ Body: `{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function
_M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\")"
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusBadRequest,
+ },
+ {
+ Desc: "delete the route (r1)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodDelete,
+ Path: "/apisix/admin/routes/r1",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "create route with script of invalid lua code",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r1",
+ Body: `{
+ "uri": "/hello",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }]
+ },
+ "script": "local _M = {} \n function
_M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\")"
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusBadRequest,
+ },
+ }
+
+ for _, tc := range tests {
+ testCaseCheck(tc, t)
+ }
+
+ // sleep for process log
+ time.Sleep(1500 * time.Millisecond)
+
+ // verify the log generated by script set in Step-3 above
+ logContent := ReadAPISIXErrorLog(t)
+ assert.Contains(t, logContent, "hit access phase")
+
+ // clean log
+ CleanAPISIXErrorLog(t)
+}