You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by su...@apache.org on 2020/12/18 07:47:26 UTC
[apisix-dashboard] branch master updated: feat: support search
route by label (#1061)
This is an automated email from the ASF dual-hosted git repository.
sunyi 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 c2e0e6f feat: support search route by label (#1061)
c2e0e6f is described below
commit c2e0e6fe511e534c071cb460f49727427634126f
Author: Peter Zhu <st...@gmail.com>
AuthorDate: Fri Dec 18 15:47:18 2020 +0800
feat: support search route by label (#1061)
* feat: support search route by label (#861)
* doc: add search by route
* feat: add more unit test
* update: check the label parameter
* fix: unit test
---
api/internal/handler/route/route.go | 31 +++--
api/internal/handler/route/route_test.go | 117 +++++++++++++++---
api/internal/utils/utils.go | 48 ++++++++
api/internal/utils/utils_test.go | 40 +++++++
api/test/e2e/route_with_management_fileds_test.go | 137 +++++++++++++++++++++-
docs/api/api.md | 1 +
6 files changed, 345 insertions(+), 29 deletions(-)
diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go
index 01ef7bf..da7a9d7 100644
--- a/api/internal/handler/route/route.go
+++ b/api/internal/handler/route/route.go
@@ -106,6 +106,11 @@ type GetInput struct {
// description: uri of route
// required: false
// type: string
+// - name: label
+// in: query
+// description: label of route
+// required: false
+// type: string
// responses:
// '0':
// description: list response
@@ -141,8 +146,9 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) {
}
type ListInput struct {
- Name string `auto_read:"name,query"`
- URI string `auto_read:"uri,query"`
+ Name string `auto_read:"name,query"`
+ URI string `auto_read:"uri,query"`
+ Label string `auto_read:"label,query"`
store.Pagination
}
@@ -161,21 +167,26 @@ func uriContains(obj *entity.Route, uri string) bool {
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.routeStore.List(store.ListInput{
Predicate: func(obj interface{}) bool {
- if input.Name != "" && input.URI != "" {
- if strings.Contains(obj.(*entity.Route).Name, input.Name) {
- return uriContains(obj.(*entity.Route), input.URI)
- }
+ if input.Name != "" && !strings.Contains(obj.(*entity.Route).Name, input.Name) {
return false
}
- if input.Name != "" {
- return strings.Contains(obj.(*entity.Route).Name, input.Name)
+
+ if input.URI != "" && !uriContains(obj.(*entity.Route), input.URI) {
+ return false
}
- if input.URI != "" {
- return uriContains(obj.(*entity.Route), input.URI)
+
+ if input.Label != "" && !utils.LabelContains(obj.(*entity.Route).Labels, labelMap) {
+ return false
}
+
return true
},
Format: func(obj interface{}) interface{} {
diff --git a/api/internal/handler/route/route_test.go b/api/internal/handler/route/route_test.go
index bb906f4..39214b9 100644
--- a/api/internal/handler/route/route_test.go
+++ b/api/internal/handler/route/route_test.go
@@ -415,6 +415,10 @@ func TestRoute(t *testing.T) {
"hosts": ["foo.com", "*.bar.com"],
"remote_addrs": ["127.0.0.0/8"],
"methods": ["PUT", "GET"],
+ "labels": {
+ "l1": "v1",
+ "l2": "v2"
+ },
"upstream": {
"type": "roundrobin",
"nodes": [{
@@ -765,38 +769,115 @@ func TestRoute(t *testing.T) {
assert.Equal(t, len(dataPage.Rows), 1)
//list search match
- listInput2 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 1, "page": 1, "name": "a", "uri": "index"}`
- err = json.Unmarshal([]byte(reqBody), listInput2)
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput2)
+ ctx.SetInput(listInput)
retPage, err = handler.List(ctx)
assert.Nil(t, err)
dataPage = retPage.(*store.ListOutput)
assert.Equal(t, len(dataPage.Rows), 1)
//list search name not match
- listInput3 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 1, "page": 1, "name": "not-exists", "uri": "index"}`
- err = json.Unmarshal([]byte(reqBody), listInput3)
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput3)
+ ctx.SetInput(listInput)
retPage, err = handler.List(ctx)
assert.Nil(t, err)
dataPage = retPage.(*store.ListOutput)
assert.Equal(t, len(dataPage.Rows), 0)
//list search uri not match
- listInput4 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 1, "page": 1, "name": "a", "uri": "not-exists"}`
- err = json.Unmarshal([]byte(reqBody), listInput4)
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 0)
+
+ //list search label not match
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "label":"l3"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 0)
+
+ //list search label match
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "label":"l1"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 1)
+
+ //list search label match
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "label":"l1:v1"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 1)
+
+ //list search and label not match
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "label":"l1:v2"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput4)
+ ctx.SetInput(listInput)
retPage, err = handler.List(ctx)
assert.Nil(t, err)
dataPage = retPage.(*store.ListOutput)
assert.Equal(t, len(dataPage.Rows), 0)
+ //list search with name and label
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "name": "a", "label":"l1:v1"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 1)
+
+ //list search with uri and label
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "uri": "index", "label":"l1:v1"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 1)
+
+ //list search with uri,name and label
+ listInput = &ListInput{}
+ reqBody = `{"page_size": 1, "page": 1, "name": "a", "uri": "index", "label":"l1:v1"}`
+ err = json.Unmarshal([]byte(reqBody), listInput)
+ assert.Nil(t, err)
+ ctx.SetInput(listInput)
+ retPage, err = handler.List(ctx)
+ assert.Nil(t, err)
+ dataPage = retPage.(*store.ListOutput)
+ assert.Equal(t, len(dataPage.Rows), 1)
+
//create route using uris
route3 := &entity.Route{}
reqBody = `{
@@ -821,11 +902,11 @@ func TestRoute(t *testing.T) {
time.Sleep(time.Duration(100) * time.Millisecond)
//list search match uris
- listInput5 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 1, "page": 1, "name": "bbb", "uri": "bb"}`
- err = json.Unmarshal([]byte(reqBody), listInput5)
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput5)
+ ctx.SetInput(listInput)
retPage, err = handler.List(ctx)
assert.Nil(t, err)
dataPage = retPage.(*store.ListOutput)
@@ -959,20 +1040,20 @@ func TestRoute(t *testing.T) {
assert.Equal(t, "11", stored.ID)
//list
- listInput11 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 10, "page": 1}`
- err = json.Unmarshal([]byte(reqBody), listInput11)
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput11)
+ ctx.SetInput(listInput)
_, err = handler.List(ctx)
assert.Nil(t, err)
//list search match
- listInput12 := &ListInput{}
+ listInput = &ListInput{}
reqBody = `{"page_size": 1, "page": 1, "uri": "r11"}`
- err = json.Unmarshal([]byte(reqBody), listInput12)
+ err = json.Unmarshal([]byte(reqBody), listInput)
assert.Nil(t, err)
- ctx.SetInput(listInput12)
+ ctx.SetInput(listInput)
retPage, err = handler.List(ctx)
assert.Nil(t, err)
dataPage = retPage.(*store.ListOutput)
diff --git a/api/internal/utils/utils.go b/api/internal/utils/utils.go
index 3d2cc88..ad47792 100644
--- a/api/internal/utils/utils.go
+++ b/api/internal/utils/utils.go
@@ -17,10 +17,12 @@
package utils
import (
+ "errors"
"fmt"
"net"
"os"
"strconv"
+ "strings"
"github.com/sony/sonyflake"
)
@@ -94,3 +96,49 @@ func InterfaceToString(val interface{}) string {
str := fmt.Sprintf("%v", val)
return str
}
+
+func GenLabelMap(label string) (map[string]string, error) {
+ var err = errors.New("malformed label")
+ mp := make(map[string]string)
+
+ if label == "" {
+ return mp, nil
+ }
+
+ labels := strings.Split(label, ",")
+ for _, l := range labels {
+ kv := strings.Split(l, ":")
+ if len(kv) == 2 {
+ if kv[0] == "" || kv[1] == "" {
+ return nil, err
+ }
+
+ mp[kv[0]] = kv[1]
+ } else if len(kv) == 1 {
+ if kv[0] == "" {
+ return nil, err
+ }
+
+ mp[kv[0]] = ""
+ } else {
+ return nil, err
+ }
+ }
+
+ return mp, nil
+}
+
+func LabelContains(labels, reqLabels map[string]string) bool {
+ if len(reqLabels) == 0 {
+ return true
+ }
+
+ for k, v := range labels {
+ l, exist := reqLabels[k]
+ if exist && ((l == "") || v == l) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/api/internal/utils/utils_test.go b/api/internal/utils/utils_test.go
index 5c8e718..0d8427d 100644
--- a/api/internal/utils/utils_test.go
+++ b/api/internal/utils/utils_test.go
@@ -17,6 +17,7 @@
package utils
import (
+ "errors"
"testing"
"github.com/stretchr/testify/assert"
@@ -42,3 +43,42 @@ func TestSumIPs_with_nil(t *testing.T) {
total := sumIPs(nil)
assert.Equal(t, uint16(0), total)
}
+
+func TestGenLabelMap(t *testing.T) {
+ expectedErr := errors.New("malformed label")
+ mp, err := GenLabelMap("l1")
+ assert.Nil(t, err)
+ assert.Equal(t, mp["l1"], "")
+
+ mp, err = GenLabelMap("l1,l2:v2")
+ assert.Nil(t, err)
+ assert.Equal(t, mp["l1"], "")
+ assert.Equal(t, mp["l2"], "v2")
+
+ mp, err = GenLabelMap(",")
+ assert.Equal(t, expectedErr, err)
+ assert.Nil(t, mp)
+
+ mp, err = GenLabelMap(",l2:,")
+ assert.Equal(t, expectedErr, err)
+ assert.Nil(t, mp)
+}
+
+func TestLabelContains(t *testing.T) {
+ mp1, _ := GenLabelMap("l1,l2:v2")
+ mp2 := map[string]string{
+ "l1": "v1",
+ }
+ assert.True(t, LabelContains(mp2, mp1))
+
+ mp3 := map[string]string{
+ "l1": "v1",
+ "l2": "v3",
+ }
+ assert.True(t, LabelContains(mp3, mp1))
+
+ mp4 := map[string]string{
+ "l2": "v3",
+ }
+ assert.False(t, LabelContains(mp4, mp1))
+}
diff --git a/api/test/e2e/route_with_management_fileds_test.go b/api/test/e2e/route_with_management_fileds_test.go
index faa7c99..59c40fd 100644
--- a/api/test/e2e/route_with_management_fileds_test.go
+++ b/api/test/e2e/route_with_management_fileds_test.go
@@ -160,7 +160,7 @@ func TestRoute_with_name_desc(t *testing.T) {
}
}
-func TestRoute_with_lable(t *testing.T) {
+func TestRoute_with_label(t *testing.T) {
tests := []HttpTestCase{
{
caseDesc: "config route with labels (r1)",
@@ -228,3 +228,138 @@ func TestRoute_with_lable(t *testing.T) {
testCaseCheck(tc)
}
}
+
+func TestRoute_search_by_label(t *testing.T) {
+ tests := []HttpTestCase{
+ {
+ caseDesc: "config route with labels (r1)",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes/r1",
+ Method: http.MethodPut,
+ Body: `{
+ "uri": "/hello",
+ "labels": {
+ "build":"16",
+ "env":"production",
+ "version":"v2"
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }]
+ }
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ caseDesc: "config route with labels (r2)",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes/r2",
+ Method: http.MethodPut,
+ Body: `{
+ "uri": "/hello2",
+ "labels": {
+ "build":"17",
+ "env":"dev",
+ "version":"v2",
+ "extra": "test"
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }]
+ }
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ caseDesc: "access the route's uri (r1)",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/hello",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "hello world",
+ Sleep: sleepTime,
+ },
+ {
+ caseDesc: "verify the route's detail (r1)",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes/r1",
+ Method: http.MethodGet,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "\"labels\":{\"build\":\"16\",\"env\":\"production\",\"version\":\"v2\"",
+ Sleep: sleepTime,
+ },
+ {
+ caseDesc: "search the route by label",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes",
+ Query: "label=build:16",
+ Method: http.MethodGet,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "\"labels\":{\"build\":\"16\",\"env\":\"production\",\"version\":\"v2\"",
+ Sleep: sleepTime,
+ },
+ {
+ caseDesc: "search the route by label (only key)",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes",
+ Query: "label=extra",
+ Method: http.MethodGet,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "\"labels\":{\"build\":\"17\",\"env\":\"dev\",\"extra\":\"test\",\"version\":\"v2\"",
+ Sleep: sleepTime,
+ },
+ {
+ caseDesc: "search the route by label (combination)",
+ Object: ManagerApiExpect(t),
+ Path: "/apisix/admin/routes",
+ Query: "label=extra,build:16",
+ Method: http.MethodGet,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "\"total_size\":2",
+ Sleep: sleepTime,
+ },
+ {
+ caseDesc: "delete the route (r1)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodDelete,
+ Path: "/apisix/admin/routes/r1",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ caseDesc: "delete the route (r2)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodDelete,
+ Path: "/apisix/admin/routes/r2",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ caseDesc: "access the route after delete it",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/hello",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusNotFound,
+ Sleep: sleepTime,
+ },
+ }
+ for _, tc := range tests {
+ testCaseCheck(tc)
+ }
+}
diff --git a/docs/api/api.md b/docs/api/api.md
index 2b3554d..6bd430c 100644
--- a/docs/api/api.md
+++ b/docs/api/api.md
@@ -122,6 +122,7 @@ Return the route list according to the specified page number and page size, and
| page_size | query | page size | No | integer |
| name | query | name of route | No | string |
| uri | query | uri of route | No | string |
+| label | query | label of route | No | string |
##### Responses