This is an automated email from the ASF dual-hosted git repository.
bzp2010 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 13670d24 feat: integrate data loader interface to import handler
(#2474)
13670d24 is described below
commit 13670d242a09bde9bb5382f4485c65fd95046e11
Author: Zeping Bai <[email protected]>
AuthorDate: Thu Jun 23 13:17:03 2022 +0800
feat: integrate data loader interface to import handler (#2474)
---
api/go.mod | 1 +
api/go.sum | 7 +-
.../handler/data_loader/loader/openapi3/import.go | 3 +
api/internal/handler/data_loader/route_import.go | 653 +++++++--------------
.../handler/data_loader/route_import_test.go | 145 +----
api/test/e2e/data_loader/data_loader_suite_test.go | 39 ++
api/test/e2e/data_loader/openapi3_test.go | 322 ++++++++++
api/test/testdata/import/default.json | 2 +-
api/test/testdata/import/httpbin.yaml | 36 ++
.../integration/route/import_export_route.spec.js | 208 -------
web/src/pages/Route/List.tsx | 1 +
11 files changed, 628 insertions(+), 789 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index d45f9a96..3584277b 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -22,6 +22,7 @@ require (
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
+ github.com/juliangruber/go-intersect v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.8.0 // indirect
github.com/satori/go.uuid v1.2.0
diff --git a/api/go.sum b/api/go.sum
index 19d50c6e..a273ba52 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -64,6 +64,8 @@ github.com/beorn7/perks v1.0.1
h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod
h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod
h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod
h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod
h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/casbin/casbin/v2 v2.1.2/go.mod
h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod
h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod
h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -295,6 +297,8 @@ github.com/jstemmer/go-junit-report
v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod
h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible
h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod
h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/juliangruber/go-intersect v1.1.0
h1:sc+y5dCjMMx0pAdYk/N6KBm00tD/f3tq+Iox7dYDUrY=
+github.com/juliangruber/go-intersect v1.1.0/go.mod
h1:WMau+1kAmnlQnKiikekNJbtGtfmILU/mMU6H7AgKbWQ=
github.com/julienschmidt/httprouter v1.2.0/go.mod
h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod
h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod
h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -304,8 +308,9 @@ github.com/konsorten/go-windows-terminal-sequences
v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod
h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
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/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod
h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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=
diff --git a/api/internal/handler/data_loader/loader/openapi3/import.go
b/api/internal/handler/data_loader/loader/openapi3/import.go
index 04ecf3bd..8ff332a7 100644
--- a/api/internal/handler/data_loader/loader/openapi3/import.go
+++ b/api/internal/handler/data_loader/loader/openapi3/import.go
@@ -81,6 +81,9 @@ func (o Loader) convertToEntities(s *openapi3.Swagger)
(*loader.DataSets, error)
UpstreamDef: entity.UpstreamDef{
Name: globalUpstreamID,
Type: "roundrobin",
+ Nodes: map[string]float64{
+ "0.0.0.0": 1,
+ },
},
}
data.Upstreams = append(data.Upstreams, upstream)
diff --git a/api/internal/handler/data_loader/route_import.go
b/api/internal/handler/data_loader/route_import.go
index 4f17dd08..e94dec02 100644
--- a/api/internal/handler/data_loader/route_import.go
+++ b/api/internal/handler/data_loader/route_import.go
@@ -19,18 +19,14 @@ package data_loader
import (
"bytes"
"context"
- "encoding/json"
"fmt"
- "net/http"
"path"
"reflect"
- "regexp"
- "strings"
- "github.com/getkin/kin-openapi/openapi3"
"github.com/gin-gonic/gin"
+ "github.com/juliangruber/go-intersect"
+ "github.com/pkg/errors"
"github.com/shiningrush/droplet"
- "github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/wrapper"
wgin "github.com/shiningrush/droplet/wrapper/gin"
@@ -38,516 +34,279 @@ import (
"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/core/store"
"github.com/apisix/manager-api/internal/handler"
- "github.com/apisix/manager-api/internal/log"
- "github.com/apisix/manager-api/internal/utils"
- "github.com/apisix/manager-api/internal/utils/consts"
+ loader
"github.com/apisix/manager-api/internal/handler/data_loader/loader"
+
"github.com/apisix/manager-api/internal/handler/data_loader/loader/openapi3"
)
type ImportHandler struct {
- routeStore *store.GenericStore
- svcStore store.Interface
- upstreamStore store.Interface
+ routeStore store.Interface
+ upstreamStore store.Interface
+ serviceStore store.Interface
+ consumerStore store.Interface
+ sslStore store.Interface
+ streamRouteStore store.Interface
+ globalPluginStore store.Interface
+ pluginConfigStore store.Interface
+ protoStore store.Interface
}
func NewImportHandler() (handler.RouteRegister, error) {
return &ImportHandler{
- routeStore: store.GetStore(store.HubKeyRoute),
- svcStore: store.GetStore(store.HubKeyService),
- upstreamStore: store.GetStore(store.HubKeyUpstream),
+ routeStore: store.GetStore(store.HubKeyRoute),
+ upstreamStore: store.GetStore(store.HubKeyUpstream),
+ serviceStore: store.GetStore(store.HubKeyService),
+ consumerStore: store.GetStore(store.HubKeyConsumer),
+ sslStore: store.GetStore(store.HubKeySsl),
+ streamRouteStore: store.GetStore(store.HubKeyStreamRoute),
+ globalPluginStore: store.GetStore(store.HubKeyGlobalRule),
+ pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+ protoStore: store.GetStore(store.HubKeyProto),
}, nil
}
-var regPathVar = regexp.MustCompile(`{[\w.]*}`)
-var regPathRepeat = regexp.MustCompile(`-APISIX-REPEAT-URI-[\d]*`)
-
func (h *ImportHandler) ApplyRoute(r *gin.Engine) {
r.POST("/apisix/admin/import/routes", wgin.Wraps(h.Import,
wrapper.InputType(reflect.TypeOf(ImportInput{}))))
}
+type ImportResult struct {
+ Total int `json:"total"`
+ Failed int `json:"failed"`
+ Errors []string `json:"errors"`
+}
+
+type LoaderType string
+
type ImportInput struct {
- Force bool `auto_read:"force,query"`
+ Type string `auto_read:"type"`
+ TaskName string `auto_read:"task_name"`
FileName string `auto_read:"_file"`
FileContent []byte `auto_read:"file"`
+
+ MergeMethod string `auto_read:"merge_method"`
}
+const (
+ LoaderTypeOpenAPI3 LoaderType = "openapi3"
+)
+
func (h *ImportHandler) Import(c droplet.Context) (interface{}, error) {
input := c.Input().(*ImportInput)
- Force := input.Force
- // file check
+ // input file content check
suffix := path.Ext(input.FileName)
if suffix != ".json" && suffix != ".yaml" && suffix != ".yml" {
- return nil, fmt.Errorf("required file type is .yaml, .yml or
.json but got: %s", suffix)
+ return nil, errors.Errorf("required file type is .yaml, .yml or
.json but got: %s", suffix)
}
-
contentLen := bytes.Count(input.FileContent, nil) - 1
- if contentLen > conf.ImportSizeLimit {
- log.Warnf("upload file size exceeds limit: %d", contentLen)
- return nil, fmt.Errorf("the file size exceeds the limit; limit
%d", conf.ImportSizeLimit)
+ if contentLen <= 0 {
+ return nil, errors.New("uploaded file is empty")
}
-
- swagger, err :=
openapi3.NewSwaggerLoader().LoadSwaggerFromData(input.FileContent)
- if err != nil {
- return nil, err
+ if contentLen > conf.ImportSizeLimit {
+ return nil, errors.Errorf("uploaded file size exceeds the
limit, limit is %d", conf.ImportSizeLimit)
}
- if len(swagger.Paths) < 1 {
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
- consts.ErrImportFile
+ var l loader.Loader
+ switch LoaderType(input.Type) {
+ case LoaderTypeOpenAPI3:
+ l = &openapi3.Loader{
+ MergeMethod: input.MergeMethod == "true",
+ TaskName: input.TaskName,
+ }
+ break
+ default:
+ return nil, fmt.Errorf("unsupported data loader type: %s",
input.Type)
}
- routes, err := OpenAPI3ToRoute(swagger)
+ dataSets, err := l.Import(input.FileContent)
if err != nil {
return nil, err
}
- // check route
- for _, route := range routes {
- err := checkRouteExist(c.Context(), h.routeStore, route)
- if err != nil && !Force {
- log.Warnf("import duplicate: %s, route: %#v", err,
route)
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
- fmt.Errorf("route(uris:%v) conflict, %s",
route.Uris, err)
- }
- if route.ServiceID != nil {
- _, err := h.svcStore.Get(c.Context(),
utils.InterfaceToString(route.ServiceID))
- if err != nil {
- if err == data.ErrNotFound {
- return
&data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
- fmt.Errorf(consts.IDNotFound,
"service", route.ServiceID)
- }
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
- }
- }
- if route.UpstreamID != nil {
- _, err := h.upstreamStore.Get(c.Context(),
utils.InterfaceToString(route.UpstreamID))
- if err != nil {
- if err == data.ErrNotFound {
- return
&data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
- fmt.Errorf(consts.IDNotFound,
"upstream", route.UpstreamID)
- }
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
- }
- }
-
- if _, err := h.routeStore.CreateCheck(route); err != nil {
- return handler.SpecCodeResponse(err),
- fmt.Errorf("create route(uris:%v) failed: %s",
route.Uris, err)
- }
- }
-
- // merge route
- idRoute := make(map[string]*entity.Route)
- for _, route := range routes {
- if existRoute, ok := idRoute[route.ID.(string)]; ok {
- uris := append(existRoute.Uris, route.Uris...)
- existRoute.Uris = uris
- } else {
- idRoute[route.ID.(string)] = route
- }
- }
-
- routes = make([]*entity.Route, 0, len(idRoute))
- for _, route := range idRoute {
- routes = append(routes, route)
+ // Pre-checking for route duplication
+ preCheckErrs := h.preCheck(c.Context(), dataSets)
+ if _, ok := preCheckErrs[store.HubKeyRoute]; ok &&
len(preCheckErrs[store.HubKeyRoute]) > 0 {
+ return h.convertToImportResult(dataSets, preCheckErrs), nil
}
- // create route
- for _, route := range routes {
- if Force && route.ID != nil {
- if _, err := h.routeStore.Update(c.Context(), route,
true); err != nil {
- return handler.SpecCodeResponse(err),
- fmt.Errorf("update route(uris:%v)
failed: %s", route.Uris, err)
- }
- } else {
- if _, err := h.routeStore.Create(c.Context(), route);
err != nil {
- return handler.SpecCodeResponse(err),
- fmt.Errorf("create route(uris:%v)
failed: %s", route.Uris, err)
- }
- }
- }
-
- return map[string]int{
- "paths": len(swagger.Paths),
- "routes": len(routes),
- }, nil
+ // Create APISIX resources
+ createErrs := h.createEntities(c.Context(), dataSets)
+ return h.convertToImportResult(dataSets, createErrs), nil
}
-func checkRouteExist(ctx context.Context, routeStore *store.GenericStore,
route *entity.Route) error {
- //routeStore := store.GetStore(store.HubKeyRoute)
- ret, err := routeStore.List(ctx, store.ListInput{
- Predicate: func(obj interface{}) bool {
- id := utils.InterfaceToString(route.ID)
- item := obj.(*entity.Route)
- if id != "" && id != utils.InterfaceToString(item.ID) {
- return false
- }
-
- itemUris := item.Uris
- if item.URI != "" {
- if itemUris == nil {
- itemUris = []string{item.URI}
- } else {
- itemUris = append(itemUris, item.URI)
- }
- }
-
- routeUris := route.Uris
- if route.URI != "" {
- if routeUris == nil {
- routeUris = []string{route.URI}
- } else {
- routeUris = append(routeUris, route.URI)
+// Pre-check imported data for duplicates
+// The main problem facing duplication is routing, so here
+// we mainly check the duplication of routes, based on
+// domain name and uri.
+func (h *ImportHandler) preCheck(ctx context.Context, data *loader.DataSets)
map[store.HubKey][]string {
+ errs := make(map[store.HubKey][]string)
+ for _, route := range data.Routes {
+ errs[store.HubKeyRoute] = make([]string, 0)
+ o, err := h.routeStore.List(ctx, store.ListInput{
+ // The check logic here is that if when a duplicate
HOST or URI
+ // has been found, the HTTP method is checked for
overlap, and
+ // if there is overlap it is determined to be a
duplicate route
+ // and the import is rejected.
+ Predicate: func(obj interface{}) bool {
+ r := obj.(*entity.Route)
+
+ // Check URI and host duplication
+ isURIDuplicated := r.URI != "" && route.URI !=
"" && r.URI == route.URI
+ isURIsDuplicated := len(r.Uris) > 0 &&
len(route.Uris) > 0 &&
+ len(intersect.Hash(r.Uris, route.Uris))
> 0
+ isMethodDuplicated :=
len(intersect.Hash(r.Methods, route.Methods)) > 0
+
+ // First check for duplicate URIs
+ if isURIDuplicated || isURIsDuplicated {
+ // Then check if the host field exists,
and if it does, check for duplicates
+ if r.Host != "" && route.Host != "" {
+ return r.Host == route.Host &&
isMethodDuplicated
+ } else if len(r.Hosts) > 0 &&
len(route.Hosts) > 0 {
+ return
len(intersect.Hash(r.Hosts, route.Hosts)) > 0 && isMethodDuplicated
+ }
+ // If the host field does not exist,
only the presence or absence
+ // of HTTP method duplication is
returned by default.
+ return isMethodDuplicated
}
- }
-
- if !(item.Host == route.Host &&
utils.StringSliceContains(itemUris, routeUris) &&
- utils.StringSliceEqual(item.RemoteAddrs,
route.RemoteAddrs) && item.RemoteAddr == route.RemoteAddr &&
- utils.StringSliceEqual(item.Hosts, route.Hosts)
&& item.Priority == route.Priority &&
- utils.ValueEqual(item.Vars, route.Vars) &&
item.FilterFunc == route.FilterFunc) {
return false
+ },
+ PageSize: 0,
+ PageNumber: 0,
+ })
+ if err != nil {
+ // When a special storage layer error occurs, return
directly.
+ return map[store.HubKey][]string{
+ store.HubKeyRoute: {err.Error()},
}
- return true
- },
- PageSize: 0,
- PageNumber: 0,
- })
- if err != nil {
- return err
- }
- if len(ret.Rows) > 0 {
- return consts.InvalidParam("route is duplicate")
- }
- return nil
-}
-
-func parseExtension(val *openapi3.Operation) (*entity.Route, error) {
- routeMap := map[string]interface{}{}
- for key, val := range val.Extensions {
- if strings.HasPrefix(key, "x-apisix-") {
- routeMap[strings.TrimPrefix(key, "x-apisix-")] = val
}
- }
-
- route := new(entity.Route)
- routeJson, err := json.Marshal(routeMap)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(routeJson, &route)
- if err != nil {
- return nil, err
- }
-
- return route, nil
-}
-
-type PathValue struct {
- Method string
- Value *openapi3.Operation
-}
-
-func mergePathValue(key string, values []PathValue, swagger *openapi3.Swagger)
(map[string]*entity.Route, error) {
- var parsed []PathValue
- var routes = map[string]*entity.Route{}
- for _, value := range values {
- value.Value.OperationID =
strings.Replace(value.Value.OperationID, value.Method, "", 1)
- var eq = false
- for _, v := range parsed {
- if utils.ValueEqual(v.Value, value.Value) {
- eq = true
- if routes[v.Method].Methods == nil {
- routes[v.Method].Methods = []string{}
+ // Duplicate routes found
+ if o.TotalSize > 0 {
+ for _, row := range o.Rows {
+ r, ok := row.(*entity.Route)
+ if ok {
+ errs[store.HubKeyRoute] =
append(errs[store.HubKeyRoute],
+ errors.Errorf("%s is duplicated
with route %s",
+ route.Uris[0],
+ r.Name).
+ Error())
}
- routes[v.Method].Methods =
append(routes[v.Method].Methods, value.Method)
- }
- }
- // not equal to the previous ones
- if !eq {
- route, err := getRouteFromPaths(value.Method, key,
value.Value, swagger)
- if err != nil {
- return nil, err
}
- routes[value.Method] = route
- parsed = append(parsed, value)
}
}
- return routes, nil
+ return errs
}
-func OpenAPI3ToRoute(swagger *openapi3.Swagger) ([]*entity.Route, error) {
- var routes []*entity.Route
- paths := swagger.Paths
- var upstream *entity.UpstreamDef
- var err error
- for k, v := range paths {
- k = regPathRepeat.ReplaceAllString(k, "")
- upstream = &entity.UpstreamDef{}
- if up, ok := v.Extensions["x-apisix-upstream"]; ok {
- err = json.Unmarshal(up.(json.RawMessage), upstream)
- if err != nil {
- return nil, err
- }
- }
+// Create parsed resources
+func (h *ImportHandler) createEntities(ctx context.Context, data
*loader.DataSets) map[store.HubKey][]string {
+ errs := make(map[store.HubKey][]string)
- var values []PathValue
- if v.Get != nil {
- value := PathValue{
- Method: http.MethodGet,
- Value: v.Get,
- }
- values = append(values, value)
- }
- if v.Post != nil {
- value := PathValue{
- Method: http.MethodPost,
- Value: v.Post,
- }
- values = append(values, value)
- }
- if v.Head != nil {
- value := PathValue{
- Method: http.MethodHead,
- Value: v.Head,
- }
- values = append(values, value)
- }
- if v.Put != nil {
- value := PathValue{
- Method: http.MethodPut,
- Value: v.Put,
- }
- values = append(values, value)
- }
- if v.Patch != nil {
- value := PathValue{
- Method: http.MethodPatch,
- Value: v.Patch,
- }
- values = append(values, value)
- }
- if v.Delete != nil {
- value := PathValue{
- Method: http.MethodDelete,
- Value: v.Delete,
- }
- values = append(values, value)
- }
-
- // merge same route
- tmp, err := mergePathValue(k, values, swagger)
+ for _, route := range data.Routes {
+ _, err := h.routeStore.Create(ctx, &route)
if err != nil {
- return nil, err
- }
-
- for _, route := range tmp {
- routes = append(routes, route)
+ errs[store.HubKeyRoute] =
append(errs[store.HubKeyRoute], err.Error())
}
}
-
- return routes, nil
-}
-
-func parseParameters(parameters openapi3.Parameters, plugins
map[string]interface{}) {
- props := make(map[string]interface{})
- var required []string
- for _, v := range parameters {
- if v.Value.Schema != nil {
- v.Value.Schema.Value.Format = ""
- v.Value.Schema.Value.XML = nil
- }
-
- switch v.Value.In {
- case "header":
- if v.Value.Schema != nil && v.Value.Schema.Value != nil
{
- props[v.Value.Name] = v.Value.Schema.Value
- }
- if v.Value.Required {
- required = append(required, v.Value.Name)
- }
+ for _, upstream := range data.Upstreams {
+ _, err := h.upstreamStore.Create(ctx, &upstream)
+ if err != nil {
+ errs[store.HubKeyUpstream] =
append(errs[store.HubKeyUpstream], err.Error())
}
}
-
- requestValidation := make(map[string]interface{})
- if rv, ok := plugins["request-validation"]; ok {
- requestValidation = rv.(map[string]interface{})
- }
- requestValidation["header_schema"] = &entity.RequestValidation{
- Type: "object",
- Required: required,
- Properties: props,
- }
- plugins["request-validation"] = requestValidation
-}
-
-func parseRequestBody(requestBody *openapi3.RequestBodyRef, swagger
*openapi3.Swagger, plugins map[string]interface{}) {
- schema := requestBody.Value.Content
- requestValidation := make(map[string]interface{})
- if rv, ok := plugins["request-validation"]; ok {
- requestValidation = rv.(map[string]interface{})
- }
- for _, v := range schema {
- if v.Schema.Ref != "" {
- s := getParameters(v.Schema.Ref,
&swagger.Components).Value
- requestValidation["body_schema"] =
&entity.RequestValidation{
- Type: s.Type,
- Required: s.Required,
- Properties: s.Properties,
- }
- plugins["request-validation"] = requestValidation
- } else if v.Schema.Value != nil {
- if v.Schema.Value.Properties != nil {
- for k1, v1 := range v.Schema.Value.Properties {
- if v1.Ref != "" {
- s := getParameters(v1.Ref,
&swagger.Components)
- v.Schema.Value.Properties[k1] =
s
- }
- v1.Value.Format = ""
- }
- requestValidation["body_schema"] =
&entity.RequestValidation{
- Type: v.Schema.Value.Type,
- Required: v.Schema.Value.Required,
- Properties: v.Schema.Value.Properties,
- }
- plugins["request-validation"] =
requestValidation
- } else if v.Schema.Value.Items != nil {
- if v.Schema.Value.Items.Ref != "" {
- s :=
getParameters(v.Schema.Value.Items.Ref, &swagger.Components).Value
- requestValidation["body_schema"] =
&entity.RequestValidation{
- Type: s.Type,
- Required: s.Required,
- Properties: s.Properties,
- }
- plugins["request-validation"] =
requestValidation
- }
- } else {
- requestValidation["body_schema"] =
&entity.RequestValidation{
- Type: "object",
- Required: []string{},
- Properties: v.Schema.Value.Properties,
- }
- }
+ for _, service := range data.Services {
+ _, err := h.serviceStore.Create(ctx, &service)
+ if err != nil {
+ errs[store.HubKeyService] =
append(errs[store.HubKeyService], err.Error())
}
- plugins["request-validation"] = requestValidation
}
-}
-
-func parseSecurity(security openapi3.SecurityRequirements, securitySchemes
openapi3.SecuritySchemes, plugins map[string]interface{}) {
- // todo: import consumers
- for _, securities := range security {
- for name := range securities {
- if schema, ok := securitySchemes[name]; ok {
- value := schema.Value
- if value == nil {
- continue
- }
-
- // basic auth
- if value.Type == "http" && value.Scheme ==
"basic" {
- plugins["basic-auth"] =
map[string]interface{}{}
- //username, ok :=
value.Extensions["username"]
- //if !ok {
- // continue
- //}
- //password, ok :=
value.Extensions["password"]
- //if !ok {
- // continue
- //}
- //plugins["basic-auth"] =
map[string]interface{}{
- // "username": username,
- // "password": password,
- //}
- // jwt auth
- } else if value.Type == "http" && value.Scheme
== "bearer" && value.BearerFormat == "JWT" {
- plugins["jwt-auth"] =
map[string]interface{}{}
- //key, ok := value.Extensions["key"]
- //if !ok {
- // continue
- //}
- //secret, ok :=
value.Extensions["secret"]
- //if !ok {
- // continue
- //}
- //plugins["jwt-auth"] =
map[string]interface{}{
- // "key": key,
- // "secret": secret,
- //}
- // key auth
- } else if value.Type == "apiKey" {
- plugins["key-auth"] =
map[string]interface{}{}
- //key, ok := value.Extensions["key"]
- //if !ok {
- // continue
- //}
- //plugins["key-auth"] =
map[string]interface{}{
- // "key": key,
- //}
- }
- }
+ for _, consumer := range data.Consumers {
+ _, err := h.consumerStore.Create(ctx, &consumer)
+ if err != nil {
+ errs[store.HubKeyConsumer] =
append(errs[store.HubKeyConsumer], err.Error())
}
}
-}
-
-func getRouteFromPaths(method, key string, value *openapi3.Operation, swagger
*openapi3.Swagger) (*entity.Route, error) {
- // transform /path/{var} to /path/*
- foundStr := regPathVar.FindString(key)
- if foundStr != "" {
- key = strings.Split(key, foundStr)[0] + "*"
- }
-
- route, err := parseExtension(value)
- if err != nil {
- return nil, err
+ for _, ssl := range data.SSLs {
+ _, err := h.sslStore.Create(ctx, &ssl)
+ if err != nil {
+ errs[store.HubKeySsl] = append(errs[store.HubKeySsl],
err.Error())
+ }
}
-
- route.Uris = []string{key}
- route.Name = value.OperationID
- route.Desc = value.Summary
- route.Methods = []string{method}
-
- if route.Plugins == nil {
- route.Plugins = make(map[string]interface{})
+ for _, route := range data.StreamRoutes {
+ _, err := h.streamRouteStore.Create(ctx, &route)
+ if err != nil {
+ errs[store.HubKeyStreamRoute] =
append(errs[store.HubKeyStreamRoute], err.Error())
+ }
}
-
- if value.Parameters != nil {
- parseParameters(value.Parameters, route.Plugins)
+ for _, plugin := range data.GlobalPlugins {
+ _, err := h.globalPluginStore.Create(ctx, &plugin)
+ if err != nil {
+ errs[store.HubKeyGlobalRule] =
append(errs[store.HubKeyGlobalRule], err.Error())
+ }
}
-
- if value.RequestBody != nil {
- parseRequestBody(value.RequestBody, swagger, route.Plugins)
+ for _, config := range data.PluginConfigs {
+ _, err := h.pluginConfigStore.Create(ctx, &config)
+ if err != nil {
+ errs[store.HubKeyPluginConfig] =
append(errs[store.HubKeyPluginConfig], err.Error())
+ }
}
-
- if value.Security != nil && swagger.Components.SecuritySchemes != nil {
- parseSecurity(*value.Security,
swagger.Components.SecuritySchemes, route.Plugins)
+ for _, proto := range data.Protos {
+ _, err := h.protoStore.Create(ctx, &proto)
+ if err != nil {
+ errs[store.HubKeyProto] =
append(errs[store.HubKeyProto], err.Error())
+ }
}
- return route, nil
+ return errs
}
-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 = ""
- }
- }
+// Convert import errors to response result
+func (ImportHandler) convertToImportResult(data *loader.DataSets, errs
map[store.HubKey][]string) map[store.HubKey]ImportResult {
+ return map[store.HubKey]ImportResult{
+ store.HubKeyRoute: {
+ Total: len(data.Routes),
+ Failed: len(errs[store.HubKeyRoute]),
+ Errors: errs[store.HubKeyRoute],
+ },
+ store.HubKeyUpstream: {
+ Total: len(data.Upstreams),
+ Failed: len(errs[store.HubKeyUpstream]),
+ Errors: errs[store.HubKeyUpstream],
+ },
+ store.HubKeyService: {
+ Total: len(data.Services),
+ Failed: len(errs[store.HubKeyService]),
+ Errors: errs[store.HubKeyService],
+ },
+ store.HubKeyConsumer: {
+ Total: len(data.Consumers),
+ Failed: len(errs[store.HubKeyConsumer]),
+ Errors: errs[store.HubKeyConsumer],
+ },
+ store.HubKeySsl: {
+ Total: len(data.SSLs),
+ Failed: len(errs[store.HubKeySsl]),
+ Errors: errs[store.HubKeySsl],
+ },
+ store.HubKeyStreamRoute: {
+ Total: len(data.StreamRoutes),
+ Failed: len(errs[store.HubKeyStreamRoute]),
+ Errors: errs[store.HubKeyStreamRoute],
+ },
+ store.HubKeyGlobalRule: {
+ Total: len(data.GlobalPlugins),
+ Failed: len(errs[store.HubKeyGlobalRule]),
+ Errors: errs[store.HubKeyGlobalRule],
+ },
+ store.HubKeyPluginConfig: {
+ Total: len(data.PluginConfigs),
+ Failed: len(errs[store.HubKeyPluginConfig]),
+ Errors: errs[store.HubKeyPluginConfig],
+ },
+ store.HubKeyProto: {
+ Total: len(data.Protos),
+ Failed: len(errs[store.HubKeyProto]),
+ Errors: errs[store.HubKeyProto],
+ },
}
- return schemaRef
}
diff --git a/api/internal/handler/data_loader/route_import_test.go
b/api/internal/handler/data_loader/route_import_test.go
index 437bce3c..548ef1cb 100644
--- a/api/internal/handler/data_loader/route_import_test.go
+++ b/api/internal/handler/data_loader/route_import_test.go
@@ -17,57 +17,16 @@
package data_loader
import (
- "bytes"
- "errors"
- "io/ioutil"
- "mime/multipart"
- "net/http"
- "os"
- "path/filepath"
- "runtime"
- "strings"
"testing"
- "github.com/shiningrush/droplet/data"
-
"github.com/shiningrush/droplet"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
-
- "github.com/apisix/manager-api/internal/core/store"
)
-type testFile struct {
- FieldName string
- FileName string
- Content []byte
-}
-
-func createRequestMultipartFiles(t *testing.T, files ...testFile)
*http.Request {
- var body bytes.Buffer
-
- mw := multipart.NewWriter(&body)
- for _, file := range files {
- fw, err := mw.CreateFormFile(file.FieldName, file.FileName)
- assert.NoError(t, err)
-
- n, err := fw.Write(file.Content)
- assert.NoError(t, err)
- assert.Equal(t, len(file.Content), n)
- }
- err := mw.Close()
- assert.NoError(t, err)
-
- req, err := http.NewRequest("POST", "/", &body)
- assert.NoError(t, err)
-
- req.Header.Set("Content-Type", "multipart/form-data;
boundary="+mw.Boundary())
- return req
-}
-
-func TestImport_invalid_file_type(t *testing.T) {
+func TestImport_invalid_loader(t *testing.T) {
input := &ImportInput{}
- input.FileName = "file1.txt"
+ input.Type = "test"
+ input.FileName = "file1.yaml"
input.FileContent = []byte("hello")
h := ImportHandler{}
@@ -75,110 +34,32 @@ func TestImport_invalid_file_type(t *testing.T) {
ctx.SetInput(input)
_, err := h.Import(ctx)
- assert.EqualError(t, err, "required file type is .yaml, .yml or .json
but got: .txt")
+ assert.EqualError(t, err, "unsupported data loader type: test")
}
-func TestImport_invalid_content(t *testing.T) {
+func TestImport_openapi3_invalid_file_type(t *testing.T) {
input := &ImportInput{}
- input.FileName = "file1.json"
- input.FileContent = []byte(`{"test": "a"}`)
+ input.FileName = "file1.txt"
+ input.FileContent = []byte("hello")
h := ImportHandler{}
ctx := droplet.NewContext()
ctx.SetInput(input)
_, err := h.Import(ctx)
- assert.EqualError(t, err, "empty or invalid imported file")
-}
-
-func ReadFile(t *testing.T, filePath string) []byte {
- pwd, err := os.Getwd()
- assert.Nil(t, err)
-
- bound := "/api/"
- if runtime.GOOS == "windows" {
- bound = `\api\`
- }
- apiDir := filepath.Join(strings.Split(pwd, bound)[0], bound)
- fileContent, err := ioutil.ReadFile(filepath.Join(apiDir, filePath))
- assert.Nil(t, err)
-
- return fileContent
-}
-
-func TestImport_with_service_id(t *testing.T) {
- fileContent := ReadFile(t, "test/testdata/import/with-service-id.yaml")
- input := &ImportInput{}
- input.FileName = "file1.json"
- input.FileContent = fileContent
-
- mStore := &store.MockInterface{}
- mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
- }).Return(nil, errors.New("data not found by key: service1"))
-
- h := ImportHandler{
- routeStore: &store.GenericStore{},
- svcStore: mStore,
- upstreamStore: mStore,
- }
- ctx := droplet.NewContext()
- ctx.SetInput(input)
-
- _, err := h.Import(ctx)
- assert.EqualError(t, err, "data not found by key: service1")
-
- //
- mStore = &store.MockInterface{}
- mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
- }).Return(nil, data.ErrNotFound)
-
- h = ImportHandler{
- routeStore: &store.GenericStore{},
- svcStore: mStore,
- upstreamStore: mStore,
- }
- ctx = droplet.NewContext()
- ctx.SetInput(input)
-
- _, err = h.Import(ctx)
- assert.EqualError(t, err, "service id: service1 not found")
+ assert.EqualError(t, err, "required file type is .yaml, .yml or .json
but got: .txt")
}
-func TestImport_with_upstream_id(t *testing.T) {
- fileContent := ReadFile(t, "test/testdata/import/with-upstream-id.yaml")
+func TestImport_openapi3_invalid_content(t *testing.T) {
input := &ImportInput{}
+ input.Type = "openapi3"
input.FileName = "file1.json"
- input.FileContent = fileContent
-
- mStore := &store.MockInterface{}
- mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
- }).Return(nil, errors.New("data not found by key: upstream1"))
+ input.FileContent = []byte(`{"test": "a"}`)
- h := ImportHandler{
- routeStore: &store.GenericStore{},
- svcStore: mStore,
- upstreamStore: mStore,
- }
+ h := ImportHandler{}
ctx := droplet.NewContext()
ctx.SetInput(input)
_, err := h.Import(ctx)
- assert.EqualError(t, err, "data not found by key: upstream1")
-
- //
- mStore = &store.MockInterface{}
- mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) {
- }).Return(nil, data.ErrNotFound)
-
- h = ImportHandler{
- routeStore: &store.GenericStore{},
- svcStore: mStore,
- upstreamStore: mStore,
- }
- ctx = droplet.NewContext()
- ctx.SetInput(input)
-
- _, err = h.Import(ctx)
- assert.EqualError(t, err, "upstream id: upstream1 not found")
-
+ assert.EqualError(t, err, "empty or invalid imported file: OpenAPI
documentation does not contain any paths")
}
diff --git a/api/test/e2e/data_loader/data_loader_suite_test.go
b/api/test/e2e/data_loader/data_loader_suite_test.go
new file mode 100644
index 00000000..4489e7c0
--- /dev/null
+++ b/api/test/e2e/data_loader/data_loader_suite_test.go
@@ -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.
+ */
+package data_loader_test
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "github.com/apisix/manager-api/test/e2e/base"
+)
+
+func TestDataLoader(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Data Loader Suite")
+}
+
+var _ = AfterSuite(func() {
+ base.CleanResource("routes")
+ base.CleanResource("upstreams")
+ base.CleanResource("services")
+ time.Sleep(base.SleepTime)
+})
diff --git a/api/test/e2e/data_loader/openapi3_test.go
b/api/test/e2e/data_loader/openapi3_test.go
new file mode 100644
index 00000000..ba19cf9c
--- /dev/null
+++ b/api/test/e2e/data_loader/openapi3_test.go
@@ -0,0 +1,322 @@
+/*
+ * 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_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "path/filepath"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/ginkgo/extensions/table"
+ . "github.com/onsi/gomega"
+ "github.com/savsgio/gotils/bytes"
+ "github.com/tidwall/gjson"
+
+ "github.com/apisix/manager-api/test/e2e/base"
+)
+
+var _ = Describe("OpenAPI 3", func() {
+ DescribeTable("Import cases",
+ func(f func()) {
+ f()
+ },
+ Entry("default.yaml", func() {
+ path, err :=
filepath.Abs("../../testdata/import/default.yaml")
+ Expect(err).To(BeNil())
+
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "test_default_yaml",
+ "_file": "default.yaml",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
+ r = r.Get("data")
+ for s, result := range r.Map() {
+ if s == "route" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(1)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ }
+ }),
+ Entry("default.json", func() {
+ path, err :=
filepath.Abs("../../testdata/import/default.json")
+ Expect(err).To(BeNil())
+
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "test_default_json",
+ "_file": "default.json",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
+ r = r.Get("data")
+ for s, result := range r.Map() {
+ if s == "route" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(1)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ }
+ }),
+ Entry("Postman-API101.yaml merge method", func() {
+ path, err :=
filepath.Abs("../../testdata/import/Postman-API101.yaml")
+ Expect(err).To(BeNil())
+
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "test_postman_api101_yaml_mm",
+ "_file": "Postman-API101.yaml",
+ "merge_method": "true",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
+ r = r.Get("data")
+ for s, result := range r.Map() {
+ if s == "route" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(3)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ if s == "upstream" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(1)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ }
+ }),
+ Entry("Postman-API101.yaml non-merge method", func() {
+ // clean routes
+ base.CleanResource("routes")
+ path, err :=
filepath.Abs("../../testdata/import/Postman-API101.yaml")
+ Expect(err).To(BeNil())
+
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "test_postman_api101_yaml_nmm",
+ "_file": "Postman-API101.yaml",
+ "merge_method": "false",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
+ r = r.Get("data")
+ for s, result := range r.Map() {
+ if s == "route" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(5)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ if s == "upstream" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(1)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ }
+ }),
+ Entry("Clean resources", func() {
+ base.CleanResource("routes")
+ base.CleanResource("upstreams")
+ base.CleanResource("services")
+ }),
+ )
+ DescribeTable("Exception cases",
+ func(f func()) {
+ f()
+ },
+ Entry("Empty upload file", func() {
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "empty_upload",
+ "_file": "default.yaml",
+ })
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(10000)))
+ Expect(r.Get("message").String()).To(Equal("uploaded
file is empty"))
+ }),
+ Entry("Exceed limit upload file", func() {
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "exceed_limit_upload",
+ "_file": "default.yaml",
+ })
+
+ req.WithMultipart().WithFileBytes("file",
"default.yaml", bytes.Rand(make([]byte, 10*1024*1024+1)))
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(10000)))
+ Expect(r.Get("message").String()).To(Equal("uploaded
file size exceeds the limit, limit is 10485760"))
+ }),
+ Entry("Routes duplicate #1", func() {
+ path, err :=
filepath.Abs("../../testdata/import/Postman-API101.yaml")
+ Expect(err).To(BeNil())
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "duplicate",
+ "_file": "Postman-API101.yaml",
+ "merge_method": "true",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+ }),
+ Entry("Route duplicate #2", func() {
+ path, err :=
filepath.Abs("../../testdata/import/Postman-API101.yaml")
+ Expect(err).To(BeNil())
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "duplicate",
+ "_file": "Postman-API101.yaml",
+ "merge_method": "true",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
Expect(r.Get("data").Map()["route"].Get("failed").Uint()).To(Equal(uint64(1)))
+
Expect(r.Get("data").Map()["route"].Get("errors").Array()[0].String()).
+ To(ContainSubstring("is duplicated with route
duplicate_"))
+
+ }),
+ Entry("Clean resources", func() {
+ base.CleanResource("routes")
+ base.CleanResource("upstreams")
+ base.CleanResource("services")
+ }),
+ )
+ DescribeTable("Real API cases",
+ func(f func()) {
+ f()
+ },
+ Entry("Import httpbin.org YAML", func() {
+ path, err :=
filepath.Abs("../../testdata/import/httpbin.yaml")
+ Expect(err).To(BeNil())
+
+ req :=
base.ManagerApiExpect().POST("/apisix/admin/import/routes")
+ req.WithMultipart().WithForm(map[string]string{
+ "type": "openapi3",
+ "task_name": "httpbin",
+ "_file": "httpbin.yaml",
+ })
+ req.WithMultipart().WithFile("file", path)
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+
+ r = r.Get("data")
+ for s, result := range r.Map() {
+ if s == "route" {
+
Expect(result.Get("total").Uint()).To(Equal(uint64(1)))
+
Expect(result.Get("failed").Uint()).To(Equal(uint64(0)))
+ }
+ }
+ }),
+ Entry("Modify upstream", func() {
+ body := make(map[string]interface{})
+ body["nodes"] = []map[string]interface{}{
+ {
+ "host": "httpbin.org",
+ "port": 80,
+ "weight": 1,
+ },
+ }
+ body["type"] = "roundrobin"
+ _body, err := json.Marshal(body)
+ Expect(err).To(BeNil())
+ base.RunTestCase(base.HttpTestCase{
+ Object: base.ManagerApiExpect(),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/upstreams/httpbin",
+ Body: string(_body),
+ Headers:
map[string]string{"Authorization": base.GetToken()},
+ ExpectStatus: http.StatusOK,
+ })
+ }),
+ Entry("Enable route", func() {
+ // get route id
+ req :=
base.ManagerApiExpect().GET("/apisix/admin/routes")
+ req.WithHeader("Authorization", base.GetToken())
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+ Expect(r.Get("code").Uint()).To(Equal(uint64(0)))
+ id :=
r.Get("data").Get("rows").Array()[0].Get("id").String()
+
+ body := make(map[string]interface{})
+ body["status"] = 1
+ _body, err := json.Marshal(body)
+ Expect(err).To(BeNil())
+ base.RunTestCase(base.HttpTestCase{
+ Object: base.ManagerApiExpect(),
+ Method: http.MethodPatch,
+ Path: "/apisix/admin/routes/" + id,
+ Body: string(_body),
+ Headers:
map[string]string{"Authorization": base.GetToken()},
+ ExpectStatus: http.StatusOK,
+ })
+ }),
+ Entry("Request API", func() {
+ req := base.APISIXExpect().GET("/get")
+ resp := req.Expect()
+ resp.Status(http.StatusOK)
+ r := gjson.ParseBytes([]byte(resp.Body().Raw()))
+
Expect(r.Get("headers").Get("User-Agent").String()).To(Equal("Go-http-client/1.1"))
+ }),
+ )
+})
diff --git a/api/test/testdata/import/default.json
b/api/test/testdata/import/default.json
index 2df6f9dd..a9ebb82e 100644
--- a/api/test/testdata/import/default.json
+++ b/api/test/testdata/import/default.json
@@ -11,7 +11,7 @@
},
"paths": {
"/hello": {
- "get": {
+ "post": {
"x-api-limit": 20,
"description": "hello world.",
"operationId": "hello",
diff --git a/api/test/testdata/import/httpbin.yaml
b/api/test/testdata/import/httpbin.yaml
new file mode 100644
index 00000000..cdd392c8
--- /dev/null
+++ b/api/test/testdata/import/httpbin.yaml
@@ -0,0 +1,36 @@
+#
+# 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:
+ title: httpbin
+ version: 1.0.0
+servers:
+ - url: https://httpbin.org
+paths:
+ /get:
+ get:
+ tags:
+ - default
+ summary: GET request
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json: {}
diff --git a/web/cypress/integration/route/import_export_route.spec.js
b/web/cypress/integration/route/import_export_route.spec.js
deleted file mode 100644
index c27708e1..00000000
--- a/web/cypress/integration/route/import_export_route.spec.js
+++ /dev/null
@@ -1,208 +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.
- */
-/* eslint-disable no-undef */
-/* eslint-disable @typescript-eslint/no-invalid-this */
-/* eslint-disable @typescript-eslint/no-loop-func */
-
-import actionBarUS from '../../../src/components/ActionBar/locales/en-US';
-import componentLocaleUS from '../../../src/locales/en-US/component';
-import menuLocaleUS from '../../../src/locales/en-US/menu';
-import routeLocaleUS from '../../../src/pages/Route/locales/en-US';
-import yaml from 'js-yaml';
-
-context('import and export routes', () => {
- const selector = {
- name: '#name',
- description: '#desc',
- nodes_0_host: '#submitNodes_0_host',
- nodes_0_port: '#submitNodes_0_port',
- nodes_0_weight: '#submitNodes_0_weight',
- fileTypeRadio: '[type=radio]',
- deleteAlert: '.ant-modal-body',
- refresh: '.anticon-reload',
- notification: '.ant-notification-notice-message',
- notificationCloseIcon: '.ant-notification-close-icon',
- fileSelector: '[type=file]',
- notificationDesc: '.ant-notification-notice-description',
- };
- const data = {
- route_name_0: 'route_name_0',
- route_name_1: 'route_name_1',
- upstream_node0_host_0: '1.1.1.1',
- upstream_node0_host_1: '2.2.2.2',
- importErrorMsg: 'required file type is .yaml, .yml or .json but got: .txt',
- uploadRouteFiles: [
- '../../../api/test/testdata/import/default.json',
- '../../../api/test/testdata/import/default.yaml',
- 'import-error.txt',
- ],
- // Note: export file's name will be end of a timestamp
- jsonMask: 'cypress/downloads/*.json',
- yamlMask: 'cypress/downloads/*.yaml',
- port: '80',
- weight: 1,
- deleteRouteSuccess: 'Delete Route Successfully',
- };
-
- beforeEach(() => {
- cy.login();
-
- cy.fixture('selector.json').as('domSelector');
- cy.fixture('data.json').as('data');
- cy.fixture('export-route-dataset.json').as('exportFile');
- });
-
- it('should create route1 and route2', function () {
- cy.visit('/');
- // create two routes
- for (let i = 0; i < 2; i += 1) {
- cy.contains(menuLocaleUS['menu.routes']).click();
- cy.contains(componentLocaleUS['component.global.create']).click();
- // input name, click Next
- cy.contains('Next').click().click();
- cy.get(selector.name).type(data[`route_name_${i}`]);
- // FIXME: only GET in methods
- cy.get('#methods').click();
- for (let j = 0; j < 7; j += 1) {
- cy.get('#methods').type('{backspace}');
- }
- cy.get('#methods').type('GET');
- cy.get('#methods').type('{enter}');
-
- cy.contains(actionBarUS['component.actionbar.button.nextStep']).click();
- // input nodes_0_host, click Next
- cy.get(selector.nodes_0_host).type(data[`upstream_node0_host_${i}`]);
- cy.get(selector.nodes_0_port).type(data.port);
- cy.get(selector.nodes_0_weight).clear().type(data.weight);
- cy.contains(actionBarUS['component.actionbar.button.nextStep']).click();
- // do not config plugins, click Next
- cy.contains(actionBarUS['component.actionbar.button.nextStep']).click();
- // click submit to create route
- cy.contains(componentLocaleUS['component.global.submit']).click();
- // submit successfully
- cy.contains(
- `${componentLocaleUS['component.global.submit']}
${componentLocaleUS['component.status.success']}`,
- ).should('exist');
- }
- });
- it('should export route: route_name_0, route_name_1', function () {
- cy.visit('/');
- cy.contains('Route').click();
-
- // export button should be disabled without any route items selected
-
cy.contains(routeLocaleUS['page.route.button.exportOpenApi']).should('be.disabled');
- // popup confirm should not exit when click disabled export button
-
cy.contains(routeLocaleUS['page.route.exportRoutesTips']).should('not.exist');
-
- // export one route with type json
- cy.contains(data['route_name_0']).prev().click();
- // after select route item(s), export button should not be disabled
-
cy.contains(routeLocaleUS['page.route.button.exportOpenApi']).should('not.disabled');
- // click Export OpenAPI Button
- cy.contains(routeLocaleUS['page.route.button.exportOpenApi']).click();
- // after click enabled export button, popup confirm should appear
- cy.contains(routeLocaleUS['page.route.exportRoutesTips']).should('exist');
- // click Confirm button in the popup to download Json file(default option)
- cy.contains(componentLocaleUS['component.global.confirm']).click();
-
- // export 2 routes with type yaml
- cy.contains(data['route_name_1']).prev().click();
- cy.contains(routeLocaleUS['page.route.button.exportOpenApi']).click();
- cy.contains(routeLocaleUS['page.route.exportRoutesTips']).should('exist');
- // click Confirm button in the popup to download Yaml file
- cy.get(selector.fileTypeRadio).check('1');
- cy.contains(componentLocaleUS['component.global.confirm']).click();
-
- cy.task('findFile', data.jsonMask).then((jsonFile) => {
- cy.log(`found file ${jsonFile}`);
- cy.log('**confirm downloaded json file**');
- cy.readFile(jsonFile).then((fileContent) => {
- const json = fileContent;
- delete json['paths']['/{params}']['post']['x-apisix-id'];
-
expect(JSON.stringify(json)).to.equal(JSON.stringify(this.exportFile.jsonFile));
- });
- });
- cy.task('findFile', data.yamlMask).then((yamlFile) => {
- cy.log(`found file ${yamlFile}`);
- cy.log('**confirm downloaded yaml file**');
- cy.readFile(yamlFile).then((fileContent) => {
- const json = yaml.load(fileContent);
- delete json['paths']['/{params}']['post']['x-apisix-id'];
- delete
json['paths']['/{params}-APISIX-REPEAT-URI-2']['post']['x-apisix-id'];
- expect(JSON.stringify(json, null,
null)).to.equal(JSON.stringify(this.exportFile.yamlFile));
- });
- });
- });
-
- it('should delete the route', function () {
- cy.visit('/routes/list');
- cy.get(selector.refresh).click();
-
- for (let i = 0; i < 2; i += 1) {
- cy.contains(data[`route_name_${i}`]).siblings().contains('More').click();
- cy.contains('Delete').click();
- cy.get(selector.deleteAlert)
- .should('be.visible')
- .within(() => {
- cy.contains('OK').click();
- });
- cy.get(selector.notification).should('contain', data.deleteRouteSuccess);
- cy.get(selector.notificationCloseIcon).click().should('not.exist');
- cy.reload();
- }
- });
-
- it('should import route(s) from be test files', function () {
- cy.visit('/');
- cy.contains('Route').click();
-
- data.uploadRouteFiles.forEach((file) => {
- // click import button
- cy.get(selector.refresh).click();
- cy.contains('Advanced').click();
-
cy.contains(routeLocaleUS['page.route.button.importOpenApi']).should('be.visible').click();
- // select file
- cy.get(selector.fileSelector).attachFile(file);
- // click submit
- cy.contains(componentLocaleUS['component.global.confirm']).click();
-
- // show upload notification
- if (file === 'import-error.txt') {
- // show error msg
- cy.get(selector.notificationDesc).should('contain',
data.importErrorMsg);
- // close modal
- cy.contains(componentLocaleUS['component.global.cancel']).click();
- cy.get(selector.notificationCloseIcon).click();
- } else if (file !== 'import-error.txt') {
- cy.get(selector.notification).should('contain', 'Success');
- cy.get(selector.notificationCloseIcon).click().should('not.exist');
- // delete route just imported
- cy.reload();
- cy.contains('More').click();
- cy.contains('Delete').should('be.visible').click();
- cy.get(selector.deleteAlert)
- .should('be.visible')
- .within(() => {
- cy.contains('OK').click();
- });
- // show delete successfully notification
- cy.get(selector.notification).should('contain',
data.deleteRouteSuccess);
- cy.get(selector.notificationCloseIcon).click();
- }
- });
- });
-});
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index 89bf7083..4f516425 100755
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -163,6 +163,7 @@ const Page: React.FC = () => {
return;
}
formData.append('file', uploadFileList[0]);
+ formData.append('type', 'openapi3');
importRoutes(formData).then(() => {
handleTableActionSuccessResponse(