mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-20 01:01:22 +00:00
Aggregate OpenAPI specs
This commit is contained in:
parent
fccff9adb6
commit
1a1d9a0394
@ -50,7 +50,6 @@ func createAggregatorConfig(kubeAPIServerConfig genericapiserver.Config, command
|
|||||||
|
|
||||||
// the aggregator doesn't wire these up. It just delegates them to the kubeapiserver
|
// the aggregator doesn't wire these up. It just delegates them to the kubeapiserver
|
||||||
genericConfig.EnableSwaggerUI = false
|
genericConfig.EnableSwaggerUI = false
|
||||||
genericConfig.OpenAPIConfig = nil
|
|
||||||
genericConfig.SwaggerConfig = nil
|
genericConfig.SwaggerConfig = nil
|
||||||
|
|
||||||
// copy the etcd options so we don't mutate originals.
|
// copy the etcd options so we don't mutate originals.
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/emicklei/go-restful-swagger12"
|
"github.com/emicklei/go-restful-swagger12"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
"k8s.io/apimachinery/pkg/apimachinery"
|
"k8s.io/apimachinery/pkg/apimachinery"
|
||||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -43,6 +44,7 @@ import (
|
|||||||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/apiserver/pkg/server/healthz"
|
"k8s.io/apiserver/pkg/server/healthz"
|
||||||
|
"k8s.io/apiserver/pkg/server/openapi"
|
||||||
"k8s.io/apiserver/pkg/server/routes"
|
"k8s.io/apiserver/pkg/server/routes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
@ -130,6 +132,9 @@ type GenericAPIServer struct {
|
|||||||
swaggerConfig *swagger.Config
|
swaggerConfig *swagger.Config
|
||||||
openAPIConfig *openapicommon.Config
|
openAPIConfig *openapicommon.Config
|
||||||
|
|
||||||
|
// Enables updating OpenAPI spec using update method.
|
||||||
|
OpenAPIService *openapi.OpenAPIService
|
||||||
|
|
||||||
// PostStartHooks are each called after the server has started listening, in a separate go func for each
|
// PostStartHooks are each called after the server has started listening, in a separate go func for each
|
||||||
// with no guarantee of ordering between them. The map key is a name used for error reporting.
|
// with no guarantee 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.
|
// It may kill the process with a panic if it wishes to by returning an error.
|
||||||
@ -165,6 +170,9 @@ type DelegationTarget interface {
|
|||||||
|
|
||||||
// ListedPaths returns the paths for supporting an index
|
// ListedPaths returns the paths for supporting an index
|
||||||
ListedPaths() []string
|
ListedPaths() []string
|
||||||
|
|
||||||
|
// OpenAPISpec returns the OpenAPI spec of the delegation target if exists, nil otherwise.
|
||||||
|
OpenAPISpec() *spec.Swagger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GenericAPIServer) UnprotectedHandler() http.Handler {
|
func (s *GenericAPIServer) UnprotectedHandler() http.Handler {
|
||||||
@ -180,6 +188,9 @@ func (s *GenericAPIServer) HealthzChecks() []healthz.HealthzChecker {
|
|||||||
func (s *GenericAPIServer) ListedPaths() []string {
|
func (s *GenericAPIServer) ListedPaths() []string {
|
||||||
return s.listedPathProvider.ListedPaths()
|
return s.listedPathProvider.ListedPaths()
|
||||||
}
|
}
|
||||||
|
func (s *GenericAPIServer) OpenAPISpec() *spec.Swagger {
|
||||||
|
return s.OpenAPIService.GetSpec()
|
||||||
|
}
|
||||||
|
|
||||||
var EmptyDelegate = emptyDelegate{
|
var EmptyDelegate = emptyDelegate{
|
||||||
requestContextMapper: apirequest.NewRequestContextMapper(),
|
requestContextMapper: apirequest.NewRequestContextMapper(),
|
||||||
@ -204,6 +215,9 @@ func (s emptyDelegate) ListedPaths() []string {
|
|||||||
func (s emptyDelegate) RequestContextMapper() apirequest.RequestContextMapper {
|
func (s emptyDelegate) RequestContextMapper() apirequest.RequestContextMapper {
|
||||||
return s.requestContextMapper
|
return s.requestContextMapper
|
||||||
}
|
}
|
||||||
|
func (s emptyDelegate) OpenAPISpec() *spec.Swagger {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Send correct mime type for .svg files.
|
// Send correct mime type for .svg files.
|
||||||
@ -233,17 +247,22 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
|
|||||||
if s.swaggerConfig != nil {
|
if s.swaggerConfig != nil {
|
||||||
routes.Swagger{Config: s.swaggerConfig}.Install(s.Handler.GoRestfulContainer)
|
routes.Swagger{Config: s.swaggerConfig}.Install(s.Handler.GoRestfulContainer)
|
||||||
}
|
}
|
||||||
if s.openAPIConfig != nil {
|
s.PrepareOpenAPIService()
|
||||||
routes.OpenAPI{
|
|
||||||
Config: s.openAPIConfig,
|
|
||||||
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.installHealthz()
|
s.installHealthz()
|
||||||
|
|
||||||
return preparedGenericAPIServer{s}
|
return preparedGenericAPIServer{s}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrepareOpenAPIService installs OpenAPI handler if it does not exists.
|
||||||
|
func (s *GenericAPIServer) PrepareOpenAPIService() {
|
||||||
|
if s.openAPIConfig != nil && s.OpenAPIService == nil {
|
||||||
|
s.OpenAPIService = routes.OpenAPI{
|
||||||
|
Config: s.openAPIConfig,
|
||||||
|
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run spawns the secure http server. It only returns if stopCh is closed
|
// Run spawns the secure http server. It only returns if stopCh is closed
|
||||||
// or the secure port cannot be listened on initially.
|
// or the secure port cannot be listened on initially.
|
||||||
func (s preparedGenericAPIServer) Run(stopCh <-chan struct{}) error {
|
func (s preparedGenericAPIServer) Run(stopCh <-chan struct{}) error {
|
||||||
|
@ -0,0 +1,482 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 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 (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/conversion"
|
||||||
|
"k8s.io/apiserver/pkg/util/trie"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFINITION_PREFIX = "#/definitions/"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cloner = conversion.NewCloner()
|
||||||
|
|
||||||
|
// Run a walkRefCallback method on all references of an OpenAPI spec
|
||||||
|
type walkAllRefs struct {
|
||||||
|
// walkRefCallback will be called on each reference and the return value
|
||||||
|
// will replace that reference. This will allow the callers to change
|
||||||
|
// all/some references of an spec (e.g. useful in renaming definitions).
|
||||||
|
walkRefCallback func(ref spec.Ref) spec.Ref
|
||||||
|
|
||||||
|
// The spec to walk through.
|
||||||
|
root *spec.Swagger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWalkAllRefs(walkRef func(ref spec.Ref) spec.Ref, sp *spec.Swagger) *walkAllRefs {
|
||||||
|
return &walkAllRefs{
|
||||||
|
walkRefCallback: walkRef,
|
||||||
|
root: sp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) walkRef(ref spec.Ref) spec.Ref {
|
||||||
|
if ref.String() != "" {
|
||||||
|
refStr := ref.String()
|
||||||
|
// References that start with #/definitions/ has a definition
|
||||||
|
// inside the same spec file. If that is the case, walk through
|
||||||
|
// those definitions too.
|
||||||
|
// We do not support external references yet.
|
||||||
|
if strings.HasPrefix(refStr, DEFINITION_PREFIX) {
|
||||||
|
def := s.root.Definitions[refStr[len(DEFINITION_PREFIX):]]
|
||||||
|
s.walkSchema(&def)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.walkRefCallback(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) walkSchema(schema *spec.Schema) {
|
||||||
|
if schema == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schema.Ref = s.walkRef(schema.Ref)
|
||||||
|
for _, v := range schema.Definitions {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
for _, v := range schema.Properties {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
for _, v := range schema.PatternProperties {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
for _, v := range schema.AllOf {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
for _, v := range schema.AnyOf {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
for _, v := range schema.OneOf {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
if schema.Not != nil {
|
||||||
|
s.walkSchema(schema.Not)
|
||||||
|
}
|
||||||
|
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
|
||||||
|
s.walkSchema(schema.AdditionalProperties.Schema)
|
||||||
|
}
|
||||||
|
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
|
||||||
|
s.walkSchema(schema.AdditionalItems.Schema)
|
||||||
|
}
|
||||||
|
if schema.Items != nil {
|
||||||
|
if schema.Items.Schema != nil {
|
||||||
|
s.walkSchema(schema.Items.Schema)
|
||||||
|
}
|
||||||
|
for _, v := range schema.Items.Schemas {
|
||||||
|
s.walkSchema(&v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) walkParams(params []spec.Parameter) {
|
||||||
|
if params == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, param := range params {
|
||||||
|
param.Ref = s.walkRef(param.Ref)
|
||||||
|
s.walkSchema(param.Schema)
|
||||||
|
if param.Items != nil {
|
||||||
|
param.Items.Ref = s.walkRef(param.Items.Ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) walkResponse(resp *spec.Response) {
|
||||||
|
if resp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Ref = s.walkRef(resp.Ref)
|
||||||
|
s.walkSchema(resp.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) walkOperation(op *spec.Operation) {
|
||||||
|
if op == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.walkParams(op.Parameters)
|
||||||
|
if op.Responses == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.walkResponse(op.Responses.Default)
|
||||||
|
for _, r := range op.Responses.StatusCodeResponses {
|
||||||
|
s.walkResponse(&r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *walkAllRefs) Start() {
|
||||||
|
for _, pathItem := range s.root.Paths.Paths {
|
||||||
|
s.walkParams(pathItem.Parameters)
|
||||||
|
s.walkOperation(pathItem.Delete)
|
||||||
|
s.walkOperation(pathItem.Get)
|
||||||
|
s.walkOperation(pathItem.Head)
|
||||||
|
s.walkOperation(pathItem.Options)
|
||||||
|
s.walkOperation(pathItem.Patch)
|
||||||
|
s.walkOperation(pathItem.Post)
|
||||||
|
s.walkOperation(pathItem.Put)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterSpecByPaths remove unnecessary paths and unused definitions.
|
||||||
|
func FilterSpecByPaths(sp *spec.Swagger, keepPathPrefixes []string) {
|
||||||
|
// First remove unwanted paths
|
||||||
|
prefixes := trie.New(keepPathPrefixes)
|
||||||
|
orgPaths := sp.Paths
|
||||||
|
if orgPaths == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sp.Paths = &spec.Paths{
|
||||||
|
VendorExtensible: orgPaths.VendorExtensible,
|
||||||
|
Paths: map[string]spec.PathItem{},
|
||||||
|
}
|
||||||
|
for path, pathItem := range orgPaths.Paths {
|
||||||
|
if !prefixes.HasPrefix(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sp.Paths.Paths[path] = pathItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk all references to find all definition references.
|
||||||
|
usedDefinitions := map[string]bool{}
|
||||||
|
|
||||||
|
newWalkAllRefs(func(ref spec.Ref) spec.Ref {
|
||||||
|
if ref.String() != "" {
|
||||||
|
refStr := ref.String()
|
||||||
|
if strings.HasPrefix(refStr, DEFINITION_PREFIX) {
|
||||||
|
usedDefinitions[refStr[len(DEFINITION_PREFIX):]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ref
|
||||||
|
}, sp).Start()
|
||||||
|
|
||||||
|
// Remove unused definitions
|
||||||
|
orgDefinitions := sp.Definitions
|
||||||
|
sp.Definitions = spec.Definitions{}
|
||||||
|
for k, v := range orgDefinitions {
|
||||||
|
if usedDefinitions[k] {
|
||||||
|
sp.Definitions[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalSchemaMap(s1, s2 map[string]spec.Schema) bool {
|
||||||
|
if len(s1) != len(s2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range s1 {
|
||||||
|
v2, found := s2[k]
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !EqualSchema(&v, &v2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalSchemaArray(s1, s2 []spec.Schema) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == nil && s2 == nil
|
||||||
|
}
|
||||||
|
if len(s1) != len(s2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v1 := range s1 {
|
||||||
|
found := false
|
||||||
|
for _, v2 := range s2 {
|
||||||
|
if EqualSchema(&v1, &v2) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v2 := range s2 {
|
||||||
|
found := false
|
||||||
|
for _, v1 := range s1 {
|
||||||
|
if EqualSchema(&v1, &v2) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalSchemaOrBool(s1, s2 *spec.SchemaOrBool) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
if s1.Allows != s2.Allows {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !EqualSchema(s1.Schema, s2.Schema) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalSchemaOrArray(s1, s2 *spec.SchemaOrArray) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
if !EqualSchema(s1.Schema, s2.Schema) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaArray(s1.Schemas, s2.Schemas) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalStringArray(s1, s2 []string) bool {
|
||||||
|
if len(s1) != len(s2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v1 := range s1 {
|
||||||
|
found := false
|
||||||
|
for _, v2 := range s2 {
|
||||||
|
if v1 == v2 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v2 := range s2 {
|
||||||
|
found := false
|
||||||
|
for _, v1 := range s1 {
|
||||||
|
if v1 == v2 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalFloatPointer(s1, s2 *float64) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
return *s1 == *s2
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalIntPointer(s1, s2 *int64) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
return *s1 == *s2
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualSchema returns true if models have the same properties and references
|
||||||
|
// even if they have different documentation.
|
||||||
|
func EqualSchema(s1, s2 *spec.Schema) bool {
|
||||||
|
if s1 == nil || s2 == nil {
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
if s1.Ref.String() != s2.Ref.String() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaMap(s1.Definitions, s2.Definitions) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaMap(s1.Properties, s2.Properties) {
|
||||||
|
fmt.Println("Not equal props")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaMap(s1.PatternProperties, s2.PatternProperties) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaArray(s1.AllOf, s2.AllOf) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaArray(s1.AnyOf, s2.AnyOf) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaArray(s1.OneOf, s2.OneOf) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !EqualSchema(s1.Not, s2.Not) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaOrBool(s1.AdditionalProperties, s2.AdditionalProperties) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaOrBool(s1.AdditionalItems, s2.AdditionalItems) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalSchemaOrArray(s1.Items, s2.Items) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalStringArray(s1.Type, s2.Type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s1.Format != s2.Format {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalFloatPointer(s1.Minimum, s2.Minimum) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalFloatPointer(s1.Maximum, s2.Maximum) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s1.ExclusiveMaximum != s2.ExclusiveMaximum {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s1.ExclusiveMinimum != s2.ExclusiveMinimum {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalFloatPointer(s1.MultipleOf, s2.MultipleOf) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MaxLength, s2.MaxLength) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MinLength, s2.MinLength) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MaxItems, s2.MaxItems) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MinItems, s2.MinItems) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s1.Pattern != s2.Pattern {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s1.UniqueItems != s2.UniqueItems {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MaxProperties, s2.MaxProperties) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalIntPointer(s1.MinProperties, s2.MinProperties) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !equalStringArray(s1.Required, s2.Required) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(s1.Enum) == 0 && len(s2.Enum) == 0 && len(s1.Dependencies) == 0 && len(s2.Dependencies) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func renameDefinition(s *spec.Swagger, old, new string) {
|
||||||
|
old_ref := DEFINITION_PREFIX + old
|
||||||
|
new_ref := DEFINITION_PREFIX + new
|
||||||
|
newWalkAllRefs(func(ref spec.Ref) spec.Ref {
|
||||||
|
if ref.String() == old_ref {
|
||||||
|
return spec.MustCreateRef(new_ref)
|
||||||
|
}
|
||||||
|
return ref
|
||||||
|
}, s).Start()
|
||||||
|
s.Definitions[new] = s.Definitions[old]
|
||||||
|
delete(s.Definitions, old)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy paths and definitions from source to dest, rename definitions if needed.
|
||||||
|
// dest will be mutated, and source will not be changed.
|
||||||
|
func MergeSpecs(dest, source *spec.Swagger) error {
|
||||||
|
source, err := CloneSpec(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range source.Paths.Paths {
|
||||||
|
if _, found := dest.Paths.Paths[k]; found {
|
||||||
|
return fmt.Errorf("Unable to merge: Duplicated path %s", k)
|
||||||
|
}
|
||||||
|
dest.Paths.Paths[k] = v
|
||||||
|
}
|
||||||
|
usedNames := map[string]bool{}
|
||||||
|
for k := range dest.Definitions {
|
||||||
|
usedNames[k] = true
|
||||||
|
}
|
||||||
|
type Rename struct {
|
||||||
|
from, to string
|
||||||
|
}
|
||||||
|
renames := []Rename{}
|
||||||
|
for k, v := range source.Definitions {
|
||||||
|
v2, found := dest.Definitions[k]
|
||||||
|
if found || usedNames[k] {
|
||||||
|
if found && EqualSchema(&v, &v2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i := 2
|
||||||
|
newName := fmt.Sprintf("%s_v%d", k, i)
|
||||||
|
for usedNames[newName] {
|
||||||
|
i += 1
|
||||||
|
newName = fmt.Sprintf("%s_v%d", k, i)
|
||||||
|
}
|
||||||
|
renames = append(renames, Rename{from: k, to: newName})
|
||||||
|
usedNames[newName] = true
|
||||||
|
} else {
|
||||||
|
usedNames[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range renames {
|
||||||
|
renameDefinition(source, r.from, r.to)
|
||||||
|
}
|
||||||
|
for k, v := range source.Definitions {
|
||||||
|
if _, found := dest.Definitions[k]; !found {
|
||||||
|
dest.Definitions[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone OpenAPI spec
|
||||||
|
func CloneSpec(source *spec.Swagger) (*spec.Swagger, error) {
|
||||||
|
if ret, err := cloner.DeepCopy(source); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return ret.(*spec.Swagger), nil
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,520 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilterSpecs(t *testing.T) {
|
||||||
|
var spec1, spec1_filtered *spec.Swagger
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test2"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
Test2:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
other:
|
||||||
|
$ref: "#/definitions/Other"
|
||||||
|
Other:
|
||||||
|
type: "string"
|
||||||
|
`), &spec1)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec1_filtered)
|
||||||
|
assert := assert.New(t)
|
||||||
|
FilterSpecByPaths(spec1, []string{"/test"})
|
||||||
|
assert.Equal(spec1_filtered, spec1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSpecsSimple(t *testing.T) {
|
||||||
|
var spec1, spec2, expected *spec.Swagger
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec1)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test2"
|
||||||
|
definitions:
|
||||||
|
Test2:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
other:
|
||||||
|
$ref: "#/definitions/Other"
|
||||||
|
Other:
|
||||||
|
type: "string"
|
||||||
|
`), &spec2)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test2"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
Test2:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
other:
|
||||||
|
$ref: "#/definitions/Other"
|
||||||
|
Other:
|
||||||
|
type: "string"
|
||||||
|
`), &expected)
|
||||||
|
assert := assert.New(t)
|
||||||
|
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(expected, spec1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSpecsReuseModel(t *testing.T) {
|
||||||
|
var spec1, spec2, expected *spec.Swagger
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec1)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
description: "This Test has a description"
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "This status has another description"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec2)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &expected)
|
||||||
|
assert := assert.New(t)
|
||||||
|
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(expected, spec1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSpecsRenameModel(t *testing.T) {
|
||||||
|
var spec1, spec2, expected *spec.Swagger
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec1)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
description: "This Test has a description"
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &spec2)
|
||||||
|
yaml.Unmarshal([]byte(`
|
||||||
|
swagger: "2.0"
|
||||||
|
paths:
|
||||||
|
/test:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test"
|
||||||
|
summary: "Test API"
|
||||||
|
operationId: "addTest"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test"
|
||||||
|
responses:
|
||||||
|
405:
|
||||||
|
description: "Invalid input"
|
||||||
|
$ref: "#/definitions/InvalidInput"
|
||||||
|
/othertest:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "test2"
|
||||||
|
summary: "Test2 API"
|
||||||
|
operationId: "addTest2"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/xml"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "test2 object"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Test_v2"
|
||||||
|
definitions:
|
||||||
|
Test:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
description: "Status"
|
||||||
|
Test_v2:
|
||||||
|
description: "This Test has a description"
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
InvalidInput:
|
||||||
|
type: "string"
|
||||||
|
format: "string"
|
||||||
|
`), &expected)
|
||||||
|
assert := assert.New(t)
|
||||||
|
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_yaml, _ := yaml.Marshal(expected)
|
||||||
|
spec1_yaml, _ := yaml.Marshal(spec1)
|
||||||
|
|
||||||
|
assert.Equal(string(expected_yaml), string(spec1_yaml))
|
||||||
|
}
|
@ -17,8 +17,11 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||||
@ -37,6 +40,14 @@ import (
|
|||||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||||
"k8s.io/client-go/pkg/version"
|
"k8s.io/client-go/pkg/version"
|
||||||
|
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
"k8s.io/apiserver/pkg/server/openapi"
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration"
|
"k8s.io/kube-aggregator/pkg/apis/apiregistration"
|
||||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/install"
|
"k8s.io/kube-aggregator/pkg/apis/apiregistration/install"
|
||||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
||||||
@ -54,6 +65,10 @@ var (
|
|||||||
Codecs = serializer.NewCodecFactory(Scheme)
|
Codecs = serializer.NewCodecFactory(Scheme)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LOAD_OPENAPI_SPEC_MAX_RETRIES = 10
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
install.Install(groupFactoryRegistry, registry, Scheme)
|
install.Install(groupFactoryRegistry, registry, Scheme)
|
||||||
|
|
||||||
@ -118,6 +133,24 @@ type APIAggregator struct {
|
|||||||
// handledGroups are the groups that already have routes
|
// handledGroups are the groups that already have routes
|
||||||
handledGroups sets.String
|
handledGroups sets.String
|
||||||
|
|
||||||
|
// Swagger spec for each api service
|
||||||
|
apiServiceSpecs map[string]*spec.Swagger
|
||||||
|
|
||||||
|
// List of the specs that needs to be loaded. When a spec is successfully loaded
|
||||||
|
// it will be removed from this list and added to apiServiceSpecs.
|
||||||
|
// Map values are retry counts. After a preset retries, it will stop
|
||||||
|
// trying.
|
||||||
|
toLoadAPISpec map[string]int
|
||||||
|
|
||||||
|
// protecting toLoadAPISpec and apiServiceSpecs
|
||||||
|
specMutex sync.Mutex
|
||||||
|
|
||||||
|
// rootSpec is the OpenAPI spec of the Aggregator server.
|
||||||
|
rootSpec *spec.Swagger
|
||||||
|
|
||||||
|
// delegationSpec is the delegation API Server's spec (most of API groups are in this spec).
|
||||||
|
delegationSpec *spec.Swagger
|
||||||
|
|
||||||
// lister is used to add group handling for /apis/<group> aggregator lookups based on
|
// lister is used to add group handling for /apis/<group> aggregator lookups based on
|
||||||
// controller state
|
// controller state
|
||||||
lister listers.APIServiceLister
|
lister listers.APIServiceLister
|
||||||
@ -191,11 +224,14 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg
|
|||||||
s := &APIAggregator{
|
s := &APIAggregator{
|
||||||
GenericAPIServer: genericServer,
|
GenericAPIServer: genericServer,
|
||||||
delegateHandler: delegationTarget.UnprotectedHandler(),
|
delegateHandler: delegationTarget.UnprotectedHandler(),
|
||||||
|
delegationSpec: delegationTarget.OpenAPISpec(),
|
||||||
contextMapper: c.GenericConfig.RequestContextMapper,
|
contextMapper: c.GenericConfig.RequestContextMapper,
|
||||||
proxyClientCert: c.ProxyClientCert,
|
proxyClientCert: c.ProxyClientCert,
|
||||||
proxyClientKey: c.ProxyClientKey,
|
proxyClientKey: c.ProxyClientKey,
|
||||||
proxyTransport: proxyTransport,
|
proxyTransport: proxyTransport,
|
||||||
proxyHandlers: map[string]*proxyHandler{},
|
proxyHandlers: map[string]*proxyHandler{},
|
||||||
|
apiServiceSpecs: map[string]*spec.Swagger{},
|
||||||
|
toLoadAPISpec: map[string]int{},
|
||||||
handledGroups: sets.String{},
|
handledGroups: sets.String{},
|
||||||
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
|
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
|
||||||
APIRegistrationInformers: informerFactory,
|
APIRegistrationInformers: informerFactory,
|
||||||
@ -244,6 +280,20 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.GenericAPIServer.PrepareOpenAPIService()
|
||||||
|
|
||||||
|
if s.GenericAPIServer.OpenAPIService != nil {
|
||||||
|
s.rootSpec = s.GenericAPIServer.OpenAPIService.GetSpec()
|
||||||
|
if err := s.updateOpenAPISpec(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.GenericAPIServer.OpenAPIService.AddUpdateHook(func(r *http.Request) {
|
||||||
|
if s.tryLoadingOpenAPISpecs(r) {
|
||||||
|
s.updateOpenAPISpec()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +324,9 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) {
|
|||||||
}
|
}
|
||||||
proxyHandler.updateAPIService(apiService)
|
proxyHandler.updateAPIService(apiService)
|
||||||
s.proxyHandlers[apiService.Name] = proxyHandler
|
s.proxyHandlers[apiService.Name] = proxyHandler
|
||||||
|
|
||||||
|
s.deferLoadAPISpec(apiService.Name)
|
||||||
|
|
||||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
|
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
|
||||||
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
|
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
|
||||||
|
|
||||||
@ -302,6 +355,19 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) {
|
|||||||
s.handledGroups.Insert(apiService.Spec.Group)
|
s.handledGroups.Insert(apiService.Spec.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIAggregator) deferLoadAPISpec(name string) {
|
||||||
|
s.specMutex.Lock()
|
||||||
|
defer s.specMutex.Unlock()
|
||||||
|
s.toLoadAPISpec[name] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIAggregator) deleteApiSpec(name string) {
|
||||||
|
s.specMutex.Lock()
|
||||||
|
defer s.specMutex.Unlock()
|
||||||
|
delete(s.apiServiceSpecs, name)
|
||||||
|
delete(s.toLoadAPISpec, name)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveAPIService removes the APIService from being handled. It is not thread-safe, so only call it on one thread at a time please.
|
// RemoveAPIService removes the APIService from being handled. It is not thread-safe, so only call it on one thread at a time please.
|
||||||
// It's a slow moving API, so its ok to run the controller on a single thread.
|
// It's a slow moving API, so its ok to run the controller on a single thread.
|
||||||
func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
||||||
@ -315,7 +381,124 @@ func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
|||||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath)
|
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath)
|
||||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath + "/")
|
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath + "/")
|
||||||
delete(s.proxyHandlers, apiServiceName)
|
delete(s.proxyHandlers, apiServiceName)
|
||||||
|
s.deleteApiSpec(apiServiceName)
|
||||||
|
s.updateOpenAPISpec()
|
||||||
|
|
||||||
// TODO unregister group level discovery when there are no more versions for the group
|
// TODO unregister group level discovery when there are no more versions for the group
|
||||||
// We don't need this right away because the handler properly delegates when no versions are present
|
// We don't need this right away because the handler properly delegates when no versions are present
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_ *APIAggregator) loadOpenAPISpec(p *proxyHandler, r *http.Request) (*spec.Swagger, error) {
|
||||||
|
value := p.handlingInfo.Load()
|
||||||
|
if value == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
handlingInfo := value.(proxyHandlingInfo)
|
||||||
|
if handlingInfo.local {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
loc, err := p.routing.ResolveEndpoint(handlingInfo.serviceNamespace, handlingInfo.serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("missing route")
|
||||||
|
}
|
||||||
|
host := loc.Host
|
||||||
|
|
||||||
|
var w io.Reader
|
||||||
|
req, err := http.NewRequest("GET", "/swagger.json", w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.URL.Scheme = "https"
|
||||||
|
req.URL.Host = host
|
||||||
|
|
||||||
|
req = req.WithContext(context.Background())
|
||||||
|
// Get user from the original request
|
||||||
|
ctx, ok := p.contextMapper.Get(r)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("missing context")
|
||||||
|
}
|
||||||
|
user, ok := genericapirequest.UserFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("missing user")
|
||||||
|
}
|
||||||
|
proxyRoundTripper := transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), handlingInfo.proxyRoundTripper)
|
||||||
|
res, err := proxyRoundTripper.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.New(res.Status)
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(res.Body)
|
||||||
|
bytes := buf.Bytes()
|
||||||
|
var s spec.Swagger
|
||||||
|
if err := json.Unmarshal(bytes, &s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if any Spec is loaded
|
||||||
|
func (s *APIAggregator) tryLoadingOpenAPISpecs(r *http.Request) bool {
|
||||||
|
s.specMutex.Lock()
|
||||||
|
defer s.specMutex.Unlock()
|
||||||
|
if len(s.toLoadAPISpec) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
loaded := false
|
||||||
|
newList := map[string]int{}
|
||||||
|
for name, retries := range s.toLoadAPISpec {
|
||||||
|
if retries >= LOAD_OPENAPI_SPEC_MAX_RETRIES {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxyHandler := s.proxyHandlers[name]
|
||||||
|
if spec, err := s.loadOpenAPISpec(proxyHandler, r); err != nil {
|
||||||
|
glog.Warningf("Failed to Load OpenAPI spec (try %d of %d) for %s, err=%s", retries+1, LOAD_OPENAPI_SPEC_MAX_RETRIES, name, err)
|
||||||
|
newList[name] = retries + 1
|
||||||
|
} else if spec != nil {
|
||||||
|
s.apiServiceSpecs[name] = spec
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
s.toLoadAPISpec = newList
|
||||||
|
}
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIAggregator) updateOpenAPISpec() error {
|
||||||
|
s.specMutex.Lock()
|
||||||
|
defer s.specMutex.Unlock()
|
||||||
|
if s.GenericAPIServer.OpenAPIService == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sp, err := openapi.CloneSpec(s.rootSpec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
openapi.FilterSpecByPaths(sp, []string{"/apis/apiregistration.k8s.io/"})
|
||||||
|
if _, found := sp.Paths.Paths["/version/"]; found {
|
||||||
|
return fmt.Errorf("Cleanup didn't work")
|
||||||
|
}
|
||||||
|
if err := openapi.MergeSpecs(sp, s.delegationSpec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.apiServiceSpecs {
|
||||||
|
version := apiregistration.APIServiceNameToGroupVersion(k)
|
||||||
|
|
||||||
|
proxyPath := "/apis/" + version.Group + "/"
|
||||||
|
// v1. is a special case for the legacy API. It proxies to a wider set of endpoints.
|
||||||
|
if k == legacyAPIServiceName {
|
||||||
|
proxyPath = "/api/"
|
||||||
|
}
|
||||||
|
spc, err := openapi.CloneSpec(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
openapi.FilterSpecByPaths(spc, []string{proxyPath})
|
||||||
|
if err := openapi.MergeSpecs(sp, spc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.GenericAPIServer.OpenAPIService.UpdateSpec(sp)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user