Skip to content

Commit

Permalink
Decouple REST Framework
Browse files Browse the repository at this point in the history
Takes ownership of the route interfaces
and removes exposing the `go-restful`
dependency.

Adds an adapter for the currently used
version of `go-restful`.

Closes kubernetes#250 (kubernetes#250)
  • Loading branch information
austince committed Oct 7, 2021
1 parent 94abced commit 4f2f372
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 62 deletions.
93 changes: 46 additions & 47 deletions pkg/builder/openapi.go
Expand Up @@ -22,8 +22,6 @@ import (
"net/http"
"strings"

restful "github.com/emicklei/go-restful"

"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/util"
"k8s.io/kube-openapi/pkg/validation/spec"
Expand All @@ -40,10 +38,10 @@ type openAPI struct {
definitions map[string]common.OpenAPIDefinition
}

// BuildOpenAPISpec builds OpenAPI spec given a list of webservices (containing routes) and common.Config to customize it.
func BuildOpenAPISpec(webServices []*restful.WebService, config *common.Config) (*spec.Swagger, error) {
// BuildOpenAPISpec builds OpenAPI spec given a list of route containers and common.Config to customize it.
func BuildOpenAPISpec(routeContainers []common.RouteContainer, config *common.Config) (*spec.Swagger, error) {
o := newOpenAPI(config)
err := o.buildPaths(webServices)
err := o.buildPaths(routeContainers)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -96,8 +94,8 @@ func newOpenAPI(config *common.Config) openAPI {
},
}
if o.config.GetOperationIDAndTags == nil {
o.config.GetOperationIDAndTags = func(r *restful.Route) (string, []string, error) {
return r.Operation, nil, nil
o.config.GetOperationIDAndTags = func(r common.Route) (string, []string, error) {
return r.OperationName(), nil, nil
}
}
if o.config.GetDefinitionName == nil {
Expand Down Expand Up @@ -181,7 +179,7 @@ func (o *openAPI) buildDefinitionForType(name string) (string, error) {
}

// buildPaths builds OpenAPI paths using go-restful's web services.
func (o *openAPI) buildPaths(webServices []*restful.WebService) error {
func (o *openAPI) buildPaths(webServices []common.RouteContainer) error {
pathsToIgnore := util.NewTrie(o.config.IgnorePrefixes)
duplicateOpId := make(map[string]string)
for _, w := range webServices {
Expand Down Expand Up @@ -234,7 +232,7 @@ func (o *openAPI) buildPaths(webServices []*restful.WebService) error {
} else {
duplicateOpId[op.ID] = path
}
switch strings.ToUpper(route.Method) {
switch strings.ToUpper(route.Method()) {
case "GET":
pathItem.Get = op
case "POST":
Expand All @@ -258,12 +256,12 @@ func (o *openAPI) buildPaths(webServices []*restful.WebService) error {
}

// buildOperations builds operations for each webservice path
func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map[interface{}]spec.Parameter) (ret *spec.Operation, err error) {
func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[interface{}]spec.Parameter) (ret *spec.Operation, err error) {
ret = &spec.Operation{
OperationProps: spec.OperationProps{
Description: route.Doc,
Consumes: route.Consumes,
Produces: route.Produces,
Description: route.Description(),
Consumes: route.Consumes(),
Produces: route.Produces(),
Schemes: o.config.ProtocolList,
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
Expand All @@ -272,28 +270,28 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
},
},
}
for k, v := range route.Metadata {
for k, v := range route.Metadata() {
if strings.HasPrefix(k, common.ExtensionPrefix) {
if ret.Extensions == nil {
ret.Extensions = spec.Extensions{}
}
ret.Extensions.Add(k, v)
}
}
if ret.ID, ret.Tags, err = o.config.GetOperationIDAndTags(&route); err != nil {
if ret.ID, ret.Tags, err = o.config.GetOperationIDAndTags(route); err != nil {
return ret, err
}

// Build responses
for _, resp := range route.ResponseErrors {
ret.Responses.StatusCodeResponses[resp.Code], err = o.buildResponse(resp.Model, resp.Message)
for _, resp := range route.StatusCodeResponses() {
ret.Responses.StatusCodeResponses[resp.Code()], err = o.buildResponse(resp.Model(), resp.Message())
if err != nil {
return ret, err
}
}
// If there is no response but a write sample, assume that write sample is an http.StatusOK response.
if len(ret.Responses.StatusCodeResponses) == 0 && route.WriteSample != nil {
ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.WriteSample, "OK")
if len(ret.Responses.StatusCodeResponses) == 0 && route.ResponsePayloadSample() != nil {
ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.ResponsePayloadSample(), "OK")
if err != nil {
return ret, err
}
Expand All @@ -310,9 +308,9 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map

// Build non-common Parameters
ret.Parameters = make([]spec.Parameter, 0)
for _, param := range route.ParameterDocs {
for _, param := range route.Parameters() {
if _, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]; !isCommon {
openAPIParam, err := o.buildParameter(param.Data(), route.ReadSample)
openAPIParam, err := o.buildParameter(param, route.RequestPayloadSample())
if err != nil {
return ret, err
}
Expand All @@ -335,29 +333,30 @@ func (o *openAPI) buildResponse(model interface{}, description string) (spec.Res
}, nil
}

func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}]spec.Parameter, error) {
func (o *openAPI) findCommonParameters(routes []common.Route) (map[interface{}]spec.Parameter, error) {
commonParamsMap := make(map[interface{}]spec.Parameter, 0)
paramOpsCountByName := make(map[interface{}]int, 0)
paramNameKindToDataMap := make(map[interface{}]restful.ParameterData, 0)
paramNameKindToDataMap := make(map[interface{}]common.Parameter, 0)
for _, route := range routes {
routeParamDuplicateMap := make(map[interface{}]bool)
s := ""
for _, param := range route.ParameterDocs {
m, _ := json.Marshal(param.Data())
params := route.Parameters()
for _, param := range params {
m, _ := json.Marshal(param)
s += string(m) + "\n"
key := mapKeyFromParam(param)
if routeParamDuplicateMap[key] {
msg, _ := json.Marshal(route.ParameterDocs)
return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v", param.Data().Name, string(msg), s)
msg, _ := json.Marshal(params)
return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v", param.Name(), string(msg), s)
}
routeParamDuplicateMap[key] = true
paramOpsCountByName[key]++
paramNameKindToDataMap[key] = param.Data()
paramNameKindToDataMap[key] = param
}
}
for key, count := range paramOpsCountByName {
paramData := paramNameKindToDataMap[key]
if count == len(routes) && paramData.Kind != restful.BodyParameterKind {
if count == len(routes) && paramData.Kind() != common.BodyParameterKind {
openAPIParam, err := o.buildParameter(paramData, nil)
if err != nil {
return commonParamsMap, err
Expand Down Expand Up @@ -389,16 +388,16 @@ func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) {
}
}

func (o *openAPI) buildParameter(restParam restful.ParameterData, bodySample interface{}) (ret spec.Parameter, err error) {
func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interface{}) (ret spec.Parameter, err error) {
ret = spec.Parameter{
ParamProps: spec.ParamProps{
Name: restParam.Name,
Description: restParam.Description,
Required: restParam.Required,
Name: restParam.Name(),
Description: restParam.Description(),
Required: restParam.Required(),
},
}
switch restParam.Kind {
case restful.BodyParameterKind:
switch restParam.Kind() {
case common.BodyParameterKind:
if bodySample != nil {
ret.In = "body"
ret.Schema, err = o.toSchema(util.GetCanonicalTypeName(bodySample))
Expand All @@ -407,36 +406,36 @@ func (o *openAPI) buildParameter(restParam restful.ParameterData, bodySample int
// There is not enough information in the body parameter to build the definition.
// Body parameter has a data type that is a short name but we need full package name
// of the type to create a definition.
return ret, fmt.Errorf("restful body parameters are not supported: %v", restParam.DataType)
return ret, fmt.Errorf("restful body parameters are not supported: %v", restParam.DataType())
}
case restful.PathParameterKind:
case common.PathParameterKind:
ret.In = "path"
if !restParam.Required {
if !restParam.Required() {
return ret, fmt.Errorf("path parameters should be marked at required for parameter %v", restParam)
}
case restful.QueryParameterKind:
case common.QueryParameterKind:
ret.In = "query"
case restful.HeaderParameterKind:
case common.HeaderParameterKind:
ret.In = "header"
case restful.FormParameterKind:
case common.FormParameterKind:
ret.In = "formData"
default:
return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind)
return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind())
}
openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType)
openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType())
if openAPIType == "" {
return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType)
return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType())
}
ret.Type = openAPIType
ret.Format = openAPIFormat
ret.UniqueItems = !restParam.AllowMultiple
ret.UniqueItems = !restParam.AllowMultiple()
return ret, nil
}

func (o *openAPI) buildParameters(restParam []*restful.Parameter) (ret []spec.Parameter, err error) {
func (o *openAPI) buildParameters(restParam []common.Parameter) (ret []spec.Parameter, err error) {
ret = make([]spec.Parameter, len(restParam))
for i, v := range restParam {
ret[i], err = o.buildParameter(v.Data(), nil)
ret[i], err = o.buildParameter(v, nil)
if err != nil {
return ret, err
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/builder/openapi_test.go
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/emicklei/go-restful"
"github.com/stretchr/testify/assert"
openapi "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/common/restfuladapter"
"k8s.io/kube-openapi/pkg/validation/spec"
)

Expand Down Expand Up @@ -471,7 +472,8 @@ func TestBuildOpenAPISpec(t *testing.T) {
},
},
}
swagger, err := BuildOpenAPISpec(container.RegisteredWebServices(), config)
routeContainers := restfuladapter.AdaptWebServices(container.RegisteredWebServices())
swagger, err := BuildOpenAPISpec(routeContainers, config)
if !assert.NoError(err) {
return
}
Expand Down
16 changes: 8 additions & 8 deletions pkg/builder/util.go
Expand Up @@ -19,7 +19,7 @@ package builder
import (
"sort"

"github.com/emicklei/go-restful"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)

Expand All @@ -42,20 +42,20 @@ func sortParameters(p []spec.Parameter) {
sort.Sort(byNameIn{p})
}

func groupRoutesByPath(routes []restful.Route) map[string][]restful.Route {
pathToRoutes := make(map[string][]restful.Route)
func groupRoutesByPath(routes []common.Route) map[string][]common.Route {
pathToRoutes := make(map[string][]common.Route)
for _, r := range routes {
pathToRoutes[r.Path] = append(pathToRoutes[r.Path], r)
pathToRoutes[r.Path()] = append(pathToRoutes[r.Path()], r)
}
return pathToRoutes
}

func mapKeyFromParam(param *restful.Parameter) interface{} {
func mapKeyFromParam(param common.Parameter) interface{} {
return struct {
Name string
Kind int
Kind common.ParameterKind
}{
Name: param.Data().Name,
Kind: param.Data().Kind,
Name: param.Name(),
Kind: param.Kind(),
}
}
3 changes: 1 addition & 2 deletions pkg/common/common.go
Expand Up @@ -20,7 +20,6 @@ import (
"net/http"
"strings"

"github.com/emicklei/go-restful"
"k8s.io/kube-openapi/pkg/validation/spec"
)

Expand Down Expand Up @@ -87,7 +86,7 @@ type Config struct {
GetDefinitions GetOpenAPIDefinitions

// GetOperationIDAndTags returns operation id and tags for a restful route. It is an optional function to customize operation IDs.
GetOperationIDAndTags func(r *restful.Route) (string, []string, error)
GetOperationIDAndTags func(r Route) (string, []string, error)

// GetDefinitionName returns a friendly name for a definition base on the serving path. parameter `name` is the full name of the definition.
// It is an optional function to customize model names.
Expand Down
84 changes: 84 additions & 0 deletions pkg/common/interfaces.go
@@ -0,0 +1,84 @@
package common

// RouteContainer is the entrypoint for a service, which may contain multiple
// routes under a common path with a common set of parameters.
type RouteContainer interface {
// RootPath is the path that all contained routes are nested under.
RootPath() string
// PathParameters are common parameters defined in the root path.
PathParameters() []Parameter
// Routes are all routes exposed under the root path.
Routes() []Route
}

// Route is a logical endpoint of the service.
type Route interface {
// Method defines the HTTP Method.
Method() string
// Path defines the route's endpoint.
Path() string
// OperationName defines a machine-readable ID for the route.
OperationName() string
// Parameters defines the list of accepted parameters.
Parameters() []Parameter
// Description is a human-readable route description.
Description() string
// Consumes defines the consumed content-types.
Consumes() []string
// Produces defines the produced content-types.
Produces() []string
// Metadata allows adding extensions to the generated spec.
Metadata() map[string]interface{}
// RequestPayloadSample defines an example request payload. Can return nil.
RequestPayloadSample() interface{}
// ResponsePayloadSample defines an example response payload. Can return nil.
ResponsePayloadSample() interface{}
// StatusCodeResponses defines a mapping of HTTP Status Codes to the specific response.
StatusCodeResponses() map[int]StatusCodeResponse
}

// StatusCodeResponse is an explicit response type with an HTTP Status Code.
type StatusCodeResponse interface {
// Code defines the HTTP Status Code.
Code() int
// Message returns the human-readable message.
Message() string
// Model defines an example payload for this response.
Model() interface{}
}

// Parameter is a Route parameter.
type Parameter interface {
// Name defines the unique-per-route identifier.
Name() string
// Description is the human-readable description of the param.
Description() string
// Required defines if this parameter must be provided.
Required() bool
// Kind defines the type of the parameter itself.
Kind() ParameterKind
// DataType defines the type of data the parameter carries.
DataType() string
// AllowMultiple defines if more than one value can be supplied for the parameter.
AllowMultiple() bool
}

// ParameterKind is an enum of route parameter types.
type ParameterKind int

const (
// PathParameterKind indicates the request parameter type is "path".
PathParameterKind = ParameterKind(iota)

// QueryParameterKind indicates the request parameter type is "query".
QueryParameterKind

// BodyParameterKind indicates the request parameter type is "body".
BodyParameterKind

// HeaderParameterKind indicates the request parameter type is "header".
HeaderParameterKind

// FormParameterKind indicates the request parameter type is "form".
FormParameterKind
)
15 changes: 15 additions & 0 deletions pkg/common/restfuladapter/adapter.go
@@ -0,0 +1,15 @@
package restfuladapter

import (
"github.com/emicklei/go-restful"
"k8s.io/kube-openapi/pkg/common"
)

func AdaptWebServices(webServices []*restful.WebService) []common.RouteContainer {
var containers []common.RouteContainer
for _, ws := range webServices {
containers = append(containers, &WebServiceAdapter{ws})
}
return containers
}

0 comments on commit 4f2f372

Please sign in to comment.