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(

Reply via email to