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 <majunj...@gmail.com>
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 <majunj...@gmail.com>
    
    * Add validating lua code when create/update routes
    
    also improve the test case in unittest and e2e
    
    Signed-off-by: imjoey <majunj...@gmail.com>
    
    * typo fix and style format
    
    Signed-off-by: imjoey <majunj...@gmail.com>
    
    * Improve testcases
    
    Signed-off-by: imjoey <majunj...@gmail.com>
    
    * Addtional check the Script via log in APISIX
    
    Signed-off-by: imjoey <majunj...@gmail.com>
    
    * ngx.log print log in error.log, instead of access.log
    
    Signed-off-by: imjoey <majunj...@gmail.com>
    
    * Use ngx.WARN instead of INFO to enable output
    
    Signed-off-by: imjoey <majunj...@gmail.com>
---
 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)
+}

Reply via email to