mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Merge pull request #33629 from mbohlool/o2
Automatic merge from submit-queue Generate unique Operation IDs for root OpenAPI spec This PR adds a customization method GetOperationID to OpenAPI spec generation and then use it to make sure root spec has unique operation IDs by mostly adding GroupVersion to the start of operation ID.
This commit is contained in:
commit
adfbe8d952
File diff suppressed because it is too large
Load Diff
@ -1245,6 +1245,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "readNamespacedDeploymentsScale",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -1267,6 +1268,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "replaceNamespacedDeploymentsScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@ -1301,6 +1303,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "patchNamespacedDeploymentsScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@ -3527,6 +3530,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "readNamespacedReplicasetsScale",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -3549,6 +3553,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "replaceNamespacedReplicasetsScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@ -3583,6 +3588,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "patchNamespacedReplicasetsScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@ -3760,6 +3766,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "readNamespacedReplicationcontrollersScale",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -3782,6 +3789,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "replaceNamespacedReplicationcontrollersScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@ -3816,6 +3824,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "patchNamespacedReplicationcontrollersScale",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
|
@ -42,13 +42,14 @@ import (
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/apiserver"
|
||||
"k8s.io/kubernetes/pkg/apiserver/authenticator"
|
||||
"k8s.io/kubernetes/pkg/apiserver/openapi"
|
||||
authorizerunion "k8s.io/kubernetes/pkg/auth/authorizer/union"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/capabilities"
|
||||
"k8s.io/kubernetes/pkg/cloudprovider"
|
||||
"k8s.io/kubernetes/pkg/controller/informers"
|
||||
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
||||
"k8s.io/kubernetes/pkg/generated/openapi"
|
||||
generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi"
|
||||
"k8s.io/kubernetes/pkg/genericapiserver"
|
||||
"k8s.io/kubernetes/pkg/genericapiserver/authorizer"
|
||||
genericoptions "k8s.io/kubernetes/pkg/genericapiserver/options"
|
||||
@ -334,8 +335,9 @@ func Run(s *options.APIServer) error {
|
||||
genericConfig.ProxyDialer = proxyDialerFn
|
||||
genericConfig.ProxyTLSClientConfig = proxyTLSClientConfig
|
||||
genericConfig.Serializer = api.Codecs
|
||||
genericConfig.OpenAPIInfo.Title = "Kubernetes"
|
||||
genericConfig.OpenAPIDefinitions = openapi.OpenAPIDefinitions
|
||||
genericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
|
||||
genericConfig.OpenAPIConfig.Definitions = generatedopenapi.OpenAPIDefinitions
|
||||
genericConfig.OpenAPIConfig.GetOperationID = openapi.GetOperationID
|
||||
genericConfig.EnableOpenAPISupport = true
|
||||
|
||||
config := &master.Config{
|
||||
|
@ -21,6 +21,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getCoreAPIVersions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -47,6 +48,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getCoreV1APIResources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -2748,6 +2750,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getAPIVersions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -2774,6 +2777,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getExtensionsAPIGroup",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -2800,6 +2804,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getExtensionsV1beta1APIResources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -4464,6 +4469,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getFederationAPIGroup",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -4490,6 +4496,7 @@
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"operationId": "getFederationV1beta1APIResources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/apiserver/authenticator"
|
||||
apiserveropenapi "k8s.io/kubernetes/pkg/apiserver/openapi"
|
||||
authorizerunion "k8s.io/kubernetes/pkg/auth/authorizer/union"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/controller/informers"
|
||||
@ -221,7 +222,10 @@ func Run(s *options.ServerRunOptions) error {
|
||||
genericConfig.APIResourceConfigSource = storageFactory.APIResourceConfigSource
|
||||
genericConfig.MasterServiceNamespace = s.MasterServiceNamespace
|
||||
genericConfig.Serializer = api.Codecs
|
||||
genericConfig.OpenAPIDefinitions = openapi.OpenAPIDefinitions
|
||||
genericConfig.OpenAPIConfig.Definitions = openapi.OpenAPIDefinitions
|
||||
// Reusing api-server's GetOperationID function. if federation and api-server spec diverge and
|
||||
// this method does not provide good operation IDs for federation, we should create federation's own GetOperationID.
|
||||
genericConfig.OpenAPIConfig.GetOperationID = apiserveropenapi.GetOperationID
|
||||
genericConfig.EnableOpenAPISupport = true
|
||||
|
||||
// TODO: Move this to generic api server (Need to move the command line flag).
|
||||
|
@ -77,6 +77,7 @@ pkg/apis/rbac/install
|
||||
pkg/apis/storage/install
|
||||
pkg/apis/storage/validation
|
||||
pkg/apiserver/audit
|
||||
pkg/apiserver/openapi
|
||||
pkg/auth/authenticator
|
||||
pkg/auth/authorizer/union
|
||||
pkg/client/metrics
|
||||
|
81
pkg/apiserver/openapi/openapi.go
Normal file
81
pkg/apiserver/openapi/openapi.go
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
var verbs = util.CreateTrie([]string{"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch"})
|
||||
|
||||
// ToValidOperationID makes an string a valid op ID (e.g. removing punctuations and whitespaces and make it camel case)
|
||||
func ToValidOperationID(s string, capitalizeFirstLetter bool) string {
|
||||
var buffer bytes.Buffer
|
||||
capitalize := capitalizeFirstLetter
|
||||
for i, r := range s {
|
||||
if unicode.IsLetter(r) || r == '_' || (i != 0 && unicode.IsDigit(r)) {
|
||||
if capitalize {
|
||||
buffer.WriteRune(unicode.ToUpper(r))
|
||||
capitalize = false
|
||||
} else {
|
||||
buffer.WriteRune(r)
|
||||
}
|
||||
} else {
|
||||
capitalize = true
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// GetOperationID returns a customize operation ID for kubernetes API server's OpenAPI spec to prevent duplicate IDs.
|
||||
func GetOperationID(servePath string, r *restful.Route) (string, error) {
|
||||
op := r.Operation
|
||||
path := r.Path
|
||||
// TODO: This is hacky, figure out where this name conflict is created and fix it at the root.
|
||||
if strings.HasPrefix(path, "/apis/extensions/v1beta1/namespaces/{namespace}/") && strings.HasSuffix(op, "ScaleScale") {
|
||||
op = op[:len(op)-10] + strings.Title(strings.Split(path[48:], "/")[0]) + "Scale"
|
||||
}
|
||||
switch servePath {
|
||||
case "/swagger.json":
|
||||
prefix, exists := verbs.GetPrefix(op)
|
||||
if !exists {
|
||||
return op, fmt.Errorf("operation names should start with a verb. Cannot determine operation verb from %v", op)
|
||||
}
|
||||
op = op[len(prefix):]
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
// Assume /api is /apis/core, remove this when we actually server /api/... on /apis/core/...
|
||||
if len(parts) >= 1 && parts[0] == "api" {
|
||||
parts = append([]string{"apis", "core"}, parts[1:]...)
|
||||
}
|
||||
if len(parts) >= 2 && parts[0] == "apis" {
|
||||
prefix = prefix + ToValidOperationID(strings.TrimSuffix(parts[1], ".k8s.io"), prefix != "")
|
||||
if len(parts) > 2 {
|
||||
prefix = prefix + ToValidOperationID(parts[2], prefix != "")
|
||||
}
|
||||
}
|
||||
return prefix + ToValidOperationID(op, prefix != ""), nil
|
||||
default:
|
||||
return op, nil
|
||||
}
|
||||
}
|
@ -156,15 +156,8 @@ type Config struct {
|
||||
// EnableOpenAPISupport enables OpenAPI support. Allow downstream customers to disable OpenAPI spec.
|
||||
EnableOpenAPISupport bool
|
||||
|
||||
// OpenAPIInfo will be directly available as Info section of Open API spec.
|
||||
OpenAPIInfo spec.Info
|
||||
|
||||
// OpenAPIDefaultResponse will be used if an web service operation does not have any responses listed.
|
||||
OpenAPIDefaultResponse spec.Response
|
||||
|
||||
// OpenAPIDefinitions is a map of type to OpenAPI spec for all types used in this API server. Failure to provide
|
||||
// this map or any of the models used by the server APIs will result in spec generation failure.
|
||||
OpenAPIDefinitions *common.OpenAPIDefinitions
|
||||
// OpenAPIConfig will be used in generating OpenAPI spec.
|
||||
OpenAPIConfig *common.Config
|
||||
|
||||
// MaxRequestsInFlight is the maximum number of parallel non-long-running requests. Every further
|
||||
// request has to wait.
|
||||
@ -253,13 +246,19 @@ func NewConfig(options *options.ServerRunOptions) *Config {
|
||||
ReadWritePort: options.SecurePort,
|
||||
ServiceClusterIPRange: &options.ServiceClusterIPRange,
|
||||
ServiceNodePortRange: options.ServiceNodePortRange,
|
||||
OpenAPIDefaultResponse: spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "Default Response."}},
|
||||
OpenAPIInfo: spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "Generic API Server",
|
||||
Version: "unversioned",
|
||||
OpenAPIConfig: &common.Config{
|
||||
ProtocolList: []string{"https"},
|
||||
IgnorePrefixes: []string{"/swaggerapi"},
|
||||
Info: &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "Generic API Server",
|
||||
Version: "unversioned",
|
||||
},
|
||||
},
|
||||
DefaultResponse: &spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "Default Response.",
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxRequestsInFlight: options.MaxRequestsInFlight,
|
||||
@ -386,10 +385,8 @@ func (c completedConfig) New() (*GenericAPIServer, error) {
|
||||
KubernetesServiceNodePort: c.KubernetesServiceNodePort,
|
||||
apiGroupsForDiscovery: map[string]unversioned.APIGroup{},
|
||||
|
||||
enableOpenAPISupport: c.EnableOpenAPISupport,
|
||||
openAPIInfo: c.OpenAPIInfo,
|
||||
openAPIDefaultResponse: c.OpenAPIDefaultResponse,
|
||||
openAPIDefinitions: c.OpenAPIDefinitions,
|
||||
enableOpenAPISupport: c.EnableOpenAPISupport,
|
||||
openAPIConfig: c.OpenAPIConfig,
|
||||
}
|
||||
|
||||
s.HandlerContainer = mux.NewAPIContainer(http.NewServeMux(), c.Serializer)
|
||||
|
@ -31,9 +31,9 @@ import (
|
||||
systemd "github.com/coreos/go-systemd/daemon"
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/emicklei/go-restful/swagger"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"k8s.io/kubernetes/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/rest"
|
||||
@ -147,6 +147,11 @@ type GenericAPIServer struct {
|
||||
apiGroupsForDiscoveryLock sync.RWMutex
|
||||
apiGroupsForDiscovery map[string]unversioned.APIGroup
|
||||
|
||||
// See Config.$name for documentation of these flags
|
||||
|
||||
enableOpenAPISupport bool
|
||||
openAPIConfig *common.Config
|
||||
|
||||
// PostStartHooks are each called after the server has started listening, in a separate go func for each
|
||||
// with no guaranteee of ordering between them. The map key is a name used for error reporting.
|
||||
// It may kill the process with a panic if it wishes to by returning an error
|
||||
@ -156,7 +161,6 @@ type GenericAPIServer struct {
|
||||
|
||||
// See Config.$name for documentation of these flags:
|
||||
|
||||
enableOpenAPISupport bool
|
||||
openAPIInfo spec.Info
|
||||
openAPIDefaultResponse spec.Response
|
||||
openAPIDefinitions *common.OpenAPIDefinitions
|
||||
@ -462,33 +466,20 @@ func (s *GenericAPIServer) InstallOpenAPI() {
|
||||
// Install one spec per web service, an ideal client will have a ClientSet containing one client
|
||||
// per each of these specs.
|
||||
for _, w := range s.HandlerContainer.RegisteredWebServices() {
|
||||
if w.RootPath() == "/swaggerapi" {
|
||||
if strings.HasPrefix(w.RootPath(), "/swaggerapi") {
|
||||
continue
|
||||
}
|
||||
info := s.openAPIInfo
|
||||
info.Title = info.Title + " " + w.RootPath()
|
||||
err := openapi.RegisterOpenAPIService(&openapi.Config{
|
||||
OpenAPIServePath: w.RootPath() + "/swagger.json",
|
||||
WebServices: []*restful.WebService{w},
|
||||
ProtocolList: []string{"https"},
|
||||
IgnorePrefixes: []string{"/swaggerapi"},
|
||||
Info: &info,
|
||||
DefaultResponse: &s.openAPIDefaultResponse,
|
||||
OpenAPIDefinitions: s.openAPIDefinitions,
|
||||
}, s.HandlerContainer.Container)
|
||||
config := *s.openAPIConfig
|
||||
config.Info = new(spec.Info)
|
||||
*config.Info = *s.openAPIConfig.Info
|
||||
config.Info.Title = config.Info.Title + " " + w.RootPath()
|
||||
err := openapi.RegisterOpenAPIService(w.RootPath()+"/swagger.json", []*restful.WebService{w}, &config, s.HandlerContainer.Container)
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to register open api spec for %v: %v", w.RootPath(), err)
|
||||
}
|
||||
}
|
||||
err := openapi.RegisterOpenAPIService(&openapi.Config{
|
||||
OpenAPIServePath: "/swagger.json",
|
||||
WebServices: s.HandlerContainer.RegisteredWebServices(),
|
||||
ProtocolList: []string{"https"},
|
||||
IgnorePrefixes: []string{"/swaggerapi"},
|
||||
Info: &s.openAPIInfo,
|
||||
DefaultResponse: &s.openAPIDefaultResponse,
|
||||
OpenAPIDefinitions: s.openAPIDefinitions,
|
||||
}, s.HandlerContainer.Container)
|
||||
err := openapi.RegisterOpenAPIService("/swagger.json", s.HandlerContainer.RegisteredWebServices(), s.openAPIConfig, s.HandlerContainer.Container)
|
||||
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to register open api spec for root: %v", err)
|
||||
}
|
||||
|
@ -16,7 +16,10 @@ limitations under the License.
|
||||
|
||||
package common
|
||||
|
||||
import "github.com/go-openapi/spec"
|
||||
import (
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/go-openapi/spec"
|
||||
)
|
||||
|
||||
// OpenAPIDefinition describes single type. Normally these definitions are auto-generated using gen-openapi.
|
||||
type OpenAPIDefinition struct {
|
||||
@ -35,6 +38,27 @@ type OpenAPIDefinitionGetter interface {
|
||||
OpenAPIDefinition() *OpenAPIDefinition
|
||||
}
|
||||
|
||||
// Config is set of configuration for openAPI spec generation.
|
||||
type Config struct {
|
||||
// List of supported protocols such as https, http, etc.
|
||||
ProtocolList []string
|
||||
|
||||
// Info is general information about the API.
|
||||
Info *spec.Info
|
||||
// DefaultResponse will be used if an operation does not have any responses listed. It
|
||||
// will show up as ... "responses" : {"default" : $DefaultResponse} in the spec.
|
||||
DefaultResponse *spec.Response
|
||||
// List of webservice's path prefixes to ignore
|
||||
IgnorePrefixes []string
|
||||
|
||||
// OpenAPIDefinitions should provide definition for all models used by routes. Failure to provide this map
|
||||
// or any of the models will result in spec generation failure.
|
||||
Definitions *OpenAPIDefinitions
|
||||
|
||||
// GetOperationID returns operation id for a restful route. It is an optional function to customize operation IDs.
|
||||
GetOperationID func(servePath string, r *restful.Route) (string, error)
|
||||
}
|
||||
|
||||
// This function is a reference for converting go (or any custom type) to a simple open API type,format pair. There are
|
||||
// two ways to customize spec for a type. If you add it here, a type will be converted to a simple type and the type
|
||||
// comment (the comment that is added before type definition) will be lost. The spec will still have the property
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
"k8s.io/kubernetes/pkg/genericapiserver/openapi/common"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
"k8s.io/kubernetes/pkg/util/json"
|
||||
)
|
||||
|
||||
@ -33,39 +34,18 @@ const (
|
||||
OpenAPIVersion = "2.0"
|
||||
)
|
||||
|
||||
// Config is set of configuration for openAPI spec generation.
|
||||
type Config struct {
|
||||
// Path to the spec file. by convention, it should name [.*/]*/swagger.json
|
||||
OpenAPIServePath string
|
||||
// List of web services for this API spec
|
||||
WebServices []*restful.WebService
|
||||
|
||||
// List of supported protocols such as https, http, etc.
|
||||
ProtocolList []string
|
||||
|
||||
// Info is general information about the API.
|
||||
Info *spec.Info
|
||||
// DefaultResponse will be used if an operation does not have any responses listed. It
|
||||
// will show up as ... "responses" : {"default" : $DefaultResponse} in the spec.
|
||||
DefaultResponse *spec.Response
|
||||
// List of webservice's path prefixes to ignore
|
||||
IgnorePrefixes []string
|
||||
|
||||
// OpenAPIDefinitions should provide definition for all models used by routes. Failure to provide this map
|
||||
// or any of the models will result in spec generation failure.
|
||||
OpenAPIDefinitions *common.OpenAPIDefinitions
|
||||
}
|
||||
|
||||
type openAPI struct {
|
||||
config *Config
|
||||
config *common.Config
|
||||
swagger *spec.Swagger
|
||||
protocolList []string
|
||||
servePath string
|
||||
}
|
||||
|
||||
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification.
|
||||
func RegisterOpenAPIService(config *Config, containers *restful.Container) (err error) {
|
||||
func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *common.Config, containers *restful.Container) (err error) {
|
||||
o := openAPI{
|
||||
config: config,
|
||||
config: config,
|
||||
servePath: servePath,
|
||||
swagger: &spec.Swagger{
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Swagger: OpenAPIVersion,
|
||||
@ -76,14 +56,14 @@ func RegisterOpenAPIService(config *Config, containers *restful.Container) (err
|
||||
},
|
||||
}
|
||||
|
||||
err = o.init()
|
||||
err = o.init(webServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers.ServeMux.HandleFunc(config.OpenAPIServePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
containers.ServeMux.HandleFunc(servePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := restful.NewResponse(w)
|
||||
if r.URL.Path != config.OpenAPIServePath {
|
||||
if r.URL.Path != servePath {
|
||||
resp.WriteErrorString(http.StatusNotFound, "Path not found!")
|
||||
}
|
||||
// TODO: we can cache json string and return it here.
|
||||
@ -92,8 +72,13 @@ func RegisterOpenAPIService(config *Config, containers *restful.Container) (err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *openAPI) init() error {
|
||||
err := o.buildPaths()
|
||||
func (o *openAPI) init(webServices []*restful.WebService) error {
|
||||
if o.config.GetOperationID == nil {
|
||||
o.config.GetOperationID = func(_ string, r *restful.Route) (string, error) {
|
||||
return r.Operation, nil
|
||||
}
|
||||
}
|
||||
err := o.buildPaths(webServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -104,7 +89,7 @@ func (o *openAPI) buildDefinitionRecursively(name string) error {
|
||||
if _, ok := o.swagger.Definitions[name]; ok {
|
||||
return nil
|
||||
}
|
||||
if item, ok := (*o.config.OpenAPIDefinitions)[name]; ok {
|
||||
if item, ok := (*o.config.Definitions)[name]; ok {
|
||||
o.swagger.Definitions[name] = item.Schema
|
||||
for _, v := range item.Dependencies {
|
||||
if err := o.buildDefinitionRecursively(v); err != nil {
|
||||
@ -133,20 +118,10 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) {
|
||||
}
|
||||
|
||||
// buildPaths builds OpenAPI paths using go-restful's web services.
|
||||
func (o *openAPI) buildPaths() error {
|
||||
pathsToIgnore := createTrie(o.config.IgnorePrefixes)
|
||||
duplicateOpId := make(map[string]bool)
|
||||
// Find duplicate operation IDs.
|
||||
for _, service := range o.config.WebServices {
|
||||
if pathsToIgnore.HasPrefix(service.RootPath()) {
|
||||
continue
|
||||
}
|
||||
for _, route := range service.Routes() {
|
||||
_, exists := duplicateOpId[route.Operation]
|
||||
duplicateOpId[route.Operation] = exists
|
||||
}
|
||||
}
|
||||
for _, w := range o.config.WebServices {
|
||||
func (o *openAPI) buildPaths(webServices []*restful.WebService) error {
|
||||
pathsToIgnore := util.CreateTrie(o.config.IgnorePrefixes)
|
||||
duplicateOpId := make(map[string]string)
|
||||
for _, w := range webServices {
|
||||
rootPath := w.RootPath()
|
||||
if pathsToIgnore.HasPrefix(rootPath) {
|
||||
continue
|
||||
@ -190,11 +165,11 @@ func (o *openAPI) buildPaths() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if duplicateOpId[op.ID] {
|
||||
// Repeated Operation IDs are not allowed in OpenAPI spec but if
|
||||
// an OperationID is empty, client generators will infer the ID
|
||||
// from the path and method of operation.
|
||||
op.ID = ""
|
||||
dpath, exists := duplicateOpId[op.ID]
|
||||
if exists {
|
||||
return fmt.Errorf("Duplicate Operation ID %v for path %v and %v.", op.ID, dpath, path)
|
||||
} else {
|
||||
duplicateOpId[op.ID] = path
|
||||
}
|
||||
switch strings.ToUpper(route.Method) {
|
||||
case "GET":
|
||||
@ -226,7 +201,6 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
|
||||
Description: route.Doc,
|
||||
Consumes: route.Consumes,
|
||||
Produces: route.Produces,
|
||||
ID: route.Operation,
|
||||
Schemes: o.config.ProtocolList,
|
||||
Responses: &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
@ -235,6 +209,9 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
|
||||
},
|
||||
},
|
||||
}
|
||||
if ret.ID, err = o.config.GetOperationID(o.servePath, &route); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Build responses
|
||||
for _, resp := range route.ResponseErrors {
|
||||
|
@ -28,9 +28,9 @@ import (
|
||||
)
|
||||
|
||||
// setUp is a convenience function for setting up for (most) tests.
|
||||
func setUp(t *testing.T, fullMethods bool) (openAPI, *assert.Assertions) {
|
||||
func setUp(t *testing.T, fullMethods bool) (openAPI, *restful.Container, *assert.Assertions) {
|
||||
assert := assert.New(t)
|
||||
config := getConfig(fullMethods)
|
||||
config, container := getConfig(fullMethods)
|
||||
return openAPI{
|
||||
config: config,
|
||||
swagger: &spec.Swagger{
|
||||
@ -41,7 +41,7 @@ func setUp(t *testing.T, fullMethods bool) (openAPI, *assert.Assertions) {
|
||||
Info: config.Info,
|
||||
},
|
||||
},
|
||||
}, assert
|
||||
}, container, assert
|
||||
}
|
||||
|
||||
func noOp(request *restful.Request, response *restful.Response) {}
|
||||
@ -130,11 +130,11 @@ func (_ TestOutput) OpenAPIDefinition() *common.OpenAPIDefinition {
|
||||
var _ common.OpenAPIDefinitionGetter = TestInput{}
|
||||
var _ common.OpenAPIDefinitionGetter = TestOutput{}
|
||||
|
||||
func getTestRoute(ws *restful.WebService, method string, additionalParams bool) *restful.RouteBuilder {
|
||||
func getTestRoute(ws *restful.WebService, method string, additionalParams bool, opPrefix string) *restful.RouteBuilder {
|
||||
ret := ws.Method(method).
|
||||
Path("/test/{path:*}").
|
||||
Doc(fmt.Sprintf("%s test input", method)).
|
||||
Operation(fmt.Sprintf("%sTestInput", method)).
|
||||
Operation(fmt.Sprintf("%s%sTestInput", method, opPrefix)).
|
||||
Produces(restful.MIME_JSON).
|
||||
Consumes(restful.MIME_JSON).
|
||||
Param(ws.PathParameter("path", "path to the resource").DataType("string")).
|
||||
@ -150,52 +150,50 @@ func getTestRoute(ws *restful.WebService, method string, additionalParams bool)
|
||||
return ret
|
||||
}
|
||||
|
||||
func getConfig(fullMethods bool) *Config {
|
||||
func getConfig(fullMethods bool) (*common.Config, *restful.Container) {
|
||||
mux := http.NewServeMux()
|
||||
container := restful.NewContainer()
|
||||
container.ServeMux = mux
|
||||
ws := new(restful.WebService)
|
||||
ws.Path("/foo")
|
||||
ws.Route(getTestRoute(ws, "get", true))
|
||||
ws.Route(getTestRoute(ws, "get", true, "foo"))
|
||||
if fullMethods {
|
||||
ws.Route(getTestRoute(ws, "post", false)).
|
||||
Route(getTestRoute(ws, "put", false)).
|
||||
Route(getTestRoute(ws, "head", false)).
|
||||
Route(getTestRoute(ws, "patch", false)).
|
||||
Route(getTestRoute(ws, "options", false)).
|
||||
Route(getTestRoute(ws, "delete", false))
|
||||
ws.Route(getTestRoute(ws, "post", false, "foo")).
|
||||
Route(getTestRoute(ws, "put", false, "foo")).
|
||||
Route(getTestRoute(ws, "head", false, "foo")).
|
||||
Route(getTestRoute(ws, "patch", false, "foo")).
|
||||
Route(getTestRoute(ws, "options", false, "foo")).
|
||||
Route(getTestRoute(ws, "delete", false, "foo"))
|
||||
|
||||
}
|
||||
ws.Path("/bar")
|
||||
ws.Route(getTestRoute(ws, "get", true))
|
||||
ws.Route(getTestRoute(ws, "get", true, "bar"))
|
||||
if fullMethods {
|
||||
ws.Route(getTestRoute(ws, "post", false)).
|
||||
Route(getTestRoute(ws, "put", false)).
|
||||
Route(getTestRoute(ws, "head", false)).
|
||||
Route(getTestRoute(ws, "patch", false)).
|
||||
Route(getTestRoute(ws, "options", false)).
|
||||
Route(getTestRoute(ws, "delete", false))
|
||||
ws.Route(getTestRoute(ws, "post", false, "bar")).
|
||||
Route(getTestRoute(ws, "put", false, "bar")).
|
||||
Route(getTestRoute(ws, "head", false, "bar")).
|
||||
Route(getTestRoute(ws, "patch", false, "bar")).
|
||||
Route(getTestRoute(ws, "options", false, "bar")).
|
||||
Route(getTestRoute(ws, "delete", false, "bar"))
|
||||
|
||||
}
|
||||
container.Add(ws)
|
||||
return &Config{
|
||||
WebServices: container.RegisteredWebServices(),
|
||||
ProtocolList: []string{"https"},
|
||||
OpenAPIServePath: "/swagger.json",
|
||||
return &common.Config{
|
||||
ProtocolList: []string{"https"},
|
||||
Info: &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "TestAPI",
|
||||
Description: "Test API",
|
||||
},
|
||||
},
|
||||
OpenAPIDefinitions: &common.OpenAPIDefinitions{
|
||||
Definitions: &common.OpenAPIDefinitions{
|
||||
"openapi.TestInput": *TestInput{}.OpenAPIDefinition(),
|
||||
"openapi.TestOutput": *TestOutput{}.OpenAPIDefinition(),
|
||||
},
|
||||
}
|
||||
}, container
|
||||
}
|
||||
|
||||
func getTestOperation(method string) *spec.Operation {
|
||||
func getTestOperation(method string, opPrefix string) *spec.Operation {
|
||||
return &spec.Operation{
|
||||
OperationProps: spec.OperationProps{
|
||||
Description: fmt.Sprintf("%s test input", method),
|
||||
@ -204,25 +202,26 @@ func getTestOperation(method string) *spec.Operation {
|
||||
Schemes: []string{"https"},
|
||||
Parameters: []spec.Parameter{},
|
||||
Responses: getTestResponses(),
|
||||
ID: fmt.Sprintf("%s%sTestInput", method, opPrefix),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getTestPathItem(allMethods bool) spec.PathItem {
|
||||
func getTestPathItem(allMethods bool, opPrefix string) spec.PathItem {
|
||||
ret := spec.PathItem{
|
||||
PathItemProps: spec.PathItemProps{
|
||||
Get: getTestOperation("get"),
|
||||
Get: getTestOperation("get", opPrefix),
|
||||
Parameters: getTestCommonParameters(),
|
||||
},
|
||||
}
|
||||
ret.Get.Parameters = getAdditionalTestParameters()
|
||||
if allMethods {
|
||||
ret.PathItemProps.Put = getTestOperation("put")
|
||||
ret.PathItemProps.Post = getTestOperation("post")
|
||||
ret.PathItemProps.Head = getTestOperation("head")
|
||||
ret.PathItemProps.Patch = getTestOperation("patch")
|
||||
ret.PathItemProps.Delete = getTestOperation("delete")
|
||||
ret.PathItemProps.Options = getTestOperation("options")
|
||||
ret.PathItemProps.Put = getTestOperation("put", opPrefix)
|
||||
ret.PathItemProps.Post = getTestOperation("post", opPrefix)
|
||||
ret.PathItemProps.Head = getTestOperation("head", opPrefix)
|
||||
ret.PathItemProps.Patch = getTestOperation("patch", opPrefix)
|
||||
ret.PathItemProps.Delete = getTestOperation("delete", opPrefix)
|
||||
ret.PathItemProps.Options = getTestOperation("options", opPrefix)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -380,7 +379,7 @@ func getTestOutputDefinition() spec.Schema {
|
||||
}
|
||||
|
||||
func TestBuildSwaggerSpec(t *testing.T) {
|
||||
o, assert := setUp(t, true)
|
||||
o, container, assert := setUp(t, true)
|
||||
expected := &spec.Swagger{
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Info: &spec.Info{
|
||||
@ -392,8 +391,8 @@ func TestBuildSwaggerSpec(t *testing.T) {
|
||||
Swagger: "2.0",
|
||||
Paths: &spec.Paths{
|
||||
Paths: map[string]spec.PathItem{
|
||||
"/foo/test/{path}": getTestPathItem(true),
|
||||
"/bar/test/{path}": getTestPathItem(true),
|
||||
"/foo/test/{path}": getTestPathItem(true, "foo"),
|
||||
"/bar/test/{path}": getTestPathItem(true, "bar"),
|
||||
},
|
||||
},
|
||||
Definitions: spec.Definitions{
|
||||
@ -402,7 +401,7 @@ func TestBuildSwaggerSpec(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := o.init()
|
||||
err := o.init(container.RegisteredWebServices())
|
||||
if assert.NoError(err) {
|
||||
assert.Equal(expected, o.swagger)
|
||||
}
|
||||
|
@ -59,54 +59,3 @@ func mapKeyFromParam(param *restful.Parameter) interface{} {
|
||||
Kind: param.Data().Kind,
|
||||
}
|
||||
}
|
||||
|
||||
// A simple trie implementation with Add an HasPrefix methods only.
|
||||
type trie struct {
|
||||
children map[byte]*trie
|
||||
wordTail bool
|
||||
}
|
||||
|
||||
func createTrie(list []string) trie {
|
||||
ret := trie{
|
||||
children: make(map[byte]*trie),
|
||||
wordTail: false,
|
||||
}
|
||||
for _, v := range list {
|
||||
ret.Add(v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *trie) Add(v string) {
|
||||
root := t
|
||||
for _, b := range []byte(v) {
|
||||
child, exists := root.children[b]
|
||||
if !exists {
|
||||
child = &trie{
|
||||
children: make(map[byte]*trie),
|
||||
wordTail: false,
|
||||
}
|
||||
root.children[b] = child
|
||||
}
|
||||
root = child
|
||||
}
|
||||
root.wordTail = true
|
||||
}
|
||||
|
||||
func (t *trie) HasPrefix(v string) bool {
|
||||
root := t
|
||||
if root.wordTail {
|
||||
return true
|
||||
}
|
||||
for _, b := range []byte(v) {
|
||||
child, exists := root.children[b]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
if child.wordTail {
|
||||
return true
|
||||
}
|
||||
root = child
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -44,10 +44,12 @@ import (
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
extensionsapiv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/apis/rbac"
|
||||
"k8s.io/kubernetes/pkg/apiserver/openapi"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/generated/openapi"
|
||||
openapigen "k8s.io/kubernetes/pkg/generated/openapi"
|
||||
"k8s.io/kubernetes/pkg/genericapiserver"
|
||||
"k8s.io/kubernetes/pkg/genericapiserver/openapi/common"
|
||||
"k8s.io/kubernetes/pkg/kubelet/client"
|
||||
ipallocator "k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
|
||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||
@ -69,7 +71,9 @@ func setUp(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *assert.
|
||||
server, storageConfig := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
|
||||
|
||||
config := &Config{
|
||||
GenericConfig: &genericapiserver.Config{},
|
||||
GenericConfig: &genericapiserver.Config{
|
||||
OpenAPIConfig: &common.Config{},
|
||||
},
|
||||
}
|
||||
|
||||
resourceEncoding := genericapiserver.NewDefaultResourceEncodingConfig()
|
||||
@ -499,15 +503,16 @@ func TestValidOpenAPISpec(t *testing.T) {
|
||||
_, etcdserver, config, assert := setUp(t)
|
||||
defer etcdserver.Terminate(t)
|
||||
|
||||
config.GenericConfig.OpenAPIDefinitions = openapi.OpenAPIDefinitions
|
||||
config.GenericConfig.OpenAPIConfig.Definitions = openapigen.OpenAPIDefinitions
|
||||
config.GenericConfig.EnableOpenAPISupport = true
|
||||
config.GenericConfig.EnableIndex = true
|
||||
config.GenericConfig.OpenAPIInfo = spec.Info{
|
||||
config.GenericConfig.OpenAPIConfig.Info = &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "Kubernetes",
|
||||
Version: "unversioned",
|
||||
},
|
||||
}
|
||||
config.GenericConfig.OpenAPIConfig.GetOperationID = openapi.GetOperationID
|
||||
master, err := config.Complete().New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error in bringing up the master: %v", err)
|
||||
@ -592,7 +597,7 @@ func TestValidOpenAPISpec(t *testing.T) {
|
||||
t.Logf("Open API spec on %v has some warnings : %v", path, warns)
|
||||
}
|
||||
} else {
|
||||
t.Logf("Validation is disabled because it is timing out on jenkins put passing locally.")
|
||||
t.Logf("Validation is disabled because it is timing out on jenkins but passing locally.")
|
||||
}
|
||||
}
|
||||
|
||||
|
79
pkg/util/trie.go
Normal file
79
pkg/util/trie.go
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
// A simple trie implementation with Add an HasPrefix methods only.
|
||||
type Trie struct {
|
||||
children map[byte]*Trie
|
||||
wordTail bool
|
||||
word string
|
||||
}
|
||||
|
||||
// CreateTrie creates a Trie and add all strings in the provided list to it.
|
||||
func CreateTrie(list []string) Trie {
|
||||
ret := Trie{
|
||||
children: make(map[byte]*Trie),
|
||||
wordTail: false,
|
||||
}
|
||||
for _, v := range list {
|
||||
ret.Add(v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add adds a word to this trie
|
||||
func (t *Trie) Add(v string) {
|
||||
root := t
|
||||
for _, b := range []byte(v) {
|
||||
child, exists := root.children[b]
|
||||
if !exists {
|
||||
child = &Trie{
|
||||
children: make(map[byte]*Trie),
|
||||
wordTail: false,
|
||||
}
|
||||
root.children[b] = child
|
||||
}
|
||||
root = child
|
||||
}
|
||||
root.wordTail = true
|
||||
root.word = v
|
||||
}
|
||||
|
||||
// HasPrefix returns true of v has any of the prefixes stored in this trie.
|
||||
func (t *Trie) HasPrefix(v string) bool {
|
||||
_, has := t.GetPrefix(v)
|
||||
return has
|
||||
}
|
||||
|
||||
// GetPrefix is like HasPrefix but return the prefix in case of match or empty string otherwise.
|
||||
func (t *Trie) GetPrefix(v string) (string, bool) {
|
||||
root := t
|
||||
if root.wordTail {
|
||||
return root.word, true
|
||||
}
|
||||
for _, b := range []byte(v) {
|
||||
child, exists := root.children[b]
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
if child.wordTail {
|
||||
return child.word, true
|
||||
}
|
||||
root = child
|
||||
}
|
||||
return "", false
|
||||
}
|
@ -68,6 +68,7 @@ import (
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/pborman/uuid"
|
||||
"k8s.io/kubernetes/pkg/genericapiserver/openapi/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -184,17 +185,18 @@ func startMasterOrDie(masterConfig *master.Config, incomingServer *httptest.Serv
|
||||
masterConfig.GenericConfig.EnableProfiling = true
|
||||
masterConfig.GenericConfig.EnableSwaggerSupport = true
|
||||
masterConfig.GenericConfig.EnableOpenAPISupport = true
|
||||
masterConfig.GenericConfig.OpenAPIInfo = spec.Info{
|
||||
masterConfig.GenericConfig.OpenAPIConfig.Info = &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "Kubernetes",
|
||||
Version: "unversioned",
|
||||
},
|
||||
}
|
||||
masterConfig.GenericConfig.OpenAPIDefaultResponse = spec.Response{
|
||||
masterConfig.GenericConfig.OpenAPIConfig.DefaultResponse = &spec.Response{
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "Default Response.",
|
||||
},
|
||||
}
|
||||
masterConfig.GenericConfig.OpenAPIConfig.Definitions = openapi.OpenAPIDefinitions
|
||||
}
|
||||
|
||||
// set the loopback client config
|
||||
@ -355,8 +357,8 @@ func NewMasterConfig() *master.Config {
|
||||
ServiceClusterIPRange: parseCIDROrDie("10.0.0.0/24"),
|
||||
ServiceNodePortRange: utilnet.PortRange{Base: 30000, Size: 2768},
|
||||
EnableVersion: true,
|
||||
OpenAPIDefinitions: openapi.OpenAPIDefinitions,
|
||||
EnableOpenAPISupport: true,
|
||||
OpenAPIConfig: &common.Config{},
|
||||
},
|
||||
StorageFactory: storageFactory,
|
||||
EnableCoreControllers: true,
|
||||
|
Loading…
Reference in New Issue
Block a user