blob: aa8218cef37532a35050eb5ee363bd0c185bc2db [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package data_loader
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gin-gonic/gin"
"github.com/shiningrush/droplet"
"github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/wrapper"
wgin "github.com/shiningrush/droplet/wrapper/gin"
"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/core/store"
"github.com/apisix/manager-api/internal/handler"
"github.com/apisix/manager-api/internal/log"
"github.com/apisix/manager-api/internal/utils"
"github.com/apisix/manager-api/internal/utils/consts"
)
type Handler struct {
routeStore store.Interface
upstreamStore store.Interface
serviceStore store.Interface
consumerStore store.Interface
}
func NewHandler() (handler.RouteRegister, error) {
return &Handler{
routeStore: store.GetStore(store.HubKeyRoute),
upstreamStore: store.GetStore(store.HubKeyUpstream),
serviceStore: store.GetStore(store.HubKeyService),
consumerStore: store.GetStore(store.HubKeyConsumer),
}, nil
}
func (h *Handler) ApplyRoute(r *gin.Engine) {
r.GET("/apisix/admin/export/routes/:ids", wgin.Wraps(h.ExportRoutes,
wrapper.InputType(reflect.TypeOf(ExportInput{}))))
r.GET("/apisix/admin/export/routes", wgin.Wraps(h.ExportAllRoutes))
}
type ExportInput struct {
IDs string `auto_read:"ids,path"`
}
//ExportRoutes Export data by passing route ID, such as "R1" or multiple route parameters, such as "R1,R2"
func (h *Handler) ExportRoutes(c droplet.Context) (interface{}, error) {
input := c.Input().(*ExportInput)
if input.IDs == "" {
return nil, consts.ErrParameterID
}
ids := strings.Split(input.IDs, ",")
routes := []*entity.Route{}
for _, id := range ids {
route, err := h.routeStore.Get(c.Context(), id)
if err != nil {
if err == data.ErrNotFound {
return nil, fmt.Errorf(consts.IDNotFound, "upstream", id)
}
return nil, err
}
routes = append(routes, route.(*entity.Route))
}
swagger, err := h.RouteToOpenAPI3(c, routes)
if err != nil {
return nil, err
}
return swagger, nil
}
type AuthType string
const (
BasicAuth AuthType = "basic-auth"
KeyAuth AuthType = "key-auth"
JWTAuth AuthType = "jwt-auth"
)
var (
openApi = "3.0.0"
title = "RoutesExport"
service interface{}
err error
routeMethods []string
_allHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodHead, http.MethodConnect, http.MethodTrace, http.MethodOptions}
)
//ExportAllRoutes All routes can be directly exported without passing parameters
func (h *Handler) ExportAllRoutes(c droplet.Context) (interface{}, error) {
routelist, err := h.routeStore.List(c.Context(), store.ListInput{})
if len(routelist.Rows) < 1 {
return nil, consts.ErrRouteData
}
if err != nil {
return nil, err
}
routes := []*entity.Route{}
for _, route := range routelist.Rows {
routes = append(routes, route.(*entity.Route))
}
swagger, err := h.RouteToOpenAPI3(c, routes)
if err != nil {
return nil, err
}
return swagger, nil
}
//RouteToOpenAPI3 Pass in route list parameter: []*entity.Route, convert route data to openapi3 and export processing function
func (h *Handler) RouteToOpenAPI3(c droplet.Context, routes []*entity.Route) (*openapi3.Swagger, error) {
paths := openapi3.Paths{}
paramsRefs := []*openapi3.ParameterRef{}
requestBody := &openapi3.RequestBody{}
components := &openapi3.Components{}
secSchemas := openapi3.SecuritySchemes{}
_pathNumber := GetPathNumber()
for _, route := range routes {
extensions := make(map[string]interface{})
servicePlugins := make(map[string]interface{})
plugins := make(map[string]interface{})
serviceLabels := make(map[string]string)
pathItem := &openapi3.PathItem{}
path := openapi3.Operation{}
path.Summary = route.Desc
path.OperationID = route.Name
if route.ServiceID != nil {
serviceID := utils.InterfaceToString(route.ServiceID)
service, err = h.serviceStore.Get(c.Context(), serviceID)
if err != nil {
if err == data.ErrNotFound {
return nil, fmt.Errorf(consts.IDNotFound, "service", route.ServiceID)
}
return nil, err
}
_service := service.(*entity.Service)
servicePlugins = _service.Plugins
serviceLabels = _service.Labels
}
//Parse upstream
_upstream, err := h.ParseRouteUpstream(c, route)
if err != nil {
log.Errorf("ParseRouteUpstream err: ", err)
return nil, err
} else if _upstream != nil {
extensions["x-apisix-upstream"] = _upstream
}
if route.Host != "" {
extensions["x-apisix-host"] = route.Host
}
if route.Hosts != nil {
extensions["x-apisix-hosts"] = route.Hosts
}
//Parse Labels
labels, err := ParseLabels(route, serviceLabels)
if err != nil {
log.Errorf("parseLabels err: ", err)
return nil, err
}
if labels != nil {
extensions["x-apisix-labels"] = labels
}
if route.RemoteAddr != "" {
extensions["x-apisix-remote_addr"] = route.RemoteAddr
}
if route.RemoteAddrs != nil {
extensions["x-apisix-remote_addrs"] = route.RemoteAddrs
}
if route.FilterFunc != "" {
extensions["x-apisix-filter_func"] = route.FilterFunc
}
if route.Script != nil {
extensions["x-apisix-script"] = route.Script
}
if route.ServiceProtocol != "" {
extensions["x-apisix-service_protocol"] = route.ServiceProtocol
}
if route.Vars != nil {
extensions["x-apisix-vars"] = route.Vars
}
// Parse Route URIs
paths, paramsRefs = ParseRouteUris(route, paths, paramsRefs, pathItem, _pathNumber())
//Parse Route Plugins
path, secSchemas, paramsRefs, plugins, err = ParseRoutePlugins(route, paramsRefs, plugins, path, servicePlugins, secSchemas, requestBody)
if err != nil {
log.Errorf("parseRoutePlugins err: ", err)
return nil, err
}
if len(plugins) > 0 {
extensions["x-apisix-plugins"] = plugins
}
extensions["x-apisix-priority"] = route.Priority
extensions["x-apisix-status"] = route.Status
extensions["x-apisix-enable_websocket"] = route.EnableWebsocket
path.Extensions = extensions
path.Parameters = paramsRefs
path.RequestBody = &openapi3.RequestBodyRef{Value: requestBody}
path.Responses = openapi3.NewResponses()
if route.Methods != nil && len(route.Methods) > 0 {
routeMethods = route.Methods
} else {
routeMethods = _allHTTPMethods
}
for i := range routeMethods {
switch strings.ToUpper(routeMethods[i]) {
case http.MethodGet:
pathItem.Get = ParsePathItem(path, http.MethodGet)
case http.MethodPost:
pathItem.Post = ParsePathItem(path, http.MethodPost)
case http.MethodPut:
pathItem.Put = ParsePathItem(path, http.MethodPut)
case http.MethodDelete:
pathItem.Delete = ParsePathItem(path, http.MethodDelete)
case http.MethodPatch:
pathItem.Patch = ParsePathItem(path, http.MethodPatch)
case http.MethodHead:
pathItem.Head = ParsePathItem(path, http.MethodHead)
case http.MethodConnect:
pathItem.Connect = ParsePathItem(path, http.MethodConnect)
case http.MethodTrace:
pathItem.Trace = ParsePathItem(path, http.MethodTrace)
case http.MethodOptions:
pathItem.Options = ParsePathItem(path, http.MethodOptions)
}
}
}
components.SecuritySchemes = secSchemas
swagger := openapi3.Swagger{
OpenAPI: openApi,
Info: &openapi3.Info{Title: title, Version: openApi},
Paths: paths,
Components: *components,
}
return &swagger, nil
}
//ParseLabels When service and route have labels at the same time, use route's label.
//When route has no label, service sometimes uses service's label. This function is used to process this logic
func ParseLabels(route *entity.Route, serviceLabels map[string]string) (map[string]string, error) {
if route.Labels != nil {
return route.Labels, nil
} else if route.ServiceID != nil {
return serviceLabels, nil
}
return nil, nil
}
//ParsePathItem Convert data in route to openapi3
func ParsePathItem(path openapi3.Operation, routeMethod string) *openapi3.Operation {
_path := &openapi3.Operation{
ExtensionProps: path.ExtensionProps,
Tags: path.Tags,
Summary: path.Summary,
Description: path.Description,
OperationID: path.OperationID + routeMethod,
Parameters: path.Parameters,
RequestBody: path.RequestBody,
Responses: path.Responses,
Callbacks: path.Callbacks,
Deprecated: path.Deprecated,
Security: path.Security,
Servers: path.Servers,
ExternalDocs: path.ExternalDocs,
}
return _path
}
// ParseRoutePlugins Merge service with plugin in route
func ParseRoutePlugins(route *entity.Route, paramsRefs []*openapi3.ParameterRef, plugins map[string]interface{}, path openapi3.Operation, servicePlugins map[string]interface{}, secSchemas openapi3.SecuritySchemes, requestBody *openapi3.RequestBody) (openapi3.Operation, openapi3.SecuritySchemes, []*openapi3.ParameterRef, map[string]interface{}, error) {
if route.Plugins != nil {
param := &openapi3.Parameter{}
secReq := &openapi3.SecurityRequirements{}
// analysis plugins
for key, value := range route.Plugins {
// analysis request-validation plugin
if key == "request-validation" {
if valueMap, ok := value.(map[string]interface{}); ok {
if hsVal, ok := valueMap["header_schema"]; ok {
param.In = "header"
requestValidation := &entity.RequestValidation{}
reqBytes, _ := json.Marshal(&hsVal)
err := json.Unmarshal(reqBytes, requestValidation)
if err != nil {
log.Errorf("json marshal failed: %s", err)
}
for key1, value1 := range requestValidation.Properties.(map[string]interface{}) {
for _, arr := range requestValidation.Required {
if arr == key1 {
param.Required = true
}
}
param.Name = key1
typeStr := value1.(map[string]interface{})
schema := &openapi3.Schema{Type: typeStr["type"].(string)}
param.Schema = &openapi3.SchemaRef{Value: schema}
paramsRefs = append(paramsRefs, &openapi3.ParameterRef{Value: param})
}
}
if bsVal, ok := valueMap["body_schema"]; ok {
m := map[string]*openapi3.MediaType{}
reqBytes, _ := json.Marshal(&bsVal)
schema := &openapi3.Schema{}
err := json.Unmarshal(reqBytes, schema)
if err != nil {
log.Errorf("json marshal failed: %s", err)
}
// In the swagger format conversion, there are many cases of content type data format
// Such as (application/json, application/xml, text/xml) and more.
// There are many matching methods, such as equal, inclusive and so on.
// Therefore, the current processing method is to use "*/*" to match all
m["*/*"] = &openapi3.MediaType{Schema: &openapi3.SchemaRef{Value: schema}}
requestBody.Content = m
}
}
continue
}
// analysis security plugins
securityEnv := &openapi3.SecurityRequirement{}
switch key {
case string(KeyAuth):
secSchemas["api_key"] = &openapi3.SecuritySchemeRef{Value: openapi3.NewCSRFSecurityScheme()}
securityEnv.Authenticate("api_key", " ")
secReq.With(*securityEnv)
continue
case string(BasicAuth):
secSchemas["basicAuth"] = &openapi3.SecuritySchemeRef{Value: &openapi3.SecurityScheme{
Type: "basicAuth",
Name: "basicAuth",
In: "header",
}}
securityEnv.Authenticate("basicAuth", " ")
secReq.With(*securityEnv)
continue
case string(JWTAuth):
secSchemas["bearerAuth"] = &openapi3.SecuritySchemeRef{Value: openapi3.NewJWTSecurityScheme()}
securityEnv.Authenticate("bearerAuth", " ")
secReq.With(*securityEnv)
continue
}
plugins[key] = value
}
path.Security = secReq
if route.ServiceID != nil && servicePlugins != nil {
_servicePlugins, err := json.Marshal(servicePlugins)
if err != nil {
log.Errorf("MapToJson err: ", err)
return path, nil, nil, nil, err
}
_plugins, err := json.Marshal(plugins)
if err != nil {
log.Errorf("MapToJson err: ", err)
return path, nil, nil, nil, err
}
bytePlugins, err := utils.MergeJson(_servicePlugins, _plugins)
if err != nil {
log.Errorf("Plugins MergeJson err: ", err)
return path, nil, nil, nil, err
}
err = json.Unmarshal([]byte(bytePlugins), &plugins)
if err != nil {
log.Errorf("JsonToMapDemo err: ", err)
return path, nil, nil, nil, err
}
}
} else if route.Plugins == nil && route.ServiceID != nil {
plugins = servicePlugins
}
return path, secSchemas, paramsRefs, plugins, nil
}
// ParseRouteUris The URI and URIs of route are converted to paths URI in openapi3
func ParseRouteUris(route *entity.Route, paths openapi3.Paths, paramsRefs []*openapi3.ParameterRef, pathItem *openapi3.PathItem, _pathNumber int) (openapi3.Paths, []*openapi3.ParameterRef) {
routeURIs := []string{}
if route.URI != "" {
routeURIs = append(routeURIs, route.URI)
}
if route.Uris != nil {
routeURIs = route.Uris
}
for _, uri := range routeURIs {
if strings.Contains(uri, "*") {
if _, ok := paths[strings.Split(uri, "*")[0]+"{params}"]; !ok {
paths[strings.Split(uri, "*")[0]+"{params}"] = pathItem
} else {
paths[strings.Split(uri, "*")[0]+"{params}"+"-APISIX-REPEAT-URI-"+strconv.Itoa(_pathNumber)] = pathItem
}
// add params introduce
paramsRefs = append(paramsRefs, &openapi3.ParameterRef{
Value: &openapi3.Parameter{
In: "path",
Name: "params",
Required: true,
Description: "params in path",
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}}}})
} else {
if _, ok := paths[uri]; !ok {
paths[uri] = pathItem
} else {
paths[uri+"-APISIX-REPEAT-URI-"+strconv.Itoa(_pathNumber)] = pathItem
}
}
}
return paths, paramsRefs
}
// ParseRouteUpstream Processing the upstream in service and route
func (h *Handler) ParseRouteUpstream(c droplet.Context, route *entity.Route) (interface{}, error) {
// The upstream data of route has the highest priority.
// If there is one, it will be used directly.
// If there is no route, the upstream data of service will be used.
// If there is no route, the upstream data of service will not be used normally.
if route.Upstream != nil {
return route.Upstream, nil
} else if route.UpstreamID != nil && route.Upstream == nil {
upstreamID := utils.InterfaceToString(route.UpstreamID)
upstream, err := h.upstreamStore.Get(c.Context(), upstreamID)
if err != nil {
if err == data.ErrNotFound {
return nil, fmt.Errorf(consts.IDNotFound, "upstream", route.UpstreamID)
}
return nil, err
}
return upstream, nil
} else if route.UpstreamID == nil && route.Upstream == nil && route.ServiceID != nil {
_service := service.(*entity.Service)
if _service.Upstream != nil {
return _service.Upstream, nil
} else if _service.Upstream == nil && _service.UpstreamID != nil {
upstreamID := utils.InterfaceToString(_service.UpstreamID)
upstream, err := h.upstreamStore.Get(c.Context(), upstreamID)
if err != nil {
if err == data.ErrNotFound {
return nil, fmt.Errorf(consts.IDNotFound, "upstream", _service.UpstreamID)
}
return nil, err
}
return upstream, nil
}
}
return nil, nil
}
func GetPathNumber() func() int {
i := 0
return func() int {
i++
return i
}
}