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/22 03:23:08 UTC

[apisix-dashboard] branch master updated: fix(import routes): merge route when route have the same name (#2330)

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 a7e700a  fix(import routes): merge route when route have the same name (#2330)
a7e700a is described below

commit a7e700a7baabd9bd5f7250e9f102dd94109f202d
Author: 吴治国 <wz...@gmail.com>
AuthorDate: Tue Mar 22 11:23:00 2022 +0800

    fix(import routes): merge route when route have the same name (#2330)
---
 api/internal/handler/data_loader/route_export.go   |   4 +
 api/internal/handler/data_loader/route_import.go   |  36 +++++-
 api/internal/utils/utils.go                        |  16 +++
 api/test/e2e/route_import_test.go                  | 134 +++++++++++++++++++++
 api/test/e2enew/route/route_export_test.go         |  16 +++
 .../integration/route/import_export_route.spec.js  |  11 +-
 6 files changed, 212 insertions(+), 5 deletions(-)

diff --git a/api/internal/handler/data_loader/route_export.go b/api/internal/handler/data_loader/route_export.go
index dd0c3c5..a61d5d1 100644
--- a/api/internal/handler/data_loader/route_export.go
+++ b/api/internal/handler/data_loader/route_export.go
@@ -224,6 +224,10 @@ func (h *Handler) RouteToOpenAPI3(c droplet.Context, routes []*entity.Route) (*o
 			extensions["x-apisix-vars"] = route.Vars
 		}
 
+		if route.ID != nil {
+			extensions["x-apisix-id"] = route.ID
+		}
+
 		// Parse Route URIs
 		paths, paramsRefs = ParseRouteUris(route, paths, paramsRefs, pathItem, _pathNumber())
 
diff --git a/api/internal/handler/data_loader/route_import.go b/api/internal/handler/data_loader/route_import.go
index e34a852..4f17dd0 100644
--- a/api/internal/handler/data_loader/route_import.go
+++ b/api/internal/handler/data_loader/route_import.go
@@ -137,6 +137,22 @@ func (h *ImportHandler) Import(c droplet.Context) (interface{}, error) {
 		}
 	}
 
+	// merge route
+	idRoute := make(map[string]*entity.Route)
+	for _, route := range routes {
+		if existRoute, ok := idRoute[route.ID.(string)]; ok {
+			uris := append(existRoute.Uris, route.Uris...)
+			existRoute.Uris = uris
+		} else {
+			idRoute[route.ID.(string)] = route
+		}
+	}
+
+	routes = make([]*entity.Route, 0, len(idRoute))
+	for _, route := range idRoute {
+		routes = append(routes, route)
+	}
+
 	// create route
 	for _, route := range routes {
 		if Force && route.ID != nil {
@@ -168,7 +184,25 @@ func checkRouteExist(ctx context.Context, routeStore *store.GenericStore, route
 				return false
 			}
 
-			if !(item.Host == route.Host && item.URI == route.URI && utils.StringSliceEqual(item.Uris, route.Uris) &&
+			itemUris := item.Uris
+			if item.URI != "" {
+				if itemUris == nil {
+					itemUris = []string{item.URI}
+				} else {
+					itemUris = append(itemUris, item.URI)
+				}
+			}
+
+			routeUris := route.Uris
+			if route.URI != "" {
+				if routeUris == nil {
+					routeUris = []string{route.URI}
+				} else {
+					routeUris = append(routeUris, route.URI)
+				}
+			}
+
+			if !(item.Host == route.Host && utils.StringSliceContains(itemUris, routeUris) &&
 				utils.StringSliceEqual(item.RemoteAddrs, route.RemoteAddrs) && item.RemoteAddr == route.RemoteAddr &&
 				utils.StringSliceEqual(item.Hosts, route.Hosts) && item.Priority == route.Priority &&
 				utils.ValueEqual(item.Vars, route.Vars) && item.FilterFunc == route.FilterFunc) {
diff --git a/api/internal/utils/utils.go b/api/internal/utils/utils.go
index 4d8150b..c7962be 100644
--- a/api/internal/utils/utils.go
+++ b/api/internal/utils/utils.go
@@ -172,6 +172,22 @@ func ValidateLuaCode(code string) error {
 	return err
 }
 
+func StringSliceContains(a, b []string) bool {
+	if (a == nil) != (b == nil) {
+		return false
+	}
+
+	for i := range a {
+		for j := range b {
+			if a[i] == b[j] {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
 //
 func StringSliceEqual(a, b []string) bool {
 	if (a == nil) != (b == nil) {
diff --git a/api/test/e2e/route_import_test.go b/api/test/e2e/route_import_test.go
index a57533b..3043abb 100644
--- a/api/test/e2e/route_import_test.go
+++ b/api/test/e2e/route_import_test.go
@@ -577,3 +577,137 @@ func TestRoute_export_import(t *testing.T) {
 		testCaseCheck(tc, t)
 	}
 }
+
+func TestRoute_export_import_merge(t *testing.T) {
+	// create routes
+	tests := []HttpTestCase{
+		{
+			Desc:   "Create a route",
+			Object: ManagerApiExpect(t),
+			Method: http.MethodPut,
+			Path:   "/apisix/admin/routes/r1",
+			Body: `{
+					"id": "r1",
+					"uris": ["/test1", "/test2"],
+					"name": "route_all",
+					"desc": "所有",
+					"methods": ["GET","POST","PUT","DELETE"],
+					"hosts": ["test.com"],
+					"status": 1,
+					"upstream": {
+						"nodes": {
+							"` + UpstreamIp + `:1980": 1
+						},
+						"type": "roundrobin"
+					}
+			}`,
+			Headers:      map[string]string{"Authorization": token},
+			ExpectStatus: http.StatusOK,
+			Sleep:        sleepTime,
+		},
+	}
+	for _, tc := range tests {
+		testCaseCheck(tc, t)
+	}
+
+	// export routes
+	time.Sleep(sleepTime)
+	tmpPath := "/tmp/export.json"
+	headers := map[string]string{
+		"Authorization": token,
+	}
+	body, status, err := httpGet(ManagerAPIHost+"/apisix/admin/export/routes", headers)
+	assert.Nil(t, err)
+	assert.Equal(t, http.StatusOK, status)
+
+	content := gjson.Get(string(body), "data")
+	err = ioutil.WriteFile(tmpPath, []byte(content.Raw), 0644)
+	assert.Nil(t, err)
+
+	// import routes (should failed -- duplicate)
+	files := []UploadFile{
+		{Name: "file", Filepath: tmpPath},
+	}
+	respBody, status, err := PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers)
+	assert.Nil(t, err)
+	assert.Equal(t, 400, status)
+	assert.True(t, strings.Contains(string(respBody), "duplicate"))
+	time.Sleep(sleepTime)
+
+	// delete routes
+	tests = []HttpTestCase{
+		{
+			Desc:         "delete the route1 just created",
+			Object:       ManagerApiExpect(t),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/routes/r1",
+			Headers:      map[string]string{"Authorization": token},
+			ExpectStatus: http.StatusOK,
+		},
+	}
+	for _, tc := range tests {
+		testCaseCheck(tc, t)
+	}
+
+	// import again
+	time.Sleep(sleepTime)
+	respBody, status, err = PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers)
+	assert.Nil(t, err)
+	assert.Equal(t, 200, status)
+	assert.True(t, strings.Contains(string(respBody), `"data":{"paths":2,"routes":1}`))
+	time.Sleep(sleepTime)
+
+	// sleep for data sync
+	time.Sleep(sleepTime)
+
+	request, _ := http.NewRequest("GET", ManagerAPIHost+"/apisix/admin/routes", nil)
+	request.Header.Add("Authorization", token)
+	resp, err := http.DefaultClient.Do(request)
+	assert.Nil(t, err)
+	defer resp.Body.Close()
+	respBody, _ = ioutil.ReadAll(resp.Body)
+	list := gjson.Get(string(respBody), "data.rows").Value().([]interface{})
+
+	assert.Equal(t, 1, len(list))
+
+	// verify route data
+	tests = []HttpTestCase{}
+	for _, item := range list {
+		route := item.(map[string]interface{})
+		tcDataVerify := HttpTestCase{
+			Desc:         "verify data of route2",
+			Object:       ManagerApiExpect(t),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/routes/" + route["id"].(string),
+			Headers:      map[string]string{"Authorization": token},
+			ExpectStatus: http.StatusOK,
+			ExpectBody: []string{`"methods":["GET","POST","PUT","DELETE"]`,
+				`"/test1"`,
+				`"/test2"`,
+				`"desc":"所有`,
+				`"hosts":["test.com"]`,
+				`"upstream":{"nodes":[{"host":"` + UpstreamIp + `","port":1980,"weight":1}],"type":"roundrobin"}`,
+			},
+			Sleep: sleepTime,
+		}
+		tests = append(tests, tcDataVerify)
+	}
+
+	// delete test data
+	for _, item := range list {
+		route := item.(map[string]interface{})
+		tc := HttpTestCase{
+			Desc:         "delete route",
+			Object:       ManagerApiExpect(t),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/routes/" + route["id"].(string),
+			Headers:      map[string]string{"Authorization": token},
+			ExpectStatus: http.StatusOK,
+		}
+		tests = append(tests, tc)
+	}
+
+	for _, tc := range tests {
+		testCaseCheck(tc, t)
+	}
+}
diff --git a/api/test/e2enew/route/route_export_test.go b/api/test/e2enew/route/route_export_test.go
index 2497f98..7e245df 100644
--- a/api/test/e2enew/route/route_export_test.go
+++ b/api/test/e2enew/route/route_export_test.go
@@ -56,6 +56,7 @@ var _ = ginkgo.Describe("Route", func() {
 					"security": [],
 					"x-apisix-enable_websocket": false,
 					"x-apisix-hosts": ["foo.com", "*.bar.com"],
+					"x-apisix-id":"r1",
 					"x-apisix-labels": {
 						"build": "16",
 						"env": "production",
@@ -89,6 +90,7 @@ var _ = ginkgo.Describe("Route", func() {
 					"security": [],
 					"x-apisix-enable_websocket": false,
 					"x-apisix-hosts": ["foo.com", "*.bar.com"],
+					"x-apisix-id":"r1",
 					"x-apisix-labels": {
 						"build": "16",
 						"env": "production",
@@ -183,6 +185,7 @@ var _ = ginkgo.Describe("Route", func() {
 					"security": [],
 					"x-apisix-enable_websocket": false,
 					"x-apisix-host": "*.bar.com",
+					"x-apisix-id":"r2",
 					"x-apisix-labels": {
 						"build": "16",
 						"env": "production",
@@ -216,6 +219,7 @@ var _ = ginkgo.Describe("Route", func() {
 					"security": [],
 					"x-apisix-enable_websocket": false,
 					"x-apisix-host": "*.bar.com",
+					"x-apisix-id":"r2",
 					"x-apisix-labels": {
 						"build": "16",
 						"env": "production",
@@ -403,6 +407,7 @@ var _ = ginkgo.Describe("Route", func() {
 							}
 						},
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r3",
 						"x-apisix-labels": {
 							"build": "16",
 							"env": "production",
@@ -583,6 +588,7 @@ var _ = ginkgo.Describe("Route", func() {
 						},
 						"security": [],
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r4",
 						"x-apisix-labels": {
 							"build": "16",
 							"env": "production",
@@ -771,6 +777,7 @@ var _ = ginkgo.Describe("Route", func() {
 						},
 						"security": [],
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r5",
 						"x-apisix-labels": {
 							"build": "16",
 							"env": "production",
@@ -955,6 +962,7 @@ var _ = ginkgo.Describe("Route", func() {
 						},
 						"security": [],
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r8",
 						"x-apisix-plugins": {
 							"prometheus": {
 								"disable": false
@@ -1077,6 +1085,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"security": [],
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r9",
 						"x-apisix-labels": {
 							"API_VERSION": "v1",
 							"test": "1"
@@ -1276,6 +1285,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"security": [],
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r10",
 						"x-apisix-labels": {
 							"API_VERSION": "v1",
 							"test": "1"
@@ -1901,6 +1911,7 @@ var _ = ginkgo.Describe("Route", func() {
 						},
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r1",
 						"x-apisix-labels": {
 							"build": "16",
 							"env": "production",
@@ -2043,6 +2054,7 @@ var _ = ginkgo.Describe("Route", func() {
 						},
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
+						"x-apisix-id":"r2",
 						"x-apisix-labels": {
 							"build": "16",
 							"env": "production",
@@ -2197,6 +2209,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
 						"x-apisix-hosts": ["test.com"],
+						"x-apisix-id":"r1",
 						"x-apisix-priority": 0,
 						"x-apisix-status": 1
 					}
@@ -2300,6 +2313,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"summary": "所有",
 						"x-apisix-enable_websocket": false,
 						"x-apisix-hosts": ["test.com"],
+						"x-apisix-id":"r1",
 						"x-apisix-priority": 0,
 						"x-apisix-status": 1,
 						"x-apisix-upstream": {
@@ -2322,6 +2336,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"summary": "所有1",
 						"x-apisix-enable_websocket": false,
 						"x-apisix-hosts": ["test.com"],
+						"x-apisix-id":"r2",
 						"x-apisix-priority": 0,
 						"x-apisix-status": 1,
 						"x-apisix-upstream": {
@@ -2344,6 +2359,7 @@ var _ = ginkgo.Describe("Route", func() {
 						"summary": "所有2",
 						"x-apisix-enable_websocket": false,
 						"x-apisix-hosts": ["test.com"],
+						"x-apisix-id":"r3",
 						"x-apisix-priority": 0,
 						"x-apisix-status": 1,
 						"x-apisix-upstream": {
diff --git a/web/cypress/integration/route/import_export_route.spec.js b/web/cypress/integration/route/import_export_route.spec.js
index ec93db3..c27708e 100644
--- a/web/cypress/integration/route/import_export_route.spec.js
+++ b/web/cypress/integration/route/import_export_route.spec.js
@@ -131,16 +131,19 @@ context('import and export routes', () => {
       cy.log(`found file ${jsonFile}`);
       cy.log('**confirm downloaded json file**');
       cy.readFile(jsonFile).then((fileContent) => {
-        expect(JSON.stringify(fileContent)).to.equal(JSON.stringify(this.exportFile.jsonFile));
+        const json = fileContent;
+        delete json['paths']['/{params}']['post']['x-apisix-id'];
+        expect(JSON.stringify(json)).to.equal(JSON.stringify(this.exportFile.jsonFile));
       });
     });
     cy.task('findFile', data.yamlMask).then((yamlFile) => {
       cy.log(`found file ${yamlFile}`);
       cy.log('**confirm downloaded yaml file**');
       cy.readFile(yamlFile).then((fileContent) => {
-        expect(JSON.stringify(yaml.load(fileContent), null, null)).to.equal(
-          JSON.stringify(this.exportFile.yamlFile),
-        );
+        const json = yaml.load(fileContent);
+        delete json['paths']['/{params}']['post']['x-apisix-id'];
+        delete json['paths']['/{params}-APISIX-REPEAT-URI-2']['post']['x-apisix-id'];
+        expect(JSON.stringify(json, null, null)).to.equal(JSON.stringify(this.exportFile.yamlFile));
       });
     });
   });