Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: OpenAPI 3 parse and convert #2460

Merged
merged 23 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.8.0 // indirect
github.com/satori/go.uuid v1.2.0
github.com/shiningrush/droplet v0.2.6-0.20210127040147-53817015cd1b
Expand Down
5 changes: 0 additions & 5 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down Expand Up @@ -358,9 +357,7 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
Expand Down Expand Up @@ -881,15 +878,13 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
Expand Down
24 changes: 24 additions & 0 deletions api/internal/handler/data_loader/loader/openapi3/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 openapi3

import "github.com/apisix/manager-api/internal/handler/data_loader/loader"

func (Loader) Export(data loader.DataSets) (interface{}, error) {
//TODO implement me
panic("implement me")
}
128 changes: 128 additions & 0 deletions api/internal/handler/data_loader/loader/openapi3/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 openapi3

import (
"fmt"
"reflect"
"strings"
"time"

"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"

"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/handler/data_loader/loader"
"github.com/apisix/manager-api/internal/utils/consts"
)

func (o Loader) Import(input interface{}) (*loader.DataSets, error) {
if input == nil {
panic("input is nil")
}

d, ok := input.([]byte)
if !ok {
panic(fmt.Sprintf("input format error: expected []byte but it is %s", reflect.TypeOf(input).Kind().String()))
}

// load OAS3 document
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(d)
if err != nil {
return nil, err
}

// no paths in OAS3 document
if len(swagger.Paths) <= 0 {
return nil, errors.Wrap(errors.New("OpenAPI documentation does not contain any paths"), consts.ErrImportFile.Error())
}

if o.TaskName == "" {
o.TaskName = "openapi_" + time.Now().Format("20060102150405")
}

data, err := o.convertToEntities(swagger)
if err != nil {
return nil, err
}

return data, nil
}

func (o Loader) convertToEntities(s *openapi3.Swagger) (*loader.DataSets, error) {
var (
// temporarily save the parsed data
data = &loader.DataSets{}
// global upstream ID
globalUpstreamID = o.TaskName
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the taskName as upstream ID ?
It looks strange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The taskname can be set to a service name, such as user or order, so that it will generate an upstream named order and generate a route name similar to order_user/list. I think this is clearer.

// global uri prefix
globalPath = ""
)

// create upstream when servers field not empty
if len(s.Servers) > 0 {
var upstream entity.Upstream
upstream = entity.Upstream{
BaseInfo: entity.BaseInfo{ID: globalUpstreamID},
UpstreamDef: entity.UpstreamDef{
Name: globalUpstreamID,
Type: "roundrobin",
},
}
data.Upstreams = append(data.Upstreams, upstream)
}

// each one will correspond to a route
for uri, v := range s.Paths {
// replace parameter in uri to wildcard
realUri := regURIVar.ReplaceAllString(uri, "*")
// generate route Name
routeName := o.TaskName + "_" + strings.TrimPrefix(uri, "/")
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can follow APISIX's schema to check resources.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tao12345666333 Yes, the ability to check the schema is already built into the storage layer, so no matter what APISIX resource we try to create, it will perform the schema check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I'm still not sure on the route name generation and can see from #2460 (comment), do you have any suggestions?


// decide whether to merge multi-method routes based on configuration
if o.MergeMethod {
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
// create a single route for each path, merge all methods
route := generateBaseRoute(routeName, v.Summary)
route.Uris = []string{globalPath + realUri}
route.UpstreamID = globalUpstreamID
for method := range v.Operations() {
route.Methods = append(route.Methods, strings.ToUpper(method))
}
data.Routes = append(data.Routes, route)
} else {
// create routes for each method of each path
for method, operation := range v.Operations() {
subRouteID := routeName + "_" + method
route := generateBaseRoute(subRouteID, operation.Summary)
route.Uris = []string{globalPath + realUri}
route.Methods = []string{strings.ToUpper(method)}
route.UpstreamID = globalUpstreamID
data.Routes = append(data.Routes, route)
}
}
}
return data, nil
}

// Generate a base route for customize
func generateBaseRoute(name string, desc string) entity.Route {
return entity.Route{
Name: name,
Desc: desc,
Plugins: make(map[string]interface{}),
}
}
119 changes: 119 additions & 0 deletions api/internal/handler/data_loader/loader/openapi3/import_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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 openapi3

import (
"io/ioutil"
"testing"

"github.com/stretchr/testify/assert"

"github.com/apisix/manager-api/internal/core/entity"
)

