From 6b4abc5da052fe2ffeb781a4a35c0392b9eba6b8 Mon Sep 17 00:00:00 2001 From: Jacean Date: Wed, 23 Dec 2020 10:13:18 +0800 Subject: [PATCH 01/36] Import & Export route from OpenAPI Specification3.0 Signed-off-by: Jacean --- api/go.mod | 1 + api/go.sum | 13 ++ api/internal/core/entity/entity.go | 9 + api/internal/handler/route/route.go | 282 ++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+) diff --git a/api/go.mod b/api/go.mod index 3c483718f7..77561c629d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -11,6 +11,7 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible + 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 diff --git a/api/go.sum b/api/go.sum index d245362801..b7ddc7faa0 100644 --- a/api/go.sum +++ b/api/go.sum @@ -22,6 +22,10 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0= github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0= github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= @@ -35,6 +39,10 @@ github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -94,6 +102,9 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkL github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= github.com/lestrrat-go/strftime v1.0.3 h1:qqOPU7y+TM8Y803I8fG9c/DyKG3xH/xkng6keC1015Q= github.com/lestrrat-go/strftime v1.0.3/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +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-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -118,6 +129,7 @@ github.com/shiningrush/droplet v0.2.1 h1:p2utttTbCfgiL+x0Zrb2jFeWspB7/o+v3e+R94G github.com/shiningrush/droplet v0.2.1/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M= github.com/shiningrush/droplet v0.2.2 h1:jEqSGoJXlybt1yQdauu+waE+l7KYlguNs8VayMfQ96Q= github.com/shiningrush/droplet v0.2.2/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M= +github.com/shiningrush/droplet v0.2.3 h1:bzPDzkE0F54r94XsultGS8uAPeL3pZIRmjqM0zIlpeI= github.com/shiningrush/droplet v0.2.3/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M= github.com/shiningrush/droplet/wrapper/gin v0.2.0 h1:LHkU+TbSkpePgXrTg3hJoSZlCMS03GeWMl0t+oLkd44= github.com/shiningrush/droplet/wrapper/gin v0.2.0/go.mod h1:ZJu+sCRrVXn5Pg618c1KK3Ob2UiXGuPM1ROx5uMM9YQ= @@ -130,6 +142,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws= diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index eb30ba7172..8888fdca7c 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -220,3 +220,12 @@ type Script struct { ID string `json:"id"` Script interface{} `json:"script,omitempty"` } + +type SecurityPlugin struct { +} + +type RequestValidation struct { + Type string `json:"type,omitempty"` + Required []string `json:"required,omitempty"` + Properties interface{} `json:"properties,omitempty"` +} diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go index 7d1413dfef..63fb75c2ef 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -17,14 +17,18 @@ package route import ( + "bufio" "encoding/json" "fmt" "io/ioutil" "net/http" "os/exec" "reflect" + "regexp" "strings" + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" "github.com/shiningrush/droplet" "github.com/shiningrush/droplet/data" @@ -72,6 +76,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { wrapper.InputType(reflect.TypeOf(BatchDelete{})))) r.GET("/apisix/admin/notexist/routes", consts.ErrorWrapper(Exist)) + + r.POST("/apisix/admin/routes/import", consts.ErrorWrapper(ImportRoutes)) } type GetInput struct { @@ -397,3 +403,279 @@ func Exist(c *gin.Context) (interface{}, error) { return nil, nil } + +func ImportRoutes(c *gin.Context) (interface{}, error) { + limit := 8 << 20 + file, err := c.FormFile("file") + if err != nil { + return nil, err + } + fileType := file.Filename[strings.LastIndex(file.Filename, ".")+1:] + if fileType != "json" { + return nil, fmt.Errorf("the file type error: need json,given %s", fileType) + } + if file.Size > int64(limit) { + return nil, fmt.Errorf("the file size exceeds the limit; limit 8M") + } + open, err := file.Open() + if err != nil { + return nil, err + } + defer open.Close() + reader := bufio.NewReader(open) + // set []byte volume is 8 M + bytes := make([]byte, file.Size) + _, _ = reader.Read(bytes) + swagger := &openapi3.Swagger{} + _ = json.Unmarshal(bytes, swagger) + routes := TransformOpenToRoute(swagger) + routeStore := store.GetStore(store.HubKeyRoute) + upstreamStore := store.GetStore(store.HubKeyUpstream) + scriptStore := store.GetStore(store.HubKeyScript) + //upstreamStr := `{"type":"roundrobin","timeout":{"connect":6000,"send":6000,"read":6000},"nodes":[{"host":"127.0.0.1","port":80,"weight":1}]}` + for _, route := range routes { + //upstream := &entity.Upstream{} + //_ = json.Unmarshal([]byte(upstreamStr), upstream) + //route.Upstream = &upstream.UpstreamDef + // check route name + _, err := checkRouteName(route.Name) + if err != nil { + continue + } + if route.ServiceID != "" { + _, err := routeStore.Get(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 != "" { + _, err := upstreamStore.Get(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 route.Script != nil { + if route.ID == "" { + route.ID = utils.GetFlakeUidStr() + } + script := &entity.Script{} + script.ID = utils.InterfaceToString(route.ID) + script.Script = route.Script + //to lua + var err error + route.Script, err = generateLuaCode(route.Script.(map[string]interface{})) + if err != nil { + return nil, err + } + //save original conf + if err = scriptStore.Create(c, script); err != nil { + return nil, err + } + } + if err := routeStore.Create(c, route); err != nil { + println(err.Error()) + return handler.SpecCodeResponse(err), err + } + } + return nil, nil +} + +func checkRouteName(name string) (bool, error) { + routeStore := store.GetStore(store.HubKeyRoute) + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + if err != nil { + return false, err + } + sort := store.NewSort(nil) + filter := store.NewFilter([]string{"name", name}) + pagination := store.NewPagination(0, 0) + query := store.NewQuery(sort, filter, pagination) + rows := store.NewFilterSelector(toRows(ret), query) + if len(rows) > 0 { + return false, consts.InvalidParam("Route name is reduplicate") + } + return true, nil +} + +func TransformOpenToRoute(swagger *openapi3.Swagger) []*entity.Route { + var routes []*entity.Route + swagger.Extensions = nil + paths := swagger.Paths + for k, v := range paths { + if v.Get != nil { + routes = append(routes, getRouteFromPaths("GET", k, v.Get, swagger)) + } + if v.Post != nil { + routes = append(routes, getRouteFromPaths("POST", k, v.Post, swagger)) + } + if v.Head != nil { + routes = append(routes, getRouteFromPaths("HEAD", k, v.Head, swagger)) + } + if v.Put != nil { + routes = append(routes, getRouteFromPaths("PUT", k, v.Put, swagger)) + } + if v.Patch != nil { + routes = append(routes, getRouteFromPaths("PATCH", k, v.Patch, swagger)) + } + if v.Delete != nil { + routes = append(routes, getRouteFromPaths("DELETE", k, v.Delete, swagger)) + } + } + return routes +} + +func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) *entity.Route { + reg := regexp.MustCompile(`{[\w.]*\}`) + findString := reg.FindString(key) + if findString != "" { + key = strings.Split(key, findString)[0] + "*" + } + r := &entity.Route{ + URI: key, + Name: value.OperationID, + Desc: value.Summary, + Methods: []string{method}, + } + var parameters *openapi3.Parameters + plugins := make(map[string]interface{}) + requestValidation := make(map[string]*entity.RequestValidation) + if value.Parameters != nil { + parameters = &value.Parameters + } + if parameters != nil { + for _, v := range *parameters { + if v.Value.Schema != nil { + v.Value.Schema.Value.Extensions = nil + v.Value.Schema.Value.Format = "" + v.Value.Schema.Value.XML = nil + } + props := make(map[string]interface{}) + switch v.Value.In { + case "header": + props[v.Value.Name] = v.Value.Schema.Value + var required []string + if v.Value.Required { + required = append(required, v.Value.Name) + } + requestValidation["header_schema"] = &entity.RequestValidation{ + Type: "object", + Required: required, + Properties: props, + } + plugins["request-validation"] = requestValidation + } + } + } + + if value.RequestBody != nil { + vars := make([]interface{}, 0) + schema := value.RequestBody.Value.Content + for k, v := range schema { + item := []string{"http_Content-type", "==", ""} + item[2] = k + vars = append(vars, item) + 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.Extensions = nil + 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 + } + r.Vars = vars + } + + if value.Security != nil { + p := &entity.SecurityPlugin{} + for _, v := range *value.Security { + for v1 := range v { + switch v1 { + case "api_key": + plugins["key-auth"] = p + case "basicAuth": + plugins["basic-auth"] = p + case "bearerAuth": + plugins["jwt-auth"] = p + } + } + } + } + r.Plugins = plugins + return r +} + +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 + schemaRef.Value.Extensions = 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 = "" + v.Value.Extensions = nil + } + 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.Extensions = nil + v.Value.Items.Value.Format = "" + } + } + } + return schemaRef +} From 72ba9c0d89c2923f9bcae272f0acc5b34d2bbc65 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 5 Jan 2021 14:57:36 +0800 Subject: [PATCH 02/36] feat: Import route from OpenAPI Specification3.0 --- api/go.mod | 2 +- api/go.sum | 38 +- api/internal/conf/conf.go | 1 + api/internal/core/entity/entity.go | 7 +- api/internal/core/store/store.go | 31 +- api/internal/core/store/validate.go | 6 +- api/internal/filter/schema.go | 1 - api/internal/handler/data_loader/export.go | 396 ++++++++ api/internal/handler/handler_test.go | 2 +- api/internal/handler/route/route.go | 294 +----- api/internal/route.go | 2 + api/test/docker/Dockerfile-apisix | 5 +- docs/api/api.json | 1031 ++++++++++++++++++++ docs/api/api.yaml | 463 +++++++++ docs/api/api.yml | 741 ++++++++++++++ 15 files changed, 2718 insertions(+), 302 deletions(-) create mode 100644 api/internal/handler/data_loader/export.go create mode 100644 docs/api/api.json create mode 100644 docs/api/api.yaml create mode 100644 docs/api/api.yml diff --git a/api/go.mod b/api/go.mod index 5c045ff8f5..7a9f613c57 100644 --- a/api/go.mod +++ b/api/go.mod @@ -15,9 +15,9 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/getkin/kin-openapi v0.33.0 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 diff --git a/api/go.sum b/api/go.sum index 33962240d4..8a2936f526 100644 --- a/api/go.sum +++ b/api/go.sum @@ -76,7 +76,6 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg= github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -112,6 +111,7 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe 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= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -129,9 +129,11 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -150,11 +152,13 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -171,11 +175,15 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 h1:FlFbCRLd5Jr4iYXZufAvgWN6Ao0JrI5chLINnUXDDr0= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -199,17 +207,20 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -221,8 +232,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= @@ -239,6 +252,7 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -250,8 +264,10 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -296,23 +312,27 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= 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/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= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -334,9 +354,11 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM= @@ -364,6 +386,7 @@ github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0 github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 h1:j6JEOq5QWFker+d7mFQYOhjTZonQ7YkLTHm56dbn+yM= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -377,10 +400,13 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY= @@ -397,6 +423,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= @@ -408,6 +435,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -417,10 +445,12 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu32eiRR/CEYdjOjOd4zE6Y= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -484,6 +514,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -497,10 +528,12 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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/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= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -529,6 +562,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba 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/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= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -552,7 +586,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/api/internal/conf/conf.go b/api/internal/conf/conf.go index 76230d31f0..02ac57e2fe 100644 --- a/api/internal/conf/conf.go +++ b/api/internal/conf/conf.go @@ -51,6 +51,7 @@ var ( UserList = make(map[string]User, 2) AuthConf Authentication SSLDefaultStatus = 1 //enable ssl by default + ImportSizeLimit = 10 * 1024 *1024 ) type Etcd struct { diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index 3068092a50..b3ccb2a860 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -84,7 +84,7 @@ type Route struct { Vars interface{} `json:"vars,omitempty"` FilterFunc string `json:"filter_func,omitempty"` Script interface{} `json:"script,omitempty"` - Plugins map[string]interface{} `json:"plugins,omitempty"` + Plugins map[string]interface{} `json:"plugins"` Upstream *UpstreamDef `json:"upstream,omitempty"` ServiceID interface{} `json:"service_id,omitempty"` UpstreamID interface{} `json:"upstream_id,omitempty"` @@ -94,6 +94,11 @@ type Route struct { Status Status `json:"status"` } +type RouteImport struct { + Plugins map[string]interface{} `json:"plugins"` + Route +} + // --- structures for upstream start --- type Timeout struct { Connect int `json:"connect,omitempty"` diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go index 9a4b11314a..f2abccf245 100644 --- a/api/internal/core/store/store.go +++ b/api/internal/core/store/store.go @@ -224,7 +224,7 @@ func (s *GenericStore) List(input ListInput) (*ListOutput, error) { 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 } } @@ -240,32 +240,47 @@ func (s *GenericStore) ingestValidate(obj interface{}) (err error) { return err } -func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { +func (s *GenericStore) CreateCheck(obj interface{}) ([]byte, error) { if setter, ok := obj.(entity.BaseInfoSetter); ok { info := setter.GetBaseInfo() info.Creating() } if err := s.ingestValidate(obj); err != nil { - return err + return nil, err } key := s.opt.KeyFunc(obj) if key == "" { - return fmt.Errorf("key is required") + return nil, fmt.Errorf("key is required") } _, ok := s.cache.Load(key) if ok { log.Warnf("key: %s is conflicted", key) - return fmt.Errorf("key: %s is conflicted", key) + 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 fmt.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{}) error { + if setter, ok := obj.(entity.BaseInfoSetter); ok { + info := setter.GetBaseInfo() + info.Creating() + } + + bytes, err := s.CreateCheck(obj) + if err != nil{ + return err + } + + if err := s.Stg.Create(ctx, s.GetObjStorageKey(obj), string(bytes)); err != nil { return err } diff --git a/api/internal/core/store/validate.go b/api/internal/core/store/validate.go index 493c1698ab..045c360157 100644 --- a/api/internal/core/store/validate.go +++ b/api/internal/core/store/validate.go @@ -72,6 +72,7 @@ func (v *JsonSchemaValidator) Validate(obj interface{}) error { type APISIXJsonSchemaValidator struct { schema *gojsonschema.Schema + schemaDef string } func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) { @@ -88,6 +89,7 @@ func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) { } return &APISIXJsonSchemaValidator{ 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,8 @@ func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error { } errString.AppendString(vErr.String()) } + j, _ := json.Marshal(obj) + log.Errorf("schema validate failed:s: %v, obj: %s", v.schemaDef, string(j)) return fmt.Errorf("schema validate failed: %s", errString.String()) } diff --git a/api/internal/filter/schema.go b/api/internal/filter/schema.go index e9d50f2a94..120d8bcfc6 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/export.go b/api/internal/handler/data_loader/export.go new file mode 100644 index 0000000000..4f9d25b4ed --- /dev/null +++ b/api/internal/handler/data_loader/export.go @@ -0,0 +1,396 @@ +package data_loader + +import ( + "bufio" + "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/data" + + "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" + routeHandler "github.com/apisix/manager-api/internal/handler/route" + "github.com/apisix/manager-api/internal/utils" + "github.com/apisix/manager-api/internal/utils/consts" +) + +type Handler struct { + routeStore store.Interface + svcStore store.Interface + upstreamStore store.Interface + scriptStore store.Interface +} + +func NewHandler() (handler.RouteRegister, error) { + return &Handler{ + routeStore: store.GetStore(store.HubKeyRoute), + svcStore: store.GetStore(store.HubKeyService), + upstreamStore: store.GetStore(store.HubKeyUpstream), + scriptStore: store.GetStore(store.HubKeyScript), + }, nil +} + +func (h *Handler) ApplyRoute(r *gin.Engine) { + r.POST("/apisix/admin/import", consts.ErrorWrapper(Import)) +} + +func Import(c *gin.Context) (interface{}, error) { + file, err := c.FormFile("file") + if err != nil { + return nil, err + } + + // file check + suffix := path.Ext(file.Filename) + if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" { + return nil, fmt.Errorf("the file type error: %s", suffix) + } + if file.Size > int64(conf.ImportSizeLimit) { + return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) + } + + // read file and parse + open, err := file.Open() + if err != nil { + return nil, err + } + defer open.Close() + reader := bufio.NewReader(open) + bytes := make([]byte, file.Size) + _, _ = reader.Read(bytes) + + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(bytes) + + routes := OpenAPI3ToRoute(swagger) + routeStore := store.GetStore(store.HubKeyRoute) + upstreamStore := store.GetStore(store.HubKeyUpstream) + scriptStore := store.GetStore(store.HubKeyScript) + + // check route + for _, route := range routes { + _, err := checkRouteName(route.Name) + if err != nil { + continue + } + if route.ServiceID != nil { + _, err := routeStore.Get(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 := upstreamStore.Get(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 route.Script != nil { + if route.ID == "" { + route.ID = utils.GetFlakeUidStr() + } + script := &entity.Script{} + script.ID = utils.InterfaceToString(route.ID) + script.Script = route.Script + //to lua + var err error + route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) + if err != nil { + return nil, err + } + //save original conf + if err = scriptStore.Create(c, script); err != nil { + return nil, err + } + } + + if _, err := routeStore.CreateCheck(route); err != nil { + return handler.SpecCodeResponse(err), err + } + } + + // create route + for _, route := range routes { + if err := routeStore.Create(c, route); err != nil { + println(err.Error()) + return handler.SpecCodeResponse(err), err + } + } + + return nil, nil +} + + +func checkRouteName(name string) (bool, error) { + routeStore := store.GetStore(store.HubKeyRoute) + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + if err != nil { + return false, err + } + + sort := store.NewSort(nil) + filter := store.NewFilter([]string{"name", name}) + pagination := store.NewPagination(0, 0) + query := store.NewQuery(sort, filter, pagination) + rows := store.NewFilterSelector(routeHandler.ToRows(ret), query) + if len(rows) > 0 { + return false, consts.InvalidParam("route name is duplicate") + } + + return true, nil +} + +func structByReflect(data map[string]interface{}, inStructPtr interface{}) error { + rType := reflect.TypeOf(inStructPtr) + rVal := reflect.ValueOf(inStructPtr) + if rType.Kind() != reflect.Ptr { + return errors.New("inStructPtr must be ptr to struct") + } + rType = rType.Elem() + rVal = rVal.Elem() + + for i := 0; i < rType.NumField(); i++ { + t := rType.Field(i) + f := rVal.Field(i) + v, ok := data[t.Name] + if !ok { + continue + } + + f.Set(reflect.ValueOf(v)) + } + + return nil +} + +func parseExtension(route *entity.Route, val *openapi3.Operation, upstream, emptyUpstream *entity.UpstreamDef) *entity.Route { + fmt.Printf("v.Extensions: %s", val.Extensions) + if up, ok := val.Extensions["x-apisix-upstream"]; ok { + json.Unmarshal(up.(json.RawMessage), upstream) + } + + fmt.Printf("upstream: %v, %v, %v", upstream, emptyUpstream, upstream != nil && upstream != emptyUpstream) + if upstream != nil && upstream == emptyUpstream { + route.Upstream = upstream + } + + return route +} + +func OpenAPI3ToRoute(swagger *openapi3.Swagger) []*entity.Route { + var routes []*entity.Route + //if globalUpstreams, ok := swagger.Extensions["x-apisix-upstreams"]; ok { + // globalUpstreams = globalUpstreams.([]map[string]interface{}) + // + //} + paths := swagger.Paths + var upstream *entity.UpstreamDef + emptyUpstream := &entity.UpstreamDef{} + for k, v := range paths { + upstream = &entity.UpstreamDef{} + if up, ok := v.Extensions["x-apisix-upstream"]; ok { + json.Unmarshal(up.(json.RawMessage), upstream) + } + + if v.Get != nil { + route := getRouteFromPaths("GET", k, v.Get, swagger) + route = parseExtension(route, v.Get, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Post != nil { + route := getRouteFromPaths("POST", k, v.Post, swagger) + route = parseExtension(route, v.Post, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Head != nil { + route := getRouteFromPaths("HEAD", k, v.Head, swagger) + route = parseExtension(route, v.Head, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Put != nil { + route := getRouteFromPaths("PUT", k, v.Put, swagger) + route = parseExtension(route, v.Put, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Patch != nil { + route := getRouteFromPaths("PATCH", k, v.Patch, swagger) + route = parseExtension(route, v.Patch, upstream, emptyUpstream) + + routes = append(routes, route) + } + + if v.Delete != nil { + route := getRouteFromPaths("DELETE", k, v.Delete, swagger) + route = parseExtension(route, v.Delete, upstream, emptyUpstream) + + routes = append(routes, route) + } + } + return routes +} + +func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) *entity.Route { + reg := regexp.MustCompile(`{[\w.]*\}`) + findString := reg.FindString(key) + if findString != "" { + key = strings.Split(key, findString)[0] + "*" + } + r := &entity.Route{ + URI: key, + Name: value.OperationID, + Desc: value.Summary, + Methods: []string{method}, + } + var parameters *openapi3.Parameters + plugins := make(map[string]interface{}) + requestValidation := make(map[string]*entity.RequestValidation) + if value.Parameters != nil { + parameters = &value.Parameters + } + if parameters != nil { + for _, v := range *parameters { + if v.Value.Schema != nil { + v.Value.Schema.Value.Format = "" + v.Value.Schema.Value.XML = nil + } + props := make(map[string]interface{}) + switch v.Value.In { + case "header": + props[v.Value.Name] = v.Value.Schema.Value + var required []string + if v.Value.Required { + required = append(required, v.Value.Name) + } + requestValidation["header_schema"] = &entity.RequestValidation{ + Type: "object", + Required: required, + Properties: props, + } + plugins["request-validation"] = requestValidation + } + } + } + + if value.RequestBody != nil { + vars := make([]interface{}, 0) + schema := value.RequestBody.Value.Content + for k, v := range schema { + item := []string{"http_Content-type", "==", ""} + item[2] = k + vars = append(vars, item) + 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 + } + r.Vars = vars + } + + if value.Security != nil { + p := &entity.SecurityPlugin{} + for _, v := range *value.Security { + for v1 := range v { + switch v1 { + case "api_key": + plugins["key-auth"] = p + case "basicAuth": + plugins["basic-auth"] = p + case "bearerAuth": + plugins["jwt-auth"] = p + } + } + } + } + r.Plugins = plugins + + fmt.Printf("r: %v", r) + return r +} + +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/handler_test.go b/api/internal/handler/handler_test.go index 7fd288c041..e095bb6f73 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/handler/route/route.go b/api/internal/handler/route/route.go index ab1eec63a6..21be72e29e 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -17,24 +17,21 @@ package route import ( - "bufio" "encoding/json" "fmt" "net/http" "os" "path/filepath" "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" - "github.com/yuin/gopher-lua" wgin "github.com/shiningrush/droplet/wrapper/gin" + "github.com/yuin/gopher-lua" + "github.com/apisix/manager-api/internal/conf" "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" @@ -79,8 +76,6 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { r.PATCH("/apisix/admin/routes/:id/*path", consts.ErrorWrapper(Patch)) r.GET("/apisix/admin/notexist/routes", consts.ErrorWrapper(Exist)) - - r.POST("/apisix/admin/routes/import", consts.ErrorWrapper(ImportRoutes)) } func Patch(c *gin.Context) (interface{}, error) { @@ -257,7 +252,7 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { return ret, nil } -func generateLuaCode(script map[string]interface{}) (string, error) { +func GenerateLuaCode(script map[string]interface{}) (string, error) { scriptString, err := json.Marshal(script) if err != nil { return "", err @@ -329,7 +324,7 @@ func (h *Handler) Create(c droplet.Context) (interface{}, error) { script.Script = input.Script //to lua var err error - input.Script, err = generateLuaCode(input.Script.(map[string]interface{})) + input.Script, err = GenerateLuaCode(input.Script.(map[string]interface{})) if err != nil { return nil, err } @@ -399,7 +394,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, fmt.Errorf("invalid `script`") } - input.Route.Script, err = generateLuaCode(scriptConf) + input.Route.Script, err = GenerateLuaCode(scriptConf) if err != nil { return &data.SpecCodeResponse{StatusCode: http.StatusInternalServerError}, err } @@ -460,7 +455,7 @@ type ExistInput struct { Name string `auto_read:"name,query"` } -func toRows(list *store.ListOutput) []store.Row { +func ToRows(list *store.ListOutput) []store.Row { rows := make([]store.Row, list.TotalSize) for i := range list.Rows { rows[i] = list.Rows[i].(*entity.Route) @@ -517,7 +512,7 @@ func Exist(c *gin.Context) (interface{}, error) { filter := store.NewFilter([]string{"name", name}) pagination := store.NewPagination(0, 0) query := store.NewQuery(sort, filter, pagination) - rows := store.NewFilterSelector(toRows(ret), query) + rows := store.NewFilterSelector(ToRows(ret), query) if len(rows) > 0 { r := rows[0].(*entity.Route) @@ -530,278 +525,3 @@ func Exist(c *gin.Context) (interface{}, error) { return nil, nil } -func ImportRoutes(c *gin.Context) (interface{}, error) { - limit := 8 << 20 - file, err := c.FormFile("file") - if err != nil { - return nil, err - } - fileType := file.Filename[strings.LastIndex(file.Filename, ".")+1:] - if fileType != "json" { - return nil, fmt.Errorf("the file type error: need json,given %s", fileType) - } - if file.Size > int64(limit) { - return nil, fmt.Errorf("the file size exceeds the limit; limit 8M") - } - open, err := file.Open() - if err != nil { - return nil, err - } - defer open.Close() - reader := bufio.NewReader(open) - // set []byte volume is 8 M - bytes := make([]byte, file.Size) - _, _ = reader.Read(bytes) - swagger := &openapi3.Swagger{} - _ = json.Unmarshal(bytes, swagger) - routes := TransformOpenToRoute(swagger) - routeStore := store.GetStore(store.HubKeyRoute) - upstreamStore := store.GetStore(store.HubKeyUpstream) - scriptStore := store.GetStore(store.HubKeyScript) - //upstreamStr := `{"type":"roundrobin","timeout":{"connect":6000,"send":6000,"read":6000},"nodes":[{"host":"127.0.0.1","port":80,"weight":1}]}` - for _, route := range routes { - //upstream := &entity.Upstream{} - //_ = json.Unmarshal([]byte(upstreamStr), upstream) - //route.Upstream = &upstream.UpstreamDef - // check route name - _, err := checkRouteName(route.Name) - if err != nil { - continue - } - if route.ServiceID != "" { - _, err := routeStore.Get(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 != "" { - _, err := upstreamStore.Get(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 route.Script != nil { - if route.ID == "" { - route.ID = utils.GetFlakeUidStr() - } - script := &entity.Script{} - script.ID = utils.InterfaceToString(route.ID) - script.Script = route.Script - //to lua - var err error - route.Script, err = generateLuaCode(route.Script.(map[string]interface{})) - if err != nil { - return nil, err - } - //save original conf - if err = scriptStore.Create(c, script); err != nil { - return nil, err - } - } - if err := routeStore.Create(c, route); err != nil { - println(err.Error()) - return handler.SpecCodeResponse(err), err - } - } - return nil, nil -} - -func checkRouteName(name string) (bool, error) { - routeStore := store.GetStore(store.HubKeyRoute) - ret, err := routeStore.List(store.ListInput{ - Predicate: nil, - PageSize: 0, - PageNumber: 0, - }) - if err != nil { - return false, err - } - sort := store.NewSort(nil) - filter := store.NewFilter([]string{"name", name}) - pagination := store.NewPagination(0, 0) - query := store.NewQuery(sort, filter, pagination) - rows := store.NewFilterSelector(toRows(ret), query) - if len(rows) > 0 { - return false, consts.InvalidParam("Route name is reduplicate") - } - return true, nil -} - -func TransformOpenToRoute(swagger *openapi3.Swagger) []*entity.Route { - var routes []*entity.Route - swagger.Extensions = nil - paths := swagger.Paths - for k, v := range paths { - if v.Get != nil { - routes = append(routes, getRouteFromPaths("GET", k, v.Get, swagger)) - } - if v.Post != nil { - routes = append(routes, getRouteFromPaths("POST", k, v.Post, swagger)) - } - if v.Head != nil { - routes = append(routes, getRouteFromPaths("HEAD", k, v.Head, swagger)) - } - if v.Put != nil { - routes = append(routes, getRouteFromPaths("PUT", k, v.Put, swagger)) - } - if v.Patch != nil { - routes = append(routes, getRouteFromPaths("PATCH", k, v.Patch, swagger)) - } - if v.Delete != nil { - routes = append(routes, getRouteFromPaths("DELETE", k, v.Delete, swagger)) - } - } - return routes -} - -func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) *entity.Route { - reg := regexp.MustCompile(`{[\w.]*\}`) - findString := reg.FindString(key) - if findString != "" { - key = strings.Split(key, findString)[0] + "*" - } - r := &entity.Route{ - URI: key, - Name: value.OperationID, - Desc: value.Summary, - Methods: []string{method}, - } - var parameters *openapi3.Parameters - plugins := make(map[string]interface{}) - requestValidation := make(map[string]*entity.RequestValidation) - if value.Parameters != nil { - parameters = &value.Parameters - } - if parameters != nil { - for _, v := range *parameters { - if v.Value.Schema != nil { - v.Value.Schema.Value.Extensions = nil - v.Value.Schema.Value.Format = "" - v.Value.Schema.Value.XML = nil - } - props := make(map[string]interface{}) - switch v.Value.In { - case "header": - props[v.Value.Name] = v.Value.Schema.Value - var required []string - if v.Value.Required { - required = append(required, v.Value.Name) - } - requestValidation["header_schema"] = &entity.RequestValidation{ - Type: "object", - Required: required, - Properties: props, - } - plugins["request-validation"] = requestValidation - } - } - } - - if value.RequestBody != nil { - vars := make([]interface{}, 0) - schema := value.RequestBody.Value.Content - for k, v := range schema { - item := []string{"http_Content-type", "==", ""} - item[2] = k - vars = append(vars, item) - 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.Extensions = nil - 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 - } - r.Vars = vars - } - - if value.Security != nil { - p := &entity.SecurityPlugin{} - for _, v := range *value.Security { - for v1 := range v { - switch v1 { - case "api_key": - plugins["key-auth"] = p - case "basicAuth": - plugins["basic-auth"] = p - case "bearerAuth": - plugins["jwt-auth"] = p - } - } - } - } - r.Plugins = plugins - return r -} - -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 - schemaRef.Value.Extensions = 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 = "" - v.Value.Extensions = nil - } - 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.Extensions = nil - v.Value.Items.Value.Format = "" - } - } - } - return schemaRef -} diff --git a/api/internal/route.go b/api/internal/route.go index 02e1650e47..439727e272 100644 --- a/api/internal/route.go +++ b/api/internal/route.go @@ -18,6 +18,7 @@ package internal import ( "fmt" + "github.com/apisix/manager-api/internal/handler/data_loader" "path/filepath" "github.com/gin-contrib/pprof" @@ -73,6 +74,7 @@ func SetUpRouter() *gin.Engine { route_online_debug.NewHandler, server_info.NewHandler, label.NewHandler, + data_loader.NewHandler, } for i := range factories { diff --git a/api/test/docker/Dockerfile-apisix b/api/test/docker/Dockerfile-apisix index 5c4bd3178a..e27fc6ef9a 100644 --- a/api/test/docker/Dockerfile-apisix +++ b/api/test/docker/Dockerfile-apisix @@ -20,6 +20,9 @@ FROM openresty/openresty:alpine-fat AS production-stage ARG APISIX_VERSION=master LABEL apisix_version="${APISIX_VERSION}" + +COPY test/docker/apisix-master-0.rockspec ./apisix-master-0.rockspec + RUN set -x \ && /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories \ && apk add --no-cache --virtual .builddeps \ @@ -29,7 +32,7 @@ RUN set -x \ pkgconfig \ cmake \ git \ - && luarocks install https://github.com/apache/apisix/raw/master/rockspec/apisix-${APISIX_VERSION}-0.rockspec --tree=/usr/local/apisix/deps \ + && luarocks install ./apisix-master-0.rockspec --tree=/usr/local/apisix/deps \ && cp -v /usr/local/apisix/deps/lib/luarocks/rocks-5.1/apisix/${APISIX_VERSION}-0/bin/apisix /usr/bin/ \ && bin='#! /usr/local/openresty/luajit/bin/luajit\npackage.path = "/usr/local/apisix/?.lua;" .. package.path' \ && sed -i "1s@.*@$bin@" /usr/bin/apisix \ diff --git a/docs/api/api.json b/docs/api/api.json new file mode 100644 index 0000000000..fd35c224dd --- /dev/null +++ b/docs/api/api.json @@ -0,0 +1,1031 @@ +{ + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\nAPI doc of Manager API.\n\nManager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients.", + "title": "Licensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements. See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License. You may obtain a copy of the License at", + "license": { + "name": "Apache License 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0" + } + }, + "host": "127.0.0.1", + "paths": { + "/api/labels": { + "get": { + "description": "Return the labels list among `route,ssl,consumer,upstream,service`\naccording to the specified page number and page size, and can search labels by label.", + "produces": [ + "application/json" + ], + "operationId": "getLabelsList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "label filter of labels", + "name": "label", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/service" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/check_ssl_cert": { + "post": { + "produces": [ + "application/json" + ], + "summary": "verify SSL cert and key.", + "operationId": "checkSSL", + "parameters": [ + { + "type": "string", + "description": "cert of SSL", + "name": "cert", + "in": "body", + "required": true + }, + { + "type": "string", + "description": "key of SSL", + "name": "key", + "in": "body", + "required": true + } + ], + "responses": { + "0": { + "description": "SSL verify passed", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/check_ssl_exists": { + "post": { + "produces": [ + "application/json" + ], + "summary": "Check whether the SSL exists.", + "operationId": "checkSSLExist", + "parameters": [ + { + "type": "string", + "description": "cert of SSL", + "name": "cert", + "in": "body", + "required": true + }, + { + "type": "string", + "description": "key of SSL", + "name": "key", + "in": "body", + "required": true + } + ], + "responses": { + "0": { + "description": "SSL exists", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/consumers": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the consumer list according to the specified page number and page size, and can search consumers by username.", + "operationId": "getConsumerList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "username of consumer", + "name": "username", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/consumer" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/global_rules": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the global rule list according to the specified page number and page size.", + "operationId": "getGlobalRuleList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/GlobalPlugins" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/notexist/routes": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return result of route exists checking by name and exclude id.", + "operationId": "checkRouteExist", + "parameters": [ + { + "type": "string", + "description": "name of route", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "id of route that exclude checking", + "name": "exclude", + "in": "query" + } + ], + "responses": { + "0": { + "description": "route not exists", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/routes": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the route list according to the specified page number and page size, and can search routes by name and uri.", + "operationId": "getRouteList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "name of route", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "uri of route", + "name": "uri", + "in": "query" + }, + { + "type": "string", + "description": "label of route", + "name": "label", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/route" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/services": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the service list according to the specified page number and page size, and can search services by name.", + "operationId": "getServiceList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "name of service", + "name": "name", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/service" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/ssl": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the SSL list according to the specified page number and page size, and can SSLs search by sni.", + "operationId": "getSSLList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "sni of SSL", + "name": "sni", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ssl" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/upstreams": { + "get": { + "produces": [ + "application/json" + ], + "summary": "Return the upstream list according to the specified page number and page size, and can search upstreams by name.", + "operationId": "getUpstreamList", + "parameters": [ + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "name of upstream", + "name": "name", + "in": "query" + } + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/upstream" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/apisix/admin/user/login": { + "post": { + "produces": [ + "application/json" + ], + "summary": "user login.", + "operationId": "userLogin", + "parameters": [ + { + "type": "string", + "description": "user name", + "name": "username", + "in": "body", + "required": true + }, + { + "type": "string", + "description": "password", + "name": "password", + "in": "body", + "required": true + } + ], + "responses": { + "0": { + "description": "login success", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + } + }, + "definitions": { + "ApiError": { + "type": "object", + "properties": { + "code": { + "description": "response code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "message": { + "description": "response message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/utils/consts" + }, + "BaseInfo": { + "type": "object", + "properties": { + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "Consumer": { + "type": "object", + "properties": { + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "desc": { + "type": "string", + "x-go-name": "Desc" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Plugins" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + }, + "username": { + "type": "string", + "x-go-name": "Username" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "GlobalPlugins": { + "type": "object", + "properties": { + "id": { + "type": "object", + "x-go-name": "ID" + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Plugins" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "LoginInput": { + "type": "object", + "properties": { + "password": { + "description": "password", + "type": "string", + "x-go-name": "Password" + }, + "username": { + "description": "user name", + "type": "string", + "x-go-name": "Username" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/handler/authentication" + }, + "Route": { + "type": "object", + "properties": { + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "desc": { + "type": "string", + "x-go-name": "Desc" + }, + "enable_websocket": { + "type": "boolean", + "x-go-name": "EnableWebsocket" + }, + "filter_func": { + "type": "string", + "x-go-name": "FilterFunc" + }, + "host": { + "type": "string", + "x-go-name": "Host" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Hosts" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "methods": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Methods" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Plugins" + }, + "priority": { + "type": "integer", + "format": "int64", + "x-go-name": "Priority" + }, + "remote_addr": { + "type": "string", + "x-go-name": "RemoteAddr" + }, + "remote_addrs": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "RemoteAddrs" + }, + "script": { + "type": "object", + "x-go-name": "Script" + }, + "service_id": { + "type": "object", + "x-go-name": "ServiceID" + }, + "service_protocol": { + "type": "string", + "x-go-name": "ServiceProtocol" + }, + "status": { + "$ref": "#/definitions/Status" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + }, + "upstream": { + "$ref": "#/definitions/UpstreamDef" + }, + "upstream_id": { + "type": "object", + "x-go-name": "UpstreamID" + }, + "uri": { + "type": "string", + "x-go-name": "URI" + }, + "uris": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Uris" + }, + "vars": { + "type": "object", + "x-go-name": "Vars" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "SSL": { + "type": "object", + "properties": { + "cert": { + "type": "string", + "x-go-name": "Cert" + }, + "certs": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Certs" + }, + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "exptime": { + "type": "integer", + "format": "int64", + "x-go-name": "ExpTime" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "key": { + "type": "string", + "x-go-name": "Key" + }, + "keys": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Keys" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "sni": { + "type": "string", + "x-go-name": "Sni" + }, + "snis": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Snis" + }, + "status": { + "type": "integer", + "format": "int64", + "x-go-name": "Status" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + }, + "validity_end": { + "type": "integer", + "format": "int64", + "x-go-name": "ValidityEnd" + }, + "validity_start": { + "type": "integer", + "format": "int64", + "x-go-name": "ValidityStart" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "Service": { + "type": "object", + "properties": { + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "desc": { + "type": "string", + "x-go-name": "Desc" + }, + "enable_websocket": { + "type": "boolean", + "x-go-name": "EnableWebsocket" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Plugins" + }, + "script": { + "type": "string", + "x-go-name": "Script" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + }, + "upstream": { + "$ref": "#/definitions/UpstreamDef" + }, + "upstream_id": { + "type": "object", + "x-go-name": "UpstreamID" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "Status": { + "type": "integer", + "format": "uint8", + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "Upstream": { + "type": "object", + "properties": { + "checks": { + "type": "object", + "x-go-name": "Checks" + }, + "create_time": { + "type": "integer", + "format": "int64", + "x-go-name": "CreateTime" + }, + "desc": { + "type": "string", + "x-go-name": "Desc" + }, + "hash_on": { + "type": "string", + "x-go-name": "HashOn" + }, + "id": { + "type": "object", + "x-go-name": "ID" + }, + "k8s_deployment_info": { + "type": "object", + "x-go-name": "K8sInfo" + }, + "key": { + "type": "string", + "x-go-name": "Key" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "nodes": { + "type": "object", + "x-go-name": "Nodes" + }, + "pass_host": { + "type": "string", + "x-go-name": "PassHost" + }, + "retries": { + "type": "integer", + "format": "int64", + "x-go-name": "Retries" + }, + "service_name": { + "type": "string", + "x-go-name": "ServiceName" + }, + "timeout": { + "type": "object", + "x-go-name": "Timeout" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "update_time": { + "type": "integer", + "format": "int64", + "x-go-name": "UpdateTime" + }, + "upstream_host": { + "type": "string", + "x-go-name": "UpstreamHost" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + }, + "UpstreamDef": { + "type": "object", + "properties": { + "checks": { + "type": "object", + "x-go-name": "Checks" + }, + "desc": { + "type": "string", + "x-go-name": "Desc" + }, + "hash_on": { + "type": "string", + "x-go-name": "HashOn" + }, + "k8s_deployment_info": { + "type": "object", + "x-go-name": "K8sInfo" + }, + "key": { + "type": "string", + "x-go-name": "Key" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "nodes": { + "type": "object", + "x-go-name": "Nodes" + }, + "pass_host": { + "type": "string", + "x-go-name": "PassHost" + }, + "retries": { + "type": "integer", + "format": "int64", + "x-go-name": "Retries" + }, + "service_name": { + "type": "string", + "x-go-name": "ServiceName" + }, + "timeout": { + "type": "object", + "x-go-name": "Timeout" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "upstream_host": { + "type": "string", + "x-go-name": "UpstreamHost" + } + }, + "x-go-package": "github.com/apisix/manager-api/internal/core/entity" + } + } +} \ No newline at end of file diff --git a/docs/api/api.yaml b/docs/api/api.yaml new file mode 100644 index 0000000000..fe27d6787b --- /dev/null +++ b/docs/api/api.yaml @@ -0,0 +1,463 @@ +consumes: +- application/json +- application/xml +definitions: + ApiError: + properties: + code: + description: response code + format: int64 + type: integer + x-go-name: Code + message: + description: response message + type: string + x-go-name: Message + type: object + x-go-package: github.com/apisix/manager-api/internal/utils/consts + BaseInfo: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + id: + type: object + x-go-name: ID + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Consumer: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + username: + type: string + x-go-name: Username + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + GlobalPlugins: + properties: + id: + type: object + x-go-name: ID + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + LoginInput: + properties: + password: + description: password + type: string + x-go-name: Password + username: + description: user name + type: string + x-go-name: Username + type: object + x-go-package: github.com/apisix/manager-api/internal/handler/authentication + Route: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + enable_websocket: + type: boolean + x-go-name: EnableWebsocket + filter_func: + type: string + x-go-name: FilterFunc + host: + type: string + x-go-name: Host + hosts: + items: + type: string + type: array + x-go-name: Hosts + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + methods: + items: + type: string + type: array + x-go-name: Methods + name: + type: string + x-go-name: Name + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + priority: + format: int64 + type: integer + x-go-name: Priority + remote_addr: + type: string + x-go-name: RemoteAddr + remote_addrs: + items: + type: string + type: array + x-go-name: RemoteAddrs + script: + type: object + x-go-name: Script + service_id: + type: object + x-go-name: ServiceID + service_protocol: + type: string + x-go-name: ServiceProtocol + status: + $ref: '#/definitions/Status' + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream: + $ref: '#/definitions/UpstreamDef' + upstream_id: + type: object + x-go-name: UpstreamID + uri: + type: string + x-go-name: URI + uris: + items: + type: string + type: array + x-go-name: Uris + vars: + type: object + x-go-name: Vars + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + SSL: + properties: + cert: + type: string + x-go-name: Cert + certs: + items: + type: string + type: array + x-go-name: Certs + create_time: + format: int64 + type: integer + x-go-name: CreateTime + exptime: + format: int64 + type: integer + x-go-name: ExpTime + id: + type: object + x-go-name: ID + key: + type: string + x-go-name: Key + keys: + items: + type: string + type: array + x-go-name: Keys + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + sni: + type: string + x-go-name: Sni + snis: + items: + type: string + type: array + x-go-name: Snis + status: + format: int64 + type: integer + x-go-name: Status + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + validity_end: + format: int64 + type: integer + x-go-name: ValidityEnd + validity_start: + format: int64 + type: integer + x-go-name: ValidityStart + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Service: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + enable_websocket: + type: boolean + x-go-name: EnableWebsocket + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + script: + type: string + x-go-name: Script + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream: + $ref: '#/definitions/UpstreamDef' + upstream_id: + type: object + x-go-name: UpstreamID + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Status: + format: uint8 + type: integer + x-go-package: github.com/apisix/manager-api/internal/core/entity + Upstream: + properties: + checks: + type: object + x-go-name: Checks + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + hash_on: + type: string + x-go-name: HashOn + id: + type: object + x-go-name: ID + k8s_deployment_info: + type: object + x-go-name: K8sInfo + key: + type: string + x-go-name: Key + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + nodes: + type: object + x-go-name: Nodes + pass_host: + type: string + x-go-name: PassHost + retries: + format: int64 + type: integer + x-go-name: Retries + service_name: + type: string + x-go-name: ServiceName + timeout: + type: object + x-go-name: Timeout + type: + type: string + x-go-name: Type + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream_host: + type: string + x-go-name: UpstreamHost + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + UpstreamDef: + properties: + checks: + type: object + x-go-name: Checks + desc: + type: string + x-go-name: Desc + hash_on: + type: string + x-go-name: HashOn + k8s_deployment_info: + type: object + x-go-name: K8sInfo + key: + type: string + x-go-name: Key + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + nodes: + type: object + x-go-name: Nodes + pass_host: + type: string + x-go-name: PassHost + retries: + format: int64 + type: integer + x-go-name: Retries + service_name: + type: string + x-go-name: ServiceName + timeout: + type: object + x-go-name: Timeout + type: + type: string + x-go-name: Type + upstream_host: + type: string + x-go-name: UpstreamHost + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity +host: 127.0.0.1 +info: + description: |- + 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. + + API doc of Manager API. + + Manager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients. + license: + name: Apache License 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 + title: |- + 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 +paths: + /api/labels: + get: + x-api-limit: 20 + description: |- + Return the labels list among `route,ssl,consumer,upstream,service` + according to the specified page number and page size, and can search labels by label1. + operationId: getLabelsList1 + + x-apisix-upstream: + type: roundrobin + nodes: + - host: "127.0.0.1" + port: 80 + weight: 1 + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: label filter of labels + in: query + name: label + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/service' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' +produces: +- application/json +- application/xml +schemes: +- http +- https +swagger: "2.0" diff --git a/docs/api/api.yml b/docs/api/api.yml new file mode 100644 index 0000000000..5b924f00f6 --- /dev/null +++ b/docs/api/api.yml @@ -0,0 +1,741 @@ +consumes: +- application/json +- application/xml +definitions: + ApiError: + properties: + code: + description: response code + format: int64 + type: integer + x-go-name: Code + message: + description: response message + type: string + x-go-name: Message + type: object + x-go-package: github.com/apisix/manager-api/internal/utils/consts + BaseInfo: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + id: + type: object + x-go-name: ID + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Consumer: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + username: + type: string + x-go-name: Username + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + GlobalPlugins: + properties: + id: + type: object + x-go-name: ID + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + LoginInput: + properties: + password: + description: password + type: string + x-go-name: Password + username: + description: user name + type: string + x-go-name: Username + type: object + x-go-package: github.com/apisix/manager-api/internal/handler/authentication + Route: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + enable_websocket: + type: boolean + x-go-name: EnableWebsocket + filter_func: + type: string + x-go-name: FilterFunc + host: + type: string + x-go-name: Host + hosts: + items: + type: string + type: array + x-go-name: Hosts + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + methods: + items: + type: string + type: array + x-go-name: Methods + name: + type: string + x-go-name: Name + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + priority: + format: int64 + type: integer + x-go-name: Priority + remote_addr: + type: string + x-go-name: RemoteAddr + remote_addrs: + items: + type: string + type: array + x-go-name: RemoteAddrs + script: + type: object + x-go-name: Script + service_id: + type: object + x-go-name: ServiceID + service_protocol: + type: string + x-go-name: ServiceProtocol + status: + $ref: '#/definitions/Status' + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream: + $ref: '#/definitions/UpstreamDef' + upstream_id: + type: object + x-go-name: UpstreamID + uri: + type: string + x-go-name: URI + uris: + items: + type: string + type: array + x-go-name: Uris + vars: + type: object + x-go-name: Vars + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + SSL: + properties: + cert: + type: string + x-go-name: Cert + certs: + items: + type: string + type: array + x-go-name: Certs + create_time: + format: int64 + type: integer + x-go-name: CreateTime + exptime: + format: int64 + type: integer + x-go-name: ExpTime + id: + type: object + x-go-name: ID + key: + type: string + x-go-name: Key + keys: + items: + type: string + type: array + x-go-name: Keys + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + sni: + type: string + x-go-name: Sni + snis: + items: + type: string + type: array + x-go-name: Snis + status: + format: int64 + type: integer + x-go-name: Status + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + validity_end: + format: int64 + type: integer + x-go-name: ValidityEnd + validity_start: + format: int64 + type: integer + x-go-name: ValidityStart + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Service: + properties: + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + enable_websocket: + type: boolean + x-go-name: EnableWebsocket + id: + type: object + x-go-name: ID + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + plugins: + additionalProperties: + type: object + type: object + x-go-name: Plugins + script: + type: string + x-go-name: Script + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream: + $ref: '#/definitions/UpstreamDef' + upstream_id: + type: object + x-go-name: UpstreamID + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + Status: + format: uint8 + type: integer + x-go-package: github.com/apisix/manager-api/internal/core/entity + Upstream: + properties: + checks: + type: object + x-go-name: Checks + create_time: + format: int64 + type: integer + x-go-name: CreateTime + desc: + type: string + x-go-name: Desc + hash_on: + type: string + x-go-name: HashOn + id: + type: object + x-go-name: ID + k8s_deployment_info: + type: object + x-go-name: K8sInfo + key: + type: string + x-go-name: Key + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + nodes: + type: object + x-go-name: Nodes + pass_host: + type: string + x-go-name: PassHost + retries: + format: int64 + type: integer + x-go-name: Retries + service_name: + type: string + x-go-name: ServiceName + timeout: + type: object + x-go-name: Timeout + type: + type: string + x-go-name: Type + update_time: + format: int64 + type: integer + x-go-name: UpdateTime + upstream_host: + type: string + x-go-name: UpstreamHost + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity + UpstreamDef: + properties: + checks: + type: object + x-go-name: Checks + desc: + type: string + x-go-name: Desc + hash_on: + type: string + x-go-name: HashOn + k8s_deployment_info: + type: object + x-go-name: K8sInfo + key: + type: string + x-go-name: Key + labels: + additionalProperties: + type: string + type: object + x-go-name: Labels + name: + type: string + x-go-name: Name + nodes: + type: object + x-go-name: Nodes + pass_host: + type: string + x-go-name: PassHost + retries: + format: int64 + type: integer + x-go-name: Retries + service_name: + type: string + x-go-name: ServiceName + timeout: + type: object + x-go-name: Timeout + type: + type: string + x-go-name: Type + upstream_host: + type: string + x-go-name: UpstreamHost + type: object + x-go-package: github.com/apisix/manager-api/internal/core/entity +host: 127.0.0.1 +info: + description: |- + 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. + + API doc of Manager API. + + Manager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients. + license: + name: Apache License 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 + title: |- + 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 +paths: + /api/labels: + get: + description: |- + Return the labels list among `route,ssl,consumer,upstream,service` + according to the specified page number and page size, and can search labels by label. + operationId: getLabelsList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: label filter of labels + in: query + name: label + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/service' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + /apisix/admin/check_ssl_cert: + post: + operationId: checkSSL + parameters: + - description: cert of SSL + in: body + name: cert + required: true + type: string + - description: key of SSL + in: body + name: key + required: true + type: string + produces: + - application/json + responses: + "0": + description: SSL verify passed + schema: + $ref: '#/definitions/ApiError' + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: verify SSL cert and key. + /apisix/admin/check_ssl_exists: + post: + operationId: checkSSLExist + parameters: + - description: cert of SSL + in: body + name: cert + required: true + type: string + - description: key of SSL + in: body + name: key + required: true + type: string + produces: + - application/json + responses: + "0": + description: SSL exists + schema: + $ref: '#/definitions/ApiError' + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Check whether the SSL exists. + /apisix/admin/consumers: + get: + operationId: getConsumerList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: username of consumer + in: query + name: username + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/consumer' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the consumer list according to the specified page number and page size, and can search consumers by username. + /apisix/admin/global_rules: + get: + operationId: getGlobalRuleList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/GlobalPlugins' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the global rule list according to the specified page number and page size. + /apisix/admin/notexist/routes: + get: + operationId: checkRouteExist + parameters: + - description: name of route + in: query + name: name + type: string + - description: id of route that exclude checking + in: query + name: exclude + type: string + produces: + - application/json + responses: + "0": + description: route not exists + schema: + $ref: '#/definitions/ApiError' + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return result of route exists checking by name and exclude id. + /apisix/admin/routes: + get: + operationId: getRouteList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: name of route + in: query + name: name + type: string + - description: uri of route + in: query + name: uri + type: string + - description: label of route + in: query + name: label + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/route' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the route list according to the specified page number and page size, and can search routes by name and uri. + /apisix/admin/services: + get: + operationId: getServiceList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: name of service + in: query + name: name + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/service' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the service list according to the specified page number and page size, and can search services by name. + /apisix/admin/ssl: + get: + operationId: getSSLList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: sni of SSL + in: query + name: sni + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/ssl' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the SSL list according to the specified page number and page size, and can SSLs search by sni. + /apisix/admin/upstreams: + get: + operationId: getUpstreamList + parameters: + - description: page number + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: name of upstream + in: query + name: name + type: string + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/upstream' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: Return the upstream list according to the specified page number and page size, and can search upstreams by name. + /apisix/admin/user/login: + post: + operationId: userLogin + parameters: + - description: user name + in: body + name: username + required: true + type: string + - description: password + in: body + name: password + required: true + type: string + produces: + - application/json + responses: + "0": + description: login success + schema: + $ref: '#/definitions/ApiError' + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' + summary: user login. +produces: +- application/json +- application/xml +schemes: +- http +- https +swagger: "2.0" From a476126a13ba6a6d6252b806753d113df3ee17b8 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 5 Jan 2021 18:12:24 +0800 Subject: [PATCH 03/36] chore: go fmt --- api/internal/conf/conf.go | 2 +- api/internal/core/entity/entity.go | 2 +- api/internal/core/store/store.go | 2 +- api/internal/core/store/validate.go | 4 +- api/internal/handler/data_loader/export.go | 719 ++++++++++----------- api/internal/handler/route/route.go | 1 - 6 files changed, 364 insertions(+), 366 deletions(-) diff --git a/api/internal/conf/conf.go b/api/internal/conf/conf.go index 02ac57e2fe..edccaf3711 100644 --- a/api/internal/conf/conf.go +++ b/api/internal/conf/conf.go @@ -51,7 +51,7 @@ var ( UserList = make(map[string]User, 2) AuthConf Authentication SSLDefaultStatus = 1 //enable ssl by default - ImportSizeLimit = 10 * 1024 *1024 + ImportSizeLimit = 10 * 1024 * 1024 ) type Etcd struct { diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index b3ccb2a860..3a2f3b112c 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -95,7 +95,7 @@ type Route struct { } type RouteImport struct { - Plugins map[string]interface{} `json:"plugins"` + Plugins map[string]interface{} `json:"plugins"` Route } diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go index f2abccf245..fc0e40a131 100644 --- a/api/internal/core/store/store.go +++ b/api/internal/core/store/store.go @@ -276,7 +276,7 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { } bytes, err := s.CreateCheck(obj) - if err != nil{ + if err != nil { return err } diff --git a/api/internal/core/store/validate.go b/api/internal/core/store/validate.go index 045c360157..4ec0b14812 100644 --- a/api/internal/core/store/validate.go +++ b/api/internal/core/store/validate.go @@ -71,7 +71,7 @@ func (v *JsonSchemaValidator) Validate(obj interface{}) error { } type APISIXJsonSchemaValidator struct { - schema *gojsonschema.Schema + schema *gojsonschema.Schema schemaDef string } @@ -88,7 +88,7 @@ func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) { return nil, fmt.Errorf("new schema failed: %s", err) } return &APISIXJsonSchemaValidator{ - schema: s, + schema: s, schemaDef: schemaDef, }, nil } diff --git a/api/internal/handler/data_loader/export.go b/api/internal/handler/data_loader/export.go index 4f9d25b4ed..eb24bef40e 100644 --- a/api/internal/handler/data_loader/export.go +++ b/api/internal/handler/data_loader/export.go @@ -1,396 +1,395 @@ package data_loader import ( - "bufio" - "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/data" - - "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" - routeHandler "github.com/apisix/manager-api/internal/handler/route" - "github.com/apisix/manager-api/internal/utils" - "github.com/apisix/manager-api/internal/utils/consts" + "bufio" + "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/data" + + "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" + routeHandler "github.com/apisix/manager-api/internal/handler/route" + "github.com/apisix/manager-api/internal/utils" + "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { - routeStore store.Interface - svcStore store.Interface - upstreamStore store.Interface - scriptStore store.Interface + routeStore store.Interface + svcStore store.Interface + upstreamStore store.Interface + scriptStore store.Interface } func NewHandler() (handler.RouteRegister, error) { - return &Handler{ - routeStore: store.GetStore(store.HubKeyRoute), - svcStore: store.GetStore(store.HubKeyService), - upstreamStore: store.GetStore(store.HubKeyUpstream), - scriptStore: store.GetStore(store.HubKeyScript), - }, nil + return &Handler{ + routeStore: store.GetStore(store.HubKeyRoute), + svcStore: store.GetStore(store.HubKeyService), + upstreamStore: store.GetStore(store.HubKeyUpstream), + scriptStore: store.GetStore(store.HubKeyScript), + }, nil } func (h *Handler) ApplyRoute(r *gin.Engine) { - r.POST("/apisix/admin/import", consts.ErrorWrapper(Import)) + r.POST("/apisix/admin/import", consts.ErrorWrapper(Import)) } func Import(c *gin.Context) (interface{}, error) { - file, err := c.FormFile("file") - if err != nil { - return nil, err - } - - // file check - suffix := path.Ext(file.Filename) - if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" { - return nil, fmt.Errorf("the file type error: %s", suffix) - } - if file.Size > int64(conf.ImportSizeLimit) { - return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) - } - - // read file and parse - open, err := file.Open() - if err != nil { - return nil, err - } - defer open.Close() - reader := bufio.NewReader(open) - bytes := make([]byte, file.Size) - _, _ = reader.Read(bytes) - - swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(bytes) - - routes := OpenAPI3ToRoute(swagger) - routeStore := store.GetStore(store.HubKeyRoute) - upstreamStore := store.GetStore(store.HubKeyUpstream) - scriptStore := store.GetStore(store.HubKeyScript) - - // check route - for _, route := range routes { - _, err := checkRouteName(route.Name) - if err != nil { - continue - } - if route.ServiceID != nil { - _, err := routeStore.Get(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 := upstreamStore.Get(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 route.Script != nil { - if route.ID == "" { - route.ID = utils.GetFlakeUidStr() - } - script := &entity.Script{} - script.ID = utils.InterfaceToString(route.ID) - script.Script = route.Script - //to lua - var err error - route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) - if err != nil { - return nil, err - } - //save original conf - if err = scriptStore.Create(c, script); err != nil { - return nil, err - } - } - - if _, err := routeStore.CreateCheck(route); err != nil { - return handler.SpecCodeResponse(err), err - } - } - - // create route - for _, route := range routes { - if err := routeStore.Create(c, route); err != nil { - println(err.Error()) - return handler.SpecCodeResponse(err), err - } - } - - return nil, nil + file, err := c.FormFile("file") + if err != nil { + return nil, err + } + + // file check + suffix := path.Ext(file.Filename) + if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" { + return nil, fmt.Errorf("the file type error: %s", suffix) + } + if file.Size > int64(conf.ImportSizeLimit) { + return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) + } + + // read file and parse + open, err := file.Open() + if err != nil { + return nil, err + } + defer open.Close() + reader := bufio.NewReader(open) + bytes := make([]byte, file.Size) + _, _ = reader.Read(bytes) + + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(bytes) + + routes := OpenAPI3ToRoute(swagger) + routeStore := store.GetStore(store.HubKeyRoute) + upstreamStore := store.GetStore(store.HubKeyUpstream) + scriptStore := store.GetStore(store.HubKeyScript) + + // check route + for _, route := range routes { + _, err := checkRouteName(route.Name) + if err != nil { + continue + } + if route.ServiceID != nil { + _, err := routeStore.Get(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 := upstreamStore.Get(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 route.Script != nil { + if route.ID == "" { + route.ID = utils.GetFlakeUidStr() + } + script := &entity.Script{} + script.ID = utils.InterfaceToString(route.ID) + script.Script = route.Script + //to lua + var err error + route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) + if err != nil { + return nil, err + } + //save original conf + if err = scriptStore.Create(c, script); err != nil { + return nil, err + } + } + + if _, err := routeStore.CreateCheck(route); err != nil { + return handler.SpecCodeResponse(err), err + } + } + + // create route + for _, route := range routes { + if err := routeStore.Create(c, route); err != nil { + println(err.Error()) + return handler.SpecCodeResponse(err), err + } + } + + return nil, nil } - func checkRouteName(name string) (bool, error) { - routeStore := store.GetStore(store.HubKeyRoute) - ret, err := routeStore.List(store.ListInput{ - Predicate: nil, - PageSize: 0, - PageNumber: 0, - }) - if err != nil { - return false, err - } - - sort := store.NewSort(nil) - filter := store.NewFilter([]string{"name", name}) - pagination := store.NewPagination(0, 0) - query := store.NewQuery(sort, filter, pagination) - rows := store.NewFilterSelector(routeHandler.ToRows(ret), query) - if len(rows) > 0 { - return false, consts.InvalidParam("route name is duplicate") - } - - return true, nil + routeStore := store.GetStore(store.HubKeyRoute) + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + if err != nil { + return false, err + } + + sort := store.NewSort(nil) + filter := store.NewFilter([]string{"name", name}) + pagination := store.NewPagination(0, 0) + query := store.NewQuery(sort, filter, pagination) + rows := store.NewFilterSelector(routeHandler.ToRows(ret), query) + if len(rows) > 0 { + return false, consts.InvalidParam("route name is duplicate") + } + + return true, nil } func structByReflect(data map[string]interface{}, inStructPtr interface{}) error { - rType := reflect.TypeOf(inStructPtr) - rVal := reflect.ValueOf(inStructPtr) - if rType.Kind() != reflect.Ptr { - return errors.New("inStructPtr must be ptr to struct") - } - rType = rType.Elem() - rVal = rVal.Elem() - - for i := 0; i < rType.NumField(); i++ { - t := rType.Field(i) - f := rVal.Field(i) - v, ok := data[t.Name] - if !ok { - continue - } - - f.Set(reflect.ValueOf(v)) - } - - return nil + rType := reflect.TypeOf(inStructPtr) + rVal := reflect.ValueOf(inStructPtr) + if rType.Kind() != reflect.Ptr { + return errors.New("inStructPtr must be ptr to struct") + } + rType = rType.Elem() + rVal = rVal.Elem() + + for i := 0; i < rType.NumField(); i++ { + t := rType.Field(i) + f := rVal.Field(i) + v, ok := data[t.Name] + if !ok { + continue + } + + f.Set(reflect.ValueOf(v)) + } + + return nil } func parseExtension(route *entity.Route, val *openapi3.Operation, upstream, emptyUpstream *entity.UpstreamDef) *entity.Route { - fmt.Printf("v.Extensions: %s", val.Extensions) - if up, ok := val.Extensions["x-apisix-upstream"]; ok { - json.Unmarshal(up.(json.RawMessage), upstream) - } + fmt.Printf("v.Extensions: %s", val.Extensions) + if up, ok := val.Extensions["x-apisix-upstream"]; ok { + json.Unmarshal(up.(json.RawMessage), upstream) + } - fmt.Printf("upstream: %v, %v, %v", upstream, emptyUpstream, upstream != nil && upstream != emptyUpstream) - if upstream != nil && upstream == emptyUpstream { - route.Upstream = upstream - } + fmt.Printf("upstream: %v, %v, %v", upstream, emptyUpstream, upstream != nil && upstream != emptyUpstream) + if upstream != nil && upstream == emptyUpstream { + route.Upstream = upstream + } - return route + return route } func OpenAPI3ToRoute(swagger *openapi3.Swagger) []*entity.Route { - var routes []*entity.Route - //if globalUpstreams, ok := swagger.Extensions["x-apisix-upstreams"]; ok { - // globalUpstreams = globalUpstreams.([]map[string]interface{}) - // - //} - paths := swagger.Paths - var upstream *entity.UpstreamDef - emptyUpstream := &entity.UpstreamDef{} - for k, v := range paths { - upstream = &entity.UpstreamDef{} - if up, ok := v.Extensions["x-apisix-upstream"]; ok { - json.Unmarshal(up.(json.RawMessage), upstream) - } - - if v.Get != nil { - route := getRouteFromPaths("GET", k, v.Get, swagger) - route = parseExtension(route, v.Get, upstream, emptyUpstream) - - routes = append(routes, route) - } - if v.Post != nil { - route := getRouteFromPaths("POST", k, v.Post, swagger) - route = parseExtension(route, v.Post, upstream, emptyUpstream) - - routes = append(routes, route) - } - if v.Head != nil { - route := getRouteFromPaths("HEAD", k, v.Head, swagger) - route = parseExtension(route, v.Head, upstream, emptyUpstream) - - routes = append(routes, route) - } - if v.Put != nil { - route := getRouteFromPaths("PUT", k, v.Put, swagger) - route = parseExtension(route, v.Put, upstream, emptyUpstream) - - routes = append(routes, route) - } - if v.Patch != nil { - route := getRouteFromPaths("PATCH", k, v.Patch, swagger) - route = parseExtension(route, v.Patch, upstream, emptyUpstream) - - routes = append(routes, route) - } - - if v.Delete != nil { - route := getRouteFromPaths("DELETE", k, v.Delete, swagger) - route = parseExtension(route, v.Delete, upstream, emptyUpstream) - - routes = append(routes, route) - } - } - return routes + var routes []*entity.Route + //if globalUpstreams, ok := swagger.Extensions["x-apisix-upstreams"]; ok { + // globalUpstreams = globalUpstreams.([]map[string]interface{}) + // + //} + paths := swagger.Paths + var upstream *entity.UpstreamDef + emptyUpstream := &entity.UpstreamDef{} + for k, v := range paths { + upstream = &entity.UpstreamDef{} + if up, ok := v.Extensions["x-apisix-upstream"]; ok { + json.Unmarshal(up.(json.RawMessage), upstream) + } + + if v.Get != nil { + route := getRouteFromPaths("GET", k, v.Get, swagger) + route = parseExtension(route, v.Get, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Post != nil { + route := getRouteFromPaths("POST", k, v.Post, swagger) + route = parseExtension(route, v.Post, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Head != nil { + route := getRouteFromPaths("HEAD", k, v.Head, swagger) + route = parseExtension(route, v.Head, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Put != nil { + route := getRouteFromPaths("PUT", k, v.Put, swagger) + route = parseExtension(route, v.Put, upstream, emptyUpstream) + + routes = append(routes, route) + } + if v.Patch != nil { + route := getRouteFromPaths("PATCH", k, v.Patch, swagger) + route = parseExtension(route, v.Patch, upstream, emptyUpstream) + + routes = append(routes, route) + } + + if v.Delete != nil { + route := getRouteFromPaths("DELETE", k, v.Delete, swagger) + route = parseExtension(route, v.Delete, upstream, emptyUpstream) + + routes = append(routes, route) + } + } + return routes } func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) *entity.Route { - reg := regexp.MustCompile(`{[\w.]*\}`) - findString := reg.FindString(key) - if findString != "" { - key = strings.Split(key, findString)[0] + "*" - } - r := &entity.Route{ - URI: key, - Name: value.OperationID, - Desc: value.Summary, - Methods: []string{method}, - } - var parameters *openapi3.Parameters - plugins := make(map[string]interface{}) - requestValidation := make(map[string]*entity.RequestValidation) - if value.Parameters != nil { - parameters = &value.Parameters - } - if parameters != nil { - for _, v := range *parameters { - if v.Value.Schema != nil { - v.Value.Schema.Value.Format = "" - v.Value.Schema.Value.XML = nil - } - props := make(map[string]interface{}) - switch v.Value.In { - case "header": - props[v.Value.Name] = v.Value.Schema.Value - var required []string - if v.Value.Required { - required = append(required, v.Value.Name) - } - requestValidation["header_schema"] = &entity.RequestValidation{ - Type: "object", - Required: required, - Properties: props, - } - plugins["request-validation"] = requestValidation - } - } - } - - if value.RequestBody != nil { - vars := make([]interface{}, 0) - schema := value.RequestBody.Value.Content - for k, v := range schema { - item := []string{"http_Content-type", "==", ""} - item[2] = k - vars = append(vars, item) - 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 - } - r.Vars = vars - } - - if value.Security != nil { - p := &entity.SecurityPlugin{} - for _, v := range *value.Security { - for v1 := range v { - switch v1 { - case "api_key": - plugins["key-auth"] = p - case "basicAuth": - plugins["basic-auth"] = p - case "bearerAuth": - plugins["jwt-auth"] = p - } - } - } - } - r.Plugins = plugins - - fmt.Printf("r: %v", r) - return r + reg := regexp.MustCompile(`{[\w.]*\}`) + findString := reg.FindString(key) + if findString != "" { + key = strings.Split(key, findString)[0] + "*" + } + r := &entity.Route{ + URI: key, + Name: value.OperationID, + Desc: value.Summary, + Methods: []string{method}, + } + var parameters *openapi3.Parameters + plugins := make(map[string]interface{}) + requestValidation := make(map[string]*entity.RequestValidation) + if value.Parameters != nil { + parameters = &value.Parameters + } + if parameters != nil { + for _, v := range *parameters { + if v.Value.Schema != nil { + v.Value.Schema.Value.Format = "" + v.Value.Schema.Value.XML = nil + } + props := make(map[string]interface{}) + switch v.Value.In { + case "header": + props[v.Value.Name] = v.Value.Schema.Value + var required []string + if v.Value.Required { + required = append(required, v.Value.Name) + } + requestValidation["header_schema"] = &entity.RequestValidation{ + Type: "object", + Required: required, + Properties: props, + } + plugins["request-validation"] = requestValidation + } + } + } + + if value.RequestBody != nil { + vars := make([]interface{}, 0) + schema := value.RequestBody.Value.Content + for k, v := range schema { + item := []string{"http_Content-type", "==", ""} + item[2] = k + vars = append(vars, item) + 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 + } + r.Vars = vars + } + + if value.Security != nil { + p := &entity.SecurityPlugin{} + for _, v := range *value.Security { + for v1 := range v { + switch v1 { + case "api_key": + plugins["key-auth"] = p + case "basicAuth": + plugins["basic-auth"] = p + case "bearerAuth": + plugins["jwt-auth"] = p + } + } + } + } + r.Plugins = plugins + + fmt.Printf("r: %v", r) + return r } 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 + 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/route/route.go b/api/internal/handler/route/route.go index 21be72e29e..5010bfa0d2 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -524,4 +524,3 @@ func Exist(c *gin.Context) (interface{}, error) { return nil, nil } - From 29cfc8535fdcce3e1d713e0a99c5da946ded88d8 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Sun, 10 Jan 2021 22:06:08 +0800 Subject: [PATCH 04/36] feat: Import route from OpenAPI Specification3.0 --- .../data_loader/{export.go => import.go} | 348 +++++++++++------- docs/api/api.yaml | 49 ++- 2 files changed, 254 insertions(+), 143 deletions(-) rename api/internal/handler/data_loader/{export.go => import.go} (53%) diff --git a/api/internal/handler/data_loader/export.go b/api/internal/handler/data_loader/import.go similarity index 53% rename from api/internal/handler/data_loader/export.go rename to api/internal/handler/data_loader/import.go index eb24bef40e..586f2a9670 100644 --- a/api/internal/handler/data_loader/export.go +++ b/api/internal/handler/data_loader/import.go @@ -60,18 +60,31 @@ func Import(c *gin.Context) (interface{}, error) { } // read file and parse - open, err := file.Open() + handle, err := file.Open() + defer func() { + err = handle.Close() + }() if err != nil { return nil, err } - defer open.Close() - reader := bufio.NewReader(open) + + reader := bufio.NewReader(handle) bytes := make([]byte, file.Size) - _, _ = reader.Read(bytes) + _, err = reader.Read(bytes) + if err != nil { + return nil, err + } swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(bytes) + if err != nil { + return nil, err + } + + routes, err := OpenAPI3ToRoute(swagger) + if err != nil { + return nil, err + } - routes := OpenAPI3ToRoute(swagger) routeStore := store.GetStore(store.HubKeyRoute) upstreamStore := store.GetStore(store.HubKeyUpstream) scriptStore := store.GetStore(store.HubKeyScript) @@ -109,13 +122,13 @@ func Import(c *gin.Context) (interface{}, error) { script := &entity.Script{} script.ID = utils.InterfaceToString(route.ID) script.Script = route.Script - //to lua + // to lua var err error route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) if err != nil { return nil, err } - //save original conf + // save original conf if err = scriptStore.Create(c, script); err != nil { return nil, err } @@ -183,21 +196,24 @@ func structByReflect(data map[string]interface{}, inStructPtr interface{}) error return nil } -func parseExtension(route *entity.Route, val *openapi3.Operation, upstream, emptyUpstream *entity.UpstreamDef) *entity.Route { - fmt.Printf("v.Extensions: %s", val.Extensions) - if up, ok := val.Extensions["x-apisix-upstream"]; ok { - json.Unmarshal(up.(json.RawMessage), upstream) +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[key] = val + } } - fmt.Printf("upstream: %v, %v, %v", upstream, emptyUpstream, upstream != nil && upstream != emptyUpstream) - if upstream != nil && upstream == emptyUpstream { - route.Upstream = upstream + route := new(entity.Route) + err := structByReflect(routeMap, route) + if err != nil { + return nil, err } - return route + return route, nil } -func OpenAPI3ToRoute(swagger *openapi3.Swagger) []*entity.Route { +func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) { var routes []*entity.Route //if globalUpstreams, ok := swagger.Extensions["x-apisix-upstreams"]; ok { // globalUpstreams = globalUpstreams.([]map[string]interface{}) @@ -205,168 +221,240 @@ func OpenAPI3ToRoute(swagger *openapi3.Swagger) []*entity.Route { //} paths := swagger.Paths var upstream *entity.UpstreamDef - emptyUpstream := &entity.UpstreamDef{} + var err error for k, v := range paths { upstream = &entity.UpstreamDef{} if up, ok := v.Extensions["x-apisix-upstream"]; ok { - json.Unmarshal(up.(json.RawMessage), upstream) + err = json.Unmarshal(up.(json.RawMessage), upstream) + if err != nil { + return nil, err + } } if v.Get != nil { - route := getRouteFromPaths("GET", k, v.Get, swagger) - route = parseExtension(route, v.Get, upstream, emptyUpstream) - + route, err := getRouteFromPaths("GET", k, v.Get, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } if v.Post != nil { - route := getRouteFromPaths("POST", k, v.Post, swagger) - route = parseExtension(route, v.Post, upstream, emptyUpstream) - + route, err := getRouteFromPaths("POST", k, v.Post, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } if v.Head != nil { - route := getRouteFromPaths("HEAD", k, v.Head, swagger) - route = parseExtension(route, v.Head, upstream, emptyUpstream) - + route, err := getRouteFromPaths("HEAD", k, v.Head, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } if v.Put != nil { - route := getRouteFromPaths("PUT", k, v.Put, swagger) - route = parseExtension(route, v.Put, upstream, emptyUpstream) - + route, err := getRouteFromPaths("PUT", k, v.Put, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } if v.Patch != nil { - route := getRouteFromPaths("PATCH", k, v.Patch, swagger) - route = parseExtension(route, v.Patch, upstream, emptyUpstream) - + route, err := getRouteFromPaths("PATCH", k, v.Patch, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } if v.Delete != nil { - route := getRouteFromPaths("DELETE", k, v.Delete, swagger) - route = parseExtension(route, v.Delete, upstream, emptyUpstream) - + route, err := getRouteFromPaths("DELETE", k, v.Delete, swagger) + if err != nil { + return nil, err + } routes = append(routes, route) } } - return routes + + return routes, nil } -func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) *entity.Route { - reg := regexp.MustCompile(`{[\w.]*\}`) - findString := reg.FindString(key) - if findString != "" { - key = strings.Split(key, findString)[0] + "*" - } - r := &entity.Route{ - URI: key, - Name: value.OperationID, - Desc: value.Summary, - Methods: []string{method}, - } - var parameters *openapi3.Parameters - plugins := make(map[string]interface{}) - requestValidation := make(map[string]*entity.RequestValidation) - if value.Parameters != nil { - parameters = &value.Parameters - } - if parameters != nil { - for _, v := range *parameters { - if v.Value.Schema != nil { - v.Value.Schema.Value.Format = "" - v.Value.Schema.Value.XML = nil - } - props := make(map[string]interface{}) - switch v.Value.In { - case "header": +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 - var required []string - if v.Value.Required { - required = append(required, v.Value.Name) - } - requestValidation["header_schema"] = &entity.RequestValidation{ - Type: "object", - Required: required, - Properties: props, - } - plugins["request-validation"] = requestValidation + } + if v.Value.Required { + required = append(required, v.Value.Name) } } } + requestValidation := make(map[string]interface{}) - if value.RequestBody != nil { - vars := make([]interface{}, 0) - schema := value.RequestBody.Value.Content - for k, v := range schema { - item := []string{"http_Content-type", "==", ""} - item[2] = k - vars = append(vars, item) - if v.Schema.Ref != "" { - s := getParameters(v.Schema.Ref, &swagger.Components).Value + 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{}, route *entity.Route) { + vars := make([]interface{}, 0) + schema := requestBody.Value.Content + requestValidation := make(map[string]interface{}) + for k, v := range schema { + item := []string{"http_Content-type", "==", ""} + item[2] = k + vars = append(vars, item) + + 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: s.Type, - Required: s.Required, - Properties: s.Properties, + Type: v.Schema.Value.Type, + Required: v.Schema.Value.Required, + Properties: v.Schema.Value.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 = "" - } + } 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: v.Schema.Value.Type, - Required: v.Schema.Value.Required, - Properties: v.Schema.Value.Properties, + Type: s.Type, + Required: s.Required, + Properties: s.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, - } + } + } else { + requestValidation["body_schema"] = &entity.RequestValidation{ + Type: "object", + Required: []string{}, + Properties: v.Schema.Value.Properties, } } - plugins["request-validation"] = requestValidation } - r.Vars = vars + plugins["request-validation"] = requestValidation } - if value.Security != nil { - p := &entity.SecurityPlugin{} - for _, v := range *value.Security { - for v1 := range v { - switch v1 { - case "api_key": - plugins["key-auth"] = p - case "basicAuth": - plugins["basic-auth"] = p - case "bearerAuth": - plugins["jwt-auth"] = p + route.Vars = vars +} + +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, + //} } } } } - r.Plugins = plugins +} + +func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) (*entity.Route, error) { + // transform /path/{var} to /path/* + reg := regexp.MustCompile(`{[\w.]*}`) + foundStr := reg.FindString(key) + if foundStr != "" { + key = strings.Split(key, foundStr)[0] + "*" + } + + route, err := parseExtension(value) + if err != nil { + return nil, err + } + + route.URI = 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, route) + } + + if value.Security != nil && swagger.Components.SecuritySchemes != nil { + parseSecurity(*value.Security, swagger.Components.SecuritySchemes, route.Plugins) + } - fmt.Printf("r: %v", r) - return r + return route, nil } func getParameters(ref string, components *openapi3.Components) *openapi3.SchemaRef { diff --git a/docs/api/api.yaml b/docs/api/api.yaml index fe27d6787b..c5e328e8b8 100644 --- a/docs/api/api.yaml +++ b/docs/api/api.yaml @@ -390,6 +390,13 @@ definitions: type: object x-go-package: github.com/apisix/manager-api/internal/core/entity host: 127.0.0.1 +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + username: test + password: testp info: description: |- http://www.apache.org/licenses/LICENSE-2.0 @@ -414,7 +421,7 @@ info: (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at paths: - /api/labels: + /api/labels/{aaa}: get: x-api-limit: 20 description: |- @@ -429,20 +436,36 @@ paths: port: 80 weight: 1 parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: label filter of labels - in: query - name: label - type: string + - name: id + in: header + description: ID of pet to use + required: true + schema: + type: array + items: + type: string + style: simple + + requestBody: + content: + 'application/x-www-form-urlencoded': + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + required: + - status + produces: - application/json + + security: + - basicAuth: [] + responses: "0": description: list response From 2b9c57cf5aa470b1bbc1d5a922431ed5925cf054 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Sun, 10 Jan 2021 23:25:07 +0800 Subject: [PATCH 05/36] feat: Import route from OpenAPI Specification3.0 --- api/internal/handler/data_loader/import.go | 51 +- api/test/e2e/http.go | 91 +++ {docs/api => api/test/testdata}/api.json | 0 {docs/api => api/test/testdata}/api.yaml | 0 docs/api/api.yml | 741 --------------------- 5 files changed, 101 insertions(+), 782 deletions(-) create mode 100644 api/test/e2e/http.go rename {docs/api => api/test/testdata}/api.json (100%) rename {docs/api => api/test/testdata}/api.yaml (100%) delete mode 100644 docs/api/api.yml diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 586f2a9670..ebcd8dde6b 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -3,11 +3,9 @@ package data_loader import ( "bufio" "encoding/json" - "errors" "fmt" "net/http" "path" - "reflect" "regexp" "strings" @@ -173,39 +171,22 @@ func checkRouteName(name string) (bool, error) { return true, nil } -func structByReflect(data map[string]interface{}, inStructPtr interface{}) error { - rType := reflect.TypeOf(inStructPtr) - rVal := reflect.ValueOf(inStructPtr) - if rType.Kind() != reflect.Ptr { - return errors.New("inStructPtr must be ptr to struct") - } - rType = rType.Elem() - rVal = rVal.Elem() - - for i := 0; i < rType.NumField(); i++ { - t := rType.Field(i) - f := rVal.Field(i) - v, ok := data[t.Name] - if !ok { - continue - } - - f.Set(reflect.ValueOf(v)) - } - - 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[key] = val + routeMap[strings.TrimPrefix(key, "x-apisix-")] = val } } route := new(entity.Route) - err := structByReflect(routeMap, route) + routeJson, err := json.Marshal(routeMap) + if err != nil { + return nil, err + } + + err = json.Unmarshal(routeJson, &route) if err != nil { return nil, err } @@ -215,10 +196,6 @@ func parseExtension(val *openapi3.Operation) (*entity.Route, error) { func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) { var routes []*entity.Route - //if globalUpstreams, ok := swagger.Extensions["x-apisix-upstreams"]; ok { - // globalUpstreams = globalUpstreams.([]map[string]interface{}) - // - //} paths := swagger.Paths var upstream *entity.UpstreamDef var err error @@ -230,7 +207,6 @@ func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) { return nil, err } } - if v.Get != nil { route, err := getRouteFromPaths("GET", k, v.Get, swagger) if err != nil { @@ -308,15 +284,10 @@ func parseParameters(parameters openapi3.Parameters, plugins map[string]interfac plugins["request-validation"] = requestValidation } -func parseRequestBody(requestBody *openapi3.RequestBodyRef, swagger *openapi3.Swagger, plugins map[string]interface{}, route *entity.Route) { - vars := make([]interface{}, 0) +func parseRequestBody(requestBody *openapi3.RequestBodyRef, swagger *openapi3.Swagger, plugins map[string]interface{}) { schema := requestBody.Value.Content requestValidation := make(map[string]interface{}) - for k, v := range schema { - item := []string{"http_Content-type", "==", ""} - item[2] = k - vars = append(vars, item) - + for _, v := range schema { if v.Schema.Ref != "" { s := getParameters(v.Schema.Ref, &swagger.Components).Value requestValidation["body_schema"] = &entity.RequestValidation{ @@ -360,8 +331,6 @@ func parseRequestBody(requestBody *openapi3.RequestBodyRef, swagger *openapi3.Sw } plugins["request-validation"] = requestValidation } - - route.Vars = vars } func parseSecurity(security openapi3.SecurityRequirements, securitySchemes openapi3.SecuritySchemes, plugins map[string]interface{}) { @@ -447,7 +416,7 @@ func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *o } if value.RequestBody != nil { - parseRequestBody(value.RequestBody, swagger, route.Plugins, route) + parseRequestBody(value.RequestBody, swagger, route.Plugins) } if value.Security != nil && swagger.Components.SecuritySchemes != nil { diff --git a/api/test/e2e/http.go b/api/test/e2e/http.go new file mode 100644 index 0000000000..0d91f93967 --- /dev/null +++ b/api/test/e2e/http.go @@ -0,0 +1,91 @@ +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) string { + 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) string { + 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) + + defer func() { + err = resp.Body.Close() + }() + + if err != nil { + panic(err) + } + + response, _ := ioutil.ReadAll(resp.Body) + + return string(response) +} + +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/docs/api/api.json b/api/test/testdata/api.json similarity index 100% rename from docs/api/api.json rename to api/test/testdata/api.json diff --git a/docs/api/api.yaml b/api/test/testdata/api.yaml similarity index 100% rename from docs/api/api.yaml rename to api/test/testdata/api.yaml diff --git a/docs/api/api.yml b/docs/api/api.yml deleted file mode 100644 index 5b924f00f6..0000000000 --- a/docs/api/api.yml +++ /dev/null @@ -1,741 +0,0 @@ -consumes: -- application/json -- application/xml -definitions: - ApiError: - properties: - code: - description: response code - format: int64 - type: integer - x-go-name: Code - message: - description: response message - type: string - x-go-name: Message - type: object - x-go-package: github.com/apisix/manager-api/internal/utils/consts - BaseInfo: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - id: - type: object - x-go-name: ID - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Consumer: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - username: - type: string - x-go-name: Username - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - GlobalPlugins: - properties: - id: - type: object - x-go-name: ID - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - LoginInput: - properties: - password: - description: password - type: string - x-go-name: Password - username: - description: user name - type: string - x-go-name: Username - type: object - x-go-package: github.com/apisix/manager-api/internal/handler/authentication - Route: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - enable_websocket: - type: boolean - x-go-name: EnableWebsocket - filter_func: - type: string - x-go-name: FilterFunc - host: - type: string - x-go-name: Host - hosts: - items: - type: string - type: array - x-go-name: Hosts - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - methods: - items: - type: string - type: array - x-go-name: Methods - name: - type: string - x-go-name: Name - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - priority: - format: int64 - type: integer - x-go-name: Priority - remote_addr: - type: string - x-go-name: RemoteAddr - remote_addrs: - items: - type: string - type: array - x-go-name: RemoteAddrs - script: - type: object - x-go-name: Script - service_id: - type: object - x-go-name: ServiceID - service_protocol: - type: string - x-go-name: ServiceProtocol - status: - $ref: '#/definitions/Status' - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream: - $ref: '#/definitions/UpstreamDef' - upstream_id: - type: object - x-go-name: UpstreamID - uri: - type: string - x-go-name: URI - uris: - items: - type: string - type: array - x-go-name: Uris - vars: - type: object - x-go-name: Vars - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - SSL: - properties: - cert: - type: string - x-go-name: Cert - certs: - items: - type: string - type: array - x-go-name: Certs - create_time: - format: int64 - type: integer - x-go-name: CreateTime - exptime: - format: int64 - type: integer - x-go-name: ExpTime - id: - type: object - x-go-name: ID - key: - type: string - x-go-name: Key - keys: - items: - type: string - type: array - x-go-name: Keys - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - sni: - type: string - x-go-name: Sni - snis: - items: - type: string - type: array - x-go-name: Snis - status: - format: int64 - type: integer - x-go-name: Status - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - validity_end: - format: int64 - type: integer - x-go-name: ValidityEnd - validity_start: - format: int64 - type: integer - x-go-name: ValidityStart - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Service: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - enable_websocket: - type: boolean - x-go-name: EnableWebsocket - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - script: - type: string - x-go-name: Script - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream: - $ref: '#/definitions/UpstreamDef' - upstream_id: - type: object - x-go-name: UpstreamID - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Status: - format: uint8 - type: integer - x-go-package: github.com/apisix/manager-api/internal/core/entity - Upstream: - properties: - checks: - type: object - x-go-name: Checks - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - hash_on: - type: string - x-go-name: HashOn - id: - type: object - x-go-name: ID - k8s_deployment_info: - type: object - x-go-name: K8sInfo - key: - type: string - x-go-name: Key - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - nodes: - type: object - x-go-name: Nodes - pass_host: - type: string - x-go-name: PassHost - retries: - format: int64 - type: integer - x-go-name: Retries - service_name: - type: string - x-go-name: ServiceName - timeout: - type: object - x-go-name: Timeout - type: - type: string - x-go-name: Type - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream_host: - type: string - x-go-name: UpstreamHost - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - UpstreamDef: - properties: - checks: - type: object - x-go-name: Checks - desc: - type: string - x-go-name: Desc - hash_on: - type: string - x-go-name: HashOn - k8s_deployment_info: - type: object - x-go-name: K8sInfo - key: - type: string - x-go-name: Key - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - nodes: - type: object - x-go-name: Nodes - pass_host: - type: string - x-go-name: PassHost - retries: - format: int64 - type: integer - x-go-name: Retries - service_name: - type: string - x-go-name: ServiceName - timeout: - type: object - x-go-name: Timeout - type: - type: string - x-go-name: Type - upstream_host: - type: string - x-go-name: UpstreamHost - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity -host: 127.0.0.1 -info: - description: |- - 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. - - API doc of Manager API. - - Manager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients. - license: - name: Apache License 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0 - title: |- - 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 -paths: - /api/labels: - get: - description: |- - Return the labels list among `route,ssl,consumer,upstream,service` - according to the specified page number and page size, and can search labels by label. - operationId: getLabelsList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: label filter of labels - in: query - name: label - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/service' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - /apisix/admin/check_ssl_cert: - post: - operationId: checkSSL - parameters: - - description: cert of SSL - in: body - name: cert - required: true - type: string - - description: key of SSL - in: body - name: key - required: true - type: string - produces: - - application/json - responses: - "0": - description: SSL verify passed - schema: - $ref: '#/definitions/ApiError' - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: verify SSL cert and key. - /apisix/admin/check_ssl_exists: - post: - operationId: checkSSLExist - parameters: - - description: cert of SSL - in: body - name: cert - required: true - type: string - - description: key of SSL - in: body - name: key - required: true - type: string - produces: - - application/json - responses: - "0": - description: SSL exists - schema: - $ref: '#/definitions/ApiError' - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Check whether the SSL exists. - /apisix/admin/consumers: - get: - operationId: getConsumerList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: username of consumer - in: query - name: username - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/consumer' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the consumer list according to the specified page number and page size, and can search consumers by username. - /apisix/admin/global_rules: - get: - operationId: getGlobalRuleList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/GlobalPlugins' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the global rule list according to the specified page number and page size. - /apisix/admin/notexist/routes: - get: - operationId: checkRouteExist - parameters: - - description: name of route - in: query - name: name - type: string - - description: id of route that exclude checking - in: query - name: exclude - type: string - produces: - - application/json - responses: - "0": - description: route not exists - schema: - $ref: '#/definitions/ApiError' - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return result of route exists checking by name and exclude id. - /apisix/admin/routes: - get: - operationId: getRouteList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: name of route - in: query - name: name - type: string - - description: uri of route - in: query - name: uri - type: string - - description: label of route - in: query - name: label - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/route' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the route list according to the specified page number and page size, and can search routes by name and uri. - /apisix/admin/services: - get: - operationId: getServiceList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: name of service - in: query - name: name - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/service' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the service list according to the specified page number and page size, and can search services by name. - /apisix/admin/ssl: - get: - operationId: getSSLList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: sni of SSL - in: query - name: sni - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/ssl' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the SSL list according to the specified page number and page size, and can SSLs search by sni. - /apisix/admin/upstreams: - get: - operationId: getUpstreamList - parameters: - - description: page number - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: name of upstream - in: query - name: name - type: string - produces: - - application/json - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/upstream' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: Return the upstream list according to the specified page number and page size, and can search upstreams by name. - /apisix/admin/user/login: - post: - operationId: userLogin - parameters: - - description: user name - in: body - name: username - required: true - type: string - - description: password - in: body - name: password - required: true - type: string - produces: - - application/json - responses: - "0": - description: login success - schema: - $ref: '#/definitions/ApiError' - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' - summary: user login. -produces: -- application/json -- application/xml -schemes: -- http -- https -swagger: "2.0" From fa9ed4a22c452e9ebd1fb31d47f3121b6610658d Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 00:51:46 +0800 Subject: [PATCH 06/36] test: add test case --- api/test/e2e/http.go | 135 +++--- api/test/e2e/import_test.go | 91 ++++ api/test/testdata/api.yaml | 486 --------------------- api/test/testdata/import-test-default.yaml | 44 ++ api/test/testdata/import-test-plugins.yaml | 80 ++++ 5 files changed, 282 insertions(+), 554 deletions(-) create mode 100644 api/test/e2e/import_test.go delete mode 100644 api/test/testdata/api.yaml create mode 100644 api/test/testdata/import-test-default.yaml create mode 100644 api/test/testdata/import-test-plugins.yaml diff --git a/api/test/e2e/http.go b/api/test/e2e/http.go index 0d91f93967..47e65d270d 100644 --- a/api/test/e2e/http.go +++ b/api/test/e2e/http.go @@ -1,91 +1,90 @@ package e2e import ( - "bytes" - "encoding/json" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" + "bytes" + "encoding/json" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" ) type UploadFile struct { - Name string - Filepath string + Name string + Filepath string } var httpClient = &http.Client{} func PostFile(reqUrl string, reqParams map[string]string, files []UploadFile, headers map[string]string) string { - return post(reqUrl, reqParams, "multipart/form-data", files, headers) + 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) string { + 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) -func post(reqUrl string, reqParams map[string]string, contentType string, files []UploadFile, headers map[string]string) string { - 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) + defer func() { + err = resp.Body.Close() + }() - defer func() { - err = resp.Body.Close() - }() + if err != nil { + panic(err) + } - if err != nil { - panic(err) - } + response, _ := ioutil.ReadAll(resp.Body) - response, _ := ioutil.ReadAll(resp.Body) - - return string(response) + return string(response) } -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() - } +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) - } + urlValues := url.Values{} + for key, val := range reqParams { + urlValues.Set(key, val) + } - reqBody := urlValues.Encode() + reqBody := urlValues.Encode() - return strings.NewReader(reqBody), contentType + return strings.NewReader(reqBody), contentType } diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go new file mode 100644 index 0000000000..204e45853f --- /dev/null +++ b/api/test/e2e/import_test.go @@ -0,0 +1,91 @@ +package e2e + +import ( + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +func TestImport_default(t *testing.T) { + path, err := filepath.Abs("../testdata/import-test-default.yaml") + assert.Nil(t, err) + + headers := map[string]string{ + "Authorization": token, + } + files := []UploadFile{ + {Name: "file", Filepath: path}, + } + PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + + 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{}) + + fmt.Println("list:", string(respBody)) + + 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) + } + + 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, + }) + + for _, tc := range tests { + testCaseCheck(tc, t) + } + +} + +func TestImport_delete_test_data(t *testing.T) { + // delete the route created by POST + time.Sleep(time.Duration(100) * time.Millisecond) + 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{}) + for _, item := range list { + route := item.(map[string]interface{}) + tc := HttpTestCase{ + Desc: "delete the route", + Object: ManagerApiExpect(t), + Method: http.MethodDelete, + Path: "/apisix/admin/routes/" + route["id"].(string), + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + } + testCaseCheck(tc, t) + } +} diff --git a/api/test/testdata/api.yaml b/api/test/testdata/api.yaml deleted file mode 100644 index c5e328e8b8..0000000000 --- a/api/test/testdata/api.yaml +++ /dev/null @@ -1,486 +0,0 @@ -consumes: -- application/json -- application/xml -definitions: - ApiError: - properties: - code: - description: response code - format: int64 - type: integer - x-go-name: Code - message: - description: response message - type: string - x-go-name: Message - type: object - x-go-package: github.com/apisix/manager-api/internal/utils/consts - BaseInfo: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - id: - type: object - x-go-name: ID - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Consumer: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - username: - type: string - x-go-name: Username - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - GlobalPlugins: - properties: - id: - type: object - x-go-name: ID - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - LoginInput: - properties: - password: - description: password - type: string - x-go-name: Password - username: - description: user name - type: string - x-go-name: Username - type: object - x-go-package: github.com/apisix/manager-api/internal/handler/authentication - Route: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - enable_websocket: - type: boolean - x-go-name: EnableWebsocket - filter_func: - type: string - x-go-name: FilterFunc - host: - type: string - x-go-name: Host - hosts: - items: - type: string - type: array - x-go-name: Hosts - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - methods: - items: - type: string - type: array - x-go-name: Methods - name: - type: string - x-go-name: Name - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - priority: - format: int64 - type: integer - x-go-name: Priority - remote_addr: - type: string - x-go-name: RemoteAddr - remote_addrs: - items: - type: string - type: array - x-go-name: RemoteAddrs - script: - type: object - x-go-name: Script - service_id: - type: object - x-go-name: ServiceID - service_protocol: - type: string - x-go-name: ServiceProtocol - status: - $ref: '#/definitions/Status' - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream: - $ref: '#/definitions/UpstreamDef' - upstream_id: - type: object - x-go-name: UpstreamID - uri: - type: string - x-go-name: URI - uris: - items: - type: string - type: array - x-go-name: Uris - vars: - type: object - x-go-name: Vars - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - SSL: - properties: - cert: - type: string - x-go-name: Cert - certs: - items: - type: string - type: array - x-go-name: Certs - create_time: - format: int64 - type: integer - x-go-name: CreateTime - exptime: - format: int64 - type: integer - x-go-name: ExpTime - id: - type: object - x-go-name: ID - key: - type: string - x-go-name: Key - keys: - items: - type: string - type: array - x-go-name: Keys - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - sni: - type: string - x-go-name: Sni - snis: - items: - type: string - type: array - x-go-name: Snis - status: - format: int64 - type: integer - x-go-name: Status - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - validity_end: - format: int64 - type: integer - x-go-name: ValidityEnd - validity_start: - format: int64 - type: integer - x-go-name: ValidityStart - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Service: - properties: - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - enable_websocket: - type: boolean - x-go-name: EnableWebsocket - id: - type: object - x-go-name: ID - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - plugins: - additionalProperties: - type: object - type: object - x-go-name: Plugins - script: - type: string - x-go-name: Script - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream: - $ref: '#/definitions/UpstreamDef' - upstream_id: - type: object - x-go-name: UpstreamID - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - Status: - format: uint8 - type: integer - x-go-package: github.com/apisix/manager-api/internal/core/entity - Upstream: - properties: - checks: - type: object - x-go-name: Checks - create_time: - format: int64 - type: integer - x-go-name: CreateTime - desc: - type: string - x-go-name: Desc - hash_on: - type: string - x-go-name: HashOn - id: - type: object - x-go-name: ID - k8s_deployment_info: - type: object - x-go-name: K8sInfo - key: - type: string - x-go-name: Key - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - nodes: - type: object - x-go-name: Nodes - pass_host: - type: string - x-go-name: PassHost - retries: - format: int64 - type: integer - x-go-name: Retries - service_name: - type: string - x-go-name: ServiceName - timeout: - type: object - x-go-name: Timeout - type: - type: string - x-go-name: Type - update_time: - format: int64 - type: integer - x-go-name: UpdateTime - upstream_host: - type: string - x-go-name: UpstreamHost - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity - UpstreamDef: - properties: - checks: - type: object - x-go-name: Checks - desc: - type: string - x-go-name: Desc - hash_on: - type: string - x-go-name: HashOn - k8s_deployment_info: - type: object - x-go-name: K8sInfo - key: - type: string - x-go-name: Key - labels: - additionalProperties: - type: string - type: object - x-go-name: Labels - name: - type: string - x-go-name: Name - nodes: - type: object - x-go-name: Nodes - pass_host: - type: string - x-go-name: PassHost - retries: - format: int64 - type: integer - x-go-name: Retries - service_name: - type: string - x-go-name: ServiceName - timeout: - type: object - x-go-name: Timeout - type: - type: string - x-go-name: Type - upstream_host: - type: string - x-go-name: UpstreamHost - type: object - x-go-package: github.com/apisix/manager-api/internal/core/entity -host: 127.0.0.1 -components: - securitySchemes: - basicAuth: - type: http - scheme: basic - username: test - password: testp -info: - description: |- - 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. - - API doc of Manager API. - - Manager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients. - license: - name: Apache License 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0 - title: |- - 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 -paths: - /api/labels/{aaa}: - get: - x-api-limit: 20 - description: |- - Return the labels list among `route,ssl,consumer,upstream,service` - according to the specified page number and page size, and can search labels by label1. - operationId: getLabelsList1 - - x-apisix-upstream: - type: roundrobin - nodes: - - host: "127.0.0.1" - port: 80 - weight: 1 - parameters: - - name: id - in: header - description: ID of pet to use - required: true - schema: - type: array - items: - type: string - style: simple - - requestBody: - content: - 'application/x-www-form-urlencoded': - schema: - properties: - name: - description: Updated name of the pet - type: string - status: - description: Updated status of the pet - type: string - required: - - status - - produces: - - application/json - - security: - - basicAuth: [] - - responses: - "0": - description: list response - schema: - items: - $ref: '#/definitions/service' - type: array - default: - description: unexpected error - schema: - $ref: '#/definitions/ApiError' -produces: -- application/json -- application/xml -schemes: -- http -- https -swagger: "2.0" diff --git a/api/test/testdata/import-test-default.yaml b/api/test/testdata/import-test-default.yaml new file mode 100644 index 0000000000..9be2cedc99 --- /dev/null +++ b/api/test/testdata/import-test-default.yaml @@ -0,0 +1,44 @@ +consumes: +- application/json +- application/xml +info: + 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 + produces: + - application/json + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/service' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' +produces: +- application/json +- application/xml +schemes: +- http +- https +swagger: "2.0" diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml new file mode 100644 index 0000000000..96c8f80a1e --- /dev/null +++ b/api/test/testdata/import-test-plugins.yaml @@ -0,0 +1,80 @@ +consumes: +- application/json +- application/xml +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + username: test + password: testp +info: + 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 + parameters: + - name: id + in: header + description: ID of pet to use + required: true + schema: + type: array + items: + type: string + style: simple + + requestBody: + content: + 'application/x-www-form-urlencoded': + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + required: + - status + + produces: + - application/json + + security: + - basicAuth: [] + + responses: + "0": + description: list response + schema: + items: + $ref: '#/definitions/service' + type: array + default: + description: unexpected error + schema: + $ref: '#/definitions/ApiError' +produces: +- application/json +- application/xml +schemes: +- http +- https +swagger: "2.0" From 65ac184629c846f13ba1065aa6db92afa9b19f73 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 01:07:47 +0800 Subject: [PATCH 07/36] test: add test case --- api/test/e2e/import_test.go | 74 +- api/test/testdata/api.json | 1031 ---------------------------- api/test/testdata/import-test.json | 62 ++ 3 files changed, 127 insertions(+), 1040 deletions(-) delete mode 100644 api/test/testdata/api.json create mode 100644 api/test/testdata/import-test.json diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index 204e45853f..ff6f15ccd6 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -2,14 +2,12 @@ package e2e import ( "fmt" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" "io/ioutil" "net/http" "path/filepath" "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/tidwall/gjson" ) func TestImport_default(t *testing.T) { @@ -50,6 +48,7 @@ func TestImport_default(t *testing.T) { tests = append(tests, tc) } + // verify route tests = append(tests, HttpTestCase{ Desc: "verify the route just imported", Object: APISIXExpect(t), @@ -60,15 +59,37 @@ func TestImport_default(t *testing.T) { 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_delete_test_data(t *testing.T) { - // delete the route created by POST - time.Sleep(time.Duration(100) * time.Millisecond) +func TestImport_json(t *testing.T) { + path, err := filepath.Abs("../testdata/import-test.json") + assert.Nil(t, err) + + headers := map[string]string{ + "Authorization": token, + } + files := []UploadFile{ + {Name: "file", Filepath: path}, + } + PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + request, _ := http.NewRequest("GET", ManagerAPIHost+"/apisix/admin/routes", nil) request.Header.Add("Authorization", token) resp, err := http.DefaultClient.Do(request) @@ -76,16 +97,51 @@ func TestImport_delete_test_data(t *testing.T) { defer resp.Body.Close() respBody, _ := ioutil.ReadAll(resp.Body) list := gjson.Get(string(respBody), "data.rows").Value().([]interface{}) + + fmt.Println("list:", string(respBody)) + + 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 the route", + 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/testdata/api.json b/api/test/testdata/api.json deleted file mode 100644 index fd35c224dd..0000000000 --- a/api/test/testdata/api.json +++ /dev/null @@ -1,1031 +0,0 @@ -{ - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "schemes": [ - "http", - "https" - ], - "swagger": "2.0", - "info": { - "description": "http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\nAPI doc of Manager API.\n\nManager API directly operates ETCD and provides data management for Apache APISIX, provides APIs for Front-end or other clients.", - "title": "Licensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements. See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License. You may obtain a copy of the License at", - "license": { - "name": "Apache License 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0" - } - }, - "host": "127.0.0.1", - "paths": { - "/api/labels": { - "get": { - "description": "Return the labels list among `route,ssl,consumer,upstream,service`\naccording to the specified page number and page size, and can search labels by label.", - "produces": [ - "application/json" - ], - "operationId": "getLabelsList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "label filter of labels", - "name": "label", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/service" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/check_ssl_cert": { - "post": { - "produces": [ - "application/json" - ], - "summary": "verify SSL cert and key.", - "operationId": "checkSSL", - "parameters": [ - { - "type": "string", - "description": "cert of SSL", - "name": "cert", - "in": "body", - "required": true - }, - { - "type": "string", - "description": "key of SSL", - "name": "key", - "in": "body", - "required": true - } - ], - "responses": { - "0": { - "description": "SSL verify passed", - "schema": { - "$ref": "#/definitions/ApiError" - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/check_ssl_exists": { - "post": { - "produces": [ - "application/json" - ], - "summary": "Check whether the SSL exists.", - "operationId": "checkSSLExist", - "parameters": [ - { - "type": "string", - "description": "cert of SSL", - "name": "cert", - "in": "body", - "required": true - }, - { - "type": "string", - "description": "key of SSL", - "name": "key", - "in": "body", - "required": true - } - ], - "responses": { - "0": { - "description": "SSL exists", - "schema": { - "$ref": "#/definitions/ApiError" - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/consumers": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the consumer list according to the specified page number and page size, and can search consumers by username.", - "operationId": "getConsumerList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username of consumer", - "name": "username", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/consumer" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/global_rules": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the global rule list according to the specified page number and page size.", - "operationId": "getGlobalRuleList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/GlobalPlugins" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/notexist/routes": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return result of route exists checking by name and exclude id.", - "operationId": "checkRouteExist", - "parameters": [ - { - "type": "string", - "description": "name of route", - "name": "name", - "in": "query" - }, - { - "type": "string", - "description": "id of route that exclude checking", - "name": "exclude", - "in": "query" - } - ], - "responses": { - "0": { - "description": "route not exists", - "schema": { - "$ref": "#/definitions/ApiError" - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/routes": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the route list according to the specified page number and page size, and can search routes by name and uri.", - "operationId": "getRouteList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "name of route", - "name": "name", - "in": "query" - }, - { - "type": "string", - "description": "uri of route", - "name": "uri", - "in": "query" - }, - { - "type": "string", - "description": "label of route", - "name": "label", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/route" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/services": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the service list according to the specified page number and page size, and can search services by name.", - "operationId": "getServiceList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "name of service", - "name": "name", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/service" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/ssl": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the SSL list according to the specified page number and page size, and can SSLs search by sni.", - "operationId": "getSSLList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "sni of SSL", - "name": "sni", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/ssl" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/upstreams": { - "get": { - "produces": [ - "application/json" - ], - "summary": "Return the upstream list according to the specified page number and page size, and can search upstreams by name.", - "operationId": "getUpstreamList", - "parameters": [ - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "name of upstream", - "name": "name", - "in": "query" - } - ], - "responses": { - "0": { - "description": "list response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/upstream" - } - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - }, - "/apisix/admin/user/login": { - "post": { - "produces": [ - "application/json" - ], - "summary": "user login.", - "operationId": "userLogin", - "parameters": [ - { - "type": "string", - "description": "user name", - "name": "username", - "in": "body", - "required": true - }, - { - "type": "string", - "description": "password", - "name": "password", - "in": "body", - "required": true - } - ], - "responses": { - "0": { - "description": "login success", - "schema": { - "$ref": "#/definitions/ApiError" - } - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } - } - } - } - } - }, - "definitions": { - "ApiError": { - "type": "object", - "properties": { - "code": { - "description": "response code", - "type": "integer", - "format": "int64", - "x-go-name": "Code" - }, - "message": { - "description": "response message", - "type": "string", - "x-go-name": "Message" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/utils/consts" - }, - "BaseInfo": { - "type": "object", - "properties": { - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "Consumer": { - "type": "object", - "properties": { - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "desc": { - "type": "string", - "x-go-name": "Desc" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object" - }, - "x-go-name": "Plugins" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - }, - "username": { - "type": "string", - "x-go-name": "Username" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "GlobalPlugins": { - "type": "object", - "properties": { - "id": { - "type": "object", - "x-go-name": "ID" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object" - }, - "x-go-name": "Plugins" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "LoginInput": { - "type": "object", - "properties": { - "password": { - "description": "password", - "type": "string", - "x-go-name": "Password" - }, - "username": { - "description": "user name", - "type": "string", - "x-go-name": "Username" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/handler/authentication" - }, - "Route": { - "type": "object", - "properties": { - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "desc": { - "type": "string", - "x-go-name": "Desc" - }, - "enable_websocket": { - "type": "boolean", - "x-go-name": "EnableWebsocket" - }, - "filter_func": { - "type": "string", - "x-go-name": "FilterFunc" - }, - "host": { - "type": "string", - "x-go-name": "Host" - }, - "hosts": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Hosts" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "methods": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Methods" - }, - "name": { - "type": "string", - "x-go-name": "Name" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object" - }, - "x-go-name": "Plugins" - }, - "priority": { - "type": "integer", - "format": "int64", - "x-go-name": "Priority" - }, - "remote_addr": { - "type": "string", - "x-go-name": "RemoteAddr" - }, - "remote_addrs": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "RemoteAddrs" - }, - "script": { - "type": "object", - "x-go-name": "Script" - }, - "service_id": { - "type": "object", - "x-go-name": "ServiceID" - }, - "service_protocol": { - "type": "string", - "x-go-name": "ServiceProtocol" - }, - "status": { - "$ref": "#/definitions/Status" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - }, - "upstream": { - "$ref": "#/definitions/UpstreamDef" - }, - "upstream_id": { - "type": "object", - "x-go-name": "UpstreamID" - }, - "uri": { - "type": "string", - "x-go-name": "URI" - }, - "uris": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Uris" - }, - "vars": { - "type": "object", - "x-go-name": "Vars" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "SSL": { - "type": "object", - "properties": { - "cert": { - "type": "string", - "x-go-name": "Cert" - }, - "certs": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Certs" - }, - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "exptime": { - "type": "integer", - "format": "int64", - "x-go-name": "ExpTime" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "key": { - "type": "string", - "x-go-name": "Key" - }, - "keys": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Keys" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "sni": { - "type": "string", - "x-go-name": "Sni" - }, - "snis": { - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Snis" - }, - "status": { - "type": "integer", - "format": "int64", - "x-go-name": "Status" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - }, - "validity_end": { - "type": "integer", - "format": "int64", - "x-go-name": "ValidityEnd" - }, - "validity_start": { - "type": "integer", - "format": "int64", - "x-go-name": "ValidityStart" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "Service": { - "type": "object", - "properties": { - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "desc": { - "type": "string", - "x-go-name": "Desc" - }, - "enable_websocket": { - "type": "boolean", - "x-go-name": "EnableWebsocket" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "name": { - "type": "string", - "x-go-name": "Name" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object" - }, - "x-go-name": "Plugins" - }, - "script": { - "type": "string", - "x-go-name": "Script" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - }, - "upstream": { - "$ref": "#/definitions/UpstreamDef" - }, - "upstream_id": { - "type": "object", - "x-go-name": "UpstreamID" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "Status": { - "type": "integer", - "format": "uint8", - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "Upstream": { - "type": "object", - "properties": { - "checks": { - "type": "object", - "x-go-name": "Checks" - }, - "create_time": { - "type": "integer", - "format": "int64", - "x-go-name": "CreateTime" - }, - "desc": { - "type": "string", - "x-go-name": "Desc" - }, - "hash_on": { - "type": "string", - "x-go-name": "HashOn" - }, - "id": { - "type": "object", - "x-go-name": "ID" - }, - "k8s_deployment_info": { - "type": "object", - "x-go-name": "K8sInfo" - }, - "key": { - "type": "string", - "x-go-name": "Key" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "name": { - "type": "string", - "x-go-name": "Name" - }, - "nodes": { - "type": "object", - "x-go-name": "Nodes" - }, - "pass_host": { - "type": "string", - "x-go-name": "PassHost" - }, - "retries": { - "type": "integer", - "format": "int64", - "x-go-name": "Retries" - }, - "service_name": { - "type": "string", - "x-go-name": "ServiceName" - }, - "timeout": { - "type": "object", - "x-go-name": "Timeout" - }, - "type": { - "type": "string", - "x-go-name": "Type" - }, - "update_time": { - "type": "integer", - "format": "int64", - "x-go-name": "UpdateTime" - }, - "upstream_host": { - "type": "string", - "x-go-name": "UpstreamHost" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - }, - "UpstreamDef": { - "type": "object", - "properties": { - "checks": { - "type": "object", - "x-go-name": "Checks" - }, - "desc": { - "type": "string", - "x-go-name": "Desc" - }, - "hash_on": { - "type": "string", - "x-go-name": "HashOn" - }, - "k8s_deployment_info": { - "type": "object", - "x-go-name": "K8sInfo" - }, - "key": { - "type": "string", - "x-go-name": "Key" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Labels" - }, - "name": { - "type": "string", - "x-go-name": "Name" - }, - "nodes": { - "type": "object", - "x-go-name": "Nodes" - }, - "pass_host": { - "type": "string", - "x-go-name": "PassHost" - }, - "retries": { - "type": "integer", - "format": "int64", - "x-go-name": "Retries" - }, - "service_name": { - "type": "string", - "x-go-name": "ServiceName" - }, - "timeout": { - "type": "object", - "x-go-name": "Timeout" - }, - "type": { - "type": "string", - "x-go-name": "Type" - }, - "upstream_host": { - "type": "string", - "x-go-name": "UpstreamHost" - } - }, - "x-go-package": "github.com/apisix/manager-api/internal/core/entity" - } - } -} \ No newline at end of file diff --git a/api/test/testdata/import-test.json b/api/test/testdata/import-test.json new file mode 100644 index 0000000000..278134864b --- /dev/null +++ b/api/test/testdata/import-test.json @@ -0,0 +1,62 @@ +{ + "consumes": [ + "application/json", + "application/xml" + ], + "info": { + "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 + } + ] + }, + "produces": [ + "application/json" + ], + "responses": { + "0": { + "description": "list response", + "schema": { + "items": { + "$ref": "#/definitions/service" + }, + "type": "array" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + } + }, + "produces": [ + "application/json", + "application/xml" + ], + "schemes": [ + "http", + "https" + ], + "swagger": "2.0" +} \ No newline at end of file From 8244358ef84c6c8bc2ed85a76b8a7317c5ff7415 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 14:06:31 +0800 Subject: [PATCH 08/36] test: add more test cases --- api/test/e2e/import_test.go | 89 ++++++++++++++++++++-- api/test/testdata/import-test-plugins.yaml | 2 +- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index ff6f15ccd6..3c3e100631 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -1,7 +1,6 @@ package e2e import ( - "fmt" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" "io/ioutil" @@ -30,9 +29,7 @@ func TestImport_default(t *testing.T) { respBody, _ := ioutil.ReadAll(resp.Body) list := gjson.Get(string(respBody), "data.rows").Value().([]interface{}) - fmt.Println("list:", string(respBody)) - - tests := []HttpTestCase{} + var tests []HttpTestCase for _, item := range list { route := item.(map[string]interface{}) tc := HttpTestCase{ @@ -98,9 +95,7 @@ func TestImport_json(t *testing.T) { respBody, _ := ioutil.ReadAll(resp.Body) list := gjson.Get(string(respBody), "data.rows").Value().([]interface{}) - fmt.Println("list:", string(respBody)) - - tests := []HttpTestCase{} + var tests []HttpTestCase for _, item := range list { route := item.(map[string]interface{}) tc := HttpTestCase{ @@ -145,3 +140,83 @@ func TestImport_json(t *testing.T) { testCaseCheck(tc, t) } } + +func TestImport_with_plugins(t *testing.T) { + path, err := filepath.Abs("../testdata/import-test-plugins.yaml") + assert.Nil(t, err) + + headers := map[string]string{ + "Authorization": token, + } + files := []UploadFile{ + {Name: "file", Filepath: path}, + } + PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + + 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 "status" is required`, + Sleep: sleepTime, + }, + { + Desc: "verify the route just imported", + Object: APISIXExpect(t), + Method: http.MethodPost, + Path: "/hello", + 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) + } +} diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index 96c8f80a1e..e4869da137 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -18,7 +18,7 @@ info: test title paths: /hello: - get: + post: x-api-limit: 20 description: |- hello world. From 12a6191ae12b46bfacfb1325eebac2d80684d4a7 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 14:41:41 +0800 Subject: [PATCH 09/36] fix: license --- api/internal/handler/data_loader/import.go | 16 ++++++++++++++++ api/test/e2e/http.go | 16 ++++++++++++++++ api/test/e2e/import_test.go | 16 ++++++++++++++++ api/test/testdata/import-test-default.yaml | 19 +++++++++++++++++++ api/test/testdata/import-test-plugins.yaml | 19 +++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index ebcd8dde6b..b3b4fd7974 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -1,3 +1,19 @@ +/* + * 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 ( diff --git a/api/test/e2e/http.go b/api/test/e2e/http.go index 47e65d270d..575490fe36 100644 --- a/api/test/e2e/http.go +++ b/api/test/e2e/http.go @@ -1,3 +1,19 @@ +/* + * 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 ( diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index 3c3e100631..1110047b0c 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -1,3 +1,19 @@ +/* + * 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 ( diff --git a/api/test/testdata/import-test-default.yaml b/api/test/testdata/import-test-default.yaml index 9be2cedc99..992320dfbf 100644 --- a/api/test/testdata/import-test-default.yaml +++ b/api/test/testdata/import-test-default.yaml @@ -1,3 +1,22 @@ +# +# 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: +# consumes: - application/json - application/xml diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index e4869da137..7484c8d236 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -1,3 +1,22 @@ +# +# 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: +# consumes: - application/json - application/xml From 3854f03aa9b948b1ff98e449615ae6bc0624be8c Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 14:56:23 +0800 Subject: [PATCH 10/36] fix: dockerfile --- api/test/docker/Dockerfile-apisix | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/test/docker/Dockerfile-apisix b/api/test/docker/Dockerfile-apisix index e27fc6ef9a..5c4bd3178a 100644 --- a/api/test/docker/Dockerfile-apisix +++ b/api/test/docker/Dockerfile-apisix @@ -20,9 +20,6 @@ FROM openresty/openresty:alpine-fat AS production-stage ARG APISIX_VERSION=master LABEL apisix_version="${APISIX_VERSION}" - -COPY test/docker/apisix-master-0.rockspec ./apisix-master-0.rockspec - RUN set -x \ && /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories \ && apk add --no-cache --virtual .builddeps \ @@ -32,7 +29,7 @@ RUN set -x \ pkgconfig \ cmake \ git \ - && luarocks install ./apisix-master-0.rockspec --tree=/usr/local/apisix/deps \ + && luarocks install https://github.com/apache/apisix/raw/master/rockspec/apisix-${APISIX_VERSION}-0.rockspec --tree=/usr/local/apisix/deps \ && cp -v /usr/local/apisix/deps/lib/luarocks/rocks-5.1/apisix/${APISIX_VERSION}-0/bin/apisix /usr/bin/ \ && bin='#! /usr/local/openresty/luajit/bin/luajit\npackage.path = "/usr/local/apisix/?.lua;" .. package.path' \ && sed -i "1s@.*@$bin@" /usr/bin/apisix \ From c18863cd9eb067b90ae857e0a6f1bed0e1ea493c Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 11 Jan 2021 20:28:59 +0800 Subject: [PATCH 11/36] fix: CI failed --- api/internal/core/entity/entity.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index 69e4d4c6e7..090dfcd027 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -84,7 +84,7 @@ type Route struct { Vars interface{} `json:"vars,omitempty"` FilterFunc string `json:"filter_func,omitempty"` Script interface{} `json:"script,omitempty"` - Plugins map[string]interface{} `json:"plugins"` + Plugins map[string]interface{} `json:"plugins,omitempty"` Upstream *UpstreamDef `json:"upstream,omitempty"` ServiceID interface{} `json:"service_id,omitempty"` UpstreamID interface{} `json:"upstream_id,omitempty"` @@ -94,11 +94,6 @@ type Route struct { Status Status `json:"status"` } -type RouteImport struct { - Plugins map[string]interface{} `json:"plugins"` - Route -} - // --- structures for upstream start --- type Timeout struct { Connect int `json:"connect,omitempty"` @@ -240,9 +235,6 @@ type Script struct { Script interface{} `json:"script,omitempty"` } -type SecurityPlugin struct { -} - type RequestValidation struct { Type string `json:"type,omitempty"` Required []string `json:"required,omitempty"` From 60f6f30e4edf26ccd375ddeb165ccc5788c81a2d Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 12 Jan 2021 15:33:18 +0800 Subject: [PATCH 12/36] fix: use openapi 3.0 --- api/test/testdata/import-test-default.yaml | 37 ++++++---------------- api/test/testdata/import-test-plugins.yaml | 27 +++------------- api/test/testdata/import-test.json | 35 ++++---------------- 3 files changed, 20 insertions(+), 79 deletions(-) diff --git a/api/test/testdata/import-test-default.yaml b/api/test/testdata/import-test-default.yaml index 992320dfbf..3f69887cf8 100644 --- a/api/test/testdata/import-test-default.yaml +++ b/api/test/testdata/import-test-default.yaml @@ -17,47 +17,28 @@ # 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: # -consumes: -- application/json -- application/xml +openapi: 3.0.0 info: - description: |- - test desc + 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 + url: 'http://www.apache.org/licenses/LICENSE-2.0' + title: test title paths: - /hello: + /hello: get: x-api-limit: 20 - description: |- - hello world. + description: hello world. operationId: hello x-apisix-upstream: type: roundrobin nodes: - - host: "172.16.238.20" + - host: 172.16.238.20 port: 1980 weight: 1 - produces: - - application/json responses: - "0": + '200': description: list response - schema: - items: - $ref: '#/definitions/service' - type: array default: description: unexpected error - schema: - $ref: '#/definitions/ApiError' -produces: -- application/json -- application/xml -schemes: -- http -- https -swagger: "2.0" diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index 7484c8d236..aea207333a 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -17,17 +17,14 @@ # 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: # -consumes: -- application/json -- application/xml + components: securitySchemes: basicAuth: type: http scheme: basic - username: test - password: testp info: + version: "1" description: |- test desc license: @@ -73,27 +70,13 @@ paths: required: - status - produces: - - application/json - security: - basicAuth: [] responses: - "0": + 200: description: list response - schema: - items: - $ref: '#/definitions/service' - type: array default: description: unexpected error - schema: - $ref: '#/definitions/ApiError' -produces: -- application/json -- application/xml -schemes: -- http -- https -swagger: "2.0" + +openapi: 3.0.0 diff --git a/api/test/testdata/import-test.json b/api/test/testdata/import-test.json index 278134864b..1bbeb49fb8 100644 --- a/api/test/testdata/import-test.json +++ b/api/test/testdata/import-test.json @@ -1,9 +1,7 @@ { - "consumes": [ - "application/json", - "application/xml" - ], + "openapi": "3.0.0", "info": { + "version": "1.0.0-oas3", "description": "test desc", "license": { "name": "Apache License 2.0", @@ -27,36 +25,15 @@ } ] }, - "produces": [ - "application/json" - ], "responses": { - "0": { - "description": "list response", - "schema": { - "items": { - "$ref": "#/definitions/service" - }, - "type": "array" - } + "200": { + "description": "list response" }, "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/ApiError" - } + "description": "unexpected error" } } } } - }, - "produces": [ - "application/json", - "application/xml" - ], - "schemes": [ - "http", - "https" - ], - "swagger": "2.0" + } } \ No newline at end of file From eddabd2059aed170736dbffa67a076701f612de7 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 12 Jan 2021 16:43:13 +0800 Subject: [PATCH 13/36] fix: test fail --- api/test/e2e/import_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index 1110047b0c..dab3f280e8 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -23,6 +23,7 @@ import ( "net/http" "path/filepath" "testing" + "time" ) func TestImport_default(t *testing.T) { @@ -37,6 +38,9 @@ func TestImport_default(t *testing.T) { } PostFile(ManagerAPIHost+"/apisix/admin/import", 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) @@ -103,6 +107,9 @@ func TestImport_json(t *testing.T) { } PostFile(ManagerAPIHost+"/apisix/admin/import", 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) @@ -169,6 +176,9 @@ func TestImport_with_plugins(t *testing.T) { } PostFile(ManagerAPIHost+"/apisix/admin/import", 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) From f370f549d99db37778fa396b0e22ae4592c168ab Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 18 Jan 2021 11:37:55 +0800 Subject: [PATCH 14/36] fix error --- api/internal/handler/data_loader/import.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index b3b4fd7974..af31923eef 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -143,7 +143,7 @@ func Import(c *gin.Context) (interface{}, error) { return nil, err } // save original conf - if err = scriptStore.Create(c, script); err != nil { + if _, err = scriptStore.Create(c, script); err != nil { return nil, err } } @@ -155,7 +155,7 @@ func Import(c *gin.Context) (interface{}, error) { // create route for _, route := range routes { - if err := routeStore.Create(c, route); err != nil { + if _, err := routeStore.Create(c, route); err != nil { println(err.Error()) return handler.SpecCodeResponse(err), err } From 0e71f3b48c7fcae5b6b2bba3544f0128de12900a Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 18 Jan 2021 12:24:48 +0800 Subject: [PATCH 15/36] fix according to review --- api/internal/core/store/validate.go | 3 +-- api/internal/handler/data_loader/import.go | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/api/internal/core/store/validate.go b/api/internal/core/store/validate.go index 4ec0b14812..abf5933185 100644 --- a/api/internal/core/store/validate.go +++ b/api/internal/core/store/validate.go @@ -245,8 +245,7 @@ func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error { } errString.AppendString(vErr.String()) } - j, _ := json.Marshal(obj) - log.Errorf("schema validate failed:s: %v, obj: %s", v.schemaDef, string(j)) + 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/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index af31923eef..781b88105c 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -75,12 +75,12 @@ func Import(c *gin.Context) (interface{}, error) { // read file and parse handle, err := file.Open() - defer func() { - err = handle.Close() - }() if err != nil { return nil, err } + defer func() { + err = handle.Close() + }() reader := bufio.NewReader(handle) bytes := make([]byte, file.Size) @@ -133,9 +133,10 @@ func Import(c *gin.Context) (interface{}, error) { if route.ID == "" { route.ID = utils.GetFlakeUidStr() } - script := &entity.Script{} - script.ID = utils.InterfaceToString(route.ID) - script.Script = route.Script + script := &entity.Script{ + ID: utils.InterfaceToString(route.ID), + Script: route.Script, + } // to lua var err error route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) @@ -156,7 +157,6 @@ func Import(c *gin.Context) (interface{}, error) { // create route for _, route := range routes { if _, err := routeStore.Create(c, route); err != nil { - println(err.Error()) return handler.SpecCodeResponse(err), err } } @@ -187,7 +187,6 @@ func checkRouteName(name string) (bool, error) { return true, nil } - func parseExtension(val *openapi3.Operation) (*entity.Route, error) { routeMap := map[string]interface{}{} for key, val := range val.Extensions { From 8e02fdb41bfd3cd726672ca8fc6580700bdfec7b Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 18 Jan 2021 16:52:13 +0800 Subject: [PATCH 16/36] fix: no newline at end of file --- api/test/testdata/import-test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/testdata/import-test.json b/api/test/testdata/import-test.json index 1bbeb49fb8..0d20878cb2 100644 --- a/api/test/testdata/import-test.json +++ b/api/test/testdata/import-test.json @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} From a08d4e1e93972d76c1a4ec26337a6c1794dced1c Mon Sep 17 00:00:00 2001 From: nic-chen Date: Tue, 19 Jan 2021 11:15:04 +0800 Subject: [PATCH 17/36] fix: yaml format --- api/test/testdata/import-test-plugins.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index aea207333a..3ccb6895b0 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -61,17 +61,17 @@ paths: 'application/x-www-form-urlencoded': schema: properties: - name: - description: Updated name of the pet - type: string - status: - description: Updated status of the pet - type: string + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string required: - status security: - - basicAuth: [] + - basicAuth: [] responses: 200: From 6a7fdc75f10f5c6d5e1594d888ad9e5648246ae6 Mon Sep 17 00:00:00 2001 From: nic-chen <33000667+nic-chen@users.noreply.github.com> Date: Tue, 19 Jan 2021 15:41:41 +0800 Subject: [PATCH 18/36] Update api/test/testdata/import-test-plugins.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 琚致远 --- api/test/testdata/import-test-plugins.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index 3ccb6895b0..6204ef5aeb 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -62,7 +62,7 @@ paths: schema: properties: name: - description: Updated name of the pet + description: Update pet's name type: string status: description: Updated status of the pet From be06fbc7009002c4cbce7944e42196e9a17501e8 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Thu, 21 Jan 2021 11:39:59 +0800 Subject: [PATCH 19/36] fix error --- api/internal/handler/data_loader/import.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 781b88105c..9533f4fc19 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -18,6 +18,7 @@ package data_loader import ( "bufio" + "context" "encoding/json" "fmt" "net/http" @@ -105,12 +106,12 @@ func Import(c *gin.Context) (interface{}, error) { // check route for _, route := range routes { - _, err := checkRouteName(route.Name) + _, err := checkRouteName(c, route.Name) if err != nil { continue } if route.ServiceID != nil { - _, err := routeStore.Get(utils.InterfaceToString(route.ServiceID)) + _, err := routeStore.Get(c, utils.InterfaceToString(route.ServiceID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -120,7 +121,7 @@ func Import(c *gin.Context) (interface{}, error) { } } if route.UpstreamID != nil { - _, err := upstreamStore.Get(utils.InterfaceToString(route.UpstreamID)) + _, err := upstreamStore.Get(c, utils.InterfaceToString(route.UpstreamID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -164,9 +165,9 @@ func Import(c *gin.Context) (interface{}, error) { return nil, nil } -func checkRouteName(name string) (bool, error) { +func checkRouteName(ctx context.Context, name string) (bool, error) { routeStore := store.GetStore(store.HubKeyRoute) - ret, err := routeStore.List(store.ListInput{ + ret, err := routeStore.List(ctx, store.ListInput{ Predicate: nil, PageSize: 0, PageNumber: 0, From a47ef3801c8ed99ef05c202dcbd6fe72e069eaaf Mon Sep 17 00:00:00 2001 From: nic-chen Date: Thu, 21 Jan 2021 11:58:38 +0800 Subject: [PATCH 20/36] fix yaml format --- api/test/testdata/import-test-plugins.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import-test-plugins.yaml index 6204ef5aeb..26389c3fda 100644 --- a/api/test/testdata/import-test-plugins.yaml +++ b/api/test/testdata/import-test-plugins.yaml @@ -33,7 +33,7 @@ info: title: |- test title paths: - /hello: + /hello: post: x-api-limit: 20 description: |- @@ -46,15 +46,15 @@ paths: port: 1980 weight: 1 parameters: - - name: id - in: header - description: ID of pet to use - required: true - schema: - type: array - items: - type: string - style: simple + - name: id + in: header + description: ID of pet to use + required: true + schema: + type: array + items: + type: string + style: simple requestBody: content: From aff9d074c51f45fcc2248a2d2953c39b600762dd Mon Sep 17 00:00:00 2001 From: nic-chen Date: Thu, 21 Jan 2021 13:11:45 +0800 Subject: [PATCH 21/36] fix: reject empty or invalid file --- api/internal/handler/data_loader/import.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 9533f4fc19..40c2ee0cd3 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -20,6 +20,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "net/http" "path" @@ -95,6 +96,11 @@ func Import(c *gin.Context) (interface{}, error) { 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 From 1caeb3516b3b2a063eb79296d48a16e7ebd08102 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Fri, 22 Jan 2021 22:42:41 +0800 Subject: [PATCH 22/36] fix: losing `header_schema` for plugin `request-validation` --- api/internal/handler/data_loader/import.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 40c2ee0cd3..3995861ed7 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -296,8 +296,11 @@ func parseParameters(parameters openapi3.Parameters, plugins map[string]interfac } } } - requestValidation := make(map[string]interface{}) + 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, @@ -309,6 +312,9 @@ func parseParameters(parameters openapi3.Parameters, plugins map[string]interfac 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 From 052fdd1257f72995e09a26b7ea01271312f790e9 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Sat, 23 Jan 2021 23:23:49 +0800 Subject: [PATCH 23/36] feat: merge methods of path --- api/cmd/manager/main_test.go | 2 + api/internal/handler/data_loader/import.go | 177 +++++++++++++++------ api/internal/utils/utils.go | 37 +++++ 3 files changed, 165 insertions(+), 51 deletions(-) diff --git a/api/cmd/manager/main_test.go b/api/cmd/manager/main_test.go index 1e7b51d989..f89149005e 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/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 3995861ed7..08369e1339 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -29,13 +29,17 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/gin-gonic/gin" + "github.com/shiningrush/droplet" "github.com/shiningrush/droplet/data" + "github.com/shiningrush/droplet/middleware" + 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" routeHandler "github.com/apisix/manager-api/internal/handler/route" + "github.com/apisix/manager-api/internal/log" "github.com/apisix/manager-api/internal/utils" "github.com/apisix/manager-api/internal/utils/consts" ) @@ -57,26 +61,38 @@ func NewHandler() (handler.RouteRegister, error) { } func (h *Handler) ApplyRoute(r *gin.Engine) { - r.POST("/apisix/admin/import", consts.ErrorWrapper(Import)) + r.POST("/apisix/admin/import", wgin.Wraps(h.Import)) } -func Import(c *gin.Context) (interface{}, error) { - file, err := c.FormFile("file") +type ImportInput struct { + ForceCover bool `auto_read:"force_cover,query"` +} + +func (h *Handler) Import(c droplet.Context) (interface{}, error) { + httpReq := c.Get(middleware.KeyHttpRequest) + if httpReq == nil { + return nil, errors.New("input middleware cannot get http request") + } + req := httpReq.(*http.Request) + req.Body = http.MaxBytesReader(nil, req.Body, int64(conf.ImportSizeLimit)) + if err := req.ParseMultipartForm(int64(conf.ImportSizeLimit)); err != nil { + log.Warnf("upload file size exceeds limit: %s", err) + return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) + } + + _, fileHeader, err := req.FormFile("file") if err != nil { return nil, err } // file check - suffix := path.Ext(file.Filename) + suffix := path.Ext(fileHeader.Filename) if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" { return nil, fmt.Errorf("the file type error: %s", suffix) } - if file.Size > int64(conf.ImportSizeLimit) { - return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) - } // read file and parse - handle, err := file.Open() + handle, err := fileHeader.Open() if err != nil { return nil, err } @@ -85,7 +101,7 @@ func Import(c *gin.Context) (interface{}, error) { }() reader := bufio.NewReader(handle) - bytes := make([]byte, file.Size) + bytes := make([]byte, fileHeader.Size) _, err = reader.Read(bytes) if err != nil { return nil, err @@ -110,14 +126,18 @@ func Import(c *gin.Context) (interface{}, error) { upstreamStore := store.GetStore(store.HubKeyUpstream) scriptStore := store.GetStore(store.HubKeyScript) + //input := c.Input().(*ImportInput) + // check route for _, route := range routes { - _, err := checkRouteName(c, route.Name) - if err != nil { - continue + err := checkRouteExist(c.Context(), route) + if err != nil && false { + log.Warnf("import duplicate: %s, route: %#v", err, route) + return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, + errors.New("") } if route.ServiceID != nil { - _, err := routeStore.Get(c, utils.InterfaceToString(route.ServiceID)) + _, err := routeStore.Get(c.Context(), utils.InterfaceToString(route.ServiceID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -127,7 +147,7 @@ func Import(c *gin.Context) (interface{}, error) { } } if route.UpstreamID != nil { - _, err := upstreamStore.Get(c, utils.InterfaceToString(route.UpstreamID)) + _, err := upstreamStore.Get(c.Context(), utils.InterfaceToString(route.UpstreamID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -151,7 +171,7 @@ func Import(c *gin.Context) (interface{}, error) { return nil, err } // save original conf - if _, err = scriptStore.Create(c, script); err != nil { + if _, err = scriptStore.Create(c.Context(), script); err != nil { return nil, err } } @@ -163,7 +183,7 @@ func Import(c *gin.Context) (interface{}, error) { // create route for _, route := range routes { - if _, err := routeStore.Create(c, route); err != nil { + if _, err := routeStore.Create(c.Context(), route); err != nil { return handler.SpecCodeResponse(err), err } } @@ -171,27 +191,35 @@ func Import(c *gin.Context) (interface{}, error) { return nil, nil } -func checkRouteName(ctx context.Context, name string) (bool, error) { +func checkRouteExist(ctx context.Context, route *entity.Route) error { routeStore := store.GetStore(store.HubKeyRoute) ret, err := routeStore.List(ctx, store.ListInput{ - Predicate: nil, + 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 && + item.Vars == route.Vars && item.FilterFunc == route.FilterFunc { + return false + } + return true + }, PageSize: 0, PageNumber: 0, }) if err != nil { - return false, err + return err } - sort := store.NewSort(nil) - filter := store.NewFilter([]string{"name", name}) - pagination := store.NewPagination(0, 0) - query := store.NewQuery(sort, filter, pagination) - rows := store.NewFilterSelector(routeHandler.ToRows(ret), query) - if len(rows) > 0 { - return false, consts.InvalidParam("route name is duplicate") + if len(ret.Rows) > 0 { + return consts.InvalidParam("route is duplicate") } - return true, nil + return nil } func parseExtension(val *openapi3.Operation) (*entity.Route, error) { @@ -216,6 +244,42 @@ func parseExtension(val *openapi3.Operation) (*entity.Route, error) { 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 { + fmt.Println("m:", value.Method) + 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) + fmt.Printf("methods: %v, value.Method: %v, v.Method: %v", routes[v.Method].Methods, value.Method, v.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 @@ -229,47 +293,58 @@ func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) { return nil, err } } + + var values []PathValue if v.Get != nil { - route, err := getRouteFromPaths("GET", k, v.Get, swagger) - if err != nil { - return nil, err + value := PathValue{ + Method: http.MethodGet, + Value: v.Get, } - routes = append(routes, route) + values = append(values, value) } if v.Post != nil { - route, err := getRouteFromPaths("POST", k, v.Post, swagger) - if err != nil { - return nil, err + value := PathValue{ + Method: http.MethodPost, + Value: v.Post, } - routes = append(routes, route) + values = append(values, value) } if v.Head != nil { - route, err := getRouteFromPaths("HEAD", k, v.Head, swagger) - if err != nil { - return nil, err + value := PathValue{ + Method: http.MethodHead, + Value: v.Head, } - routes = append(routes, route) + values = append(values, value) } if v.Put != nil { - route, err := getRouteFromPaths("PUT", k, v.Put, swagger) - if err != nil { - return nil, err + value := PathValue{ + Method: http.MethodPut, + Value: v.Put, } - routes = append(routes, route) + values = append(values, value) } if v.Patch != nil { - route, err := getRouteFromPaths("PATCH", k, v.Patch, swagger) - if err != nil { - return nil, err + value := PathValue{ + Method: http.MethodPatch, + Value: v.Patch, } - routes = append(routes, route) + values = append(values, value) } - if v.Delete != nil { - route, err := getRouteFromPaths("DELETE", k, v.Delete, swagger) - if err != nil { - return nil, err + 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) } } diff --git a/api/internal/utils/utils.go b/api/internal/utils/utils.go index 0a612c29de..3661bcb691 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), "") 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) +} From 9576e270ee7a3bdcc0cd94508033584ca3a2dcff Mon Sep 17 00:00:00 2001 From: nic-chen Date: Sun, 24 Jan 2021 10:15:46 +0800 Subject: [PATCH 24/36] feat: force import --- api/internal/handler/data_loader/import.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 08369e1339..dd49aef8f2 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -65,7 +65,7 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { } type ImportInput struct { - ForceCover bool `auto_read:"force_cover,query"` + Force bool `auto_read:"force,query"` } func (h *Handler) Import(c droplet.Context) (interface{}, error) { @@ -80,6 +80,8 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) } + Force := req.URL.Query().Get("force") + _, fileHeader, err := req.FormFile("file") if err != nil { return nil, err @@ -131,10 +133,9 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { // check route for _, route := range routes { err := checkRouteExist(c.Context(), route) - if err != nil && false { + if err != nil && Force != "1" { log.Warnf("import duplicate: %s, route: %#v", err, route) - return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, - errors.New("") + return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err } if route.ServiceID != nil { _, err := routeStore.Get(c.Context(), utils.InterfaceToString(route.ServiceID)) @@ -197,13 +198,14 @@ func checkRouteExist(ctx context.Context, route *entity.Route) error { Predicate: func(obj interface{}) bool { id := utils.InterfaceToString(route.ID) item := obj.(*entity.Route) - if id != "" && id == utils.InterfaceToString(item.ID) { + if id != "" && id != utils.InterfaceToString(item.ID) { return false } - if item.Host == route.Host && item.URI == route.URI && utils.StringSliceEqual(item.Uris, route.Uris) && + + 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 && - item.Vars == route.Vars && item.FilterFunc == route.FilterFunc { + utils.ValueEqual(item.Vars, route.Vars) && item.FilterFunc == route.FilterFunc) { return false } return true @@ -214,11 +216,9 @@ func checkRouteExist(ctx context.Context, route *entity.Route) error { if err != nil { return err } - if len(ret.Rows) > 0 { return consts.InvalidParam("route is duplicate") } - return nil } @@ -253,7 +253,6 @@ func mergePathValue(key string, values []PathValue, swagger *openapi3.Swagger) ( var parsed []PathValue var routes = map[string]*entity.Route{} for _, value := range values { - fmt.Println("m:", value.Method) value.Value.OperationID = strings.Replace(value.Value.OperationID, value.Method, "", 1) var eq = false for _, v := range parsed { @@ -263,7 +262,6 @@ func mergePathValue(key string, values []PathValue, swagger *openapi3.Swagger) ( routes[v.Method].Methods = []string{} } routes[v.Method].Methods = append(routes[v.Method].Methods, value.Method) - fmt.Printf("methods: %v, value.Method: %v, v.Method: %v", routes[v.Method].Methods, value.Method, v.Method) } } // not equal to the previous ones From 686b7a59cd72c9e2c58402b3878c9addbea107f3 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 25 Jan 2021 00:28:26 +0800 Subject: [PATCH 25/36] test: add unit test --- api/internal/handler/data_loader/import.go | 54 ++-- .../handler/data_loader/import_test.go | 159 +++++++++++ api/test/e2e/import_test.go | 6 +- .../{import-test.json => import/default.json} | 0 .../default.yaml} | 0 api/test/testdata/import/diferent-routes.yaml | 238 ++++++++++++++++ .../import/one-route-with-methods.yaml | 260 ++++++++++++++++++ .../with-plugins.yaml} | 0 api/test/testdata/import/with-service-id.yaml | 39 +++ .../testdata/import/with-upstream-id.yaml | 39 +++ 10 files changed, 754 insertions(+), 41 deletions(-) create mode 100644 api/internal/handler/data_loader/import_test.go rename api/test/testdata/{import-test.json => import/default.json} (100%) rename api/test/testdata/{import-test-default.yaml => import/default.yaml} (100%) create mode 100644 api/test/testdata/import/diferent-routes.yaml create mode 100644 api/test/testdata/import/one-route-with-methods.yaml rename api/test/testdata/{import-test-plugins.yaml => import/with-plugins.yaml} (100%) create mode 100644 api/test/testdata/import/with-service-id.yaml create mode 100644 api/test/testdata/import/with-upstream-id.yaml diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index dd49aef8f2..f5cf9f4071 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -38,17 +38,15 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/handler" - routeHandler "github.com/apisix/manager-api/internal/handler/route" "github.com/apisix/manager-api/internal/log" "github.com/apisix/manager-api/internal/utils" "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { - routeStore store.Interface + routeStore *store.GenericStore svcStore store.Interface upstreamStore store.Interface - scriptStore store.Interface } func NewHandler() (handler.RouteRegister, error) { @@ -56,7 +54,6 @@ func NewHandler() (handler.RouteRegister, error) { routeStore: store.GetStore(store.HubKeyRoute), svcStore: store.GetStore(store.HubKeyService), upstreamStore: store.GetStore(store.HubKeyUpstream), - scriptStore: store.GetStore(store.HubKeyScript), }, nil } @@ -90,7 +87,7 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { // file check suffix := path.Ext(fileHeader.Filename) if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" { - return nil, fmt.Errorf("the file type error: %s", suffix) + return nil, fmt.Errorf("required file type is .yaml, .yml or .json but got: %s", suffix) } // read file and parse @@ -124,21 +121,15 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { return nil, err } - routeStore := store.GetStore(store.HubKeyRoute) - upstreamStore := store.GetStore(store.HubKeyUpstream) - scriptStore := store.GetStore(store.HubKeyScript) - - //input := c.Input().(*ImportInput) - // check route for _, route := range routes { - err := checkRouteExist(c.Context(), route) + err := checkRouteExist(c.Context(), h.routeStore, route) if err != nil && Force != "1" { log.Warnf("import duplicate: %s, route: %#v", err, route) return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err } if route.ServiceID != nil { - _, err := routeStore.Get(c.Context(), utils.InterfaceToString(route.ServiceID)) + _, err := h.svcStore.Get(c.Context(), utils.InterfaceToString(route.ServiceID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -148,7 +139,7 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { } } if route.UpstreamID != nil { - _, err := upstreamStore.Get(c.Context(), utils.InterfaceToString(route.UpstreamID)) + _, err := h.upstreamStore.Get(c.Context(), utils.InterfaceToString(route.UpstreamID)) if err != nil { if err == data.ErrNotFound { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, @@ -157,43 +148,30 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err } } - if route.Script != nil { - if route.ID == "" { - route.ID = utils.GetFlakeUidStr() - } - script := &entity.Script{ - ID: utils.InterfaceToString(route.ID), - Script: route.Script, - } - // to lua - var err error - route.Script, err = routeHandler.GenerateLuaCode(route.Script.(map[string]interface{})) - if err != nil { - return nil, err - } - // save original conf - if _, err = scriptStore.Create(c.Context(), script); err != nil { - return nil, err - } - } - if _, err := routeStore.CreateCheck(route); err != nil { + if _, err := h.routeStore.CreateCheck(route); err != nil { return handler.SpecCodeResponse(err), err } } // create route for _, route := range routes { - if _, err := routeStore.Create(c.Context(), route); err != nil { - return handler.SpecCodeResponse(err), err + if Force == "1" && route.ID != nil { + if _, err := h.routeStore.Update(c.Context(), route, true); err != nil { + return handler.SpecCodeResponse(err), err + } + } else { + if _, err := h.routeStore.Create(c.Context(), route); err != nil { + return handler.SpecCodeResponse(err), err + } } } return nil, nil } -func checkRouteExist(ctx context.Context, route *entity.Route) error { - routeStore := store.GetStore(store.HubKeyRoute) +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) diff --git a/api/internal/handler/data_loader/import_test.go b/api/internal/handler/data_loader/import_test.go new file mode 100644 index 0000000000..6f1ece7e24 --- /dev/null +++ b/api/internal/handler/data_loader/import_test.go @@ -0,0 +1,159 @@ +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/shiningrush/droplet/middleware" + "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) { + file := testFile{"file", "file1.txt", []byte("hello")} + req := createRequestMultipartFiles(t, file) + + h := Handler{} + ctx := droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, 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) { + file := testFile{"file", "file1.json", []byte(`{"test": "a"}`)} + req := createRequestMultipartFiles(t, file) + + h := Handler{} + ctx := droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, 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") + file := testFile{"file", "file1.yaml", bytes} + req := createRequestMultipartFiles(t, file) + + mStore := &store.MockInterface{} + mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { + }).Return(nil, errors.New("data not found by key: service1")) + + h := Handler{ + routeStore: &store.GenericStore{}, + svcStore: mStore, + upstreamStore: mStore, + } + ctx := droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, 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 = Handler{ + routeStore: &store.GenericStore{}, + svcStore: mStore, + upstreamStore: mStore, + } + ctx = droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, 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") + file := testFile{"file", "file1.yaml", bytes} + req := createRequestMultipartFiles(t, file) + + mStore := &store.MockInterface{} + mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { + }).Return(nil, errors.New("data not found by key: upstream1")) + + h := Handler{ + routeStore: &store.GenericStore{}, + svcStore: mStore, + upstreamStore: mStore, + } + ctx := droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, 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 = Handler{ + routeStore: &store.GenericStore{}, + svcStore: mStore, + upstreamStore: mStore, + } + ctx = droplet.NewContext() + ctx.Set(middleware.KeyHttpRequest, req) + + _, err = h.Import(ctx) + assert.EqualError(t, err, "upstream id: upstream1 not found") + +} diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index dab3f280e8..ac18532d85 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -27,7 +27,7 @@ import ( ) func TestImport_default(t *testing.T) { - path, err := filepath.Abs("../testdata/import-test-default.yaml") + path, err := filepath.Abs("../testdata/import/default.yaml") assert.Nil(t, err) headers := map[string]string{ @@ -96,7 +96,7 @@ func TestImport_default(t *testing.T) { } func TestImport_json(t *testing.T) { - path, err := filepath.Abs("../testdata/import-test.json") + path, err := filepath.Abs("../testdata/import/default.json") assert.Nil(t, err) headers := map[string]string{ @@ -165,7 +165,7 @@ func TestImport_json(t *testing.T) { } func TestImport_with_plugins(t *testing.T) { - path, err := filepath.Abs("../testdata/import-test-plugins.yaml") + path, err := filepath.Abs("../testdata/import/with-plugins.yaml") assert.Nil(t, err) headers := map[string]string{ diff --git a/api/test/testdata/import-test.json b/api/test/testdata/import/default.json similarity index 100% rename from api/test/testdata/import-test.json rename to api/test/testdata/import/default.json diff --git a/api/test/testdata/import-test-default.yaml b/api/test/testdata/import/default.yaml similarity index 100% rename from api/test/testdata/import-test-default.yaml rename to api/test/testdata/import/default.yaml diff --git a/api/test/testdata/import/diferent-routes.yaml b/api/test/testdata/import/diferent-routes.yaml new file mode 100644 index 0000000000..5065c4d24e --- /dev/null +++ b/api/test/testdata/import/diferent-routes.yaml @@ -0,0 +1,238 @@ +# +# 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: + /head: + delete: + operationId: api1Delete + requestBody: {} + responses: + default: + description: '' + x-apisix-enableWebsocket: false + x-apisix-labels: + API_VERSION: v2 + dev: test + x-apisix-plugins: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + proxy-rewrite: + disable: false + scheme: https + x-apisix-priority: 0 + x-apisix-status: 1 + x-apisix-upstream: + nodes: + - host: httpin.org + port: 80 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + pass_host: pass + 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: + node-status: + disable: true + 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: pass + x-apisix-vars: [] diff --git a/api/test/testdata/import/one-route-with-methods.yaml b/api/test/testdata/import/one-route-with-methods.yaml new file mode 100644 index 0000000000..6ae8cd2178 --- /dev/null +++ b/api/test/testdata/import/one-route-with-methods.yaml @@ -0,0 +1,260 @@ +# +# 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: + /test-test: + delete: + operationId: route_allDELETE + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test + get: + operationId: route_allGET + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test + head: + operationId: route_allHEAD + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test + patch: + operationId: route_allPATCH + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test + post: + operationId: route_allPOST + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test + put: + operationId: route_allPUT + requestBody: {} + responses: + default: + description: '' + security: [] + summary: 所有 + x-apisix-enableWebsocket: true + x-apisix-hosts: + - test.com + x-apisix-labels: + API_VERSION: v1 + test: '1' + x-apisix-plugins: + prometheus: + disable: false + x-apisix-priority: 0 + x-apisix-remoteAddrs: + - 192.168.1.101 + x-apisix-status: 1 + x-apisix-upstream: + id: '327205672972190305' + create_time: 1604559379 + update_time: 1604559379 + nodes: + - host: httpin.org + port: 443 + weight: 1 + timeout: + connect: 6000 + read: 6000 + send: 6000 + type: roundrobin + name: httpbin + x-apisix-vars: + - - arg_query + - '==' + - test diff --git a/api/test/testdata/import-test-plugins.yaml b/api/test/testdata/import/with-plugins.yaml similarity index 100% rename from api/test/testdata/import-test-plugins.yaml rename to api/test/testdata/import/with-plugins.yaml 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 0000000000..985b02ebd5 --- /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 0000000000..dc25276577 --- /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 From 7d60375ad9734888347647721d3bdfdbcea06b9c Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 25 Jan 2021 08:26:45 +0800 Subject: [PATCH 26/36] fix CI failed --- api/internal/handler/data_loader/import_test.go | 16 ++++++++++++++++ api/test/e2e/import_test.go | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/api/internal/handler/data_loader/import_test.go b/api/internal/handler/data_loader/import_test.go index 6f1ece7e24..a7ef262c11 100644 --- a/api/internal/handler/data_loader/import_test.go +++ b/api/internal/handler/data_loader/import_test.go @@ -1,3 +1,19 @@ +/* + * 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 ( diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index ac18532d85..306b189c38 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -212,6 +212,17 @@ func TestImport_with_plugins(t *testing.T) { 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, }, @@ -220,6 +231,7 @@ func TestImport_with_plugins(t *testing.T) { 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"}`, From 8f60f0255fc0729597c314db34627109c14a168d Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 25 Jan 2021 14:58:10 +0800 Subject: [PATCH 27/36] test: more test cases --- api/test/e2e/import_test.go | 123 ++++++++- ...diferent-routes.yaml => multi-routes.yaml} | 66 ++--- .../import/one-route-with-methods.yaml | 260 ------------------ api/test/testdata/import/with-plugins.yaml | 4 +- 4 files changed, 148 insertions(+), 305 deletions(-) rename api/test/testdata/import/{diferent-routes.yaml => multi-routes.yaml} (83%) delete mode 100644 api/test/testdata/import/one-route-with-methods.yaml diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index 306b189c38..bb64e033b5 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -17,13 +17,14 @@ package e2e import ( - "github.com/stretchr/testify/assert" - "github.com/tidwall/gjson" "io/ioutil" "net/http" "path/filepath" "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" ) func TestImport_default(t *testing.T) { @@ -258,3 +259,121 @@ func TestImport_with_plugins(t *testing.T) { 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", 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) + + // verify route data + if route["uri"].(string) == "/get" { + 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.MethodPost, + 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) + } +} diff --git a/api/test/testdata/import/diferent-routes.yaml b/api/test/testdata/import/multi-routes.yaml similarity index 83% rename from api/test/testdata/import/diferent-routes.yaml rename to api/test/testdata/import/multi-routes.yaml index 5065c4d24e..966815f714 100644 --- a/api/test/testdata/import/diferent-routes.yaml +++ b/api/test/testdata/import/multi-routes.yaml @@ -23,9 +23,9 @@ info: version: 3.0.0 openapi: 3.0.0 paths: - /head: + /get: delete: - operationId: api1Delete + operationId: api1DELETE requestBody: {} responses: default: @@ -35,8 +35,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -44,18 +42,18 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] get: - operationId: api1Get + operationId: api1GET requestBody: {} responses: default: @@ -65,8 +63,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -74,15 +70,15 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] head: operationId: api1HEAD @@ -95,8 +91,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -104,18 +98,18 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] patch: - operationId: api1Patch + operationId: api1PATCH requestBody: {} responses: default: @@ -125,8 +119,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -134,18 +126,18 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] post: - operationId: api1Post + operationId: api1POST requestBody: {} responses: default: @@ -155,8 +147,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -164,18 +154,18 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] put: - operationId: api1Put + operationId: api1PUT requestBody: {} responses: default: @@ -185,8 +175,6 @@ paths: API_VERSION: v2 dev: test x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -194,19 +182,19 @@ paths: x-apisix-status: 1 x-apisix-upstream: nodes: - - host: httpin.org - port: 80 + - host: httpbin.org + port: 443 weight: 1 timeout: connect: 6000 read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] /post: post: - operationId: test_postPost + operationId: test_postPOST requestBody: {} responses: default: @@ -217,8 +205,6 @@ paths: API_VERSION: v1 version: v1 x-apisix-plugins: - node-status: - disable: true proxy-rewrite: disable: false scheme: https @@ -234,5 +220,5 @@ paths: read: 6000 send: 6000 type: roundrobin - pass_host: pass + pass_host: node x-apisix-vars: [] diff --git a/api/test/testdata/import/one-route-with-methods.yaml b/api/test/testdata/import/one-route-with-methods.yaml deleted file mode 100644 index 6ae8cd2178..0000000000 --- a/api/test/testdata/import/one-route-with-methods.yaml +++ /dev/null @@ -1,260 +0,0 @@ -# -# 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: - /test-test: - delete: - operationId: route_allDELETE - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test - get: - operationId: route_allGET - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test - head: - operationId: route_allHEAD - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test - patch: - operationId: route_allPATCH - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test - post: - operationId: route_allPOST - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test - put: - operationId: route_allPUT - requestBody: {} - responses: - default: - description: '' - security: [] - summary: 所有 - x-apisix-enableWebsocket: true - x-apisix-hosts: - - test.com - x-apisix-labels: - API_VERSION: v1 - test: '1' - x-apisix-plugins: - prometheus: - disable: false - x-apisix-priority: 0 - x-apisix-remoteAddrs: - - 192.168.1.101 - x-apisix-status: 1 - x-apisix-upstream: - id: '327205672972190305' - create_time: 1604559379 - update_time: 1604559379 - nodes: - - host: httpin.org - port: 443 - weight: 1 - timeout: - connect: 6000 - read: 6000 - send: 6000 - type: roundrobin - name: httpbin - x-apisix-vars: - - - arg_query - - '==' - - test diff --git a/api/test/testdata/import/with-plugins.yaml b/api/test/testdata/import/with-plugins.yaml index 26389c3fda..68377d1453 100644 --- a/api/test/testdata/import/with-plugins.yaml +++ b/api/test/testdata/import/with-plugins.yaml @@ -51,9 +51,7 @@ paths: description: ID of pet to use required: true schema: - type: array - items: - type: string + type: string style: simple requestBody: From c7bc64002c8a9f8a8c832e8f55aaf175a711920b Mon Sep 17 00:00:00 2001 From: nic-chen Date: Mon, 25 Jan 2021 19:19:01 +0800 Subject: [PATCH 28/36] fix method of test case --- api/test/e2e/import_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index bb64e033b5..380e355e87 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -341,7 +341,7 @@ func TestImport_with_multi_routes(t *testing.T) { { Desc: "verify the route just imported", Object: APISIXExpect(t), - Method: http.MethodPost, + Method: http.MethodGet, Path: "/get", ExpectStatus: http.StatusOK, ExpectBody: `"url": "https://127.0.0.1/get"`, From 1d5d2f4fded11917ab43088ae0a3f016b8e01515 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 00:48:13 +0800 Subject: [PATCH 29/36] fix review --- api/internal/handler/data_loader/import.go | 82 ++++++++++------------ api/test/e2e/import_test.go | 8 +-- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index f5cf9f4071..78a66efd46 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -17,13 +17,14 @@ package data_loader import ( - "bufio" + "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "path" + "reflect" "regexp" "strings" @@ -31,7 +32,7 @@ import ( "github.com/gin-gonic/gin" "github.com/shiningrush/droplet" "github.com/shiningrush/droplet/data" - "github.com/shiningrush/droplet/middleware" + "github.com/shiningrush/droplet/wrapper" wgin "github.com/shiningrush/droplet/wrapper/gin" "github.com/apisix/manager-api/internal/conf" @@ -49,6 +50,9 @@ type Handler struct { upstreamStore store.Interface } +var regPathVar = regexp.MustCompile(`{[\w.]*}`) +var regPathRepeat = regexp.MustCompile(`-APISIX-REPEAT-URI-[\d]*`) + func NewHandler() (handler.RouteRegister, error) { return &Handler{ routeStore: store.GetStore(store.HubKeyRoute), @@ -58,55 +62,33 @@ func NewHandler() (handler.RouteRegister, error) { } func (h *Handler) ApplyRoute(r *gin.Engine) { - r.POST("/apisix/admin/import", wgin.Wraps(h.Import)) + r.POST("/apisix/admin/import/routes", wgin.Wraps(h.Import, + wrapper.InputType(reflect.TypeOf(ImportInput{})))) } type ImportInput struct { - Force bool `auto_read:"force,query"` + Force byte `auto_read:"force,query"` + FileName string `auto_read:"_file"` + FileContent []byte `auto_read:"file"` } func (h *Handler) Import(c droplet.Context) (interface{}, error) { - httpReq := c.Get(middleware.KeyHttpRequest) - if httpReq == nil { - return nil, errors.New("input middleware cannot get http request") - } - req := httpReq.(*http.Request) - req.Body = http.MaxBytesReader(nil, req.Body, int64(conf.ImportSizeLimit)) - if err := req.ParseMultipartForm(int64(conf.ImportSizeLimit)); err != nil { - log.Warnf("upload file size exceeds limit: %s", err) - return nil, fmt.Errorf("the file size exceeds the limit; limit %d", conf.ImportSizeLimit) - } - - Force := req.URL.Query().Get("force") - - _, fileHeader, err := req.FormFile("file") - if err != nil { - return nil, err - } + input := c.Input().(*ImportInput) + Force := input.Force // file check - suffix := path.Ext(fileHeader.Filename) + 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) } - // read file and parse - handle, err := fileHeader.Open() - if err != nil { - return nil, err - } - defer func() { - err = handle.Close() - }() - - reader := bufio.NewReader(handle) - bytes := make([]byte, fileHeader.Size) - _, err = reader.Read(bytes) - if err != nil { - return nil, err + 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(bytes) + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(input.FileContent) if err != nil { return nil, err } @@ -124,9 +106,10 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { // check route for _, route := range routes { err := checkRouteExist(c.Context(), h.routeStore, route) - if err != nil && Force != "1" { + if err != nil && Force != 1 { log.Warnf("import duplicate: %s, route: %#v", err, route) - return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err + 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)) @@ -150,24 +133,30 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { } if _, err := h.routeStore.CreateCheck(route); err != nil { - return handler.SpecCodeResponse(err), err + return handler.SpecCodeResponse(err), + fmt.Errorf("create route(uris:%v) failed: %s", route.Uris, err) } } // create route for _, route := range routes { - if Force == "1" && route.ID != nil { + if Force == 1 && route.ID != nil { if _, err := h.routeStore.Update(c.Context(), route, true); err != nil { - return handler.SpecCodeResponse(err), err + 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), err + return handler.SpecCodeResponse(err), + fmt.Errorf("create route(uris:%v) failed: %s", route.Uris, err) } } } - return nil, nil + return map[string]int{ + "paths": len(swagger.Paths), + "routes": len(routes), + }, nil } func checkRouteExist(ctx context.Context, routeStore *store.GenericStore, route *entity.Route) error { @@ -262,6 +251,7 @@ func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) { 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) @@ -468,10 +458,10 @@ func parseSecurity(security openapi3.SecurityRequirements, securitySchemes opena } } + func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) (*entity.Route, error) { // transform /path/{var} to /path/* - reg := regexp.MustCompile(`{[\w.]*}`) - foundStr := reg.FindString(key) + foundStr := regPathVar.FindString(key) if foundStr != "" { key = strings.Split(key, foundStr)[0] + "*" } diff --git a/api/test/e2e/import_test.go b/api/test/e2e/import_test.go index 380e355e87..a2dde73efa 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/import_test.go @@ -37,7 +37,7 @@ func TestImport_default(t *testing.T) { files := []UploadFile{ {Name: "file", Filepath: path}, } - PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers) // sleep for data sync time.Sleep(sleepTime) @@ -106,7 +106,7 @@ func TestImport_json(t *testing.T) { files := []UploadFile{ {Name: "file", Filepath: path}, } - PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers) // sleep for data sync time.Sleep(sleepTime) @@ -175,7 +175,7 @@ func TestImport_with_plugins(t *testing.T) { files := []UploadFile{ {Name: "file", Filepath: path}, } - PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers) // sleep for data sync time.Sleep(sleepTime) @@ -270,7 +270,7 @@ func TestImport_with_multi_routes(t *testing.T) { files := []UploadFile{ {Name: "file", Filepath: path}, } - PostFile(ManagerAPIHost+"/apisix/admin/import", nil, files, headers) + PostFile(ManagerAPIHost+"/apisix/admin/import/routes", nil, files, headers) // sleep for data sync time.Sleep(sleepTime) From e5742eb8ac4285bab14b825a57dacec5471ed2f8 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 12:09:17 +0800 Subject: [PATCH 30/36] fix: using new version of droplet to avoid error --- api/go.mod | 2 +- api/go.sum | 4 ++-- api/internal/handler/data_loader/import.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/go.mod b/api/go.mod index 1de4858180..e0b945774d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -30,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 5084ce7c32..d8234a3198 100644 --- a/api/go.sum +++ b/api/go.sum @@ -347,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= diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 78a66efd46..2593c1de60 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -67,7 +67,7 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { } type ImportInput struct { - Force byte `auto_read:"force,query"` + Force bool `auto_read:"force,query"` FileName string `auto_read:"_file"` FileContent []byte `auto_read:"file"` } @@ -106,7 +106,7 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { // check route for _, route := range routes { err := checkRouteExist(c.Context(), h.routeStore, route) - if err != nil && Force != 1 { + 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) @@ -140,7 +140,7 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { // create route for _, route := range routes { - if Force == 1 && route.ID != nil { + 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) From d336387e93e05b257180ab21e0b14acaede1a652 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 12:29:00 +0800 Subject: [PATCH 31/36] fix: unit test --- .../handler/data_loader/import_test.go | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/api/internal/handler/data_loader/import_test.go b/api/internal/handler/data_loader/import_test.go index a7ef262c11..35bc61a771 100644 --- a/api/internal/handler/data_loader/import_test.go +++ b/api/internal/handler/data_loader/import_test.go @@ -28,7 +28,6 @@ import ( "testing" "github.com/shiningrush/droplet" - "github.com/shiningrush/droplet/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -64,24 +63,26 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request } func TestImport_invalid_file_type(t *testing.T) { - file := testFile{"file", "file1.txt", []byte("hello")} - req := createRequestMultipartFiles(t, file) + input := &ImportInput{} + input.FileName = "file1.txt" + input.FileContent = []byte("hello") h := Handler{} ctx := droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + 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) { - file := testFile{"file", "file1.json", []byte(`{"test": "a"}`)} - req := createRequestMultipartFiles(t, file) + input := &ImportInput{} + input.FileName = "file1.json" + input.FileContent = []byte(`{"test": "a"}`) h := Handler{} ctx := droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + ctx.SetInput(input) _, err := h.Import(ctx) assert.EqualError(t, err, "empty or invalid imported file") @@ -101,8 +102,9 @@ func ReadFile(t *testing.T, filePath string) []byte { func TestImport_with_service_id(t *testing.T) { bytes := ReadFile(t, "test/testdata/import/with-service-id.yaml") - file := testFile{"file", "file1.yaml", bytes} - req := createRequestMultipartFiles(t, file) + input := &ImportInput{} + input.FileName = "file1.json" + input.FileContent = bytes mStore := &store.MockInterface{} mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { @@ -114,7 +116,7 @@ func TestImport_with_service_id(t *testing.T) { upstreamStore: mStore, } ctx := droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + ctx.SetInput(input) _, err := h.Import(ctx) assert.EqualError(t, err, "data not found by key: service1") @@ -130,7 +132,7 @@ func TestImport_with_service_id(t *testing.T) { upstreamStore: mStore, } ctx = droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + ctx.SetInput(input) _, err = h.Import(ctx) assert.EqualError(t, err, "service id: service1 not found") @@ -138,8 +140,9 @@ func TestImport_with_service_id(t *testing.T) { func TestImport_with_upstream_id(t *testing.T) { bytes := ReadFile(t, "test/testdata/import/with-upstream-id.yaml") - file := testFile{"file", "file1.yaml", bytes} - req := createRequestMultipartFiles(t, file) + input := &ImportInput{} + input.FileName = "file1.json" + input.FileContent = bytes mStore := &store.MockInterface{} mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { @@ -151,7 +154,7 @@ func TestImport_with_upstream_id(t *testing.T) { upstreamStore: mStore, } ctx := droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + ctx.SetInput(input) _, err := h.Import(ctx) assert.EqualError(t, err, "data not found by key: upstream1") @@ -167,7 +170,7 @@ func TestImport_with_upstream_id(t *testing.T) { upstreamStore: mStore, } ctx = droplet.NewContext() - ctx.Set(middleware.KeyHttpRequest, req) + ctx.SetInput(input) _, err = h.Import(ctx) assert.EqualError(t, err, "upstream id: upstream1 not found") From 74deea180d17c973c60f2f8c9f6bc9f2350e527a Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 12:55:27 +0800 Subject: [PATCH 32/36] chore: revert `route/route.go` --- api/internal/handler/route/route.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go index 103a4c75ad..2e3f15eea0 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -269,7 +269,7 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { return ret, nil } -func GenerateLuaCode(script map[string]interface{}) (string, error) { +func generateLuaCode(script map[string]interface{}) (string, error) { scriptString, err := json.Marshal(script) if err != nil { return "", err @@ -341,13 +341,12 @@ func (h *Handler) Create(c droplet.Context) (interface{}, error) { script.Script = input.Script var err error - // Explicitly to lua if input script is of the map type, otherwise // it will always represent a piece of lua code of the string type. if scriptConf, ok := input.Script.(map[string]interface{}); ok { // For lua code of map type, syntax validation is done by // the generateLuaCode function - input.Script, err = GenerateLuaCode(scriptConf) + input.Script, err = generateLuaCode(scriptConf) if err != nil { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err } @@ -421,13 +420,12 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { script.Script = input.Script var err error - // Explicitly to lua if input script is of the map type, otherwise // it will always represent a piece of lua code of the string type. if scriptConf, ok := input.Script.(map[string]interface{}); ok { // For lua code of map type, syntax validation is done by // the generateLuaCode function - input.Route.Script, err = GenerateLuaCode(scriptConf) + input.Route.Script, err = generateLuaCode(scriptConf) if err != nil { return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err } @@ -497,7 +495,7 @@ type ExistInput struct { Name string `auto_read:"name,query"` } -func ToRows(list *store.ListOutput) []store.Row { +func toRows(list *store.ListOutput) []store.Row { rows := make([]store.Row, list.TotalSize) for i := range list.Rows { rows[i] = list.Rows[i].(*entity.Route) @@ -557,7 +555,7 @@ func (h *Handler) Exist(c droplet.Context) (interface{}, error) { filter := store.NewFilter([]string{"name", name}) pagination := store.NewPagination(0, 0) query := store.NewQuery(sort, filter, pagination) - rows := store.NewFilterSelector(ToRows(ret), query) + rows := store.NewFilterSelector(toRows(ret), query) if len(rows) > 0 { r := rows[0].(*entity.Route) From 8126dc9c6961c0482896858528f76fc3ff715af2 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 13:00:46 +0800 Subject: [PATCH 33/36] fix: review --- api/internal/handler/data_loader/import.go | 9 ++++----- api/test/e2e/http.go | 7 ++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/api/internal/handler/data_loader/import.go b/api/internal/handler/data_loader/import.go index 2593c1de60..c5024855d2 100644 --- a/api/internal/handler/data_loader/import.go +++ b/api/internal/handler/data_loader/import.go @@ -67,8 +67,8 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { } type ImportInput struct { - Force bool `auto_read:"force,query"` - FileName string `auto_read:"_file"` + Force bool `auto_read:"force,query"` + FileName string `auto_read:"_file"` FileContent []byte `auto_read:"file"` } @@ -154,7 +154,7 @@ func (h *Handler) Import(c droplet.Context) (interface{}, error) { } return map[string]int{ - "paths": len(swagger.Paths), + "paths": len(swagger.Paths), "routes": len(routes), }, nil } @@ -458,7 +458,6 @@ func parseSecurity(security openapi3.SecurityRequirements, securitySchemes opena } } - func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *openapi3.Swagger) (*entity.Route, error) { // transform /path/{var} to /path/* foundStr := regPathVar.FindString(key) @@ -471,7 +470,7 @@ func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger *o return nil, err } - route.URI = key + route.Uris = []string{key} route.Name = value.OperationID route.Desc = value.Summary route.Methods = []string{method} diff --git a/api/test/e2e/http.go b/api/test/e2e/http.go index 575490fe36..130e28388b 100644 --- a/api/test/e2e/http.go +++ b/api/test/e2e/http.go @@ -50,15 +50,12 @@ func post(reqUrl string, reqParams map[string]string, contentType string, files } } resp, err := httpClient.Do(httpRequest) - - defer func() { - err = resp.Body.Close() - }() - if err != nil { panic(err) } + defer resp.Body.Close() + response, _ := ioutil.ReadAll(resp.Body) return string(response) From a99c92bc35ca6f2e1c8519fd377f9af3d96d7765 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 14:58:02 +0800 Subject: [PATCH 34/36] test: add test cases for import the file exported. --- api/test/e2e/base.go | 5 +- api/test/e2e/http.go | 11 +- api/test/e2e/route_export_test.go | 5 +- .../{import_test.go => route_import_test.go} | 194 ++++++++++++++++++ api/test/e2e/route_online_debug_test.go | 2 +- api/test/e2e/route_with_plugin_jwt_test.go | 4 +- 6 files changed, 210 insertions(+), 11 deletions(-) rename api/test/e2e/{import_test.go => route_import_test.go} (68%) diff --git a/api/test/e2e/base.go b/api/test/e2e/base.go index 1cf59eb055..0d9a333c4d 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 index 130e28388b..d6f1b6ab65 100644 --- a/api/test/e2e/http.go +++ b/api/test/e2e/http.go @@ -36,11 +36,11 @@ type UploadFile struct { var httpClient = &http.Client{} -func PostFile(reqUrl string, reqParams map[string]string, files []UploadFile, headers map[string]string) string { +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) string { +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) @@ -56,9 +56,12 @@ func post(reqUrl string, reqParams map[string]string, contentType string, files defer resp.Body.Close() - response, _ := ioutil.ReadAll(resp.Body) + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, 0, err + } - return string(response) + return body, resp.StatusCode, nil } func getReader(reqParams map[string]string, contentType string, files []UploadFile) (io.Reader, string) { diff --git a/api/test/e2e/route_export_test.go b/api/test/e2e/route_export_test.go index 5871d765aa..7c9b5c6d8f 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/import_test.go b/api/test/e2e/route_import_test.go similarity index 68% rename from api/test/e2e/import_test.go rename to api/test/e2e/route_import_test.go index a2dde73efa..2fb59e8233 100644 --- a/api/test/e2e/import_test.go +++ b/api/test/e2e/route_import_test.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "net/http" "path/filepath" + "strings" "testing" "time" @@ -377,3 +378,196 @@ func TestImport_with_multi_routes(t *testing.T) { 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 fba4f03826..4665309707 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 9ceec91d07..3bf50f1eac 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) From 8c6ffa9a426852e37f5e3aed20a3ff4b11793483 Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 15:29:52 +0800 Subject: [PATCH 35/36] fix: test cases failed --- .../handler/data_loader/route_import_test.go | 12 ++++++------ api/test/e2e/route_import_test.go | 10 ++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/internal/handler/data_loader/route_import_test.go b/api/internal/handler/data_loader/route_import_test.go index 35bc61a771..50f76f7cc9 100644 --- a/api/internal/handler/data_loader/route_import_test.go +++ b/api/internal/handler/data_loader/route_import_test.go @@ -67,7 +67,7 @@ func TestImport_invalid_file_type(t *testing.T) { input.FileName = "file1.txt" input.FileContent = []byte("hello") - h := Handler{} + h := ImportHandler{} ctx := droplet.NewContext() ctx.SetInput(input) @@ -80,7 +80,7 @@ func TestImport_invalid_content(t *testing.T) { input.FileName = "file1.json" input.FileContent = []byte(`{"test": "a"}`) - h := Handler{} + h := ImportHandler{} ctx := droplet.NewContext() ctx.SetInput(input) @@ -110,7 +110,7 @@ func TestImport_with_service_id(t *testing.T) { mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { }).Return(nil, errors.New("data not found by key: service1")) - h := Handler{ + h := ImportHandler{ routeStore: &store.GenericStore{}, svcStore: mStore, upstreamStore: mStore, @@ -126,7 +126,7 @@ func TestImport_with_service_id(t *testing.T) { mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { }).Return(nil, data.ErrNotFound) - h = Handler{ + h = ImportHandler{ routeStore: &store.GenericStore{}, svcStore: mStore, upstreamStore: mStore, @@ -148,7 +148,7 @@ func TestImport_with_upstream_id(t *testing.T) { mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { }).Return(nil, errors.New("data not found by key: upstream1")) - h := Handler{ + h := ImportHandler{ routeStore: &store.GenericStore{}, svcStore: mStore, upstreamStore: mStore, @@ -164,7 +164,7 @@ func TestImport_with_upstream_id(t *testing.T) { mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { }).Return(nil, data.ErrNotFound) - h = Handler{ + h = ImportHandler{ routeStore: &store.GenericStore{}, svcStore: mStore, upstreamStore: mStore, diff --git a/api/test/e2e/route_import_test.go b/api/test/e2e/route_import_test.go index 2fb59e8233..88b8ddbb3c 100644 --- a/api/test/e2e/route_import_test.go +++ b/api/test/e2e/route_import_test.go @@ -300,9 +300,15 @@ func TestImport_with_multi_routes(t *testing.T) { Sleep: sleepTime, } tests = append(tests, tc) - + uris := route["uris"].([]string) + isGet := false + for _, uri := range uris { + if uri == "/get" { + isGet = true + } + } // verify route data - if route["uri"].(string) == "/get" { + if isGet { tcDataVerify := HttpTestCase{ Desc: "verify data of route", Object: ManagerApiExpect(t), From e5d494d92328fa341995a2eae6542eb3a751c0af Mon Sep 17 00:00:00 2001 From: nic-chen Date: Wed, 27 Jan 2021 16:12:47 +0800 Subject: [PATCH 36/36] fix error --- api/test/e2e/route_import_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/e2e/route_import_test.go b/api/test/e2e/route_import_test.go index 88b8ddbb3c..62a6c9778c 100644 --- a/api/test/e2e/route_import_test.go +++ b/api/test/e2e/route_import_test.go @@ -300,7 +300,7 @@ func TestImport_with_multi_routes(t *testing.T) { Sleep: sleepTime, } tests = append(tests, tc) - uris := route["uris"].([]string) + uris := route["uris"].([]interface{}) isGet := false for _, uri := range uris { if uri == "/get" {