mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-05 17:27:51 +00:00
Travis seems to be having issues pulling deps, so we'll have to check in the vendor directory and prevent the makefile from trying to regenerate it normally.
443 lines
13 KiB
Go
443 lines
13 KiB
Go
package swagger
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/emicklei/go-restful"
|
|
// "github.com/emicklei/hopwatch"
|
|
"net/http"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/emicklei/go-restful/log"
|
|
)
|
|
|
|
type SwaggerService struct {
|
|
config Config
|
|
apiDeclarationMap *ApiDeclarationList
|
|
}
|
|
|
|
func newSwaggerService(config Config) *SwaggerService {
|
|
sws := &SwaggerService{
|
|
config: config,
|
|
apiDeclarationMap: new(ApiDeclarationList)}
|
|
|
|
// Build all ApiDeclarations
|
|
for _, each := range config.WebServices {
|
|
rootPath := each.RootPath()
|
|
// skip the api service itself
|
|
if rootPath != config.ApiPath {
|
|
if rootPath == "" || rootPath == "/" {
|
|
// use routes
|
|
for _, route := range each.Routes() {
|
|
entry := staticPathFromRoute(route)
|
|
_, exists := sws.apiDeclarationMap.At(entry)
|
|
if !exists {
|
|
sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
|
|
}
|
|
}
|
|
} else { // use root path
|
|
sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// if specified then call the PostBuilderHandler
|
|
if config.PostBuildHandler != nil {
|
|
config.PostBuildHandler(sws.apiDeclarationMap)
|
|
}
|
|
return sws
|
|
}
|
|
|
|
// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
|
|
var LogInfo = func(format string, v ...interface{}) {
|
|
// use the restful package-wide logger
|
|
log.Printf(format, v...)
|
|
}
|
|
|
|
// InstallSwaggerService add the WebService that provides the API documentation of all services
|
|
// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
|
|
func InstallSwaggerService(aSwaggerConfig Config) {
|
|
RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer)
|
|
}
|
|
|
|
// RegisterSwaggerService add the WebService that provides the API documentation of all services
|
|
// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
|
|
func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
|
|
sws := newSwaggerService(config)
|
|
ws := new(restful.WebService)
|
|
ws.Path(config.ApiPath)
|
|
ws.Produces(restful.MIME_JSON)
|
|
if config.DisableCORS {
|
|
ws.Filter(enableCORS)
|
|
}
|
|
ws.Route(ws.GET("/").To(sws.getListing))
|
|
ws.Route(ws.GET("/{a}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations))
|
|
ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations))
|
|
LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
|
|
wsContainer.Add(ws)
|
|
|
|
// Check paths for UI serving
|
|
if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
|
|
swaggerPathSlash := config.SwaggerPath
|
|
// path must end with slash /
|
|
if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
|
|
LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)")
|
|
swaggerPathSlash += "/"
|
|
}
|
|
|
|
LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath)
|
|
wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath))))
|
|
|
|
//if we define a custom static handler use it
|
|
} else if config.StaticHandler != nil && config.SwaggerPath != "" {
|
|
swaggerPathSlash := config.SwaggerPath
|
|
// path must end with slash /
|
|
if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
|
|
LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)")
|
|
swaggerPathSlash += "/"
|
|
|
|
}
|
|
LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler)
|
|
wsContainer.Handle(swaggerPathSlash, config.StaticHandler)
|
|
|
|
} else {
|
|
LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served")
|
|
}
|
|
}
|
|
|
|
func staticPathFromRoute(r restful.Route) string {
|
|
static := r.Path
|
|
bracket := strings.Index(static, "{")
|
|
if bracket <= 1 { // result cannot be empty
|
|
return static
|
|
}
|
|
if bracket != -1 {
|
|
static = r.Path[:bracket]
|
|
}
|
|
if strings.HasSuffix(static, "/") {
|
|
return static[:len(static)-1]
|
|
} else {
|
|
return static
|
|
}
|
|
}
|
|
|
|
func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
|
if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" {
|
|
// prevent duplicate header
|
|
if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 {
|
|
resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin)
|
|
}
|
|
}
|
|
chain.ProcessFilter(req, resp)
|
|
}
|
|
|
|
func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
|
|
listing := sws.produceListing()
|
|
resp.WriteAsJson(listing)
|
|
}
|
|
|
|
func (sws SwaggerService) produceListing() ResourceListing {
|
|
listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
|
|
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
|
|
ref := Resource{Path: k}
|
|
if len(v.Apis) > 0 { // use description of first (could still be empty)
|
|
ref.Description = v.Apis[0].Description
|
|
}
|
|
listing.Apis = append(listing.Apis, ref)
|
|
})
|
|
return listing
|
|
}
|
|
|
|
func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
|
|
decl, ok := sws.produceDeclarations(composeRootPath(req))
|
|
if !ok {
|
|
resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
|
|
return
|
|
}
|
|
// unless WebServicesUrl is given
|
|
if len(sws.config.WebServicesUrl) == 0 {
|
|
// update base path from the actual request
|
|
// TODO how to detect https? assume http for now
|
|
var host string
|
|
// X-Forwarded-Host or Host or Request.Host
|
|
hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific?
|
|
if !ok || len(hostvalues) == 0 {
|
|
forwarded, ok := req.Request.Header["Host"] // without reverse-proxy
|
|
if !ok || len(forwarded) == 0 {
|
|
// fallback to Host field
|
|
host = req.Request.Host
|
|
} else {
|
|
host = forwarded[0]
|
|
}
|
|
} else {
|
|
host = hostvalues[0]
|
|
}
|
|
// inspect Referer for the scheme (http vs https)
|
|
scheme := "http"
|
|
if referer := req.Request.Header["Referer"]; len(referer) > 0 {
|
|
if strings.HasPrefix(referer[0], "https") {
|
|
scheme = "https"
|
|
}
|
|
}
|
|
decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
|
|
}
|
|
resp.WriteAsJson(decl)
|
|
}
|
|
|
|
func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
|
|
decls := map[string]ApiDeclaration{}
|
|
sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
|
|
decls[k] = v
|
|
})
|
|
return decls
|
|
}
|
|
|
|
func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
|
|
decl, ok := sws.apiDeclarationMap.At(route)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
decl.BasePath = sws.config.WebServicesUrl
|
|
return &decl, true
|
|
}
|
|
|
|
// composeDeclaration uses all routes and parameters to create a ApiDeclaration
|
|
func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
|
|
decl := ApiDeclaration{
|
|
SwaggerVersion: swaggerVersion,
|
|
BasePath: sws.config.WebServicesUrl,
|
|
ResourcePath: pathPrefix,
|
|
Models: ModelList{},
|
|
ApiVersion: ws.Version()}
|
|
|
|
// collect any path parameters
|
|
rootParams := []Parameter{}
|
|
for _, param := range ws.PathParameters() {
|
|
rootParams = append(rootParams, asSwaggerParameter(param.Data()))
|
|
}
|
|
// aggregate by path
|
|
pathToRoutes := newOrderedRouteMap()
|
|
for _, other := range ws.Routes() {
|
|
if strings.HasPrefix(other.Path, pathPrefix) {
|
|
if len(pathPrefix) > 1 && len(other.Path) > len(pathPrefix) && other.Path[len(pathPrefix)] != '/' {
|
|
continue
|
|
}
|
|
pathToRoutes.Add(other.Path, other)
|
|
}
|
|
}
|
|
pathToRoutes.Do(func(path string, routes []restful.Route) {
|
|
api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
|
|
voidString := "void"
|
|
for _, route := range routes {
|
|
operation := Operation{
|
|
Method: route.Method,
|
|
Summary: route.Doc,
|
|
Notes: route.Notes,
|
|
// Type gets overwritten if there is a write sample
|
|
DataTypeFields: DataTypeFields{Type: &voidString},
|
|
Parameters: []Parameter{},
|
|
Nickname: route.Operation,
|
|
ResponseMessages: composeResponseMessages(route, &decl, &sws.config)}
|
|
|
|
operation.Consumes = route.Consumes
|
|
operation.Produces = route.Produces
|
|
|
|
// share root params if any
|
|
for _, swparam := range rootParams {
|
|
operation.Parameters = append(operation.Parameters, swparam)
|
|
}
|
|
// route specific params
|
|
for _, param := range route.ParameterDocs {
|
|
operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data()))
|
|
}
|
|
|
|
sws.addModelsFromRouteTo(&operation, route, &decl)
|
|
api.Operations = append(api.Operations, operation)
|
|
}
|
|
decl.Apis = append(decl.Apis, api)
|
|
})
|
|
return decl
|
|
}
|
|
|
|
func withoutWildcard(path string) string {
|
|
if strings.HasSuffix(path, ":*}") {
|
|
return path[0:len(path)-3] + "}"
|
|
}
|
|
return path
|
|
}
|
|
|
|
// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
|
|
func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) {
|
|
if route.ResponseErrors == nil {
|
|
return messages
|
|
}
|
|
// sort by code
|
|
codes := sort.IntSlice{}
|
|
for code := range route.ResponseErrors {
|
|
codes = append(codes, code)
|
|
}
|
|
codes.Sort()
|
|
for _, code := range codes {
|
|
each := route.ResponseErrors[code]
|
|
message := ResponseMessage{
|
|
Code: code,
|
|
Message: each.Message,
|
|
}
|
|
if each.Model != nil {
|
|
st := reflect.TypeOf(each.Model)
|
|
isCollection, st := detectCollectionType(st)
|
|
// collection cannot be in responsemodel
|
|
if !isCollection {
|
|
modelName := modelBuilder{}.keyFrom(st)
|
|
modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "")
|
|
message.ResponseModel = modelName
|
|
}
|
|
}
|
|
messages = append(messages, message)
|
|
}
|
|
return
|
|
}
|
|
|
|
// addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it.
|
|
func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) {
|
|
if route.ReadSample != nil {
|
|
sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models)
|
|
}
|
|
if route.WriteSample != nil {
|
|
sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models)
|
|
}
|
|
}
|
|
|
|
func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
|
|
isCollection := false
|
|
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
|
|
st = st.Elem()
|
|
isCollection = true
|
|
} else {
|
|
if st.Kind() == reflect.Ptr {
|
|
if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array {
|
|
st = st.Elem().Elem()
|
|
isCollection = true
|
|
}
|
|
}
|
|
}
|
|
return isCollection, st
|
|
}
|
|
|
|
// addModelFromSample creates and adds (or overwrites) a Model from a sample resource
|
|
func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
|
|
mb := modelBuilder{Models: models, Config: &sws.config}
|
|
if isResponse {
|
|
sampleType, items := asDataType(sample, &sws.config)
|
|
operation.Type = sampleType
|
|
operation.Items = items
|
|
}
|
|
mb.addModelFrom(sample)
|
|
}
|
|
|
|
func asSwaggerParameter(param restful.ParameterData) Parameter {
|
|
return Parameter{
|
|
DataTypeFields: DataTypeFields{
|
|
Type: ¶m.DataType,
|
|
Format: asFormat(param.DataType, param.DataFormat),
|
|
DefaultValue: Special(param.DefaultValue),
|
|
},
|
|
Name: param.Name,
|
|
Description: param.Description,
|
|
ParamType: asParamType(param.Kind),
|
|
|
|
Required: param.Required}
|
|
}
|
|
|
|
// Between 1..7 path parameters is supported
|
|
func composeRootPath(req *restful.Request) string {
|
|
path := "/" + req.PathParameter("a")
|
|
b := req.PathParameter("b")
|
|
if b == "" {
|
|
return path
|
|
}
|
|
path = path + "/" + b
|
|
c := req.PathParameter("c")
|
|
if c == "" {
|
|
return path
|
|
}
|
|
path = path + "/" + c
|
|
d := req.PathParameter("d")
|
|
if d == "" {
|
|
return path
|
|
}
|
|
path = path + "/" + d
|
|
e := req.PathParameter("e")
|
|
if e == "" {
|
|
return path
|
|
}
|
|
path = path + "/" + e
|
|
f := req.PathParameter("f")
|
|
if f == "" {
|
|
return path
|
|
}
|
|
path = path + "/" + f
|
|
g := req.PathParameter("g")
|
|
if g == "" {
|
|
return path
|
|
}
|
|
return path + "/" + g
|
|
}
|
|
|
|
func asFormat(dataType string, dataFormat string) string {
|
|
if dataFormat != "" {
|
|
return dataFormat
|
|
}
|
|
return "" // TODO
|
|
}
|
|
|
|
func asParamType(kind int) string {
|
|
switch {
|
|
case kind == restful.PathParameterKind:
|
|
return "path"
|
|
case kind == restful.QueryParameterKind:
|
|
return "query"
|
|
case kind == restful.BodyParameterKind:
|
|
return "body"
|
|
case kind == restful.HeaderParameterKind:
|
|
return "header"
|
|
case kind == restful.FormParameterKind:
|
|
return "form"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func asDataType(any interface{}, config *Config) (*string, *Item) {
|
|
// If it's not a collection, return the suggested model name
|
|
st := reflect.TypeOf(any)
|
|
isCollection, st := detectCollectionType(st)
|
|
modelName := modelBuilder{}.keyFrom(st)
|
|
// if it's not a collection we are done
|
|
if !isCollection {
|
|
return &modelName, nil
|
|
}
|
|
|
|
// XXX: This is not very elegant
|
|
// We create an Item object referring to the given model
|
|
models := ModelList{}
|
|
mb := modelBuilder{Models: &models, Config: config}
|
|
mb.addModelFrom(any)
|
|
|
|
elemTypeName := mb.getElementTypeName(modelName, "", st)
|
|
item := new(Item)
|
|
if mb.isPrimitiveType(elemTypeName) {
|
|
mapped := mb.jsonSchemaType(elemTypeName)
|
|
item.Type = &mapped
|
|
} else {
|
|
item.Ref = &elemTypeName
|
|
}
|
|
tmp := "array"
|
|
return &tmp, item
|
|
}
|