Skip to content

Commit

Permalink
fix(import routes): merge route when route have the same name (#2330)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinw66 authored Mar 22, 2022
1 parent edeed6b commit a7e700a
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 5 deletions.
4 changes: 4 additions & 0 deletions api/internal/handler/data_loader/route_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
36 changes: 35 additions & 1 deletion api/internal/handler/data_loader/route_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions api/internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
134 changes: 134 additions & 0 deletions api/test/e2e/route_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions api/test/e2enew/route/route_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down
11 changes: 7 additions & 4 deletions web/cypress/integration/route/import_export_route.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
});
Expand Down

0 comments on commit a7e700a

Please sign in to comment.