From a7e700a7baabd9bd5f7250e9f102dd94109f202d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B2=BB=E5=9B=BD?= Date: Tue, 22 Mar 2022 11:23:00 +0800 Subject: [PATCH] fix(import routes): merge route when route have the same name (#2330) --- .../handler/data_loader/route_export.go | 4 + .../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 +++ .../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 dd0c3c5086..a61d5d1ed5 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 e34a8527c9..4f17dd0849 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 4d8150b9a4..c7962bed1c 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 a57533b81a..3043abb484 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 2497f98e4e..7e245dfa5a 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 ec93db3a1e..c27708e14e 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)); }); }); });