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