var (
TestAPI101 = "../../../../../test/testdata/import/Postman-API101.yaml"
nic-6443 marked this conversation as resolved.
Show resolved Hide resolved
)

// Test API 101 on no MergeMethod mode
func TestParseAPI101NoMerge(t *testing.T) {
fileContent, err := ioutil.ReadFile(TestAPI101)
assert.NoError(t, err)

l := &Loader{MergeMethod: false, TaskName: "test"}
data, err := l.Import(fileContent)
assert.NoError(t, err)

assert.Len(t, data.Routes, 5)
assert.Len(t, data.Upstreams, 1)

// Upstream
assert.Equal(t, "test", data.Upstreams[0].Name)
assert.Equal(t, "roundrobin", data.Upstreams[0].Type)

// Route
assert.Equal(t, data.Upstreams[0].ID, data.Routes[0].UpstreamID)
for _, route := range data.Routes {
switch route.Name {
case "test_customers_GET":
assert.Contains(t, route.Uris, "/customers")
assert.Contains(t, route.Methods, "GET")
assert.Equal(t, "Get all customers", route.Desc)
assert.Equal(t, entity.Status(0), route.Status)
case "test_customer_GET":
assert.Contains(t, route.Uris, "/customer")
assert.Contains(t, route.Methods, "GET")
assert.Equal(t, "Get one customer", route.Desc)
assert.Equal(t, entity.Status(0), route.Status)
case "test_customer_POST":
assert.Contains(t, route.Uris, "/customer")
assert.Contains(t, route.Methods, "POST")
assert.Equal(t, "Add new customer", route.Desc)
assert.Equal(t, entity.Status(0), route.Status)
case "test_customer/{customer_id}_PUT":
assert.Contains(t, route.Uris, "/customer/*")
assert.Contains(t, route.Methods, "PUT")
assert.Equal(t, "Update customer", route.Desc)
assert.Equal(t, entity.Status(0), route.Status)
case "test_customer/{customer_id}_DELETE":
assert.Contains(t, route.Uris, "/customer/*")
assert.Contains(t, route.Methods, "DELETE")
assert.Equal(t, "Remove customer", route.Desc)
assert.Equal(t, entity.Status(0), route.Status)
default:
t.Fatal("bad route name exist")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add the default arm and run t.Fail since a route with bad name exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default branch added

}
}

// Test API 101 on MergeMethod mode
func TestParseAPI101Merge(t *testing.T) {
fileContent, err := ioutil.ReadFile(TestAPI101)
assert.NoError(t, err)

l := &Loader{MergeMethod: true, TaskName: "test"}
data, err := l.Import(fileContent)
assert.NoError(t, err)

assert.Len(t, data.Routes, 3)
assert.Len(t, data.Upstreams, 1)

// Upstream
assert.Equal(t, "test", data.Upstreams[0].Name)
assert.Equal(t, "roundrobin", data.Upstreams[0].Type)

// Route
assert.Equal(t, data.Upstreams[0].ID, data.Routes[0].UpstreamID)
for _, route := range data.Routes {
switch route.Name {
case "test_customer":
assert.Contains(t, route.Uris, "/customer")
assert.Contains(t, route.Methods, "GET", "GET")
assert.Equal(t, entity.Status(0), route.Status)
case "test_customers":
assert.Contains(t, route.Uris, "/customers")
assert.Contains(t, route.Methods, "GET")
assert.Equal(t, entity.Status(0), route.Status)
case "test_customer/{customer_id}":
assert.Contains(t, route.Uris, "/customer/*")
assert.Contains(t, route.Methods, "PUT", "DELETE")
assert.Equal(t, entity.Status(0), route.Status)
default:
t.Fatal("bad route name exist")
}
}
}
41 changes: 41 additions & 0 deletions api/internal/handler/data_loader/loader/openapi3/openapi3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 openapi3

import (
"regexp"

"github.com/getkin/kin-openapi/openapi3"
)

type OpenAPISpecFileType string

type Loader struct {
// MergeMethod indicates whether to merge routes when multiple HTTP methods are on the same path
MergeMethod bool
// TaskName indicates the name of current import/export task
TaskName string
tokers marked this conversation as resolved.
Show resolved Hide resolved
}

type PathValue struct {
Method string
Value *openapi3.Operation
}

var (
regURIVar = regexp.MustCompile(`{.*?}`)
)
Loading