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

Update Swagger UI and provide dynamic hostname support #97

Merged
merged 5 commits into from
Sep 14, 2023
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
104 changes: 39 additions & 65 deletions pkg/ffapi/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@ package ffapi

import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"time"

"github.com/ghodss/yaml"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"

"github.com/getkin/kin-openapi/openapi3"
"github.com/gorilla/mux"
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/fftls"
Expand All @@ -53,16 +50,17 @@ type APIServer interface {
type apiServer[T any] struct {
started chan struct{}

defaultFilterLimit uint64
maxFilterLimit uint64
maxFilterSkip uint64
requestTimeout time.Duration
requestMaxTimeout time.Duration
apiPublicURL string
alwaysPaginate bool
metricsEnabled bool
metricsPath string
metricsPublicURL string
defaultFilterLimit uint64
maxFilterLimit uint64
maxFilterSkip uint64
requestTimeout time.Duration
requestMaxTimeout time.Duration
apiPublicURL string
apiDynamicPublicURLHeader string
alwaysPaginate bool
metricsEnabled bool
metricsPath string
metricsPublicURL string

APIServerOptions[T]
}
Expand Down Expand Up @@ -90,16 +88,17 @@ type APIServerRouteExt[T any] struct {
// the supplied wrapper function - which will inject
func NewAPIServer[T any](ctx context.Context, options APIServerOptions[T]) APIServer {
as := &apiServer[T]{
defaultFilterLimit: options.APIConfig.GetUint64(ConfAPIDefaultFilterLimit),
maxFilterLimit: options.APIConfig.GetUint64(ConfAPIMaxFilterLimit),
maxFilterSkip: options.APIConfig.GetUint64(ConfAPIMaxFilterSkip),
requestTimeout: options.APIConfig.GetDuration(ConfAPIRequestTimeout),
requestMaxTimeout: options.APIConfig.GetDuration(ConfAPIRequestMaxTimeout),
metricsEnabled: options.MetricsConfig.GetBool(ConfMetricsServerEnabled),
metricsPath: options.MetricsConfig.GetString(ConfMetricsServerPath),
alwaysPaginate: options.APIConfig.GetBool(ConfAPIAlwaysPaginate),
APIServerOptions: options,
started: make(chan struct{}),
defaultFilterLimit: options.APIConfig.GetUint64(ConfAPIDefaultFilterLimit),
maxFilterLimit: options.APIConfig.GetUint64(ConfAPIMaxFilterLimit),
maxFilterSkip: options.APIConfig.GetUint64(ConfAPIMaxFilterSkip),
requestTimeout: options.APIConfig.GetDuration(ConfAPIRequestTimeout),
requestMaxTimeout: options.APIConfig.GetDuration(ConfAPIRequestMaxTimeout),
metricsEnabled: options.MetricsConfig.GetBool(ConfMetricsServerEnabled),
metricsPath: options.MetricsConfig.GetString(ConfMetricsServerPath),
alwaysPaginate: options.APIConfig.GetBool(ConfAPIAlwaysPaginate),
apiDynamicPublicURLHeader: options.APIConfig.GetString(ConfAPIDynamicPublicURLHeader),
APIServerOptions: options,
started: make(chan struct{}),
}
if as.FavIcon16 == nil {
as.FavIcon16 = ffLogo16
Expand Down Expand Up @@ -130,7 +129,7 @@ func (as *apiServer[T]) Serve(ctx context.Context) (err error) {
httpErrChan := make(chan error)
metricsErrChan := make(chan error)

apiHTTPServer, err := httpserver.NewHTTPServer(ctx, "api", as.createMuxRouter(ctx, as.apiPublicURL), httpErrChan, as.APIConfig, as.CORSConfig, &httpserver.ServerOptions{
apiHTTPServer, err := httpserver.NewHTTPServer(ctx, "api", as.createMuxRouter(ctx), httpErrChan, as.APIConfig, as.CORSConfig, &httpserver.ServerOptions{
MaximumRequestTimeout: as.requestMaxTimeout,
})
if err != nil {
Expand Down Expand Up @@ -185,43 +184,6 @@ func buildPublicURL(conf config.Section, a net.Addr) string {
return publicURL
}

func (as *apiServer[T]) swaggerGenConf(apiBaseURL string) *Options {
return &Options{
BaseURL: apiBaseURL,
Title: as.Description,
Version: "1.0",
PanicOnMissingDescription: as.PanicOnMissingDescription,
DefaultRequestTimeout: as.requestTimeout,
SupportFieldRedaction: as.SupportFieldRedaction,
}
}

func (as *apiServer[T]) swaggerHandler(generator func(req *http.Request) (*openapi3.T, error)) func(res http.ResponseWriter, req *http.Request) (status int, err error) {
return func(res http.ResponseWriter, req *http.Request) (status int, err error) {
vars := mux.Vars(req)
doc, err := generator(req)
if err != nil {
return 500, err
}
if vars["ext"] == ".json" {
res.Header().Add("Content-Type", "application/json")
b, _ := json.Marshal(&doc)
_, _ = res.Write(b)
} else {
res.Header().Add("Content-Type", "application/x-yaml")
b, _ := yaml.Marshal(&doc)
_, _ = res.Write(b)
}
return 200, nil
}
}

func (as *apiServer[T]) swaggerGenerator(apiBaseURL string) func(req *http.Request) (*openapi3.T, error) {
swg := NewSwaggerGen(as.swaggerGenConf(apiBaseURL))
return func(req *http.Request) (*openapi3.T, error) {
return swg.Generate(req.Context(), as.Routes), nil
}
}
func (as *apiServer[T]) routeHandler(hf *HandlerFactory, route *Route) http.HandlerFunc {
// We extend the base ffapi functionality, with standardized DB filter support for all core resources.
// We also pass the Orchestrator context through
Expand All @@ -248,7 +210,7 @@ func (as *apiServer[T]) handlerFactory() *HandlerFactory {
}
}

func (as *apiServer[T]) createMuxRouter(ctx context.Context, publicURL string) *mux.Router {
func (as *apiServer[T]) createMuxRouter(ctx context.Context) *mux.Router {
r := mux.NewRouter().UseEncodedPath()
hf := as.handlerFactory()

Expand All @@ -257,7 +219,6 @@ func (as *apiServer[T]) createMuxRouter(ctx context.Context, publicURL string) *
r.Use(h)
}

apiBaseURL := fmt.Sprintf("%s/api/v1", publicURL)
for _, route := range as.Routes {
ce, ok := route.Extensions.(*APIServerRouteExt[T])
if !ok {
Expand All @@ -278,8 +239,21 @@ func (as *apiServer[T]) createMuxRouter(ctx context.Context, publicURL string) *
}
}

r.HandleFunc(`/api/swagger{ext:\.yaml|\.json|}`, hf.APIWrapper(as.swaggerHandler(as.swaggerGenerator(apiBaseURL))))
r.HandleFunc(`/api`, hf.APIWrapper(hf.SwaggerUIHandler(publicURL+"/api/swagger.yaml")))
oah := &OpenAPIHandlerFactory{
BaseSwaggerGenOptions: SwaggerGenOptions{
Title: as.Description,
Version: "1.0",
PanicOnMissingDescription: as.PanicOnMissingDescription,
DefaultRequestTimeout: as.requestTimeout,
SupportFieldRedaction: as.SupportFieldRedaction,
},
StaticPublicURL: as.apiPublicURL,
}
r.HandleFunc(`/api/swagger.yaml`, hf.APIWrapper(oah.OpenAPIHandler(`/api/v1`, OpenAPIFormatYAML, as.Routes)))
r.HandleFunc(`/api/swagger.json`, hf.APIWrapper(oah.OpenAPIHandler(`/api/v1`, OpenAPIFormatJSON, as.Routes)))
r.HandleFunc(`/api/openapi.yaml`, hf.APIWrapper(oah.OpenAPIHandler(`/api/v1`, OpenAPIFormatYAML, as.Routes)))
r.HandleFunc(`/api/openapi.json`, hf.APIWrapper(oah.OpenAPIHandler(`/api/v1`, OpenAPIFormatJSON, as.Routes)))
r.HandleFunc(`/api`, hf.APIWrapper(oah.SwaggerUIHandler(`/api/openapi.yaml`)))
r.HandleFunc(`/favicon{any:.*}.png`, favIconsHandler(as.FavIcon16, as.FavIcon32))

r.NotFoundHandler = hf.APIWrapper(as.notFoundHandler)
Expand Down
14 changes: 8 additions & 6 deletions pkg/ffapi/apiserver_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ var (
ConfMetricsServerEnabled = "enabled"
ConfMetricsServerPath = "/metrics"

ConfAPIDefaultFilterLimit = "defaultFilterLimit"
ConfAPIMaxFilterLimit = "maxFilterLimit"
ConfAPIMaxFilterSkip = "maxFilterSkip"
ConfAPIRequestTimeout = "requestTimeout"
ConfAPIRequestMaxTimeout = "requestMaxTimeout"
ConfAPIAlwaysPaginate = "alwaysPaginate"
ConfAPIDefaultFilterLimit = "defaultFilterLimit"
ConfAPIMaxFilterLimit = "maxFilterLimit"
ConfAPIMaxFilterSkip = "maxFilterSkip"
ConfAPIRequestTimeout = "requestTimeout"
ConfAPIRequestMaxTimeout = "requestMaxTimeout"
ConfAPIAlwaysPaginate = "alwaysPaginate"
ConfAPIDynamicPublicURLHeader = "dynamicPublicURLHeader"
)

func InitAPIServerConfig(apiConfig, metricsConfig, corsConfig config.Section) {
Expand All @@ -41,6 +42,7 @@ func InitAPIServerConfig(apiConfig, metricsConfig, corsConfig config.Section) {
apiConfig.AddKnownKey(ConfAPIRequestTimeout, "30s")
apiConfig.AddKnownKey(ConfAPIRequestMaxTimeout, "10m")
apiConfig.AddKnownKey(ConfAPIAlwaysPaginate, false)
apiConfig.AddKnownKey(ConfAPIDynamicPublicURLHeader)

httpserver.InitCORSConfig(corsConfig)

Expand Down
17 changes: 0 additions & 17 deletions pkg/ffapi/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/go-resty/resty/v2"
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/httpserver"
Expand Down Expand Up @@ -339,21 +337,6 @@ func TestBadRoute(t *testing.T) {
assert.Panics(t, func() { as.Serve(context.Background()) })
}

func TestBadSwagger(t *testing.T) {
_, as, done := newTestAPIServer(t, false)
defer done()

h := as.swaggerHandler(func(req *http.Request) (*openapi3.T, error) {
return nil, fmt.Errorf("pop")
})
res := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, "swagger", nil)
assert.NoError(t, err)
status, err := h(res, req)
assert.Equal(t, 500, status)
assert.Regexp(t, "pop", err)
}

func TestBadMetrics(t *testing.T) {
_, as, done := newTestAPIServer(t, false)
defer done()
Expand Down
12 changes: 3 additions & 9 deletions pkg/ffapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type (
CtxFFRequestIDKey struct{}
)

type HandlerFunction func(res http.ResponseWriter, req *http.Request) (status int, err error)

type HandlerFactory struct {
DefaultRequestTimeout time.Duration
MaxTimeout time.Duration
Expand Down Expand Up @@ -216,14 +218,6 @@ func (hs *HandlerFactory) RouteHandler(route *Route) http.HandlerFunc {
})
}

func (hs *HandlerFactory) SwaggerUIHandler(url string) func(res http.ResponseWriter, req *http.Request) (status int, err error) {
return func(res http.ResponseWriter, req *http.Request) (status int, err error) {
res.Header().Add("Content-Type", "text/html")
_, _ = res.Write(SwaggerUIHTML(req.Context(), url))
return 200, nil
}
}

func (hs *HandlerFactory) handleOutput(ctx context.Context, res http.ResponseWriter, status int, output interface{}) (int, error) {
vOutput := reflect.ValueOf(output)
outputKind := vOutput.Kind()
Expand Down Expand Up @@ -286,7 +280,7 @@ func (hs *HandlerFactory) getTimeout(req *http.Request) time.Duration {
return CalcRequestTimeout(req, hs.DefaultRequestTimeout, hs.MaxTimeout)
}

func (hs *HandlerFactory) APIWrapper(handler func(res http.ResponseWriter, req *http.Request) (status int, err error)) http.HandlerFunc {
func (hs *HandlerFactory) APIWrapper(handler HandlerFunction) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {

reqTimeout := hs.getTimeout(req)
Expand Down
11 changes: 0 additions & 11 deletions pkg/ffapi/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,17 +448,6 @@ func TestMultipartBadContentType(t *testing.T) {
assert.Regexp(t, "FF00161", err)
}

func TestSwaggerUI(t *testing.T) {
hf := newTestHandlerFactory("", nil)
h := hf.SwaggerUIHandler("http://localhost:5000/api/v1")

res := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1", nil)
status, err := h(res, req)
assert.Equal(t, 200, status)
assert.NoError(t, err)
}

func TestGetTimeoutMax(t *testing.T) {
hf := newTestHandlerFactory("", nil)
hf.MaxTimeout = 1 * time.Second
Expand Down
9 changes: 4 additions & 5 deletions pkg/ffapi/openapi3.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
"github.com/hyperledger/firefly-common/pkg/i18n"
)

type Options struct {
type SwaggerGenOptions struct {
BaseURL string
BaseURLVariables map[string]BaseURLVariable
Title string
Expand All @@ -47,8 +47,7 @@ type Options struct {
APIDefaultFilterLimit string
APIMaxFilterLimit uint
SupportFieldRedaction bool

RouteCustomizations func(ctx context.Context, sg *SwaggerGen, route *Route, op *openapi3.Operation)
RouteCustomizations func(ctx context.Context, sg *SwaggerGen, route *Route, op *openapi3.Operation)
}

type BaseURLVariable struct {
Expand All @@ -59,10 +58,10 @@ type BaseURLVariable struct {
var customRegexRemoval = regexp.MustCompile(`{(\w+)\:[^}]+}`)

type SwaggerGen struct {
options *Options
options *SwaggerGenOptions
}

func NewSwaggerGen(options *Options) *SwaggerGen {
func NewSwaggerGen(options *SwaggerGenOptions) *SwaggerGen {
return &SwaggerGen{
options: options,
}
Expand Down
Loading