This is an automated email from the ASF dual-hosted git repository.
liuxiran 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 6ee8dd1 feat: import route from OpenAPI Specification3.0 (#1102)
6ee8dd1 is described below
commit 6ee8dd15f22e10182a27cbfa4a69b56a3b0f9b43
Author: JinChen <[email protected]>
AuthorDate: Wed Jan 27 17:48:02 2021 +0800
feat: import route from OpenAPI Specification3.0 (#1102)
---
api/cmd/manager/main_test.go | 2 +
api/go.mod | 3 +-
api/go.sum | 26 +-
api/internal/conf/conf.go | 1 +
api/internal/core/entity/entity.go | 12 +-
api/internal/core/store/store.go | 24 +-
api/internal/core/store/validate.go | 9 +-
api/internal/filter/schema.go | 1 -
api/internal/handler/data_loader/route_import.go | 522 +++++++++++++++++++
.../handler/data_loader/route_import_test.go | 178 +++++++
api/internal/handler/handler_test.go | 2 +-
api/internal/route.go | 1 +
api/internal/utils/utils.go | 37 ++
api/test/e2e/base.go | 5 +-
api/test/e2e/http.go | 106 ++++
api/test/e2e/route_export_test.go | 5 +-
api/test/e2e/route_import_test.go | 579 +++++++++++++++++++++
api/test/e2e/route_online_debug_test.go | 2 +-
api/test/e2e/route_with_plugin_jwt_test.go | 4 +-
api/test/testdata/import/default.json | 39 ++
api/test/testdata/import/default.yaml | 44 ++
api/test/testdata/import/multi-routes.yaml | 224 ++++++++
api/test/testdata/import/with-plugins.yaml | 80 +++
api/test/testdata/import/with-service-id.yaml | 39 ++
api/test/testdata/import/with-upstream-id.yaml | 39 ++
25 files changed, 1949 insertions(+), 35 deletions(-)
diff --git a/api/cmd/manager/main_test.go b/api/cmd/manager/main_test.go
index 1e7b51d..f891490 100644
--- a/api/cmd/manager/main_test.go
+++ b/api/cmd/manager/main_test.go
@@ -15,6 +15,7 @@
* limitations under the License.
*/
package main
+
import (
"os"
"os/signal"
@@ -22,6 +23,7 @@ import (
"syscall"
"testing"
)
+
func TestMainWrapper(t *testing.T) {
if os.Getenv("ENV") == "test" {
t.Skip("skipping build binary when execute unit test")
diff --git a/api/go.mod b/api/go.mod
index d1681ff..e0b9457 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -17,6 +17,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/evanphx/json-patch/v5 v5.1.0
+ github.com/getkin/kin-openapi v0.33.0
github.com/gin-contrib/pprof v1.3.0
github.com/gin-contrib/sessions v0.0.3
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
@@ -29,7 +30,7 @@ require (
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/prometheus/client_golang v1.8.0 // indirect
github.com/satori/go.uuid v1.2.0
- github.com/shiningrush/droplet v0.2.6-0.20210126131015-cbf9557974f7
+ github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b
github.com/shiningrush/droplet/wrapper/gin v0.2.1
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sony/sonyflake v1.0.0
diff --git a/api/go.sum b/api/go.sum
index 541e882..d8234a3 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -82,6 +82,10 @@ github.com/fatih/color v1.7.0/go.mod
h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod
h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod
h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod
h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/getkin/kin-openapi v0.33.0
h1:KccukV3/1h95R0OP7vfWB3KVy9lxA5i8i3BwlA3tRpE=
+github.com/getkin/kin-openapi v0.33.0/go.mod
h1:ZJSfy1PxJv2QQvH9EdBj3nupRTVvV42mkW6zKUlRBwk=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod
h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod
h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/pprof v1.3.0
h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
github.com/gin-contrib/pprof v1.3.0/go.mod
h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
@@ -103,6 +107,10 @@ github.com/go-kit/kit v0.10.0/go.mod
h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
github.com/go-logfmt/logfmt v0.3.0/go.mod
h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod
h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod
h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-openapi/jsonpointer v0.19.5
h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod
h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/swag v0.19.5
h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
+github.com/go-openapi/swag v0.19.5/go.mod
h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/assert/v2 v2.0.1
h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod
h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod
h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
@@ -138,7 +146,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod
h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod
h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod
h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod
h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2
h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3
h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
@@ -235,6 +242,9 @@ github.com/leodido/go-urn v1.2.0
h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod
h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lightstep/lightstep-tracer-common/golang/gogo
v0.0.0-20190605223551-bc2310a04743/go.mod
h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod
h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod
h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e
h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod
h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9/go.mod
h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod
h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod
h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -290,7 +300,6 @@ github.com/performancecopilot/speed
v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod
h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod
h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -308,7 +317,6 @@ github.com/prometheus/client_golang v1.8.0/go.mod
h1:O9VU6huf47PktckDQfMTX0Y8tY0
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod
h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod
h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod
h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4
h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod
h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod
h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0
h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
@@ -339,8 +347,8 @@ github.com/satori/go.uuid v1.2.0/go.mod
h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod
h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shiningrush/droplet v0.2.4
h1:OW4Pp+dXs9O61QKTiYSRWCdQeOyzO1n9h+i2PDJ5DK0=
github.com/shiningrush/droplet v0.2.4/go.mod
h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M=
-github.com/shiningrush/droplet v0.2.6-0.20210126131015-cbf9557974f7
h1:E0+CduActvXFpdvUXu7wxfw+trl5MKRkY4IZ1uQYsvc=
-github.com/shiningrush/droplet v0.2.6-0.20210126131015-cbf9557974f7/go.mod
h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M=
+github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b
h1:kAS+hyJuHUm/lAN4xbKY4/QHbRse95lcjxcIZwSJEvM=
+github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b/go.mod
h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M=
github.com/shiningrush/droplet/wrapper/gin v0.2.1
h1:1o+5KUF2sKsdZ7SkmOC5ahAP1qaZKqnm0c5hOYFV6YQ=
github.com/shiningrush/droplet/wrapper/gin v0.2.1/go.mod
h1:cx5BfLuStFDFIKuEOc1zBTpiT3B4Ezkg3MdlP6rW51I=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod
h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -363,13 +371,13 @@ github.com/spf13/pflag v1.0.1/go.mod
h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod
h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod
h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod
h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
-github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod
h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod
h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod
h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1
h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.7 h1:Mb1M9HZCRWEcXQ8ieJo7auYyyiSux6w9XN3AdTpxJrE=
@@ -427,7 +435,6 @@ go.uber.org/zap v1.16.0/go.mod
h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod
h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod
h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod
h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod
h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod
h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -464,7 +471,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod
h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod
h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod
h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859
h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -510,7 +516,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod
h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009
h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211
h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
@@ -533,7 +538,6 @@ golang.org/x/tools
v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod
h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod
h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5
h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod
h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -544,7 +548,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a
h1:CB3a9Nez8M13wwlr/E2Ytwo
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -574,7 +577,6 @@ google.golang.org/protobuf v1.25.0
h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4
google.golang.org/protobuf v1.25.0/go.mod
h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod
h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/internal/conf/conf.go b/api/internal/conf/conf.go
index 883b1a6..cc1a82f 100644
--- a/api/internal/conf/conf.go
+++ b/api/internal/conf/conf.go
@@ -52,6 +52,7 @@ var (
UserList = make(map[string]User, 2)
AuthConf Authentication
SSLDefaultStatus = 1 //enable ssl by default
+ ImportSizeLimit = 10 * 1024 * 1024
PIDPath = "/tmp/manager-api.pid"
)
diff --git a/api/internal/core/entity/entity.go
b/api/internal/core/entity/entity.go
index 7463bee..f0b4edf 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -171,12 +171,6 @@ type UpstreamDef struct {
Labels map[string]string `json:"labels,omitempty"`
}
-type RequestValidation struct {
- Type string `json:"type,omitempty"`
- Required []string `json:"required,omitempty"`
- Properties interface{} `json:"properties,omitempty"`
-}
-
// swagger:model Upstream
type Upstream struct {
BaseInfo
@@ -241,6 +235,12 @@ type Script struct {
Script interface{} `json:"script,omitempty"`
}
+type RequestValidation struct {
+ Type string `json:"type,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Properties interface{} `json:"properties,omitempty"`
+}
+
// swagger:model GlobalPlugins
type GlobalPlugins struct {
BaseInfo
diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go
index 2da0572..2b0031c 100644
--- a/api/internal/core/store/store.go
+++ b/api/internal/core/store/store.go
@@ -221,7 +221,7 @@ func (s *GenericStore) List(_ context.Context, input
ListInput) (*ListOutput, er
func (s *GenericStore) ingestValidate(obj interface{}) (err error) {
if s.opt.Validator != nil {
if err := s.opt.Validator.Validate(obj); err != nil {
- log.Errorf("data validate failed: %s", err)
+ log.Errorf("data validate failed: %s, %v", err, obj)
return err
}
}
@@ -237,7 +237,8 @@ func (s *GenericStore) ingestValidate(obj interface{}) (err
error) {
return err
}
-func (s *GenericStore) Create(ctx context.Context, obj interface{})
(interface{}, error) {
+func (s *GenericStore) CreateCheck(obj interface{}) ([]byte, error) {
+
if setter, ok := obj.(entity.BaseInfoSetter); ok {
info := setter.GetBaseInfo()
info.Creating()
@@ -257,12 +258,27 @@ func (s *GenericStore) Create(ctx context.Context, obj
interface{}) (interface{}
return nil, fmt.Errorf("key: %s is conflicted", key)
}
- bs, err := json.Marshal(obj)
+ bytes, err := json.Marshal(obj)
if err != nil {
log.Errorf("json marshal failed: %s", err)
return nil, fmt.Errorf("json marshal failed: %s", err)
}
- if err := s.Stg.Create(ctx, s.GetObjStorageKey(obj), string(bs)); err
!= nil {
+
+ return bytes, nil
+}
+
+func (s *GenericStore) Create(ctx context.Context, obj interface{})
(interface{}, error) {
+ if setter, ok := obj.(entity.BaseInfoSetter); ok {
+ info := setter.GetBaseInfo()
+ info.Creating()
+ }
+
+ bytes, err := s.CreateCheck(obj)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := s.Stg.Create(ctx, s.GetObjStorageKey(obj), string(bytes));
err != nil {
return nil, err
}
diff --git a/api/internal/core/store/validate.go
b/api/internal/core/store/validate.go
index 493c169..abf5933 100644
--- a/api/internal/core/store/validate.go
+++ b/api/internal/core/store/validate.go
@@ -71,7 +71,8 @@ func (v *JsonSchemaValidator) Validate(obj interface{}) error
{
}
type APISIXJsonSchemaValidator struct {
- schema *gojsonschema.Schema
+ schema *gojsonschema.Schema
+ schemaDef string
}
func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) {
@@ -87,7 +88,8 @@ func NewAPISIXJsonSchemaValidator(jsonPath string)
(Validator, error) {
return nil, fmt.Errorf("new schema failed: %s", err)
}
return &APISIXJsonSchemaValidator{
- schema: s,
+ schema: s,
+ schemaDef: schemaDef,
}, nil
}
@@ -231,7 +233,7 @@ func checkConf(reqBody interface{}) error {
func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error {
ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj))
if err != nil {
- log.Errorf("schema validate failed: %s", err)
+ log.Errorf("schema validate failed: %s, s: %v, obj: %v", err,
v.schema, obj)
return fmt.Errorf("schema validate failed: %s", err)
}
@@ -243,6 +245,7 @@ func (v *APISIXJsonSchemaValidator) Validate(obj
interface{}) error {
}
errString.AppendString(vErr.String())
}
+ log.Errorf("schema validate failed:s: %v, obj: %#v",
v.schemaDef, obj)
return fmt.Errorf("schema validate failed: %s",
errString.String())
}
diff --git a/api/internal/filter/schema.go b/api/internal/filter/schema.go
index e9d50f2..120d8bc 100644
--- a/api/internal/filter/schema.go
+++ b/api/internal/filter/schema.go
@@ -178,7 +178,6 @@ func SchemaCheck() gin.HandlerFunc {
return func(c *gin.Context) {
pathPrefix := "/apisix/admin/"
resource := strings.TrimPrefix(c.Request.URL.Path, pathPrefix)
-
idx := strings.LastIndex(resource, "/")
if idx > 1 {
resource = resource[:idx]
diff --git a/api/internal/handler/data_loader/route_import.go
b/api/internal/handler/data_loader/route_import.go
new file mode 100644
index 0000000..d1ed425
--- /dev/null
+++ b/api/internal/handler/data_loader/route_import.go
@@ -0,0 +1,522 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package data_loader
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "path"
+ "reflect"
+ "regexp"
+ "strings"
+
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/gin-gonic/gin"
+ "github.com/shiningrush/droplet"
+ "github.com/shiningrush/droplet/data"
+ "github.com/shiningrush/droplet/wrapper"
+ wgin "github.com/shiningrush/droplet/wrapper/gin"
+
+ "github.com/apisix/manager-api/internal/conf"
+ "github.com/apisix/manager-api/internal/core/entity"
+ "github.com/apisix/manager-api/internal/core/store"
+ "github.com/apisix/manager-api/internal/handler"
+ "github.com/apisix/manager-api/internal/log"
+ "github.com/apisix/manager-api/internal/utils"
+ "github.com/apisix/manager-api/internal/utils/consts"
+)
+
+type ImportHandler struct {
+ routeStore *store.GenericStore
+ svcStore store.Interface
+ upstreamStore store.Interface
+}
+
+func NewImportHandler() (handler.RouteRegister, error) {
+ return &ImportHandler{
+ routeStore: store.GetStore(store.HubKeyRoute),
+ svcStore: store.GetStore(store.HubKeyService),
+ upstreamStore: store.GetStore(store.HubKeyUpstream),
+ }, nil
+}
+
+
+var regPathVar = regexp.MustCompile(`{[\w.]*}`)
+var regPathRepeat = regexp.MustCompile(`-APISIX-REPEAT-URI-[\d]*`)
+
+
+func (h *ImportHandler) ApplyRoute(r *gin.Engine) {
+ r.POST("/apisix/admin/import/routes", wgin.Wraps(h.Import,
+ wrapper.InputType(reflect.TypeOf(ImportInput{}))))
+}
+
+type ImportInput struct {
+ Force bool `auto_read:"force,query"`
+ FileName string `auto_read:"_file"`
+ FileContent []byte `auto_read:"file"`
+}
+
+func (h *ImportHandler) Import(c droplet.Context) (interface{}, error) {
+ input := c.Input().(*ImportInput)
+ Force := input.Force
+
+ // file check
+ suffix := path.Ext(input.FileName)
+ if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" {
+ return nil, fmt.Errorf("required file type is .yaml, .yml or
.json but got: %s", suffix)
+ }
+
+ contentLen := bytes.Count(input.FileContent, nil) - 1
+ if contentLen > conf.ImportSizeLimit {
+ log.Warnf("upload file size exceeds limit: %d", contentLen)
+ return nil, fmt.Errorf("the file size exceeds the limit; limit
%d", conf.ImportSizeLimit)
+ }
+
+ swagger, err :=
openapi3.NewSwaggerLoader().LoadSwaggerFromData(input.FileContent)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(swagger.Paths) < 1 {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
+ errors.New("empty or invalid imported file")
+ }
+
+ routes, err := OpenAPI3ToRoute(swagger)
+ if err != nil {
+ return nil, err
+ }
+
+ // check route
+ for _, route := range routes {
+ err := checkRouteExist(c.Context(), h.routeStore, route)
+ if err != nil && !Force {
+ log.Warnf("import duplicate: %s, route: %#v", err,
route)
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
+ fmt.Errorf("route(uris:%v) conflict, %s",
route.Uris, err)
+ }
+ if route.ServiceID != nil {
+ _, err := h.svcStore.Get(c.Context(),
utils.InterfaceToString(route.ServiceID))
+ if err != nil {
+ if err == data.ErrNotFound {
+ return
&data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+ fmt.Errorf("service id: %s not
found", route.ServiceID)
+ }
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
+ }
+ if route.UpstreamID != nil {
+ _, err := h.upstreamStore.Get(c.Context(),
utils.InterfaceToString(route.UpstreamID))
+ if err != nil {
+ if err == data.ErrNotFound {
+ return
&data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
+ fmt.Errorf("upstream id: %s not
found", route.UpstreamID)
+ }
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
+ }
+ }
+
+ if _, err := h.routeStore.CreateCheck(route); err != nil {
+ return handler.SpecCodeResponse(err),
+ fmt.Errorf("create route(uris:%v) failed: %s",
route.Uris, err)
+ }
+ }
+
+ // create route
+ for _, route := range routes {
+ if Force && route.ID != nil {
+ if _, err := h.routeStore.Update(c.Context(), route,
true); err != nil {
+ return handler.SpecCodeResponse(err),
+ fmt.Errorf("update route(uris:%v)
failed: %s", route.Uris, err)
+ }
+ } else {
+ if _, err := h.routeStore.Create(c.Context(), route);
err != nil {
+ return handler.SpecCodeResponse(err),
+ fmt.Errorf("create route(uris:%v)
failed: %s", route.Uris, err)
+ }
+ }
+ }
+
+ return map[string]int{
+ "paths": len(swagger.Paths),
+ "routes": len(routes),
+ }, nil
+}
+
+func checkRouteExist(ctx context.Context, routeStore *store.GenericStore,
route *entity.Route) error {
+ //routeStore := store.GetStore(store.HubKeyRoute)
+ ret, err := routeStore.List(ctx, store.ListInput{
+ Predicate: func(obj interface{}) bool {
+ id := utils.InterfaceToString(route.ID)
+ item := obj.(*entity.Route)
+ if id != "" && id != utils.InterfaceToString(item.ID) {
+ return false
+ }
+
+ if !(item.Host == route.Host && item.URI == route.URI
&& utils.StringSliceEqual(item.Uris, route.Uris) &&
+ 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) {
+ return false
+ }
+ return true
+ },
+ PageSize: 0,
+ PageNumber: 0,
+ })
+ if err != nil {
+ return err
+ }
+ if len(ret.Rows) > 0 {
+ return consts.InvalidParam("route is duplicate")
+ }
+ return nil
+}
+
+func parseExtension(val *openapi3.Operation) (*entity.Route, error) {
+ routeMap := map[string]interface{}{}
+ for key, val := range val.Extensions {
+ if strings.HasPrefix(key, "x-apisix-") {
+ routeMap[strings.TrimPrefix(key, "x-apisix-")] = val
+ }
+ }
+
+ route := new(entity.Route)
+ routeJson, err := json.Marshal(routeMap)
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(routeJson, &route)
+ if err != nil {
+ return nil, err
+ }
+
+ return route, nil
+}
+
+type PathValue struct {
+ Method string
+ Value *openapi3.Operation
+}
+
+func mergePathValue(key string, values []PathValue, swagger *openapi3.Swagger)
(map[string]*entity.Route, error) {
+ var parsed []PathValue
+ var routes = map[string]*entity.Route{}
+ for _, value := range values {
+ value.Value.OperationID =
strings.Replace(value.Value.OperationID, value.Method, "", 1)
+ var eq = false
+ for _, v := range parsed {
+ if utils.ValueEqual(v.Value, value.Value) {
+ eq = true
+ if routes[v.Method].Methods == nil {
+ routes[v.Method].Methods = []string{}
+ }
+ routes[v.Method].Methods =
append(routes[v.Method].Methods, value.Method)
+ }
+ }
+ // not equal to the previous ones
+ if !eq {
+ route, err := getRouteFromPaths(value.Method, key,
value.Value, swagger)
+ if err != nil {
+ return nil, err
+ }
+ routes[value.Method] = route
+ parsed = append(parsed, value)
+ }
+ }
+
+ return routes, nil
+}
+
+func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) {
+ var routes []*entity.Route
+ paths := swagger.Paths
+ var upstream *entity.UpstreamDef
+ var err error
+ for k, v := range paths {
+ k = regPathRepeat.ReplaceAllString(k, "")
+ upstream = &entity.UpstreamDef{}
+ if up, ok := v.Extensions["x-apisix-upstream"]; ok {
+ err = json.Unmarshal(up.(json.RawMessage), upstream)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var values []PathValue
+ if v.Get != nil {
+ value := PathValue{
+ Method: http.MethodGet,
+ Value: v.Get,
+ }
+ values = append(values, value)
+ }
+ if v.Post != nil {
+ value := PathValue{
+ Method: http.MethodPost,
+ Value: v.Post,
+ }
+ values = append(values, value)
+ }
+ if v.Head != nil {
+ value := PathValue{
+ Method: http.MethodHead,
+ Value: v.Head,
+ }
+ values = append(values, value)
+ }
+ if v.Put != nil {
+ value := PathValue{
+ Method: http.MethodPut,
+ Value: v.Put,
+ }
+ values = append(values, value)
+ }
+ if v.Patch != nil {
+ value := PathValue{
+ Method: http.MethodPatch,
+ Value: v.Patch,
+ }
+ values = append(values, value)
+ }
+ if v.Delete != nil {
+ value := PathValue{
+ Method: http.MethodDelete,
+ Value: v.Delete,
+ }
+ values = append(values, value)
+ }
+
+ // merge same route
+ tmp, err := mergePathValue(k, values, swagger)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, route := range tmp {
+ routes = append(routes, route)
+ }
+ }
+
+ return routes, nil
+}
+
+func parseParameters(parameters openapi3.Parameters, plugins
map[string]interface{}) {
+ props := make(map[string]interface{})
+ var required []string
+ for _, v := range parameters {
+ if v.Value.Schema != nil {
+ v.Value.Schema.Value.Format = ""
+ v.Value.Schema.Value.XML = nil
+ }
+
+ switch v.Value.In {
+ case "header":
+ if v.Value.Schema != nil && v.Value.Schema.Value != nil
{
+ props[v.Value.Name] = v.Value.Schema.Value
+ }
+ if v.Value.Required {
+ required = append(required, v.Value.Name)
+ }
+ }
+ }
+
+ requestValidation := make(map[string]interface{})
+ if rv, ok := plugins["request-validation"]; ok {
+ requestValidation = rv.(map[string]interface{})
+ }
+ requestValidation["header_schema"] = &entity.RequestValidation{
+ Type: "object",
+ Required: required,
+ Properties: props,
+ }
+ plugins["request-validation"] = requestValidation
+}
+
+func parseRequestBody(requestBody *openapi3.RequestBodyRef, swagger
*openapi3.Swagger, plugins map[string]interface{}) {
+ schema := requestBody.Value.Content
+ requestValidation := make(map[string]interface{})
+ if rv, ok := plugins["request-validation"]; ok {
+ requestValidation = rv.(map[string]interface{})
+ }
+ for _, v := range schema {
+ if v.Schema.Ref != "" {
+ s := getParameters(v.Schema.Ref,
&swagger.Components).Value
+ requestValidation["body_schema"] =
&entity.RequestValidation{
+ Type: s.Type,
+ Required: s.Required,
+ Properties: s.Properties,
+ }
+ plugins["request-validation"] = requestValidation
+ } else if v.Schema.Value != nil {
+ if v.Schema.Value.Properties != nil {
+ for k1, v1 := range v.Schema.Value.Properties {
+ if v1.Ref != "" {
+ s := getParameters(v1.Ref,
&swagger.Components)
+ v.Schema.Value.Properties[k1] =
s
+ }
+ v1.Value.Format = ""
+ }
+ requestValidation["body_schema"] =
&entity.RequestValidation{
+ Type: v.Schema.Value.Type,
+ Required: v.Schema.Value.Required,
+ Properties: v.Schema.Value.Properties,
+ }
+ plugins["request-validation"] =
requestValidation
+ } else if v.Schema.Value.Items != nil {
+ if v.Schema.Value.Items.Ref != "" {
+ s :=
getParameters(v.Schema.Value.Items.Ref, &swagger.Components).Value
+ requestValidation["body_schema"] =
&entity.RequestValidation{
+ Type: s.Type,
+ Required: s.Required,
+ Properties: s.Properties,
+ }
+ plugins["request-validation"] =
requestValidation
+ }
+ } else {
+ requestValidation["body_schema"] =
&entity.RequestValidation{
+ Type: "object",
+ Required: []string{},
+ Properties: v.Schema.Value.Properties,
+ }
+ }
+ }
+ plugins["request-validation"] = requestValidation
+ }
+}
+
+func parseSecurity(security openapi3.SecurityRequirements, securitySchemes
openapi3.SecuritySchemes, plugins map[string]interface{}) {
+ // todo: import consumers
+ for _, securities := range security {
+ for name := range securities {
+ if schema, ok := securitySchemes[name]; ok {
+ value := schema.Value
+ if value == nil {
+ continue
+ }
+
+ // basic auth
+ if value.Type == "http" && value.Scheme ==
"basic" {
+ plugins["basic-auth"] =
map[string]interface{}{}
+ //username, ok :=
value.Extensions["username"]
+ //if !ok {
+ // continue
+ //}
+ //password, ok :=
value.Extensions["password"]
+ //if !ok {
+ // continue
+ //}
+ //plugins["basic-auth"] =
map[string]interface{}{
+ // "username": username,
+ // "password": password,
+ //}
+ // jwt auth
+ } else if value.Type == "http" && value.Scheme
== "bearer" && value.BearerFormat == "JWT" {
+ plugins["jwt-auth"] =
map[string]interface{}{}
+ //key, ok := value.Extensions["key"]
+ //if !ok {
+ // continue
+ //}
+ //secret, ok :=
value.Extensions["secret"]
+ //if !ok {
+ // continue
+ //}
+ //plugins["jwt-auth"] =
map[string]interface{}{
+ // "key": key,
+ // "secret": secret,
+ //}
+ // key auth
+ } else if value.Type == "apiKey" {
+ plugins["key-auth"] =
map[string]interface{}{}
+ //key, ok := value.Extensions["key"]
+ //if !ok {
+ // continue
+ //}
+ //plugins["key-auth"] =
map[string]interface{}{
+ // "key": key,
+ //}
+ }
+ }
+ }
+ }
+}
+
+func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger
*openapi3.Swagger) (*entity.Route, error) {
+ // transform /path/{var} to /path/*
+ foundStr := regPathVar.FindString(key)
+ if foundStr != "" {
+ key = strings.Split(key, foundStr)[0] + "*"
+ }
+
+ route, err := parseExtension(value)
+ if err != nil {
+ return nil, err
+ }
+
+ route.Uris = []string{key}
+ route.Name = value.OperationID
+ route.Desc = value.Summary
+ route.Methods = []string{method}
+
+ if route.Plugins == nil {
+ route.Plugins = make(map[string]interface{})
+ }
+
+ if value.Parameters != nil {
+ parseParameters(value.Parameters, route.Plugins)
+ }
+
+ if value.RequestBody != nil {
+ parseRequestBody(value.RequestBody, swagger, route.Plugins)
+ }
+
+ if value.Security != nil && swagger.Components.SecuritySchemes != nil {
+ parseSecurity(*value.Security,
swagger.Components.SecuritySchemes, route.Plugins)
+ }
+
+ return route, nil
+}
+
+func getParameters(ref string, components *openapi3.Components)
*openapi3.SchemaRef {
+ schemaRef := &openapi3.SchemaRef{}
+ arr := strings.Split(ref, "/")
+ if arr[0] == "#" && arr[1] == "components" && arr[2] == "schemas" {
+ schemaRef = components.Schemas[arr[3]]
+ schemaRef.Value.XML = nil
+ // traverse properties to find another ref
+ for k, v := range schemaRef.Value.Properties {
+ if v.Value != nil {
+ v.Value.XML = nil
+ v.Value.Format = ""
+ }
+ if v.Ref != "" {
+ schemaRef.Value.Properties[k] =
getParameters(v.Ref, components)
+ } else if v.Value.Items != nil && v.Value.Items.Ref !=
"" {
+ v.Value.Items =
getParameters(v.Value.Items.Ref, components)
+ } else if v.Value.Items != nil && v.Value.Items.Value
!= nil {
+ v.Value.Items.Value.XML = nil
+ v.Value.Items.Value.Format = ""
+ }
+ }
+ }
+ return schemaRef
+}
diff --git a/api/internal/handler/data_loader/route_import_test.go
b/api/internal/handler/data_loader/route_import_test.go
new file mode 100644
index 0000000..50f76f7
--- /dev/null
+++ b/api/internal/handler/data_loader/route_import_test.go
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package data_loader
+
+import (
+ "bytes"
+ "errors"
+ "github.com/shiningrush/droplet/data"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/shiningrush/droplet"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+
+ "github.com/apisix/manager-api/internal/core/store"
+)
+
+type testFile struct {
+ FieldName string
+ FileName string
+ Content []byte
+}
+
+func createRequestMultipartFiles(t *testing.T, files ...testFile)
*http.Request {
+ var body bytes.Buffer
+
+ mw := multipart.NewWriter(&body)
+ for _, file := range files {
+ fw, err := mw.CreateFormFile(file.FieldName, file.FileName)
+ assert.NoError(t, err)
+
+ n, err := fw.Write(file.Content)
+ assert.NoError(t, err)
+ assert.Equal(t, len(file.Content), n)
+ }
+ err := mw.Close()
+ assert.NoError(t, err)
+
+ req, err := http.NewRequest("POST", "/", &body)
+ assert.NoError(t, err)
+
+ req.Header.Set("Content-Type", "multipart/form-data;
boundary="+mw.Boundary())
+ return req
+}
+
+func TestImport_invalid_file_type(t *testing.T) {
+ input := &ImportInput{}
+ input.FileName = "file1.txt"
+ input.FileContent = []byte("hello")
+
+ h := ImportHandler{}
+ ctx := droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err := h.Import(ctx)
+ assert.EqualError(t, err, "required file type is .yaml, .yml or .json
but got: .txt")
+}
+
+func TestImport_invalid_content(t *testing.T) {
+ input := &ImportInput{}
+ input.FileName = "file1.json"
+ input.FileContent = []byte(`{"test": "a"}`)
+
+ h := ImportHandler{}
+ ctx := droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err := h.Import(ctx)
+ assert.EqualError(t, err, "empty or invalid imported file")
+}
+
+func ReadFile(t *testing.T, filePath string) []byte {
+ cmd := exec.Command("pwd")
+ pwdByte, err := cmd.CombinedOutput()
+ pwd := string(pwdByte)
+ pwd = strings.Replace(pwd, "\n", "", 1)
+ dir := pwd[:strings.Index(pwd, "/api/")] + "/api/"
+ bytes, err := ioutil.ReadFile(dir + filePath)
+ assert.Nil(t, err)
+
+ return bytes
+}
+
+func TestImport_with_service_id(t *testing.T) {
+ bytes := ReadFile(t, "test/testdata/import/with-service-id.yaml")
+ input := &ImportInput{}
+ input.FileName = "file1.json"
+ input.FileContent = bytes
+
+ mStore := &store.MockInterface{}
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(nil, errors.New("data not found by key: service1"))
+
+ h := ImportHandler{
+ routeStore: &store.GenericStore{},
+ svcStore: mStore,
+ upstreamStore: mStore,
+ }
+ ctx := droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err := h.Import(ctx)
+ assert.EqualError(t, err, "data not found by key: service1")
+
+ //
+ mStore = &store.MockInterface{}
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(nil, data.ErrNotFound)
+
+ h = ImportHandler{
+ routeStore: &store.GenericStore{},
+ svcStore: mStore,
+ upstreamStore: mStore,
+ }
+ ctx = droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err = h.Import(ctx)
+ assert.EqualError(t, err, "service id: service1 not found")
+}
+
+func TestImport_with_upstream_id(t *testing.T) {
+ bytes := ReadFile(t, "test/testdata/import/with-upstream-id.yaml")
+ input := &ImportInput{}
+ input.FileName = "file1.json"
+ input.FileContent = bytes
+
+ mStore := &store.MockInterface{}
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(nil, errors.New("data not found by key: upstream1"))
+
+ h := ImportHandler{
+ routeStore: &store.GenericStore{},
+ svcStore: mStore,
+ upstreamStore: mStore,
+ }
+ ctx := droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err := h.Import(ctx)
+ assert.EqualError(t, err, "data not found by key: upstream1")
+
+ //
+ mStore = &store.MockInterface{}
+ mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
+ }).Return(nil, data.ErrNotFound)
+
+ h = ImportHandler{
+ routeStore: &store.GenericStore{},
+ svcStore: mStore,
+ upstreamStore: mStore,
+ }
+ ctx = droplet.NewContext()
+ ctx.SetInput(input)
+
+ _, err = h.Import(ctx)
+ assert.EqualError(t, err, "upstream id: upstream1 not found")
+
+}
diff --git a/api/internal/handler/handler_test.go
b/api/internal/handler/handler_test.go
index 7fd288c..e095bb6 100644
--- a/api/internal/handler/handler_test.go
+++ b/api/internal/handler/handler_test.go
@@ -21,8 +21,8 @@ import (
"net/http"
"testing"
- "github.com/go-playground/assert/v2"
"github.com/shiningrush/droplet/data"
+ "github.com/stretchr/testify/assert"
)
func TestSpecCodeResponse(t *testing.T) {
diff --git a/api/internal/route.go b/api/internal/route.go
index c1ace7e..ac78a36 100644
--- a/api/internal/route.go
+++ b/api/internal/route.go
@@ -75,6 +75,7 @@ func SetUpRouter() *gin.Engine {
server_info.NewHandler,
label.NewHandler,
data_loader.NewHandler,
+ data_loader.NewImportHandler,
}
for i := range factories {
diff --git a/api/internal/utils/utils.go b/api/internal/utils/utils.go
index 0a612c2..3661bcb 100644
--- a/api/internal/utils/utils.go
+++ b/api/internal/utils/utils.go
@@ -17,11 +17,13 @@
package utils
import (
+ "bytes"
"encoding/json"
"errors"
"fmt"
"net"
"os"
+ "sort"
"strconv"
"strings"
@@ -169,3 +171,38 @@ func ValidateLuaCode(code string) error {
_, err := parse.Parse(strings.NewReader(code), "<string>")
return err
}
+
+//
+func StringSliceEqual(a, b []string) bool {
+ if (a == nil) != (b == nil) {
+ return false
+ }
+
+ if len(a) != len(b) {
+ return false
+ }
+
+ sort.Strings(a)
+ sort.Strings(b)
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+// value compare
+func ValueEqual(a interface{}, b interface{}) bool {
+ aBytes, err := json.Marshal(a)
+ if err != nil {
+ return false
+ }
+ bBytes, err := json.Marshal(b)
+ if err != nil {
+ return false
+ }
+ return bytes.Equal(aBytes, bBytes)
+}
diff --git a/api/test/e2e/base.go b/api/test/e2e/base.go
index 1cf59eb..0d9a333 100644
--- a/api/test/e2e/base.go
+++ b/api/test/e2e/base.go
@@ -77,11 +77,14 @@ func init() {
Token = token
}
-func httpGet(url string) ([]byte, int, error) {
+func httpGet(url string, headers map[string]string) ([]byte, int, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, 0, err
}
+ for key, val := range headers {
+ req.Header.Add(key, val)
+ }
client := &http.Client{}
resp, err := client.Do(req)
diff --git a/api/test/e2e/http.go b/api/test/e2e/http.go
new file mode 100644
index 0000000..d6f1b6a
--- /dev/null
+++ b/api/test/e2e/http.go
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package e2e
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type UploadFile struct {
+ Name string
+ Filepath string
+}
+
+var httpClient = &http.Client{}
+
+func PostFile(reqUrl string, reqParams map[string]string, files []UploadFile,
headers map[string]string) ([]byte, int, error) {
+ return post(reqUrl, reqParams, "multipart/form-data", files, headers)
+}
+
+func post(reqUrl string, reqParams map[string]string, contentType string,
files []UploadFile, headers map[string]string) ([]byte, int, error) {
+ requestBody, realContentType := getReader(reqParams, contentType, files)
+ httpRequest, _ := http.NewRequest("POST", reqUrl, requestBody)
+ httpRequest.Header.Add("Content-Type", realContentType)
+ if headers != nil {
+ for k, v := range headers {
+ httpRequest.Header.Add(k, v)
+ }
+ }
+ resp, err := httpClient.Do(httpRequest)
+ if err != nil {
+ panic(err)
+ }
+
+ defer resp.Body.Close()
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return body, resp.StatusCode, nil
+}
+
+func getReader(reqParams map[string]string, contentType string, files
[]UploadFile) (io.Reader, string) {
+ if strings.Index(contentType, "json") > -1 {
+ bytesData, _ := json.Marshal(reqParams)
+ return bytes.NewReader(bytesData), contentType
+ }
+ if files != nil {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ for _, uploadFile := range files {
+ file, err := os.Open(uploadFile.Filepath)
+ if err != nil {
+ panic(err)
+ }
+ part, err := writer.CreateFormFile(uploadFile.Name,
filepath.Base(uploadFile.Filepath))
+ if err != nil {
+ panic(err)
+ }
+ _, err = io.Copy(part, file)
+ file.Close()
+ }
+ for k, v := range reqParams {
+ if err := writer.WriteField(k, v); err != nil {
+ panic(err)
+ }
+ }
+ if err := writer.Close(); err != nil {
+ panic(err)
+ }
+ return body, writer.FormDataContentType()
+ }
+
+ urlValues := url.Values{}
+ for key, val := range reqParams {
+ urlValues.Set(key, val)
+ }
+
+ reqBody := urlValues.Encode()
+
+ return strings.NewReader(reqBody), contentType
+}
diff --git a/api/test/e2e/route_export_test.go
b/api/test/e2e/route_export_test.go
index 5871d76..7c9b5c6 100644
--- a/api/test/e2e/route_export_test.go
+++ b/api/test/e2e/route_export_test.go
@@ -1463,13 +1463,13 @@ func TestExportRoute_With_Jwt_Plugin(t *testing.T) {
time.Sleep(sleepTime)
// sign jwt token
- body, status, err :=
httpGet("http://127.0.0.10:9080/apisix/plugin/jwt/sign?key=user-key")
+ body, status, err :=
httpGet("http://127.0.0.10:9080/apisix/plugin/jwt/sign?key=user-key", nil)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, status)
jwtToken := string(body)
// sign jwt token with not exists key
- body, status, err =
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=not-exist-key")
+ body, status, err =
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=not-exist-key", nil)
assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, status)
@@ -2485,4 +2485,3 @@ func replaceStr(str string) string {
str = strings.Replace(str, " ", "", -1)
return str
}
-
diff --git a/api/test/e2e/route_import_test.go
b/api/test/e2e/route_import_test.go
new file mode 100644
index 0000000..62a6c97
--- /dev/null
+++ b/api/test/e2e/route_import_test.go
@@ -0,0 +1,579 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package e2e
+
+import (
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/tidwall/gjson"
+)
+
+func TestImport_default(t *testing.T) {
+ path, err := filepath.Abs("../testdata/import/default.yaml")
+ assert.Nil(t, err)
+
+ headers := map[string]string{
+ "Authorization": token,
+ }
+ files := []UploadFile{
+ {Name: "file", Filepath: path},
+ }
+ PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files,
headers)
+
+ // 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{})
+
+ var tests []HttpTestCase
+ for _, item := range list {
+ route := item.(map[string]interface{})
+ tc := HttpTestCase{
+ Desc: "route patch for update status(online)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/routes/" +
route["id"].(string),
+ Body: `{"status":1}`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tc)
+ }
+
+ // verify route
+ tests = append(tests, HttpTestCase{
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/hello",
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "hello world",
+ Sleep: sleepTime,
+ })
+
+ // 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)
+ }
+}
+
+func TestImport_json(t *testing.T) {
+ path, err := filepath.Abs("../testdata/import/default.json")
+ assert.Nil(t, err)
+
+ headers := map[string]string{
+ "Authorization": token,
+ }
+ files := []UploadFile{
+ {Name: "file", Filepath: path},
+ }
+ PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files,
headers)
+
+ // 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{})
+
+ var tests []HttpTestCase
+ for _, item := range list {
+ route := item.(map[string]interface{})
+ tc := HttpTestCase{
+ Desc: "route patch for update status(online)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/routes/" +
route["id"].(string),
+ Body: `{"status":1}`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tc)
+ }
+
+ // verify route
+ tests = append(tests, HttpTestCase{
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/hello",
+ ExpectStatus: http.StatusOK,
+ ExpectBody: "hello world",
+ Sleep: sleepTime,
+ })
+
+ // 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)
+ }
+}
+
+func TestImport_with_plugins(t *testing.T) {
+ path, err := filepath.Abs("../testdata/import/with-plugins.yaml")
+ assert.Nil(t, err)
+
+ headers := map[string]string{
+ "Authorization": token,
+ }
+ files := []UploadFile{
+ {Name: "file", Filepath: path},
+ }
+ PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files,
headers)
+
+ // 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{})
+
+ var tests []HttpTestCase
+ for _, item := range list {
+ route := item.(map[string]interface{})
+ tc := HttpTestCase{
+ Desc: "route patch for update status(online)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/routes/" +
route["id"].(string),
+ Body: `{"status":1}`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tc)
+ }
+
+ // verify route
+ verifyTests := []HttpTestCase{
+ {
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodPost,
+ Path: "/hello",
+ Body: `{}`,
+ ExpectStatus: http.StatusBadRequest,
+ ExpectBody: `property "id" is required`,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodPost,
+ Path: "/hello",
+ Headers: map[string]string{"id": "1"},
+ Body: `{}`,
+ ExpectStatus: http.StatusBadRequest,
+ ExpectBody: `property "status" is required`,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodPost,
+ Path: "/hello",
+ Headers: map[string]string{"id": "1"},
+ Body: `{"status": "1"}`,
+ ExpectStatus: http.StatusUnauthorized,
+ ExpectBody: `{"message":"Missing authorization in
request"}`,
+ Sleep: sleepTime,
+ },
+ }
+ tests = append(tests, verifyTests...)
+
+ // 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)
+ }
+}
+
+func TestImport_with_multi_routes(t *testing.T) {
+ path, err := filepath.Abs("../testdata/import/multi-routes.yaml")
+ assert.Nil(t, err)
+
+ headers := map[string]string{
+ "Authorization": token,
+ }
+ files := []UploadFile{
+ {Name: "file", Filepath: path},
+ }
+ PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files,
headers)
+
+ // 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, 2, len(list))
+
+ var tests []HttpTestCase
+ for _, item := range list {
+ route := item.(map[string]interface{})
+ tc := HttpTestCase{
+ Desc: "route patch for update status(online)",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/routes/" +
route["id"].(string),
+ Body: `{"status":1}`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tc)
+ uris := route["uris"].([]interface{})
+ isGet := false
+ for _, uri := range uris {
+ if uri == "/get" {
+ isGet = true
+ }
+ }
+ // verify route data
+ if isGet {
+ tcDataVerify := HttpTestCase{
+ Desc: "verify data of route",
+ 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","HEAD","PUT","PATCH","DELETE"]`,
+
`"proxy-rewrite":{"disable":false,"scheme":"https"}`,
+
`"labels":{"API_VERSION":"v2","dev":"test"}`,
+
`"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"read":6000,"send":6000},"type":"roundrobin","pass_host":"node"}`,
+ },
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tcDataVerify)
+ } else {
+ 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":["POST"]`,
+
`"proxy-rewrite":{"disable":false,"scheme":"https"}`,
+
`"labels":{"API_VERSION":"v1","version":"v1"}`,
+
`"upstream":{"nodes":[{"host":"httpbin.org","port":443,"weight":1}],"timeout":{"connect":6000,"read":6000,"send":6000},"type":"roundrobin","pass_host":"node"}`,
+ },
+ Sleep: sleepTime,
+ }
+ tests = append(tests, tcDataVerify)
+ }
+ }
+
+ // verify route
+ verifyTests := []HttpTestCase{
+ {
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodGet,
+ Path: "/get",
+ ExpectStatus: http.StatusOK,
+ ExpectBody: `"url": "https://127.0.0.1/get"`,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "verify the route just imported",
+ Object: APISIXExpect(t),
+ Method: http.MethodPost,
+ Path: "/post",
+ ExpectStatus: http.StatusOK,
+ ExpectBody: `"url": "https://127.0.0.1/post"`,
+ Sleep: sleepTime,
+ },
+ }
+ tests = append(tests, verifyTests...)
+
+ // 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)
+ }
+}
+
+func TestRoute_export_import(t *testing.T) {
+ // create routes
+ tests := []HttpTestCase{
+ {
+ Desc: "Create a route",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r1",
+ Body: `{
+ "uris": ["/test-test1"],
+ "name": "route_all",
+ "desc": "所有",
+ "methods": ["GET"],
+ "hosts": ["test.com"],
+ "status": 1,
+ "upstream": {
+ "nodes": {
+ "172.16.238.20:1980": 1
+ },
+ "type": "roundrobin"
+ }
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "Create a route2",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r2",
+ Body: `{
+ "uris": ["/test-test2"],
+ "name": "route_all",
+ "desc": "所有1",
+ "methods": ["GET"],
+ "hosts": ["test.com"],
+ "status": 1,
+ "upstream": {
+ "nodes": {
+ "172.16.238.20:1980": 1
+ },
+ "type": "roundrobin"
+ }
+ }`,
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ Sleep: sleepTime,
+ },
+ {
+ Desc: "Create a route3",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/r3",
+ Body: `{
+ "uris": ["/test-test3"],
+ "name": "route_all",
+ "desc": "所有2",
+ "methods": ["GET"],
+ "hosts": ["test.com"],
+ "status": 1,
+ "upstream": {
+ "nodes": {
+ "172.16.238.20: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,
+ },
+ {
+ Desc: "delete the route2 just created",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodDelete,
+ Path: "/apisix/admin/routes/r2",
+ Headers: map[string]string{"Authorization": token},
+ ExpectStatus: http.StatusOK,
+ },
+ {
+ Desc: "delete the route3 just created",
+ Object: ManagerApiExpect(t),
+ Method: http.MethodDelete,
+ Path: "/apisix/admin/routes/r3",
+ 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":3,"routes":3}`))
+ 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, 3, 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"]`,
+ `"desc":"所有`,
+ `"hosts":["test.com"]`,
+
`"upstream":{"nodes":[{"host":"172.16.238.20","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/e2e/route_online_debug_test.go
b/api/test/e2e/route_online_debug_test.go
index fba4f03..4665309 100644
--- a/api/test/e2e/route_online_debug_test.go
+++ b/api/test/e2e/route_online_debug_test.go
@@ -485,7 +485,7 @@ func TestRoute_Online_Debug_Route_With_Jwt_Auth(t
*testing.T) {
time.Sleep(sleepTime)
// sign jwt token
- body, status, err :=
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key")
+ body, status, err :=
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key", nil)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, status)
jwtToken := string(body)
diff --git a/api/test/e2e/route_with_plugin_jwt_test.go
b/api/test/e2e/route_with_plugin_jwt_test.go
index 9ceec91..3bf50f1 100644
--- a/api/test/e2e/route_with_plugin_jwt_test.go
+++ b/api/test/e2e/route_with_plugin_jwt_test.go
@@ -94,13 +94,13 @@ func TestRoute_With_Jwt_Plugin(t *testing.T) {
time.Sleep(sleepTime)
// sign jwt token
- body, status, err :=
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key")
+ body, status, err :=
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key", nil)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, status)
jwtToken := string(body)
// sign jwt token with not exists key
- body, status, err =
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=not-exist-key")
+ body, status, err =
httpGet("http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=not-exist-key", nil)
assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, status)
diff --git a/api/test/testdata/import/default.json
b/api/test/testdata/import/default.json
new file mode 100644
index 0000000..0d20878
--- /dev/null
+++ b/api/test/testdata/import/default.json
@@ -0,0 +1,39 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "version": "1.0.0-oas3",
+ "description": "test desc",
+ "license": {
+ "name": "Apache License 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0"
+ },
+ "title": "test title"
+ },
+ "paths": {
+ "/hello": {
+ "get": {
+ "x-api-limit": 20,
+ "description": "hello world.",
+ "operationId": "hello",
+ "x-apisix-upstream": {
+ "type": "roundrobin",
+ "nodes": [
+ {
+ "host": "172.16.238.20",
+ "port": 1980,
+ "weight": 1
+ }
+ ]
+ },
+ "responses": {
+ "200": {
+ "description": "list response"
+ },
+ "default": {
+ "description": "unexpected error"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/api/test/testdata/import/default.yaml
b/api/test/testdata/import/default.yaml
new file mode 100644
index 0000000..3f69887
--- /dev/null
+++ b/api/test/testdata/import/default.yaml
@@ -0,0 +1,44 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+openapi: 3.0.0
+info:
+ version: 1.0.0-oas3
+ description: test desc
+ license:
+ name: Apache License 2.0
+ url: 'http://www.apache.org/licenses/LICENSE-2.0'
+ title: test title
+paths:
+ /hello:
+ get:
+ x-api-limit: 20
+ description: hello world.
+ operationId: hello
+ x-apisix-upstream:
+ type: roundrobin
+ nodes:
+ - host: 172.16.238.20
+ port: 1980
+ weight: 1
+ responses:
+ '200':
+ description: list response
+ default:
+ description: unexpected error
diff --git a/api/test/testdata/import/multi-routes.yaml
b/api/test/testdata/import/multi-routes.yaml
new file mode 100644
index 0000000..966815f
--- /dev/null
+++ b/api/test/testdata/import/multi-routes.yaml
@@ -0,0 +1,224 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+components: {}
+info:
+ title: RoutesExport
+ version: 3.0.0
+openapi: 3.0.0
+paths:
+ /get:
+ delete:
+ operationId: api1DELETE
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ get:
+ operationId: api1GET
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ head:
+ operationId: api1HEAD
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ patch:
+ operationId: api1PATCH
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ post:
+ operationId: api1POST
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ put:
+ operationId: api1PUT
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v2
+ dev: test
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
+ /post:
+ post:
+ operationId: test_postPOST
+ requestBody: {}
+ responses:
+ default:
+ description: ''
+ security: []
+ x-apisix-enableWebsocket: false
+ x-apisix-labels:
+ API_VERSION: v1
+ version: v1
+ x-apisix-plugins:
+ proxy-rewrite:
+ disable: false
+ scheme: https
+ x-apisix-priority: 0
+ x-apisix-status: 1
+ x-apisix-upstream:
+ nodes:
+ - host: httpbin.org
+ port: 443
+ weight: 1
+ timeout:
+ connect: 6000
+ read: 6000
+ send: 6000
+ type: roundrobin
+ pass_host: node
+ x-apisix-vars: []
diff --git a/api/test/testdata/import/with-plugins.yaml
b/api/test/testdata/import/with-plugins.yaml
new file mode 100644
index 0000000..68377d1
--- /dev/null
+++ b/api/test/testdata/import/with-plugins.yaml
@@ -0,0 +1,80 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+
+components:
+ securitySchemes:
+ basicAuth:
+ type: http
+ scheme: basic
+info:
+ version: "1"
+ description: |-
+ test desc
+ license:
+ name: Apache License 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0
+ title: |-
+ test title
+paths:
+ /hello:
+ post:
+ x-api-limit: 20
+ description: |-
+ hello world.
+ operationId: hello
+ x-apisix-upstream:
+ type: roundrobin
+ nodes:
+ - host: "172.16.238.20"
+ port: 1980
+ weight: 1
+ parameters:
+ - name: id
+ in: header
+ description: ID of pet to use
+ required: true
+ schema:
+ type: string
+ style: simple
+
+ requestBody:
+ content:
+ 'application/x-www-form-urlencoded':
+ schema:
+ properties:
+ name:
+ description: Update pet's name
+ type: string
+ status:
+ description: Updated status of the pet
+ type: string
+ required:
+ - status
+
+ security:
+ - basicAuth: []
+
+ responses:
+ 200:
+ description: list response
+ default:
+ description: unexpected error
+
+openapi: 3.0.0
diff --git a/api/test/testdata/import/with-service-id.yaml
b/api/test/testdata/import/with-service-id.yaml
new file mode 100644
index 0000000..985b02e
--- /dev/null
+++ b/api/test/testdata/import/with-service-id.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+openapi: 3.0.0
+info:
+ version: 1.0.0-oas3
+ description: test desc
+ license:
+ name: Apache License 2.0
+ url: 'http://www.apache.org/licenses/LICENSE-2.0'
+ title: test title
+paths:
+ /hello:
+ get:
+ x-api-limit: 20
+ description: hello world.
+ operationId: hello
+ x-apisix-service_id: service1
+ responses:
+ '200':
+ description: list response
+ default:
+ description: unexpected error
diff --git a/api/test/testdata/import/with-upstream-id.yaml
b/api/test/testdata/import/with-upstream-id.yaml
new file mode 100644
index 0000000..dc25276
--- /dev/null
+++ b/api/test/testdata/import/with-upstream-id.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# If you want to set the specified configuration value, you can set the new
+# in this file. For example if you want to specify the etcd address:
+#
+openapi: 3.0.0
+info:
+ version: 1.0.0-oas3
+ description: test desc
+ license:
+ name: Apache License 2.0
+ url: 'http://www.apache.org/licenses/LICENSE-2.0'
+ title: test title
+paths:
+ /hello:
+ get:
+ x-api-limit: 20
+ description: hello world.
+ operationId: hello
+ x-apisix-upstream_id: upstream1
+ responses:
+ '200':
+ description: list response
+ default:
+ description: unexpected error