diff --git a/pkg/builder/openapi.go b/pkg/builder/openapi.go index 3e9e675a0..2b5a3d8b1 100644 --- a/pkg/builder/openapi.go +++ b/pkg/builder/openapi.go @@ -25,6 +25,7 @@ import ( restful "github.com/emicklei/go-restful" "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/util" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -40,10 +41,17 @@ 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. +// +// Deprecated: BuildOpenAPISpecFromRoutes should be used instead. +func BuildOpenAPISpec(routeContainers []*restful.WebService, config *common.Config) (*spec.Swagger, error) { + return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(routeContainers), config) +} + +// BuildOpenAPISpecFromRoutes builds OpenAPI spec given a list of route containers and common.Config to customize it. +func BuildOpenAPISpecFromRoutes(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 } @@ -95,11 +103,25 @@ 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 + + if o.config.GetOperationIDAndTagsFromRoute == nil { + // Map the deprecated handler to the common interface, if provided. + if o.config.GetOperationIDAndTags != nil { + o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { + restfulRouteAdapter, ok := r.(*restfuladapter.RouteAdapter) + if !ok { + return "", nil, fmt.Errorf("config.GetOperationIDAndTags specified but route is not a restful v1 Route") + } + + return o.config.GetOperationIDAndTags(restfulRouteAdapter.Route) + } + } else { + o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { + return r.OperationName(), nil, nil + } } } + if o.config.GetDefinitionName == nil { o.config.GetDefinitionName = func(name string) (string, spec.Extensions) { return name[strings.LastIndex(name, "/")+1:], nil @@ -181,10 +203,10 @@ 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(routeContainers []common.RouteContainer) error { pathsToIgnore := util.NewTrie(o.config.IgnorePrefixes) duplicateOpId := make(map[string]string) - for _, w := range webServices { + for _, w := range routeContainers { rootPath := w.RootPath() if pathsToIgnore.HasPrefix(rootPath) { continue @@ -234,7 +256,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": @@ -258,12 +280,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{ @@ -272,7 +294,7 @@ 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{} @@ -280,20 +302,20 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map 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.GetOperationIDAndTagsFromRoute(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 } @@ -310,9 +332,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 } @@ -335,29 +357,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 @@ -389,16 +412,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)) @@ -407,36 +430,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 } diff --git a/pkg/builder/util.go b/pkg/builder/util.go index c7ea40d91..3621a4de1 100644 --- a/pkg/builder/util.go +++ b/pkg/builder/util.go @@ -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" ) @@ -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(), } } diff --git a/pkg/builder3/openapi.go b/pkg/builder3/openapi.go index 6d716ed58..662e0a7d6 100644 --- a/pkg/builder3/openapi.go +++ b/pkg/builder3/openapi.go @@ -21,6 +21,7 @@ import ( "fmt" restful "github.com/emicklei/go-restful" "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/util" "k8s.io/kube-openapi/pkg/validation/spec" @@ -38,10 +39,10 @@ type openAPI struct { definitions map[string]common.OpenAPIDefinition } -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 } @@ -69,10 +70,10 @@ func (o *openAPI) buildResponse(model interface{}, description string, content [ return response, nil } -func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map[interface{}]*spec3.Parameter) (*spec3.Operation, error) { +func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[interface{}]*spec3.Parameter) (*spec3.Operation, error) { ret := &spec3.Operation{ OperationProps: spec3.OperationProps{ - Description: route.Doc, + Description: route.Description(), Responses: &spec3.Responses{ ResponsesProps: spec3.ResponsesProps{ StatusCodeResponses: make(map[int]*spec3.Response), @@ -81,21 +82,21 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map }, } var err error - if ret.OperationId, ret.Tags, err = o.config.GetOperationIDAndTags(&route); err != nil { + if ret.OperationId, ret.Tags, err = o.config.GetOperationIDAndTagsFromRoute(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, route.Produces) + for _, resp := range route.StatusCodeResponses() { + ret.Responses.StatusCodeResponses[resp.Code()], err = o.buildResponse(resp.Model(), resp.Message(), route.Produces()) 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", route.Produces) + if len(ret.Responses.StatusCodeResponses) == 0 && route.ResponsePayloadSample() != nil { + ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.ResponsePayloadSample(), "OK", route.Produces()) if err != nil { return ret, err } @@ -104,10 +105,11 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map // TODO: Default response if needed. Common Response config ret.Parameters = make([]*spec3.Parameter, 0) - for _, param := range route.ParameterDocs { + params := route.Parameters() + for _, param := range params { _, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)] - if !isCommon && param.Data().Kind != restful.BodyParameterKind { - openAPIParam, err := o.buildParameter(param.Data()) + if !isCommon && param.Kind() != common.BodyParameterKind { + openAPIParam, err := o.buildParameter(param) if err != nil { return ret, err } @@ -115,9 +117,9 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map } } - body, err := o.buildRequestBody(route.ParameterDocs, route.ReadSample) + body, err := o.buildRequestBody(params, route.RequestPayloadSample()) if err != nil { - return nil ,err + return nil, err } if body != nil { @@ -126,9 +128,9 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map return ret, nil } -func (o *openAPI) buildRequestBody(parameters []*restful.Parameter, bodySample interface{}) (*spec3.RequestBody, error) { +func (o *openAPI) buildRequestBody(parameters []common.Parameter, bodySample interface{}) (*spec3.RequestBody, error) { for _, param := range parameters { - if param.Data().Kind == restful.BodyParameterKind && bodySample != nil { + if param.Kind() == common.BodyParameterKind && bodySample != nil { schema, err := o.toSchema(util.GetCanonicalTypeName(bodySample)) if err != nil { return nil, err @@ -164,9 +166,22 @@ 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 + + if o.config.GetOperationIDAndTagsFromRoute == nil { + // Map the deprecated handler to the common interface, if provided. + if o.config.GetOperationIDAndTags != nil { + o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { + restfulRouteAdapter, ok := r.(*restfuladapter.RouteAdapter) + if !ok { + return "", nil, fmt.Errorf("config.GetOperationIDAndTags specified but route is not a restful v1 Route") + } + + return o.config.GetOperationIDAndTags(restfulRouteAdapter.Route) + } + } else { + o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { + return r.OperationName(), nil, nil + } } } @@ -184,7 +199,7 @@ func newOpenAPI(config *common.Config) openAPI { return o } -func (o *openAPI) buildOpenAPISpec(webServices []*restful.WebService) error { +func (o *openAPI) buildOpenAPISpec(webServices []common.RouteContainer) error { pathsToIgnore := util.NewTrie(o.config.IgnorePrefixes) for _, w := range webServices { rootPath := w.RootPath() @@ -233,7 +248,7 @@ func (o *openAPI) buildOpenAPISpec(webServices []*restful.WebService) error { for _, route := range routes { op, _ := o.buildOperations(route, inPathCommonParamsMap) - switch strings.ToUpper(route.Method) { + switch strings.ToUpper(route.Method()) { case "GET": pathItem.Get = op case "POST": @@ -257,7 +272,15 @@ func (o *openAPI) buildOpenAPISpec(webServices []*restful.WebService) error { return nil } +// BuildOpenAPISpec builds OpenAPI v3 spec given a list of route containers and common.Config to customize it. +// +// Deprecated: BuildOpenAPISpecFromRoutes should be used instead. func BuildOpenAPISpec(webServices []*restful.WebService, config *common.Config) (*spec3.OpenAPI, error) { + return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(webServices), config) +} + +// BuildOpenAPISpecFromRoutes builds OpenAPI v3 spec given a list of route containers and common.Config to customize it. +func BuildOpenAPISpecFromRoutes(webServices []common.RouteContainer, config *common.Config) (*spec3.OpenAPI, error) { a := newOpenAPI(config) err := a.buildOpenAPISpec(webServices) if err != nil { @@ -266,29 +289,30 @@ func BuildOpenAPISpec(webServices []*restful.WebService, config *common.Config) return a.spec, nil } -func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}]*spec3.Parameter, error) { +func (o *openAPI) findCommonParameters(routes []common.Route) (map[interface{}]*spec3.Parameter, error) { commonParamsMap := make(map[interface{}]*spec3.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) if err != nil { return commonParamsMap, err @@ -299,10 +323,10 @@ func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}] return commonParamsMap, nil } -func (o *openAPI) buildParameters(restParam []*restful.Parameter) (ret []*spec3.Parameter, err error) { +func (o *openAPI) buildParameters(restParam []common.Parameter) (ret []*spec3.Parameter, err error) { ret = make([]*spec3.Parameter, len(restParam)) for i, v := range restParam { - ret[i], err = o.buildParameter(v.Data()) + ret[i], err = o.buildParameter(v) if err != nil { return ret, err } @@ -310,40 +334,40 @@ func (o *openAPI) buildParameters(restParam []*restful.Parameter) (ret []*spec3. return ret, nil } -func (o *openAPI) buildParameter(restParam restful.ParameterData) (ret *spec3.Parameter, err error) { +func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Parameter, err error) { ret = &spec3.Parameter{ ParameterProps: spec3.ParameterProps{ - 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: return nil, nil - case restful.PathParameterKind: + case common.PathParameterKind: ret.In = "path" - if !restParam.Required { - return ret, fmt.Errorf("path parameters should be marked at required for parameter %v", restParam) + if !restParam.Required() { + return ret, fmt.Errorf("path parameters should be marked as required for parameter %v", restParam) } - case restful.QueryParameterKind: + case common.QueryParameterKind: ret.In = "query" - case restful.HeaderParameterKind: + case common.HeaderParameterKind: ret.In = "header" /* TODO: add support for the cookie param */ default: - return ret, fmt.Errorf("unsupported restful parameter kind : %v", restParam.Kind) + return ret, fmt.Errorf("unsupported restful parameter 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.Schema = &spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{openAPIType}, Format: openAPIFormat, - UniqueItems: !restParam.AllowMultiple, + UniqueItems: !restParam.AllowMultiple(), }, } return ret, nil diff --git a/pkg/builder3/util.go b/pkg/builder3/util.go index d977bdf90..a8a90fa15 100644 --- a/pkg/builder3/util.go +++ b/pkg/builder3/util.go @@ -19,17 +19,17 @@ package builder3 import ( "sort" - "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" ) -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(), } } diff --git a/pkg/common/common.go b/pkg/common/common.go index cf1d01f90..4956b3a79 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -92,8 +93,14 @@ type Config struct { GetDefinitions GetOpenAPIDefinitions // GetOperationIDAndTags returns operation id and tags for a restful route. It is an optional function to customize operation IDs. + // + // Deprecated: GetOperationIDAndTagsFromRoute should be used instead. This cannot be specified if using the new Route + // interface set of funcs. GetOperationIDAndTags func(r *restful.Route) (string, []string, error) + // GetOperationIDAndTagsFromRoute returns operation id and tags for a Route. It is an optional function to customize operation IDs. + GetOperationIDAndTagsFromRoute 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. GetDefinitionName func(name string) (string, spec.Extensions) diff --git a/pkg/common/interfaces.go b/pkg/common/interfaces.go new file mode 100644 index 000000000..059fc551b --- /dev/null +++ b/pkg/common/interfaces.go @@ -0,0 +1,88 @@ +package common + +// RouteContainer is the entrypoint for a service, which may contain multiple +// routes under a common path with a common set of path 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 a 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(s). + // Multiple responses with the same HTTP Status Code are acceptable. + StatusCodeResponses() []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 + + // UnknownParameterKind indicates the request parameter type has not been specified. + UnknownParameterKind +) diff --git a/pkg/common/restfuladapter/adapter.go b/pkg/common/restfuladapter/adapter.go new file mode 100644 index 000000000..e59697a58 --- /dev/null +++ b/pkg/common/restfuladapter/adapter.go @@ -0,0 +1,16 @@ +package restfuladapter + +import ( + "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" +) + +// AdaptWebServices adapts a slice of restful.WebService into the common interfaces. +func AdaptWebServices(webServices []*restful.WebService) []common.RouteContainer { + var containers []common.RouteContainer + for _, ws := range webServices { + containers = append(containers, &WebServiceAdapter{ws}) + } + return containers +} + diff --git a/pkg/common/restfuladapter/param_adapter.go b/pkg/common/restfuladapter/param_adapter.go new file mode 100644 index 000000000..be0b45650 --- /dev/null +++ b/pkg/common/restfuladapter/param_adapter.go @@ -0,0 +1,54 @@ +package restfuladapter + +import ( + "encoding/json" + "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" +) + +var _ common.Parameter = &ParamAdapter{} + +type ParamAdapter struct { + Param *restful.Parameter +} + +func (r *ParamAdapter) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Param) +} + +func (r *ParamAdapter) Name() string { + return r.Param.Data().Name +} + +func (r *ParamAdapter) Description() string { + return r.Param.Data().Description +} + +func (r *ParamAdapter) Required() bool { + return r.Param.Data().Required +} + +func (r *ParamAdapter) Kind() common.ParameterKind { + switch r.Param.Kind() { + case restful.PathParameterKind: + return common.PathParameterKind + case restful.QueryParameterKind: + return common.QueryParameterKind + case restful.BodyParameterKind: + return common.BodyParameterKind + case restful.HeaderParameterKind: + return common.HeaderParameterKind + case restful.FormParameterKind: + return common.FormParameterKind + default: + return common.UnknownParameterKind + } +} + +func (r *ParamAdapter) DataType() string { + return r.Param.Data().DataType +} + +func (r *ParamAdapter) AllowMultiple() bool { + return r.Param.Data().AllowMultiple +} diff --git a/pkg/common/restfuladapter/response_error_adapter.go b/pkg/common/restfuladapter/response_error_adapter.go new file mode 100644 index 000000000..019cfb8d9 --- /dev/null +++ b/pkg/common/restfuladapter/response_error_adapter.go @@ -0,0 +1,25 @@ +package restfuladapter + +import ( + "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" +) + +var _ common.StatusCodeResponse = &ResponseErrorAdapter{} + +// ResponseErrorAdapter adapts a restful.ResponseError to common.StatusCodeResponse. +type ResponseErrorAdapter struct { + Err *restful.ResponseError +} + +func (r *ResponseErrorAdapter) Message() string { + return r.Err.Message +} + +func (r *ResponseErrorAdapter) Model() interface{} { + return r.Err.Model +} + +func (r *ResponseErrorAdapter) Code() int { + return r.Err.Code +} diff --git a/pkg/common/restfuladapter/route_adapter.go b/pkg/common/restfuladapter/route_adapter.go new file mode 100644 index 000000000..0622319c3 --- /dev/null +++ b/pkg/common/restfuladapter/route_adapter.go @@ -0,0 +1,68 @@ +package restfuladapter + +import ( + "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" +) + +var _ common.Route = &RouteAdapter{} + +// RouteAdapter adapts a restful.Route to common.Route. +type RouteAdapter struct { + Route *restful.Route +} + +func (r *RouteAdapter) StatusCodeResponses() []common.StatusCodeResponse { + // go-restful uses the ResponseErrors field to contain both error and regular responses. + var responses []common.StatusCodeResponse + for _, res := range r.Route.ResponseErrors { + localRes := res + responses = append(responses, &ResponseErrorAdapter{&localRes}) + } + + return responses +} + +func (r *RouteAdapter) OperationName() string { + return r.Route.Operation +} + +func (r *RouteAdapter) Method() string { + return r.Route.Method +} + +func (r *RouteAdapter) Path() string { + return r.Route.Path +} + +func (r *RouteAdapter) Parameters() []common.Parameter { + var params []common.Parameter + for _, rParam := range r.Route.ParameterDocs { + params = append(params, &ParamAdapter{rParam}) + } + return params +} + +func (r *RouteAdapter) Description() string { + return r.Route.Doc +} + +func (r *RouteAdapter) Consumes() []string { + return r.Route.Consumes +} + +func (r *RouteAdapter) Produces() []string { + return r.Route.Produces +} + +func (r *RouteAdapter) Metadata() map[string]interface{} { + return r.Route.Metadata +} + +func (r *RouteAdapter) RequestPayloadSample() interface{} { + return r.Route.ReadSample +} + +func (r *RouteAdapter) ResponsePayloadSample() interface{} { + return r.Route.WriteSample +} diff --git a/pkg/common/restfuladapter/webservice_adapter.go b/pkg/common/restfuladapter/webservice_adapter.go new file mode 100644 index 000000000..e8ee6dee7 --- /dev/null +++ b/pkg/common/restfuladapter/webservice_adapter.go @@ -0,0 +1,34 @@ +package restfuladapter + +import ( + "github.com/emicklei/go-restful" + "k8s.io/kube-openapi/pkg/common" +) + +var _ common.RouteContainer = &WebServiceAdapter{} + +// WebServiceAdapter adapts a restful.WebService to common.RouteContainer. +type WebServiceAdapter struct { + WebService *restful.WebService +} + +func (r *WebServiceAdapter) RootPath() string { + return r.WebService.RootPath() +} + +func (r *WebServiceAdapter) PathParameters() []common.Parameter { + var params []common.Parameter + for _, rParam := range r.WebService.PathParameters() { + params = append(params, &ParamAdapter{rParam}) + } + return params +} + +func (r *WebServiceAdapter) Routes() []common.Route { + var routes []common.Route + for _, rRoute := range r.WebService.Routes() { + localRoute := rRoute + routes = append(routes, &RouteAdapter{&localRoute}) + } + return routes +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 7338094cb..48cbdc470 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -36,6 +36,7 @@ import ( klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/builder" "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -276,8 +277,16 @@ func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handl // BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it. // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService. +// +// Deprecated: BuildAndRegisterOpenAPIVersionedServiceFromRoutes should be used instead. func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) { - spec, err := builder.BuildOpenAPISpec(webServices, config) + return BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath, restfuladapter.AdaptWebServices(webServices), config, handler) +} + +// BuildAndRegisterOpenAPIVersionedServiceFromRoutes builds the spec and registers a handler to provide access to it. +// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService. +func BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath string, routeContainers []common.RouteContainer, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) { + spec, err := builder.BuildOpenAPISpecFromRoutes(routeContainers, config) if err != nil { return nil, err } diff --git a/test/integration/README.md b/test/integration/README.md index e5d3c7445..fd61d00ad 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -19,7 +19,7 @@ on API rule violations. ```bash $ go run ../../cmd/openapi-gen/openapi-gen.go \ - -i "k8s.io/kube-openapi/test/integration/testdata/custom,k8s.io/kube-openapi/test/integration/testdata/listtype,k8s.io/kube-openapi/test/integration/testdata/maptype,k8s.io/kube-openapi/test/integration/testdata/structtype,k8s.io/kube-openapi/test/integration/testdata/dummytype,k8s.io/kube-openapi/test/integration/testdata/uniontype,k8s.io/kube-openapi/test/integration/testdata/defaults" \ + -i "k8s.io/kube-openapi/test/integration/testdata/custom,k8s.io/kube-openapi/test/integration/testdata/enumtype,k8s.io/kube-openapi/test/integration/testdata/listtype,k8s.io/kube-openapi/test/integration/testdata/maptype,k8s.io/kube-openapi/test/integration/testdata/structtype,k8s.io/kube-openapi/test/integration/testdata/dummytype,k8s.io/kube-openapi/test/integration/testdata/uniontype,k8s.io/kube-openapi/test/integration/testdata/defaults" \ -o pkg \ -p generated \ -O openapi_generated \ @@ -42,10 +42,14 @@ If you've created a new type, make sure you add it in `createWebServices()` in --- **NOTE:** -If you've created a new package, make sure you also add it to to the +If you've created a new package, make sure you also add it to the `inputDir` in `integration_suite_test.go`. --- ```bash $ go run builder/main.go testdata/golden.v2.json ``` + +```bash +$ go run builder3/main.go testdata/golden.v3.json +``` diff --git a/test/integration/integration_suite_test.go b/test/integration/integration_suite_test.go index 13c55af82..fe3c9410a 100644 --- a/test/integration/integration_suite_test.go +++ b/test/integration/integration_suite_test.go @@ -29,9 +29,10 @@ import ( ) const ( - testdataDir = "./testdata" - testPkgDir = "k8s.io/kube-openapi/test/integration/testdata" - inputDir = testPkgDir + "/listtype" + + headerFilePath = "../../boilerplate/boilerplate.go.txt" + testdataDir = "./testdata" + testPkgDir = "k8s.io/kube-openapi/test/integration/testdata" + inputDir = testPkgDir + "/listtype" + "," + testPkgDir + "/maptype" + "," + testPkgDir + "/structtype" + "," + testPkgDir + "/dummytype" + @@ -39,17 +40,17 @@ const ( "," + testPkgDir + "/enumtype" + "," + testPkgDir + "/custom" + "," + testPkgDir + "/defaults" - outputBase = "pkg" - outputPackage = "generated" - outputBaseFileName = "openapi_generated" - generatedSwaggerFileName = "generated.v2.json" - generatedReportFileName = "generated.v2.report" - goldenSwaggerFileName = "golden.v2.json" - goldenReportFileName = "golden.v2.report" + outputBase = "pkg" + outputPackage = "generated" + outputBaseFileName = "openapi_generated" + generatedSwaggerFileName = "generated.v2.json" + generatedReportFileName = "generated.v2.report" + goldenSwaggerFileName = "golden.v2.json" + goldenReportFileName = "golden.v2.report" generatedOpenAPIv3FileName = "generated.v3.json" goldenOpenAPIv3Filename = "golden.v3.json" - timeoutSeconds = 5.0 + timeoutSeconds = 10.0 ) func TestGenerators(t *testing.T) { @@ -81,9 +82,9 @@ var _ = Describe("Open API Definitions Generation", func() { // Build the OpenAPI code generator. By("building openapi-gen") - binary_path, berr := gexec.Build("../../cmd/openapi-gen/openapi-gen.go") + binaryPath, berr := gexec.Build("../../cmd/openapi-gen/openapi-gen.go") Expect(berr).ShouldNot(HaveOccurred()) - openAPIGenPath = binary_path + openAPIGenPath = binaryPath // Run the OpenAPI code generator, creating OpenAPIDefinition code // to be compiled into builder. @@ -95,6 +96,7 @@ var _ = Describe("Open API Definitions Generation", func() { "-p", outputPackage, "-O", outputBaseFileName, "-r", gr, + "-h", headerFilePath, ) command.Dir = workingDirectory session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) @@ -103,13 +105,13 @@ var _ = Describe("Open API Definitions Generation", func() { By("writing swagger v2.0") // Create the OpenAPI swagger builder. - binary_path, berr = gexec.Build("./builder/main.go") + binaryPath, berr = gexec.Build("./builder/main.go") Expect(berr).ShouldNot(HaveOccurred()) // Execute the builder, generating an OpenAPI swagger file with definitions. gs := generatedFile(generatedSwaggerFileName) By("writing swagger to " + gs) - command = exec.Command(binary_path, gs) + command = exec.Command(binaryPath, gs) command.Dir = workingDirectory session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) @@ -117,13 +119,13 @@ var _ = Describe("Open API Definitions Generation", func() { By("writing OpenAPI v3.0") // Create the OpenAPI swagger builder. - binary_path, berr = gexec.Build("./builder3/main.go") + binaryPath, berr = gexec.Build("./builder3/main.go") Expect(berr).ShouldNot(HaveOccurred()) // Execute the builder, generating an OpenAPI swagger file with definitions. gov3 := generatedFile(generatedOpenAPIv3FileName) By("writing swagger to " + gov3) - command = exec.Command(binary_path, gov3) + command = exec.Command(binaryPath, gov3) command.Dir = workingDirectory session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ShouldNot(HaveOccurred()) @@ -143,6 +145,7 @@ var _ = Describe("Open API Definitions Generation", func() { "-p", outputPackage, "-O", outputBaseFileName, "-r", testdataFile(goldenReportFileName), + "-h", headerFilePath, "--verify-only", ) command.Dir = workingDirectory diff --git a/test/integration/pkg/generated/openapi_generated.go b/test/integration/pkg/generated/openapi_generated.go index 12c2f23ba..fd76ec7ae 100644 --- a/test/integration/pkg/generated/openapi_generated.go +++ b/test/integration/pkg/generated/openapi_generated.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by openapi-gen.go. DO NOT EDIT. +// Code generated by openapi-gen. DO NOT EDIT. // This file was autogenerated by openapi-gen. Do not edit it manually! @@ -40,6 +40,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/kube-openapi/test/integration/testdata/dummytype.Bar": schema_test_integration_testdata_dummytype_Bar(ref), "k8s.io/kube-openapi/test/integration/testdata/dummytype.Baz": schema_test_integration_testdata_dummytype_Baz(ref), "k8s.io/kube-openapi/test/integration/testdata/dummytype.Foo": schema_test_integration_testdata_dummytype_Foo(ref), + "k8s.io/kube-openapi/test/integration/testdata/dummytype.StatusError": schema_test_integration_testdata_dummytype_StatusError(ref), "k8s.io/kube-openapi/test/integration/testdata/dummytype.Waldo": schema_test_integration_testdata_dummytype_Waldo(ref), "k8s.io/kube-openapi/test/integration/testdata/enumtype.FruitsBasket": schema_test_integration_testdata_enumtype_FruitsBasket(ref), "k8s.io/kube-openapi/test/integration/testdata/listtype.AtomicList": schema_test_integration_testdata_listtype_AtomicList(ref), @@ -251,6 +252,33 @@ func schema_test_integration_testdata_dummytype_Foo(ref common.ReferenceCallback } } +func schema_test_integration_testdata_dummytype_StatusError(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Code": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + "Message": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"Code", "Message"}, + }, + }, + } +} + func schema_test_integration_testdata_dummytype_Waldo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/test/integration/testdata/dummytype/alpha.go b/test/integration/testdata/dummytype/alpha.go index f3e50b4a5..6b2c348d0 100644 --- a/test/integration/testdata/dummytype/alpha.go +++ b/test/integration/testdata/dummytype/alpha.go @@ -34,3 +34,9 @@ type Baz struct { Violation bool ViolationBehind bool } + +// +k8s:openapi-gen=true +type StatusError struct { + Code int + Message string +} diff --git a/test/integration/testdata/golden.v2.json b/test/integration/testdata/golden.v2.json index d23da1051..978fd9b1c 100644 --- a/test/integration/testdata/golden.v2.json +++ b/test/integration/testdata/golden.v2.json @@ -5,6 +5,28 @@ "version": "1.0" }, "paths": { + "/test/custom": { + "post": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "create-custom.Bah", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/custom.Bah" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/test/custom/bac": { "get": { "produces": [ @@ -13,7 +35,7 @@ "schemes": [ "https" ], - "operationId": "func12", + "operationId": "get-custom.Bac", "responses": { "200": { "description": "OK", @@ -35,7 +57,7 @@ "schemes": [ "https" ], - "operationId": "func13", + "operationId": "get-custom.Bah", "responses": { "200": { "description": "OK", @@ -57,7 +79,7 @@ "schemes": [ "https" ], - "operationId": "func11", + "operationId": "get-custom.Bak", "responses": { "200": { "description": "OK", @@ -79,7 +101,7 @@ "schemes": [ "https" ], - "operationId": "func10", + "operationId": "get-custom.Bal", "responses": { "200": { "description": "OK", @@ -93,18 +115,18 @@ } } }, - "/test/defaults/defaulted": { - "get": { + "/test/defaults": { + "post": { "produces": [ "application/json" ], "schemes": [ "https" ], - "operationId": "func19", + "operationId": "create-defaults.Defaulted", "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/defaults.Defaulted" } @@ -115,7 +137,7 @@ } } }, - "/test/dummytype/bar": { + "/test/defaults/defaulted": { "get": { "produces": [ "application/json" @@ -123,12 +145,12 @@ "schemes": [ "https" ], - "operationId": "func2", + "operationId": "get-defaults.Defaulted", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/dummytype.Bar" + "$ref": "#/definitions/defaults.Defaulted" } }, "404": { @@ -137,29 +159,35 @@ } } }, - "/test/dummytype/baz": { - "get": { + "/test/dummytype": { + "post": { "produces": [ "application/json" ], "schemes": [ "https" ], - "operationId": "func3", + "operationId": "create-dummytype.Waldo", "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "$ref": "#/definitions/dummytype.Baz" + "$ref": "#/definitions/dummytype.Waldo" } }, "404": { "$ref": "#/responses/NotFound" + }, + "500": { + "description": "Internal Service Error", + "schema": { + "$ref": "#/definitions/dummytype.StatusError" + } } } } }, - "/test/dummytype/foo": { + "/test/dummytype/bar": { "get": { "produces": [ "application/json" @@ -167,16 +195,60 @@ "schemes": [ "https" ], - "operationId": "func1", + "operationId": "get-dummytype.Bar", "responses": { - "200": { - "description": "OK", + "404": { + "$ref": "#/responses/NotFound" + }, + "500": { + "description": "Internal Service Error", "schema": { - "$ref": "#/definitions/dummytype.Foo" + "$ref": "#/definitions/dummytype.StatusError" } + } + } + } + }, + "/test/dummytype/baz": { + "get": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "get-dummytype.Baz", + "responses": { + "404": { + "$ref": "#/responses/NotFound" }, + "500": { + "description": "Internal Service Error", + "schema": { + "$ref": "#/definitions/dummytype.StatusError" + } + } + } + } + }, + "/test/dummytype/foo": { + "get": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "get-dummytype.Foo", + "responses": { "404": { "$ref": "#/responses/NotFound" + }, + "500": { + "description": "Internal Service Error", + "schema": { + "$ref": "#/definitions/dummytype.StatusError" + } } } } @@ -189,12 +261,34 @@ "schemes": [ "https" ], - "operationId": "func4", + "operationId": "get-dummytype.Waldo", "responses": { - "200": { - "description": "OK", + "404": { + "$ref": "#/responses/NotFound" + }, + "500": { + "description": "Internal Service Error", "schema": { - "$ref": "#/definitions/dummytype.Waldo" + "$ref": "#/definitions/dummytype.StatusError" + } + } + } + } + }, + "/test/listtype": { + "post": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "create-listtype.SetList", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/listtype.SetList" } }, "404": { @@ -211,7 +305,7 @@ "schemes": [ "https" ], - "operationId": "func5", + "operationId": "get-listtype.AtomicList", "responses": { "200": { "description": "OK", @@ -233,7 +327,7 @@ "schemes": [ "https" ], - "operationId": "func6", + "operationId": "get-listtype.MapList", "responses": { "200": { "description": "OK", @@ -255,7 +349,7 @@ "schemes": [ "https" ], - "operationId": "func7", + "operationId": "get-listtype.SetList", "responses": { "200": { "description": "OK", @@ -269,6 +363,28 @@ } } }, + "/test/maptype": { + "post": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "create-maptype.AtomicMap", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/maptype.AtomicMap" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/test/maptype/atomicmap": { "get": { "produces": [ @@ -277,7 +393,7 @@ "schemes": [ "https" ], - "operationId": "func15", + "operationId": "get-maptype.AtomicMap", "responses": { "200": { "description": "OK", @@ -299,7 +415,7 @@ "schemes": [ "https" ], - "operationId": "func14", + "operationId": "get-maptype.GranularMap", "responses": { "200": { "description": "OK", @@ -313,6 +429,28 @@ } } }, + "/test/structtype": { + "post": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "create-structtype.DeclaredAtomicStruct", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/structtype.DeclaredAtomicStruct" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/test/structtype/atomicstruct": { "get": { "produces": [ @@ -321,7 +459,7 @@ "schemes": [ "https" ], - "operationId": "func17", + "operationId": "get-structtype.AtomicStruct", "responses": { "200": { "description": "OK", @@ -343,7 +481,7 @@ "schemes": [ "https" ], - "operationId": "func18", + "operationId": "get-structtype.DeclaredAtomicStruct", "responses": { "200": { "description": "OK", @@ -365,7 +503,7 @@ "schemes": [ "https" ], - "operationId": "func16", + "operationId": "get-structtype.GranularStruct", "responses": { "200": { "description": "OK", @@ -379,6 +517,28 @@ } } }, + "/test/uniontype": { + "post": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "create-uniontype.InlinedUnion", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/uniontype.InlinedUnion" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/test/uniontype/inlinedunion": { "get": { "produces": [ @@ -387,7 +547,7 @@ "schemes": [ "https" ], - "operationId": "func9", + "operationId": "get-uniontype.InlinedUnion", "responses": { "200": { "description": "OK", @@ -409,7 +569,7 @@ "schemes": [ "https" ], - "operationId": "func8", + "operationId": "get-uniontype.TopLevelUnion", "responses": { "200": { "description": "OK", @@ -560,6 +720,24 @@ } } }, + "dummytype.StatusError": { + "type": "object", + "required": [ + "Code", + "Message" + ], + "properties": { + "Code": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "Message": { + "type": "string", + "default": "" + } + } + }, "dummytype.Waldo": { "type": "object", "required": [ diff --git a/test/integration/testdata/golden.v2.report b/test/integration/testdata/golden.v2.report index e47a350c4..e00254ef3 100644 --- a/test/integration/testdata/golden.v2.report +++ b/test/integration/testdata/golden.v2.report @@ -14,6 +14,8 @@ API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/du API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,Baz,ViolationBehind API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,Foo,First API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,Foo,Second +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,StatusError,Code +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,StatusError,Message API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,Waldo,First API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/dummytype,Waldo,Second API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/listtype,AtomicList,Field diff --git a/test/integration/testdata/golden.v3.json b/test/integration/testdata/golden.v3.json index 0ecb51da9..0f95c22e6 100644 --- a/test/integration/testdata/golden.v3.json +++ b/test/integration/testdata/golden.v3.json @@ -5,9 +5,26 @@ "version": "1.0" }, "paths": { + "/test/custom": { + "post": { + "operationId": "create-custom.Bah", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/custom.Bah" + } + } + } + } + } + } + }, "/test/custom/bac": { "get": { - "operationId": "func12", + "operationId": "get-custom.Bac", "responses": { "200": { "description": "OK", @@ -24,7 +41,7 @@ }, "/test/custom/bah": { "get": { - "operationId": "func13", + "operationId": "get-custom.Bah", "responses": { "200": { "description": "OK", @@ -41,7 +58,7 @@ }, "/test/custom/bak": { "get": { - "operationId": "func11", + "operationId": "get-custom.Bak", "responses": { "200": { "description": "OK", @@ -58,7 +75,7 @@ }, "/test/custom/bal": { "get": { - "operationId": "func10", + "operationId": "get-custom.Bal", "responses": { "200": { "description": "OK", @@ -73,9 +90,26 @@ } } }, + "/test/defaults": { + "post": { + "operationId": "create-defaults.Defaulted", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/defaults.Defaulted" + } + } + } + } + } + } + }, "/test/defaults/defaulted": { "get": { - "operationId": "func19", + "operationId": "get-defaults.Defaulted", "responses": { "200": { "description": "OK", @@ -90,16 +124,43 @@ } } }, + "/test/dummytype": { + "post": { + "operationId": "create-dummytype.Waldo", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/dummytype.Waldo" + } + } + } + }, + "500": { + "description": "Internal Service Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/dummytype.StatusError" + } + } + } + } + } + } + }, "/test/dummytype/bar": { "get": { - "operationId": "func2", + "operationId": "get-dummytype.Bar", "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal Service Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/dummytype.Bar" + "$ref": "#/components/schemas/dummytype.StatusError" } } } @@ -109,14 +170,14 @@ }, "/test/dummytype/baz": { "get": { - "operationId": "func3", + "operationId": "get-dummytype.Baz", "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal Service Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/dummytype.Baz" + "$ref": "#/components/schemas/dummytype.StatusError" } } } @@ -126,14 +187,14 @@ }, "/test/dummytype/foo": { "get": { - "operationId": "func1", + "operationId": "get-dummytype.Foo", "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal Service Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/dummytype.Foo" + "$ref": "#/components/schemas/dummytype.StatusError" } } } @@ -143,14 +204,31 @@ }, "/test/dummytype/waldo": { "get": { - "operationId": "func4", + "operationId": "get-dummytype.Waldo", "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal Service Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/dummytype.Waldo" + "$ref": "#/components/schemas/dummytype.StatusError" + } + } + } + } + } + } + }, + "/test/listtype": { + "post": { + "operationId": "create-listtype.SetList", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/listtype.SetList" } } } @@ -160,7 +238,7 @@ }, "/test/listtype/atomiclist": { "get": { - "operationId": "func5", + "operationId": "get-listtype.AtomicList", "responses": { "200": { "description": "OK", @@ -177,7 +255,7 @@ }, "/test/listtype/maplist": { "get": { - "operationId": "func6", + "operationId": "get-listtype.MapList", "responses": { "200": { "description": "OK", @@ -194,7 +272,7 @@ }, "/test/listtype/setlist": { "get": { - "operationId": "func7", + "operationId": "get-listtype.SetList", "responses": { "200": { "description": "OK", @@ -209,9 +287,26 @@ } } }, + "/test/maptype": { + "post": { + "operationId": "create-maptype.AtomicMap", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/maptype.AtomicMap" + } + } + } + } + } + } + }, "/test/maptype/atomicmap": { "get": { - "operationId": "func15", + "operationId": "get-maptype.AtomicMap", "responses": { "200": { "description": "OK", @@ -228,7 +323,7 @@ }, "/test/maptype/granularmap": { "get": { - "operationId": "func14", + "operationId": "get-maptype.GranularMap", "responses": { "200": { "description": "OK", @@ -243,9 +338,26 @@ } } }, + "/test/structtype": { + "post": { + "operationId": "create-structtype.DeclaredAtomicStruct", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/structtype.DeclaredAtomicStruct" + } + } + } + } + } + } + }, "/test/structtype/atomicstruct": { "get": { - "operationId": "func17", + "operationId": "get-structtype.AtomicStruct", "responses": { "200": { "description": "OK", @@ -262,7 +374,7 @@ }, "/test/structtype/declaredatomicstruct": { "get": { - "operationId": "func18", + "operationId": "get-structtype.DeclaredAtomicStruct", "responses": { "200": { "description": "OK", @@ -279,7 +391,7 @@ }, "/test/structtype/granularstruct": { "get": { - "operationId": "func16", + "operationId": "get-structtype.GranularStruct", "responses": { "200": { "description": "OK", @@ -294,9 +406,26 @@ } } }, + "/test/uniontype": { + "post": { + "operationId": "create-uniontype.InlinedUnion", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/uniontype.InlinedUnion" + } + } + } + } + } + } + }, "/test/uniontype/inlinedunion": { "get": { - "operationId": "func9", + "operationId": "get-uniontype.InlinedUnion", "responses": { "200": { "description": "OK", @@ -313,7 +442,7 @@ }, "/test/uniontype/toplevelunion": { "get": { - "operationId": "func8", + "operationId": "get-uniontype.TopLevelUnion", "responses": { "200": { "description": "OK", @@ -472,6 +601,24 @@ } } }, + "dummytype.StatusError": { + "type": "object", + "required": [ + "Code", + "Message" + ], + "properties": { + "Code": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "Message": { + "type": "string", + "default": "" + } + } + }, "dummytype.Waldo": { "type": "object", "required": [ diff --git a/test/integration/testutil/testutil.go b/test/integration/testutil/testutil.go index 791fa9a0c..ac23a9b1f 100644 --- a/test/integration/testutil/testutil.go +++ b/test/integration/testutil/testutil.go @@ -51,32 +51,38 @@ func CreateOpenAPIBuilderConfig() *common.Config { } } -// CreateWebServices hard-codes a simple WebService which only defines a GET path +// CreateWebServices hard-codes a simple WebService which only defines a GET and POST paths // for testing. func CreateWebServices() []*restful.WebService { w := new(restful.WebService) - w.Route(buildRouteForType(w, "dummytype", "Foo")) - w.Route(buildRouteForType(w, "dummytype", "Bar")) - w.Route(buildRouteForType(w, "dummytype", "Baz")) - w.Route(buildRouteForType(w, "dummytype", "Waldo")) - w.Route(buildRouteForType(w, "listtype", "AtomicList")) - w.Route(buildRouteForType(w, "listtype", "MapList")) - w.Route(buildRouteForType(w, "listtype", "SetList")) - w.Route(buildRouteForType(w, "uniontype", "TopLevelUnion")) - w.Route(buildRouteForType(w, "uniontype", "InlinedUnion")) - w.Route(buildRouteForType(w, "custom", "Bal")) - w.Route(buildRouteForType(w, "custom", "Bak")) - w.Route(buildRouteForType(w, "custom", "Bac")) - w.Route(buildRouteForType(w, "custom", "Bah")) - w.Route(buildRouteForType(w, "maptype", "GranularMap")) - w.Route(buildRouteForType(w, "maptype", "AtomicMap")) - w.Route(buildRouteForType(w, "structtype", "GranularStruct")) - w.Route(buildRouteForType(w, "structtype", "AtomicStruct")) - w.Route(buildRouteForType(w, "structtype", "DeclaredAtomicStruct")) - w.Route(buildRouteForType(w, "defaults", "Defaulted")) + addRoutes(w, buildRouteForType(w, "dummytype", "Foo")...) + addRoutes(w, buildRouteForType(w, "dummytype", "Bar")...) + addRoutes(w, buildRouteForType(w, "dummytype", "Baz")...) + addRoutes(w, buildRouteForType(w, "dummytype", "Waldo")...) + addRoutes(w, buildRouteForType(w, "listtype", "AtomicList")...) + addRoutes(w, buildRouteForType(w, "listtype", "MapList")...) + addRoutes(w, buildRouteForType(w, "listtype", "SetList")...) + addRoutes(w, buildRouteForType(w, "uniontype", "TopLevelUnion")...) + addRoutes(w, buildRouteForType(w, "uniontype", "InlinedUnion")...) + addRoutes(w, buildRouteForType(w, "custom", "Bal")...) + addRoutes(w, buildRouteForType(w, "custom", "Bak")...) + addRoutes(w, buildRouteForType(w, "custom", "Bac")...) + addRoutes(w, buildRouteForType(w, "custom", "Bah")...) + addRoutes(w, buildRouteForType(w, "maptype", "GranularMap")...) + addRoutes(w, buildRouteForType(w, "maptype", "AtomicMap")...) + addRoutes(w, buildRouteForType(w, "structtype", "GranularStruct")...) + addRoutes(w, buildRouteForType(w, "structtype", "AtomicStruct")...) + addRoutes(w, buildRouteForType(w, "structtype", "DeclaredAtomicStruct")...) + addRoutes(w, buildRouteForType(w, "defaults", "Defaulted")...) return []*restful.WebService{w} } +func addRoutes(ws *restful.WebService, routes ...*restful.RouteBuilder) { + for _, r := range routes { + ws.Route(r) + } +} + // Implements OpenAPICanonicalTypeNamer var _ = util.OpenAPICanonicalTypeNamer(&typeNamer{}) @@ -89,13 +95,36 @@ func (t *typeNamer) OpenAPICanonicalTypeName() string { return fmt.Sprintf("k8s.io/kube-openapi/test/integration/testdata/%s.%s", t.pkg, t.name) } -func buildRouteForType(ws *restful.WebService, pkg, name string) *restful.RouteBuilder { +func buildRouteForType(ws *restful.WebService, pkg, name string) []*restful.RouteBuilder { namer := typeNamer{ pkg: pkg, name: name, } - return ws.GET(fmt.Sprintf("test/%s/%s", pkg, strings.ToLower(name))). - Produces("application/json"). - To(func(*restful.Request, *restful.Response) {}). - Writes(&namer) + + routes := []*restful.RouteBuilder{ + ws.GET(fmt.Sprintf("test/%s/%s", pkg, strings.ToLower(name))). + Operation(fmt.Sprintf("get-%s.%s", pkg, name)). + Produces("application/json"). + To(func(*restful.Request, *restful.Response) {}). + Writes(&namer), + ws.POST(fmt.Sprintf("test/%s", pkg)). + Operation(fmt.Sprintf("create-%s.%s", pkg, name)). + Produces("application/json"). + To(func(*restful.Request, *restful.Response) {}). + Returns(201, "Created", &namer). + Writes(&namer), + } + + if pkg == "dummytype" { + statusErrType := typeNamer{ + pkg: "dummytype", + name: "StatusError", + } + + for _, route := range routes { + route.Returns(500, "Internal Service Error", &statusErrType) + } + } + + return routes }