Add OpenAPI v3 publishing under feature gate OpenAPIV3
This commit is contained in:
parent
42d8b2f3b9
commit
5bf3ed7a98
@ -32,6 +32,7 @@ import (
|
|||||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema"
|
"k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema"
|
||||||
openapicontroller "k8s.io/apiextensions-apiserver/pkg/controller/openapi"
|
openapicontroller "k8s.io/apiextensions-apiserver/pkg/controller/openapi"
|
||||||
|
openapiv3controller "k8s.io/apiextensions-apiserver/pkg/controller/openapiv3"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -41,10 +42,12 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -218,6 +221,10 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||||||
crdHandler,
|
crdHandler,
|
||||||
)
|
)
|
||||||
openapiController := openapicontroller.NewController(s.Informers.Apiextensions().V1().CustomResourceDefinitions())
|
openapiController := openapicontroller.NewController(s.Informers.Apiextensions().V1().CustomResourceDefinitions())
|
||||||
|
var openapiv3Controller *openapiv3controller.Controller
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
|
||||||
|
openapiv3Controller = openapiv3controller.NewController(s.Informers.Apiextensions().V1().CustomResourceDefinitions())
|
||||||
|
}
|
||||||
|
|
||||||
s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
|
s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
|
||||||
s.Informers.Start(context.StopCh)
|
s.Informers.Start(context.StopCh)
|
||||||
@ -230,6 +237,9 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||||||
// and StaticOpenAPISpec are both null. In that case we don't run the CRD OpenAPI controller.
|
// and StaticOpenAPISpec are both null. In that case we don't run the CRD OpenAPI controller.
|
||||||
if s.GenericAPIServer.OpenAPIVersionedService != nil && s.GenericAPIServer.StaticOpenAPISpec != nil {
|
if s.GenericAPIServer.OpenAPIVersionedService != nil && s.GenericAPIServer.StaticOpenAPISpec != nil {
|
||||||
go openapiController.Run(s.GenericAPIServer.StaticOpenAPISpec, s.GenericAPIServer.OpenAPIVersionedService, context.StopCh)
|
go openapiController.Run(s.GenericAPIServer.StaticOpenAPISpec, s.GenericAPIServer.OpenAPIVersionedService, context.StopCh)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
|
||||||
|
go openapiv3Controller.Run(s.GenericAPIServer.OpenAPIV3VersionedService, context.StopCh)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go namingController.Run(context.StopCh)
|
go namingController.Run(context.StopCh)
|
||||||
|
@ -1361,7 +1361,8 @@ func buildOpenAPIModelsForApply(staticOpenAPISpec *spec.Swagger, crd *apiextensi
|
|||||||
specs := []*spec.Swagger{}
|
specs := []*spec.Swagger{}
|
||||||
for _, v := range crd.Spec.Versions {
|
for _, v := range crd.Spec.Versions {
|
||||||
// Defaults are not pruned here, but before being served.
|
// Defaults are not pruned here, but before being served.
|
||||||
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripValueValidation: true, StripNullable: true, AllowNonStructural: false})
|
// See flag description in builder.go for flag usage
|
||||||
|
s, err := builder.BuildOpenAPIV2(crd, v.Name, builder.Options{V2: true, SkipFilterSchemaForKubectlOpenAPIV2Validation: true, StripValueValidation: true, StripNullable: true, AllowNonStructural: false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,9 @@ import (
|
|||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
||||||
openapibuilder "k8s.io/kube-openapi/pkg/builder"
|
openapibuilder "k8s.io/kube-openapi/pkg/builder"
|
||||||
|
"k8s.io/kube-openapi/pkg/builder3"
|
||||||
"k8s.io/kube-openapi/pkg/common"
|
"k8s.io/kube-openapi/pkg/common"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/kube-openapi/pkg/util"
|
"k8s.io/kube-openapi/pkg/util"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
)
|
)
|
||||||
@ -52,10 +54,13 @@ const (
|
|||||||
// Reference and Go types for built-in metadata
|
// Reference and Go types for built-in metadata
|
||||||
objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
||||||
listMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
|
listMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
|
||||||
listMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
|
|
||||||
typeMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta"
|
|
||||||
|
|
||||||
definitionPrefix = "#/definitions/"
|
listMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"
|
||||||
|
typeMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta"
|
||||||
|
objectMetaType = "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"
|
||||||
|
|
||||||
|
definitionPrefix = "#/definitions/"
|
||||||
|
v3DefinitionPrefix = "#/components/schemas/"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -66,6 +71,15 @@ var (
|
|||||||
namespaceToken = "{namespace}"
|
namespaceToken = "{namespace}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The path for definitions in OpenAPI v2 and v3 are different. Translate the path if necessary
|
||||||
|
// The provided schemaRef uses a v2 prefix and is converted to v3 if the v2 bool is false
|
||||||
|
func refForOpenAPIVersion(schemaRef string, v2 bool) string {
|
||||||
|
if v2 {
|
||||||
|
return schemaRef
|
||||||
|
}
|
||||||
|
return strings.Replace(schemaRef, definitionPrefix, v3DefinitionPrefix, 1)
|
||||||
|
}
|
||||||
|
|
||||||
var definitions map[string]common.OpenAPIDefinition
|
var definitions map[string]common.OpenAPIDefinition
|
||||||
var buildDefinitions sync.Once
|
var buildDefinitions sync.Once
|
||||||
var namer *openapi.DefinitionNamer
|
var namer *openapi.DefinitionNamer
|
||||||
@ -75,6 +89,12 @@ type Options struct {
|
|||||||
// Convert to OpenAPI v2.
|
// Convert to OpenAPI v2.
|
||||||
V2 bool
|
V2 bool
|
||||||
|
|
||||||
|
// Only takes effect if the flag and V2 and both set to true. If the condition is reached,
|
||||||
|
// publish OpenAPI V2 but skip running the spec through ToStructuralOpenAPIV2
|
||||||
|
// This prevents XPreserveUnknownFields:true fields from being cleared
|
||||||
|
// Used only by server side apply
|
||||||
|
SkipFilterSchemaForKubectlOpenAPIV2Validation bool
|
||||||
|
|
||||||
// Strip value validation.
|
// Strip value validation.
|
||||||
StripValueValidation bool
|
StripValueValidation bool
|
||||||
|
|
||||||
@ -85,8 +105,7 @@ type Options struct {
|
|||||||
AllowNonStructural bool
|
AllowNonStructural bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildSwagger builds swagger for the given crd in the given version
|
func generateBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*builder, error) {
|
||||||
func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*spec.Swagger, error) {
|
|
||||||
var schema *structuralschema.Structural
|
var schema *structuralschema.Structural
|
||||||
s, err := apiextensionshelpers.GetSchemaForVersion(crd, version)
|
s, err := apiextensionshelpers.GetSchemaForVersion(crd, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -122,7 +141,7 @@ func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string,
|
|||||||
// comes from function registerResourceHandlers() in k8s.io/apiserver.
|
// comes from function registerResourceHandlers() in k8s.io/apiserver.
|
||||||
// Alternatives are either (ideally) refactoring registerResourceHandlers() to
|
// Alternatives are either (ideally) refactoring registerResourceHandlers() to
|
||||||
// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers().
|
// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers().
|
||||||
b := newBuilder(crd, version, schema, opts.V2)
|
b := newBuilder(crd, version, schema, opts)
|
||||||
|
|
||||||
// Sample response types for building web service
|
// Sample response types for building web service
|
||||||
sample := &CRDCanonicalTypeNamer{
|
sample := &CRDCanonicalTypeNamer{
|
||||||
@ -173,13 +192,26 @@ func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string,
|
|||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
b.ws.Route(route)
|
b.ws.Route(route)
|
||||||
}
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
openAPISpec, err := openapibuilder.BuildOpenAPISpec([]*restful.WebService{b.ws}, b.getOpenAPIConfig())
|
func BuildOpenAPIV3(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*spec3.OpenAPI, error) {
|
||||||
|
b, err := generateBuilder(crd, version, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return openAPISpec, nil
|
return builder3.BuildOpenAPISpec([]*restful.WebService{b.ws}, b.getOpenAPIConfig(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildOpenAPIV2 builds OpenAPI v2 for the given crd in the given version
|
||||||
|
func BuildOpenAPIV2(crd *apiextensionsv1.CustomResourceDefinition, version string, opts Options) (*spec.Swagger, error) {
|
||||||
|
b, err := generateBuilder(crd, version, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return openapibuilder.BuildOpenAPISpec([]*restful.WebService{b.ws}, b.getOpenAPIConfig(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implements CanonicalTypeNamer
|
// Implements CanonicalTypeNamer
|
||||||
@ -349,26 +381,26 @@ func (b *builder) buildRoute(root, path, httpMethod, actionVerb, operationVerb s
|
|||||||
|
|
||||||
// buildKubeNative builds input schema with Kubernetes' native object meta, type meta and
|
// buildKubeNative builds input schema with Kubernetes' native object meta, type meta and
|
||||||
// extensions
|
// extensions
|
||||||
func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool, crdPreserveUnknownFields bool) (ret *spec.Schema) {
|
func (b *builder) buildKubeNative(schema *structuralschema.Structural, opts Options, crdPreserveUnknownFields bool) (ret *spec.Schema) {
|
||||||
// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false
|
// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false
|
||||||
// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by
|
// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by
|
||||||
// adding additionalProperties=true support to explicitly allow additional fields.
|
// adding additionalProperties=true support to explicitly allow additional fields.
|
||||||
// TODO: fix kubectl to understand additionalProperties=true
|
// TODO: fix kubectl to understand additionalProperties=true
|
||||||
if schema == nil || (v2 && (schema.XPreserveUnknownFields || crdPreserveUnknownFields)) {
|
if schema == nil || ((opts.V2 && !opts.SkipFilterSchemaForKubectlOpenAPIV2Validation) && (schema.XPreserveUnknownFields || crdPreserveUnknownFields)) {
|
||||||
ret = &spec.Schema{
|
ret = &spec.Schema{
|
||||||
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
|
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
|
||||||
}
|
}
|
||||||
// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
|
// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
|
||||||
// unknown fields for anything else.
|
// unknown fields for anything else.
|
||||||
} else {
|
} else {
|
||||||
if v2 {
|
if opts.V2 && !opts.SkipFilterSchemaForKubectlOpenAPIV2Validation {
|
||||||
schema = openapiv2.ToStructuralOpenAPIV2(schema)
|
schema = openapiv2.ToStructuralOpenAPIV2(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = schema.ToKubeOpenAPI()
|
ret = schema.ToKubeOpenAPI()
|
||||||
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
|
ret.SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(objectMetaSchemaRef, opts.V2)).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
||||||
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
addTypeMetaProperties(ret, opts.V2)
|
||||||
addTypeMetaProperties(ret)
|
addEmbeddedProperties(ret, opts)
|
||||||
addEmbeddedProperties(ret, v2)
|
|
||||||
}
|
}
|
||||||
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
|
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
@ -381,36 +413,36 @@ func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool,
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func addEmbeddedProperties(s *spec.Schema, v2 bool) {
|
func addEmbeddedProperties(s *spec.Schema, opts Options) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range s.Properties {
|
for k := range s.Properties {
|
||||||
v := s.Properties[k]
|
v := s.Properties[k]
|
||||||
addEmbeddedProperties(&v, v2)
|
addEmbeddedProperties(&v, opts)
|
||||||
s.Properties[k] = v
|
s.Properties[k] = v
|
||||||
}
|
}
|
||||||
if s.Items != nil {
|
if s.Items != nil {
|
||||||
addEmbeddedProperties(s.Items.Schema, v2)
|
addEmbeddedProperties(s.Items.Schema, opts)
|
||||||
}
|
}
|
||||||
if s.AdditionalProperties != nil {
|
if s.AdditionalProperties != nil {
|
||||||
addEmbeddedProperties(s.AdditionalProperties.Schema, v2)
|
addEmbeddedProperties(s.AdditionalProperties.Schema, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-preserve-unknown-fields"); ok && isTrue && v2 {
|
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-preserve-unknown-fields"); ok && isTrue && opts.V2 && !opts.SkipFilterSchemaForKubectlOpenAPIV2Validation {
|
||||||
// don't add metadata properties if we're publishing to openapi v2 and are allowing unknown fields.
|
// don't add metadata properties if we're publishing to openapi v2 and are allowing unknown fields.
|
||||||
// adding these metadata properties makes kubectl refuse to validate unknown fields.
|
// adding these metadata properties makes kubectl refuse to validate unknown fields.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-embedded-resource"); ok && isTrue {
|
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-embedded-resource"); ok && isTrue {
|
||||||
s.SetProperty("apiVersion", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["apiVersion"],
|
s.SetProperty("apiVersion", withDescription(getDefinition(typeMetaType, opts.V2).SchemaProps.Properties["apiVersion"],
|
||||||
"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||||
))
|
))
|
||||||
s.SetProperty("kind", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["kind"],
|
s.SetProperty("kind", withDescription(getDefinition(typeMetaType, opts.V2).SchemaProps.Properties["kind"],
|
||||||
"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||||
))
|
))
|
||||||
s.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
s.SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(objectMetaSchemaRef, opts.V2)).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
||||||
|
|
||||||
req := sets.NewString(s.Required...)
|
req := sets.NewString(s.Required...)
|
||||||
if !req.Has("kind") {
|
if !req.Has("kind") {
|
||||||
@ -424,8 +456,8 @@ func addEmbeddedProperties(s *spec.Schema, v2 bool) {
|
|||||||
|
|
||||||
// getDefinition gets definition for given Kubernetes type. This function is extracted from
|
// getDefinition gets definition for given Kubernetes type. This function is extracted from
|
||||||
// kube-openapi builder logic
|
// kube-openapi builder logic
|
||||||
func getDefinition(name string) spec.Schema {
|
func getDefinition(name string, v2 bool) spec.Schema {
|
||||||
buildDefinitions.Do(buildDefinitionsFunc)
|
buildDefinitions.Do(generateBuildDefinitionsFunc(v2))
|
||||||
return definitions[name].Schema
|
return definitions[name].Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,31 +465,37 @@ func withDescription(s spec.Schema, desc string) spec.Schema {
|
|||||||
return *s.WithDescription(desc)
|
return *s.WithDescription(desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDefinitionsFunc() {
|
func generateBuildDefinitionsFunc(v2 bool) func() {
|
||||||
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
|
return func() {
|
||||||
definitions = utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(func(name string) spec.Ref {
|
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
|
||||||
defName, _ := namer.GetDefinitionName(name)
|
definitions = utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(func(name string) spec.Ref {
|
||||||
return spec.MustCreateRef(definitionPrefix + common.EscapeJsonPointer(defName))
|
defName, _ := namer.GetDefinitionName(name)
|
||||||
})
|
prefix := v3DefinitionPrefix
|
||||||
|
if v2 {
|
||||||
|
prefix = definitionPrefix
|
||||||
|
}
|
||||||
|
return spec.MustCreateRef(prefix + common.EscapeJsonPointer(defName))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addTypeMetaProperties adds Kubernetes-specific type meta properties to input schema:
|
// addTypeMetaProperties adds Kubernetes-specific type meta properties to input schema:
|
||||||
// apiVersion and kind
|
// apiVersion and kind
|
||||||
func addTypeMetaProperties(s *spec.Schema) {
|
func addTypeMetaProperties(s *spec.Schema, v2 bool) {
|
||||||
s.SetProperty("apiVersion", getDefinition(typeMetaType).SchemaProps.Properties["apiVersion"])
|
s.SetProperty("apiVersion", getDefinition(typeMetaType, v2).SchemaProps.Properties["apiVersion"])
|
||||||
s.SetProperty("kind", getDefinition(typeMetaType).SchemaProps.Properties["kind"])
|
s.SetProperty("kind", getDefinition(typeMetaType, v2).SchemaProps.Properties["kind"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildListSchema builds the list kind schema for the CRD
|
// buildListSchema builds the list kind schema for the CRD
|
||||||
func (b *builder) buildListSchema() *spec.Schema {
|
func (b *builder) buildListSchema(v2 bool) *spec.Schema {
|
||||||
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
|
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
|
||||||
doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural)
|
doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural)
|
||||||
s := new(spec.Schema).WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
|
s := new(spec.Schema).WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
|
||||||
WithRequired("items").
|
WithRequired("items").
|
||||||
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(name)).WithDescription(doc)).
|
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(name)).WithDescription(doc)).
|
||||||
SetProperty("metadata", *spec.RefSchema(listMetaSchemaRef).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"]))
|
SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(listMetaSchemaRef, v2)).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"]))
|
||||||
|
|
||||||
addTypeMetaProperties(s)
|
addTypeMetaProperties(s, v2)
|
||||||
s.AddExtension(endpoints.ROUTE_META_GVK, []map[string]string{
|
s.AddExtension(endpoints.ROUTE_META_GVK, []map[string]string{
|
||||||
{
|
{
|
||||||
"group": b.group,
|
"group": b.group,
|
||||||
@ -469,7 +507,7 @@ func (b *builder) buildListSchema() *spec.Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getOpenAPIConfig builds config which wires up generated definitions for kube-openapi to consume
|
// getOpenAPIConfig builds config which wires up generated definitions for kube-openapi to consume
|
||||||
func (b *builder) getOpenAPIConfig() *common.Config {
|
func (b *builder) getOpenAPIConfig(v2 bool) *common.Config {
|
||||||
return &common.Config{
|
return &common.Config{
|
||||||
ProtocolList: []string{"https"},
|
ProtocolList: []string{"https"},
|
||||||
Info: &spec.Info{
|
Info: &spec.Info{
|
||||||
@ -487,13 +525,14 @@ func (b *builder) getOpenAPIConfig() *common.Config {
|
|||||||
},
|
},
|
||||||
GetOperationIDAndTags: openapi.GetOperationIDAndTags,
|
GetOperationIDAndTags: openapi.GetOperationIDAndTags,
|
||||||
GetDefinitionName: func(name string) (string, spec.Extensions) {
|
GetDefinitionName: func(name string) (string, spec.Extensions) {
|
||||||
buildDefinitions.Do(buildDefinitionsFunc)
|
buildDefinitions.Do(generateBuildDefinitionsFunc(v2))
|
||||||
return namer.GetDefinitionName(name)
|
return namer.GetDefinitionName(name)
|
||||||
},
|
},
|
||||||
GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
def := utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(ref)
|
def := utilopenapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)(ref)
|
||||||
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
|
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
|
||||||
Schema: *b.schema,
|
Schema: *b.schema,
|
||||||
|
Dependencies: []string{objectMetaType},
|
||||||
}
|
}
|
||||||
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
|
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
|
||||||
Schema: *b.listSchema,
|
Schema: *b.listSchema,
|
||||||
@ -503,7 +542,7 @@ func (b *builder) getOpenAPIConfig() *common.Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, schema *structuralschema.Structural, v2 bool) *builder {
|
func newBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, schema *structuralschema.Structural, opts Options) *builder {
|
||||||
b := &builder{
|
b := &builder{
|
||||||
schema: &spec.Schema{
|
schema: &spec.Schema{
|
||||||
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
|
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
|
||||||
@ -522,8 +561,8 @@ func newBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pre-build schema with Kubernetes native properties
|
// Pre-build schema with Kubernetes native properties
|
||||||
b.schema = b.buildKubeNative(schema, v2, crd.Spec.PreserveUnknownFields)
|
b.schema = b.buildKubeNative(schema, opts, crd.Spec.PreserveUnknownFields)
|
||||||
b.listSchema = b.buildListSchema()
|
b.listSchema = b.buildListSchema(opts.V2)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
@ -45,37 +45,43 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
wantedSchema string
|
wantedSchema string
|
||||||
wantedItemsSchema string
|
wantedItemsSchema string
|
||||||
|
|
||||||
v2 bool // produce OpenAPIv2
|
v2 bool // produce OpenAPIv2
|
||||||
|
skipFilterSchemaForKubectlOpenAPIV2Validation bool // produce OpenAPIv2 without going through the ToStructuralOpenAPIV2 path
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"nil",
|
"nil",
|
||||||
"",
|
"",
|
||||||
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`, `{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
{"with properties",
|
{"with properties",
|
||||||
`{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
|
`{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
|
||||||
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
{"type only",
|
{"type only",
|
||||||
`{"type":"object"}`,
|
`{"type":"object"}`,
|
||||||
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
{"preserve unknown at root v2",
|
{"preserve unknown at root v2",
|
||||||
`{"type":"object","x-kubernetes-preserve-unknown-fields":true}`,
|
`{"type":"object","x-kubernetes-preserve-unknown-fields":true}`,
|
||||||
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
{"preserve unknown at root v3",
|
{"preserve unknown at root v3",
|
||||||
`{"type":"object","x-kubernetes-preserve-unknown-fields":true}`,
|
`{"type":"object","x-kubernetes-preserve-unknown-fields":true}`,
|
||||||
`{"type":"object","x-kubernetes-preserve-unknown-fields":true,"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
`{"type":"object","x-kubernetes-preserve-unknown-fields":true,"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
false,
|
true,
|
||||||
|
true,
|
||||||
},
|
},
|
||||||
{"with extensions",
|
{"with extensions",
|
||||||
`
|
`
|
||||||
@ -179,6 +185,7 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
}`,
|
}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
{"with extensions as v3 schema",
|
{"with extensions as v3 schema",
|
||||||
`
|
`
|
||||||
@ -344,7 +351,8 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
||||||
}`,
|
}`,
|
||||||
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
`{"$ref":"#/definitions/io.k8s.bar.v1.Foo"}`,
|
||||||
false,
|
true,
|
||||||
|
true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -384,7 +392,7 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Scope: apiextensionsv1.NamespaceScoped,
|
Scope: apiextensionsv1.NamespaceScoped,
|
||||||
},
|
},
|
||||||
}, "v1", schema, tt.v2)
|
}, "v1", schema, Options{V2: tt.v2, SkipFilterSchemaForKubectlOpenAPIV2Validation: tt.skipFilterSchemaForKubectlOpenAPIV2Validation})
|
||||||
|
|
||||||
var wantedSchema, wantedItemsSchema spec.Schema
|
var wantedSchema, wantedItemsSchema spec.Schema
|
||||||
if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
|
if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
|
||||||
@ -500,7 +508,7 @@ func TestCRDRouteParameterBuilder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
swagger, err := BuildSwagger(testNamespacedCRD, testCRDVersion, Options{V2: true})
|
swagger, err := BuildOpenAPIV2(testNamespacedCRD, testCRDVersion, Options{V2: true})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(testCase.paths), len(swagger.Paths.Paths), testCase.scope)
|
require.Equal(t, len(testCase.paths), len(swagger.Paths.Paths), testCase.scope)
|
||||||
for path, expected := range testCase.paths {
|
for path, expected := range testCase.paths {
|
||||||
@ -567,7 +575,7 @@ func schemaDiff(a, b *spec.Schema) string {
|
|||||||
return diff.StringDiff(string(as), string(bs))
|
return diff.StringDiff(string(as), string(bs))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildSwagger(t *testing.T) {
|
func TestBuildOpenAPIV2(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
schema string
|
schema string
|
||||||
@ -622,7 +630,7 @@ func TestBuildSwagger(t *testing.T) {
|
|||||||
`{"type":"object","properties":{"foo":{"type":"string","oneOf":[{"pattern":"a"},{"pattern":"b"}]}}}`,
|
`{"type":"object","properties":{"foo":{"type":"string","oneOf":[{"pattern":"a"},{"pattern":"b"}]}}}`,
|
||||||
nil,
|
nil,
|
||||||
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"foo":{"type":"string","oneOf":[{"pattern":"a"},{"pattern":"b"}]}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"foo":{"type":"string","oneOf":[{"pattern":"a"},{"pattern":"b"}]}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
Options{V2: false},
|
Options{V2: true, SkipFilterSchemaForKubectlOpenAPIV2Validation: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -642,7 +650,7 @@ func TestBuildSwagger(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: mostly copied from the test above. reuse code to cleanup
|
// TODO: mostly copied from the test above. reuse code to cleanup
|
||||||
got, err := BuildSwagger(&apiextensionsv1.CustomResourceDefinition{
|
got, err := BuildOpenAPIV2(&apiextensionsv1.CustomResourceDefinition{
|
||||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||||
Group: "bar.k8s.io",
|
Group: "bar.k8s.io",
|
||||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
||||||
@ -691,3 +699,106 @@ func TestBuildSwagger(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildOpenAPIV3(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
schema string
|
||||||
|
preserveUnknownFields *bool
|
||||||
|
wantedSchema string
|
||||||
|
opts Options
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"nil",
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
|
Options{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with properties",
|
||||||
|
`{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
|
||||||
|
nil,
|
||||||
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
|
Options{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with v3 nullable field",
|
||||||
|
`{"type":"object","properties":{"spec":{"type":"object", "nullable": true},"status":{"type":"object"}}}`,
|
||||||
|
nil,
|
||||||
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object", "nullable": true},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
|
Options{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with default not pruned for v3",
|
||||||
|
`{"type":"object","properties":{"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}}}`,
|
||||||
|
nil,
|
||||||
|
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object","properties":{"field":{"type":"string","default":"foo"}}},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
|
||||||
|
Options{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var validation *apiextensionsv1.CustomResourceValidation
|
||||||
|
if len(tt.schema) > 0 {
|
||||||
|
v1Schema := &apiextensionsv1.JSONSchemaProps{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.schema), &v1Schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
validation = &apiextensionsv1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: v1Schema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.preserveUnknownFields != nil && *tt.preserveUnknownFields {
|
||||||
|
validation.OpenAPIV3Schema.XPreserveUnknownFields = utilpointer.BoolPtr(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := BuildOpenAPIV3(&apiextensionsv1.CustomResourceDefinition{
|
||||||
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "bar.k8s.io",
|
||||||
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1",
|
||||||
|
Schema: validation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "foos",
|
||||||
|
Singular: "foo",
|
||||||
|
Kind: "Foo",
|
||||||
|
ListKind: "FooList",
|
||||||
|
},
|
||||||
|
Scope: apiextensionsv1.NamespaceScoped,
|
||||||
|
},
|
||||||
|
}, "v1", tt.opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wantedSchema spec.Schema
|
||||||
|
if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotSchema := *got.Components.Schemas["io.k8s.bar.v1.Foo"]
|
||||||
|
gotProperties := properties(gotSchema.Properties)
|
||||||
|
wantedProperties := properties(wantedSchema.Properties)
|
||||||
|
if !gotProperties.Equal(wantedProperties) {
|
||||||
|
t.Fatalf("unexpected properties, got: %s, expected: %s", gotProperties.List(), wantedProperties.List())
|
||||||
|
}
|
||||||
|
|
||||||
|
// wipe out TypeMeta/ObjectMeta content, with those many lines of descriptions. We trust that they match here.
|
||||||
|
for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
|
||||||
|
if _, found := gotSchema.Properties["kind"]; found {
|
||||||
|
prop := gotSchema.Properties[metaField]
|
||||||
|
prop.Description = ""
|
||||||
|
gotSchema.Properties[metaField] = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(&wantedSchema, &gotSchema) {
|
||||||
|
t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, &gotSchema), &wantedSchema, &gotSchema)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,7 +17,9 @@ limitations under the License.
|
|||||||
package builder
|
package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/kube-openapi/pkg/aggregator"
|
"k8s.io/kube-openapi/pkg/aggregator"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,3 +81,51 @@ func mergeSpec(dest, source *spec.Swagger) {
|
|||||||
dest.Paths.Paths[k] = v
|
dest.Paths.Paths[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeSpecsV3 merges OpenAPI v3 specs for CRDs
|
||||||
|
// For V3, the static spec is never merged with the individual CRD specs so no conflict resolution is necessary
|
||||||
|
func MergeSpecsV3(crdSpecs ...*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||||
|
// create shallow copy of staticSpec, but replace paths and definitions because we modify them.
|
||||||
|
crdSpec := &spec3.OpenAPI{}
|
||||||
|
if len(crdSpecs) > 0 {
|
||||||
|
crdSpec.Version = crdSpecs[0].Version
|
||||||
|
crdSpec.Info = crdSpecs[0].Info
|
||||||
|
}
|
||||||
|
for _, s := range crdSpecs {
|
||||||
|
// merge specs without checking conflicts, since the naming controller prevents
|
||||||
|
// conflicts between user-defined CRDs
|
||||||
|
mergeSpecV3(crdSpec, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return crdSpec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeSpecV3 copies paths and definitions from source to dest, mutating dest, but not source.
|
||||||
|
// We assume that conflicts do not matter.
|
||||||
|
func mergeSpecV3(dest, source *spec3.OpenAPI) {
|
||||||
|
if source == nil || source.Paths == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dest.Paths == nil {
|
||||||
|
dest.Paths = &spec3.Paths{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range source.Components.Schemas {
|
||||||
|
if dest.Components == nil {
|
||||||
|
dest.Components = &spec3.Components{}
|
||||||
|
}
|
||||||
|
if dest.Components.Schemas == nil {
|
||||||
|
dest.Components.Schemas = map[string]*spec.Schema{}
|
||||||
|
}
|
||||||
|
if _, exists := dest.Components.Schemas[k]; exists {
|
||||||
|
klog.Warningf("Should not happen: OpenAPI V3 merge schema conflict on %s", k)
|
||||||
|
}
|
||||||
|
dest.Components.Schemas[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range source.Paths.Paths {
|
||||||
|
if dest.Paths.Paths == nil {
|
||||||
|
dest.Paths.Paths = map[string]*spec3.Path{}
|
||||||
|
}
|
||||||
|
dest.Paths.Paths[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -202,7 +202,7 @@ func buildVersionSpecs(crd *apiextensionsv1.CustomResourceDefinition, oldSpecs m
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Defaults are not pruned here, but before being served.
|
// Defaults are not pruned here, but before being served.
|
||||||
spec, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: true})
|
spec, err := builder.BuildOpenAPIV2(crd, v.Name, builder.Options{V2: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,272 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 openapiv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kube-openapi/pkg/handler3"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
|
|
||||||
|
apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
|
||||||
|
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller watches CustomResourceDefinitions and publishes OpenAPI v3
|
||||||
|
type Controller struct {
|
||||||
|
crdLister listers.CustomResourceDefinitionLister
|
||||||
|
crdsSynced cache.InformerSynced
|
||||||
|
|
||||||
|
// To allow injection for testing.
|
||||||
|
syncFn func(string) error
|
||||||
|
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
|
||||||
|
openAPIV3Service *handler3.OpenAPIService
|
||||||
|
|
||||||
|
// specs per version and per CRD name
|
||||||
|
lock sync.Mutex
|
||||||
|
specsByGVandName map[schema.GroupVersion]map[string]*spec3.OpenAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewController creates a new Controller with input CustomResourceDefinition informer
|
||||||
|
func NewController(crdInformer informers.CustomResourceDefinitionInformer) *Controller {
|
||||||
|
c := &Controller{
|
||||||
|
crdLister: crdInformer.Lister(),
|
||||||
|
crdsSynced: crdInformer.Informer().HasSynced,
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "crd_openapi_v3_controller"),
|
||||||
|
specsByGVandName: map[schema.GroupVersion]map[string]*spec3.OpenAPI{},
|
||||||
|
}
|
||||||
|
|
||||||
|
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: c.addCustomResourceDefinition,
|
||||||
|
UpdateFunc: c.updateCustomResourceDefinition,
|
||||||
|
DeleteFunc: c.deleteCustomResourceDefinition,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.syncFn = c.sync
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run sets openAPIAggregationManager and starts workers
|
||||||
|
func (c *Controller) Run(openAPIV3Service *handler3.OpenAPIService, stopCh <-chan struct{}) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer c.queue.ShutDown()
|
||||||
|
defer klog.Infof("Shutting down OpenAPI V3 controller")
|
||||||
|
|
||||||
|
klog.Infof("Starting OpenAPI V3 controller")
|
||||||
|
|
||||||
|
c.openAPIV3Service = openAPIV3Service
|
||||||
|
|
||||||
|
if !cache.WaitForCacheSync(stopCh, c.crdsSynced) {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
crds, err := c.crdLister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("failed to initially list all CRDs: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, crd := range crds {
|
||||||
|
if !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range crd.Spec.Versions {
|
||||||
|
if !v.Served {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.buildV3Spec(crd, crd.Name, v.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only start one worker thread since its a slow moving API
|
||||||
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||||
|
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) runWorker() {
|
||||||
|
for c.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) processNextWorkItem() bool {
|
||||||
|
key, quit := c.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer c.queue.Done(key)
|
||||||
|
|
||||||
|
// log slow aggregations
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
if elapsed > time.Second {
|
||||||
|
klog.Warningf("slow openapi aggregation of %q: %s", key.(string), elapsed)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := c.syncFn(key.(string))
|
||||||
|
if err == nil {
|
||||||
|
c.queue.Forget(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
|
||||||
|
c.queue.AddRateLimited(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) sync(name string) error {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
crd, err := c.crdLister.Get(name)
|
||||||
|
if err != nil && !errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.IsNotFound(err) || !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established) {
|
||||||
|
c.deleteCRD(name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range crd.Spec.Versions {
|
||||||
|
if !v.Served {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.buildV3Spec(crd, name, v.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) deleteCRD(name string) {
|
||||||
|
for gv, crdListForGV := range c.specsByGVandName {
|
||||||
|
_, needOpenAPIUpdate := crdListForGV[name]
|
||||||
|
if needOpenAPIUpdate {
|
||||||
|
delete(crdListForGV, name)
|
||||||
|
if len(crdListForGV) == 0 {
|
||||||
|
delete(c.specsByGVandName, gv)
|
||||||
|
}
|
||||||
|
c.updateGroupVersion(gv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) updateGroupVersion(gv schema.GroupVersion) error {
|
||||||
|
if _, ok := c.specsByGVandName[gv]; !ok {
|
||||||
|
c.openAPIV3Service.DeleteGroupVersion(groupVersionToOpenAPIV3Path(gv))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var specs []*spec3.OpenAPI
|
||||||
|
for _, spec := range c.specsByGVandName[gv] {
|
||||||
|
specs = append(specs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedSpec, err := builder.MergeSpecsV3(specs...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to merge specs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.openAPIV3Service.UpdateGroupVersion(groupVersionToOpenAPIV3Path(gv), mergedSpec)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) updateCRDSpec(crd *apiextensionsv1.CustomResourceDefinition, name, versionName string, v3 *spec3.OpenAPI) error {
|
||||||
|
gv := schema.GroupVersion{
|
||||||
|
Group: crd.Spec.Group,
|
||||||
|
Version: versionName,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := c.specsByGVandName[gv]
|
||||||
|
if !ok {
|
||||||
|
c.specsByGVandName[gv] = map[string]*spec3.OpenAPI{}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldSpec, ok := c.specsByGVandName[gv][name]
|
||||||
|
if ok {
|
||||||
|
if reflect.DeepEqual(oldSpec, v3) {
|
||||||
|
// no changes to CRD
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.specsByGVandName[gv][name] = v3
|
||||||
|
|
||||||
|
return c.updateGroupVersion(gv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) buildV3Spec(crd *apiextensionsv1.CustomResourceDefinition, name, versionName string) error {
|
||||||
|
v3, err := builder.BuildOpenAPIV3(crd, versionName, builder.Options{V2: false})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.updateCRDSpec(crd, name, versionName, v3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) addCustomResourceDefinition(obj interface{}) {
|
||||||
|
castObj := obj.(*apiextensionsv1.CustomResourceDefinition)
|
||||||
|
klog.V(4).Infof("Adding customresourcedefinition %s", castObj.Name)
|
||||||
|
c.enqueue(castObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
||||||
|
castNewObj := newObj.(*apiextensionsv1.CustomResourceDefinition)
|
||||||
|
klog.V(4).Infof("Updating customresourcedefinition %s", castNewObj.Name)
|
||||||
|
c.enqueue(castNewObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) deleteCustomResourceDefinition(obj interface{}) {
|
||||||
|
castObj, ok := obj.(*apiextensionsv1.CustomResourceDefinition)
|
||||||
|
if !ok {
|
||||||
|
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||||
|
if !ok {
|
||||||
|
klog.Errorf("Couldn't get object from tombstone %#v", obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
castObj, ok = tombstone.Obj.(*apiextensionsv1.CustomResourceDefinition)
|
||||||
|
if !ok {
|
||||||
|
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
klog.V(4).Infof("Deleting customresourcedefinition %q", castObj.Name)
|
||||||
|
c.enqueue(castObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) enqueue(obj *apiextensionsv1.CustomResourceDefinition) {
|
||||||
|
c.queue.Add(obj.Name)
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 openapiv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func groupVersionToOpenAPIV3Path(gv schema.GroupVersion) string {
|
||||||
|
return "apis/" + gv.Group + "/" + gv.Version
|
||||||
|
}
|
@ -49,9 +49,10 @@ import (
|
|||||||
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
openapibuilder "k8s.io/kube-openapi/pkg/builder"
|
openapibuilder2 "k8s.io/kube-openapi/pkg/builder"
|
||||||
openapicommon "k8s.io/kube-openapi/pkg/common"
|
openapicommon "k8s.io/kube-openapi/pkg/common"
|
||||||
"k8s.io/kube-openapi/pkg/handler"
|
"k8s.io/kube-openapi/pkg/handler"
|
||||||
|
"k8s.io/kube-openapi/pkg/handler3"
|
||||||
openapiutil "k8s.io/kube-openapi/pkg/util"
|
openapiutil "k8s.io/kube-openapi/pkg/util"
|
||||||
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
|
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
@ -144,6 +145,10 @@ type GenericAPIServer struct {
|
|||||||
// It is set during PrepareRun if `openAPIConfig` is non-nil unless `skipOpenAPIInstallation` is true.
|
// It is set during PrepareRun if `openAPIConfig` is non-nil unless `skipOpenAPIInstallation` is true.
|
||||||
OpenAPIVersionedService *handler.OpenAPIService
|
OpenAPIVersionedService *handler.OpenAPIService
|
||||||
|
|
||||||
|
// OpenAPIV3VersionedService controls the /openapi/v3 endpoint and can be used to update the served spec.
|
||||||
|
// It is set during PrepareRun if `openAPIConfig` is non-nil unless `skipOpenAPIInstallation` is true.
|
||||||
|
OpenAPIV3VersionedService *handler3.OpenAPIService
|
||||||
|
|
||||||
// StaticOpenAPISpec is the spec derived from the restful container endpoints.
|
// StaticOpenAPISpec is the spec derived from the restful container endpoints.
|
||||||
// It is set during PrepareRun.
|
// It is set during PrepareRun.
|
||||||
StaticOpenAPISpec *spec.Swagger
|
StaticOpenAPISpec *spec.Swagger
|
||||||
@ -345,7 +350,12 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
|
|||||||
if s.openAPIConfig != nil && !s.skipOpenAPIInstallation {
|
if s.openAPIConfig != nil && !s.skipOpenAPIInstallation {
|
||||||
s.OpenAPIVersionedService, s.StaticOpenAPISpec = routes.OpenAPI{
|
s.OpenAPIVersionedService, s.StaticOpenAPISpec = routes.OpenAPI{
|
||||||
Config: s.openAPIConfig,
|
Config: s.openAPIConfig,
|
||||||
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
}.InstallV2(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
|
||||||
|
s.OpenAPIV3VersionedService = routes.OpenAPI{
|
||||||
|
Config: s.openAPIConfig,
|
||||||
|
}.InstallV3(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.installHealthz()
|
s.installHealthz()
|
||||||
@ -706,7 +716,7 @@ func (s *GenericAPIServer) getOpenAPIModels(apiPrefix string, apiGroupInfos ...*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the openapi definitions for those resources and convert it to proto models
|
// Build the openapi definitions for those resources and convert it to proto models
|
||||||
openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...)
|
openAPISpec, err := openapibuilder2.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,11 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/server/mux"
|
"k8s.io/apiserver/pkg/server/mux"
|
||||||
"k8s.io/kube-openapi/pkg/builder"
|
builder2 "k8s.io/kube-openapi/pkg/builder"
|
||||||
|
"k8s.io/kube-openapi/pkg/builder3"
|
||||||
"k8s.io/kube-openapi/pkg/common"
|
"k8s.io/kube-openapi/pkg/common"
|
||||||
"k8s.io/kube-openapi/pkg/handler"
|
"k8s.io/kube-openapi/pkg/handler"
|
||||||
|
"k8s.io/kube-openapi/pkg/handler3"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,8 +35,8 @@ type OpenAPI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install adds the SwaggerUI webservice to the given mux.
|
// Install adds the SwaggerUI webservice to the given mux.
|
||||||
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) (*handler.OpenAPIService, *spec.Swagger) {
|
func (oa OpenAPI) InstallV2(c *restful.Container, mux *mux.PathRecorderMux) (*handler.OpenAPIService, *spec.Swagger) {
|
||||||
spec, err := builder.BuildOpenAPISpec(c.RegisteredWebServices(), oa.Config)
|
spec, err := builder2.BuildOpenAPISpec(c.RegisteredWebServices(), oa.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Fatalf("Failed to build open api spec for root: %v", err)
|
klog.Fatalf("Failed to build open api spec for root: %v", err)
|
||||||
}
|
}
|
||||||
@ -51,3 +53,34 @@ func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) (*hand
|
|||||||
|
|
||||||
return openAPIVersionedService, spec
|
return openAPIVersionedService, spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallV3 adds the static group/versions defined in the RegisteredWebServices to the OpenAPI v3 spec
|
||||||
|
func (oa OpenAPI) InstallV3(c *restful.Container, mux *mux.PathRecorderMux) *handler3.OpenAPIService {
|
||||||
|
openAPIVersionedService, err := handler3.NewOpenAPIService(nil)
|
||||||
|
if err != nil {
|
||||||
|
klog.Fatalf("Failed to create OpenAPIService: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openAPIVersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", mux)
|
||||||
|
if err != nil {
|
||||||
|
klog.Fatalf("Failed to register versioned open api spec for root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped := make(map[string][]*restful.WebService)
|
||||||
|
|
||||||
|
for _, t := range c.RegisteredWebServices() {
|
||||||
|
// Strip the "/" prefix from the name
|
||||||
|
gvName := t.RootPath()[1:]
|
||||||
|
grouped[gvName] = []*restful.WebService{t}
|
||||||
|
}
|
||||||
|
|
||||||
|
for gv, ws := range grouped {
|
||||||
|
spec, err := builder3.BuildOpenAPISpec(ws, oa.Config)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("Failed to build OpenAPI v3 for group %s, %q", gv, err)
|
||||||
|
|
||||||
|
}
|
||||||
|
openAPIVersionedService.UpdateGroupVersion(gv, spec)
|
||||||
|
}
|
||||||
|
return openAPIVersionedService
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
"k8s.io/apiserver/pkg/server/egressselector"
|
"k8s.io/apiserver/pkg/server/egressselector"
|
||||||
@ -46,6 +47,8 @@ import (
|
|||||||
listers "k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1"
|
listers "k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1"
|
||||||
openapicontroller "k8s.io/kube-aggregator/pkg/controllers/openapi"
|
openapicontroller "k8s.io/kube-aggregator/pkg/controllers/openapi"
|
||||||
openapiaggregator "k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator"
|
openapiaggregator "k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator"
|
||||||
|
openapiv3controller "k8s.io/kube-aggregator/pkg/controllers/openapiv3"
|
||||||
|
openapiv3aggregator "k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator"
|
||||||
statuscontrollers "k8s.io/kube-aggregator/pkg/controllers/status"
|
statuscontrollers "k8s.io/kube-aggregator/pkg/controllers/status"
|
||||||
apiservicerest "k8s.io/kube-aggregator/pkg/registry/apiservice/rest"
|
apiservicerest "k8s.io/kube-aggregator/pkg/registry/apiservice/rest"
|
||||||
)
|
)
|
||||||
@ -141,9 +144,12 @@ type APIAggregator struct {
|
|||||||
// Enable swagger and/or OpenAPI if these configs are non-nil.
|
// Enable swagger and/or OpenAPI if these configs are non-nil.
|
||||||
openAPIConfig *openapicommon.Config
|
openAPIConfig *openapicommon.Config
|
||||||
|
|
||||||
// openAPIAggregationController downloads and merges OpenAPI specs.
|
// openAPIAggregationController downloads and merges OpenAPI v2 specs.
|
||||||
openAPIAggregationController *openapicontroller.AggregationController
|
openAPIAggregationController *openapicontroller.AggregationController
|
||||||
|
|
||||||
|
// openAPIV3AggregationController downloads and caches OpenAPI v3 specs.
|
||||||
|
openAPIV3AggregationController *openapiv3controller.AggregationController
|
||||||
|
|
||||||
// egressSelector selects the proper egress dialer to communicate with the custom apiserver
|
// egressSelector selects the proper egress dialer to communicate with the custom apiserver
|
||||||
// overwrites proxyTransport dialer if not nil
|
// overwrites proxyTransport dialer if not nil
|
||||||
egressSelector *egressselector.EgressSelector
|
egressSelector *egressselector.EgressSelector
|
||||||
@ -344,6 +350,9 @@ func (s *APIAggregator) PrepareRun() (preparedAPIAggregator, error) {
|
|||||||
if s.openAPIConfig != nil {
|
if s.openAPIConfig != nil {
|
||||||
s.GenericAPIServer.AddPostStartHookOrDie("apiservice-openapi-controller", func(context genericapiserver.PostStartHookContext) error {
|
s.GenericAPIServer.AddPostStartHookOrDie("apiservice-openapi-controller", func(context genericapiserver.PostStartHookContext) error {
|
||||||
go s.openAPIAggregationController.Run(context.StopCh)
|
go s.openAPIAggregationController.Run(context.StopCh)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
|
||||||
|
go s.openAPIV3AggregationController.Run(context.StopCh)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -363,6 +372,18 @@ func (s *APIAggregator) PrepareRun() (preparedAPIAggregator, error) {
|
|||||||
return preparedAPIAggregator{}, err
|
return preparedAPIAggregator{}, err
|
||||||
}
|
}
|
||||||
s.openAPIAggregationController = openapicontroller.NewAggregationController(&specDownloader, openAPIAggregator)
|
s.openAPIAggregationController = openapicontroller.NewAggregationController(&specDownloader, openAPIAggregator)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
|
||||||
|
specDownloaderV3 := openapiv3aggregator.NewDownloader()
|
||||||
|
openAPIV3Aggregator, err := openapiv3aggregator.BuildAndRegisterAggregator(
|
||||||
|
specDownloaderV3,
|
||||||
|
s.GenericAPIServer.NextDelegate(),
|
||||||
|
s.GenericAPIServer.Handler.NonGoRestfulMux)
|
||||||
|
if err != nil {
|
||||||
|
return preparedAPIAggregator{}, err
|
||||||
|
}
|
||||||
|
_ = openAPIV3Aggregator
|
||||||
|
s.openAPIV3AggregationController = openapiv3controller.NewAggregationController(openAPIV3Aggregator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return preparedAPIAggregator{APIAggregator: s, runnable: prepared}, nil
|
return preparedAPIAggregator{APIAggregator: s, runnable: prepared}, nil
|
||||||
@ -382,6 +403,9 @@ func (s *APIAggregator) AddAPIService(apiService *v1.APIService) error {
|
|||||||
if s.openAPIAggregationController != nil {
|
if s.openAPIAggregationController != nil {
|
||||||
s.openAPIAggregationController.UpdateAPIService(proxyHandler, apiService)
|
s.openAPIAggregationController.UpdateAPIService(proxyHandler, apiService)
|
||||||
}
|
}
|
||||||
|
if s.openAPIV3AggregationController != nil {
|
||||||
|
s.openAPIV3AggregationController.UpdateAPIService(proxyHandler, apiService)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,6 +427,9 @@ func (s *APIAggregator) AddAPIService(apiService *v1.APIService) error {
|
|||||||
if s.openAPIAggregationController != nil {
|
if s.openAPIAggregationController != nil {
|
||||||
s.openAPIAggregationController.AddAPIService(proxyHandler, apiService)
|
s.openAPIAggregationController.AddAPIService(proxyHandler, apiService)
|
||||||
}
|
}
|
||||||
|
if s.openAPIV3AggregationController != nil {
|
||||||
|
s.openAPIV3AggregationController.AddAPIService(proxyHandler, apiService)
|
||||||
|
}
|
||||||
s.proxyHandlers[apiService.Name] = proxyHandler
|
s.proxyHandlers[apiService.Name] = proxyHandler
|
||||||
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)
|
||||||
@ -447,6 +474,9 @@ func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
|||||||
if s.openAPIAggregationController != nil {
|
if s.openAPIAggregationController != nil {
|
||||||
s.openAPIAggregationController.RemoveAPIService(apiServiceName)
|
s.openAPIAggregationController.RemoveAPIService(apiServiceName)
|
||||||
}
|
}
|
||||||
|
if s.openAPIV3AggregationController != nil {
|
||||||
|
s.openAPIAggregationController.RemoveAPIService(apiServiceName)
|
||||||
|
}
|
||||||
delete(s.proxyHandlers, apiServiceName)
|
delete(s.proxyHandlers, apiServiceName)
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 aggregator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/server"
|
||||||
|
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||||
|
"k8s.io/kube-openapi/pkg/common"
|
||||||
|
"k8s.io/kube-openapi/pkg/handler3"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpecAggregator calls out to http handlers of APIServices and caches specs. It keeps state of the last
|
||||||
|
// known specs including the http etag.
|
||||||
|
// TODO(jefftree): remove the downloading and caching and proxy directly to the APIServices. This is possible because we
|
||||||
|
// don't have to merge here, which is cpu intensive in v2
|
||||||
|
type SpecAggregator interface {
|
||||||
|
AddUpdateAPIService(handler http.Handler, apiService *v1.APIService)
|
||||||
|
UpdateAPIServiceSpec(apiServiceName string) error
|
||||||
|
RemoveAPIServiceSpec(apiServiceName string)
|
||||||
|
GetAPIServiceNames() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
aggregatorUser = "system:aggregator"
|
||||||
|
specDownloadTimeout = 60 * time.Second
|
||||||
|
localDelegateChainNamePrefix = "k8s_internal_local_delegation_chain_"
|
||||||
|
localDelegateChainNamePattern = localDelegateChainNamePrefix + "%010d"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsLocalAPIService returns true for local specs from delegates.
|
||||||
|
func IsLocalAPIService(apiServiceName string) bool {
|
||||||
|
return strings.HasPrefix(apiServiceName, localDelegateChainNamePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAPIServicesName returns the names of APIServices recorded in openAPIV3Specs.
|
||||||
|
// We use this function to pass the names of local APIServices to the controller in this package,
|
||||||
|
// so that the controller can periodically sync the OpenAPI spec from delegation API servers.
|
||||||
|
func (s *specAggregator) GetAPIServiceNames() []string {
|
||||||
|
s.rwMutex.Lock()
|
||||||
|
defer s.rwMutex.Unlock()
|
||||||
|
|
||||||
|
names := make([]string, len(s.openAPIV3Specs))
|
||||||
|
for key := range s.openAPIV3Specs {
|
||||||
|
names = append(names, key)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAndRegisterAggregator registered OpenAPI aggregator handler. This function is not thread safe as it only being called on startup.
|
||||||
|
func BuildAndRegisterAggregator(downloader Downloader, delegationTarget server.DelegationTarget, pathHandler common.PathHandlerByGroupVersion) (SpecAggregator, error) {
|
||||||
|
var err error
|
||||||
|
s := &specAggregator{
|
||||||
|
openAPIV3Specs: map[string]*openAPIV3APIServiceInfo{},
|
||||||
|
downloader: downloader,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.openAPIV3VersionedService, err = handler3.NewOpenAPIService(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = s.openAPIV3VersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", pathHandler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 1
|
||||||
|
for delegate := delegationTarget; delegate != nil; delegate = delegate.NextDelegate() {
|
||||||
|
handler := delegate.UnprotectedHandler()
|
||||||
|
if handler == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServiceName := fmt.Sprintf(localDelegateChainNamePattern, i)
|
||||||
|
localAPIService := v1.APIService{}
|
||||||
|
localAPIService.Name = apiServiceName
|
||||||
|
s.AddUpdateAPIService(handler, &localAPIService)
|
||||||
|
s.UpdateAPIServiceSpec(apiServiceName)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUpdateAPIService adds or updates the api service. It is thread safe.
|
||||||
|
func (s *specAggregator) AddUpdateAPIService(handler http.Handler, apiservice *v1.APIService) {
|
||||||
|
s.rwMutex.Lock()
|
||||||
|
defer s.rwMutex.Unlock()
|
||||||
|
// If the APIService is being updated, use the existing struct.
|
||||||
|
if apiServiceInfo, ok := s.openAPIV3Specs[apiservice.Name]; ok {
|
||||||
|
apiServiceInfo.apiService = *apiservice
|
||||||
|
apiServiceInfo.handler = handler
|
||||||
|
}
|
||||||
|
s.openAPIV3Specs[apiservice.Name] = &openAPIV3APIServiceInfo{
|
||||||
|
apiService: *apiservice,
|
||||||
|
handler: handler,
|
||||||
|
specs: make(map[string]*openAPIV3SpecInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIServiceSpec updates all the OpenAPI v3 specs that the APIService serves.
|
||||||
|
// It is thread safe.
|
||||||
|
func (s *specAggregator) UpdateAPIServiceSpec(apiServiceName string) error {
|
||||||
|
s.rwMutex.Lock()
|
||||||
|
defer s.rwMutex.Unlock()
|
||||||
|
|
||||||
|
apiService, exists := s.openAPIV3Specs[apiServiceName]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("APIService %s does not exist for update", apiServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass a list of old etags to the Downloader to prevent transfers if etags match
|
||||||
|
etagList := make(map[string]string)
|
||||||
|
for gv, specInfo := range apiService.specs {
|
||||||
|
etagList[gv] = specInfo.etag
|
||||||
|
}
|
||||||
|
groups, err := s.downloader.Download(apiService.handler, etagList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any groups that do not exist anymore
|
||||||
|
for group := range s.openAPIV3Specs[apiServiceName].specs {
|
||||||
|
if _, exists := groups[group]; !exists {
|
||||||
|
s.openAPIV3VersionedService.DeleteGroupVersion(group)
|
||||||
|
delete(s.openAPIV3Specs[apiServiceName].specs, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for group, info := range groups {
|
||||||
|
if info.spec == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ETag has not changed, no update is necessary
|
||||||
|
oldInfo, exists := s.openAPIV3Specs[apiServiceName].specs[group]
|
||||||
|
if exists && oldInfo.etag == info.etag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.openAPIV3Specs[apiServiceName].specs[group] = &openAPIV3SpecInfo{
|
||||||
|
spec: info.spec,
|
||||||
|
etag: info.etag,
|
||||||
|
}
|
||||||
|
s.openAPIV3VersionedService.UpdateGroupVersion(group, info.spec)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type specAggregator struct {
|
||||||
|
// mutex protects all members of this struct.
|
||||||
|
rwMutex sync.RWMutex
|
||||||
|
|
||||||
|
// OpenAPI V3 specs by APIService name
|
||||||
|
openAPIV3Specs map[string]*openAPIV3APIServiceInfo
|
||||||
|
// provided for dynamic OpenAPI spec
|
||||||
|
openAPIV3VersionedService *handler3.OpenAPIService
|
||||||
|
|
||||||
|
// For downloading the OpenAPI v3 specs from apiservices
|
||||||
|
downloader Downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ SpecAggregator = &specAggregator{}
|
||||||
|
|
||||||
|
type openAPIV3APIServiceInfo struct {
|
||||||
|
apiService v1.APIService
|
||||||
|
handler http.Handler
|
||||||
|
specs map[string]*openAPIV3SpecInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type openAPIV3SpecInfo struct {
|
||||||
|
spec *spec3.OpenAPI
|
||||||
|
etag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAPIServiceSpec removes an api service from the OpenAPI map. If it does not exist, no error is returned.
|
||||||
|
// It is thread safe.
|
||||||
|
func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) {
|
||||||
|
s.rwMutex.Lock()
|
||||||
|
defer s.rwMutex.Unlock()
|
||||||
|
if apiServiceInfo, ok := s.openAPIV3Specs[apiServiceName]; ok {
|
||||||
|
for gv := range apiServiceInfo.specs {
|
||||||
|
s.openAPIV3VersionedService.DeleteGroupVersion(gv)
|
||||||
|
}
|
||||||
|
delete(s.openAPIV3Specs, apiServiceName)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 aggregator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Downloader is the OpenAPI downloader type. It will try to download spec from /openapi/v3 and /openap/v3/<group>/<version> endpoints.
|
||||||
|
type Downloader struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDownloader creates a new OpenAPI Downloader.
|
||||||
|
func NewDownloader() Downloader {
|
||||||
|
return Downloader{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Downloader) handlerWithUser(handler http.Handler, info user.Info) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), info))
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// gvList is a struct for the response of the /openapi/v3 endpoint to unmarshal into
|
||||||
|
type gvList struct {
|
||||||
|
Paths []string `json:"Paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpecETag is a OpenAPI v3 spec and etag pair for the endpoint of each OpenAPI group/version
|
||||||
|
type SpecETag struct {
|
||||||
|
spec *spec3.OpenAPI
|
||||||
|
etag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download downloads OpenAPI v3 for all groups of a given handler
|
||||||
|
func (s *Downloader) Download(handler http.Handler, etagList map[string]string) (returnSpec map[string]*SpecETag, err error) {
|
||||||
|
// TODO(jefftree): https://github.com/kubernetes/kubernetes/pull/105945#issuecomment-966455034
|
||||||
|
// Move to proxy request in the aggregator and let the APIServices serve the OpenAPI directly
|
||||||
|
handler = s.handlerWithUser(handler, &user.DefaultInfo{Name: aggregatorUser})
|
||||||
|
handler = http.TimeoutHandler(handler, specDownloadTimeout, "request timed out")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "/openapi/v3", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
|
writer := newInMemoryResponseWriter()
|
||||||
|
handler.ServeHTTP(writer, req)
|
||||||
|
|
||||||
|
switch writer.respCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
// Gracefully skip 404, assuming the server won't provide any spec
|
||||||
|
return nil, nil
|
||||||
|
case http.StatusOK:
|
||||||
|
groups := gvList{}
|
||||||
|
aggregated := make(map[string]*SpecETag)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(writer.data, &groups); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, path := range groups.Paths {
|
||||||
|
reqPath := fmt.Sprintf("/openapi/v3/%s", path)
|
||||||
|
req, err := http.NewRequest("GET", reqPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
oldEtag, ok := etagList[path]
|
||||||
|
if ok {
|
||||||
|
req.Header.Add("If-None-Match", oldEtag)
|
||||||
|
}
|
||||||
|
openAPIWriter := newInMemoryResponseWriter()
|
||||||
|
handler.ServeHTTP(openAPIWriter, req)
|
||||||
|
|
||||||
|
switch openAPIWriter.respCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
continue
|
||||||
|
case http.StatusNotModified:
|
||||||
|
aggregated[path] = &SpecETag{
|
||||||
|
etag: oldEtag,
|
||||||
|
}
|
||||||
|
case http.StatusOK:
|
||||||
|
var spec spec3.OpenAPI
|
||||||
|
// TODO|jefftree: For OpenAPI v3 Beta, if the v3 spec is empty then
|
||||||
|
// we should request the v2 endpoint and convert it to v3
|
||||||
|
if len(openAPIWriter.data) > 0 {
|
||||||
|
err = json.Unmarshal(openAPIWriter.data, &spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
etag := openAPIWriter.Header().Get("Etag")
|
||||||
|
aggregated[path] = &SpecETag{
|
||||||
|
spec: &spec,
|
||||||
|
etag: etag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
klog.Errorf("Error: unknown status %v", openAPIWriter.respCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregated, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("failed to retrieve openAPI spec, http error: %s", writer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inMemoryResponseWriter is a http.Writer that keep the response in memory.
|
||||||
|
type inMemoryResponseWriter struct {
|
||||||
|
writeHeaderCalled bool
|
||||||
|
header http.Header
|
||||||
|
respCode int
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInMemoryResponseWriter() *inMemoryResponseWriter {
|
||||||
|
return &inMemoryResponseWriter{header: http.Header{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inMemoryResponseWriter) Header() http.Header {
|
||||||
|
return r.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inMemoryResponseWriter) WriteHeader(code int) {
|
||||||
|
r.writeHeaderCalled = true
|
||||||
|
r.respCode = code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inMemoryResponseWriter) Write(in []byte) (int, error) {
|
||||||
|
if !r.writeHeaderCalled {
|
||||||
|
r.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
r.data = append(r.data, in...)
|
||||||
|
return len(in), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inMemoryResponseWriter) String() string {
|
||||||
|
s := fmt.Sprintf("ResponseCode: %d", r.respCode)
|
||||||
|
if r.data != nil {
|
||||||
|
s += fmt.Sprintf(", Body: %s", string(r.data))
|
||||||
|
}
|
||||||
|
if r.header != nil {
|
||||||
|
s += fmt.Sprintf(", Header: %s", r.header)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
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 aggregator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handlerTest struct {
|
||||||
|
etag string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.Handler = handlerTest{}
|
||||||
|
|
||||||
|
func (h handlerTest) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Create an APIService with a handler for one group/version
|
||||||
|
group := make(map[string][]string)
|
||||||
|
group["Paths"] = []string{"apis/group/version"}
|
||||||
|
j, _ := json.Marshal(group)
|
||||||
|
if r.URL.Path == "/openapi/v3" {
|
||||||
|
w.Write(j)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/openapi/v3/apis/group/version" {
|
||||||
|
if len(h.etag) > 0 {
|
||||||
|
w.Header().Add("Etag", h.etag)
|
||||||
|
}
|
||||||
|
ifNoneMatches := r.Header["If-None-Match"]
|
||||||
|
for _, match := range ifNoneMatches {
|
||||||
|
if match == h.etag {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write(h.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertDownloadedSpec(gvSpec map[string]*SpecETag, err error, expectedSpecID string, expectedEtag string) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("downloadOpenAPISpec failed : %s", err)
|
||||||
|
}
|
||||||
|
specInfo, ok := gvSpec["apis/group/version"]
|
||||||
|
if !ok {
|
||||||
|
if expectedSpecID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("expected to download spec, no spec downloaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
if specInfo.spec != nil && expectedSpecID == "" {
|
||||||
|
return fmt.Errorf("expected ID %s, actual ID %s", expectedSpecID, specInfo.spec.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if specInfo.spec != nil && specInfo.spec.Version != expectedSpecID {
|
||||||
|
return fmt.Errorf("expected ID %s, actual ID %s", expectedSpecID, specInfo.spec.Version)
|
||||||
|
}
|
||||||
|
if specInfo.etag != expectedEtag {
|
||||||
|
return fmt.Errorf("expected ETag '%s', actual ETag '%s'", expectedEtag, specInfo.etag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadOpenAPISpec(t *testing.T) {
|
||||||
|
s := Downloader{}
|
||||||
|
|
||||||
|
// Test with eTag
|
||||||
|
gvSpec, err := s.Download(
|
||||||
|
handlerTest{data: []byte("{\"openapi\": \"test\"}"), etag: "etag_test"}, map[string]string{})
|
||||||
|
assert.NoError(t, assertDownloadedSpec(gvSpec, err, "test", "etag_test"))
|
||||||
|
|
||||||
|
// Test not modified
|
||||||
|
gvSpec, err = s.Download(
|
||||||
|
handlerTest{data: []byte("{\"openapi\": \"test\"}"), etag: "etag_test"}, map[string]string{"apis/group/version": "etag_test"})
|
||||||
|
assert.NoError(t, assertDownloadedSpec(gvSpec, err, "", "etag_test"))
|
||||||
|
|
||||||
|
// Test different eTags
|
||||||
|
gvSpec, err = s.Download(
|
||||||
|
handlerTest{data: []byte("{\"openapi\": \"test\"}"), etag: "etag_test1"}, map[string]string{"apis/group/version": "etag_test2"})
|
||||||
|
assert.NoError(t, assertDownloadedSpec(gvSpec, err, "test", "etag_test1"))
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 openapiv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||||
|
"k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
successfulUpdateDelay = time.Minute
|
||||||
|
successfulUpdateDelayLocal = time.Second
|
||||||
|
failedUpdateMaxExpDelay = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type syncAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
syncRequeue syncAction = iota
|
||||||
|
syncRequeueRateLimited
|
||||||
|
syncNothing
|
||||||
|
)
|
||||||
|
|
||||||
|
// AggregationController periodically check for changes in OpenAPI specs of APIServices and update/remove
|
||||||
|
// them if necessary.
|
||||||
|
type AggregationController struct {
|
||||||
|
openAPIAggregationManager aggregator.SpecAggregator
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
|
||||||
|
// To allow injection for testing.
|
||||||
|
syncHandler func(key string) (syncAction, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAggregationController creates new OpenAPI aggregation controller.
|
||||||
|
func NewAggregationController(openAPIAggregationManager aggregator.SpecAggregator) *AggregationController {
|
||||||
|
c := &AggregationController{
|
||||||
|
openAPIAggregationManager: openAPIAggregationManager,
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(
|
||||||
|
workqueue.NewItemExponentialFailureRateLimiter(successfulUpdateDelay, failedUpdateMaxExpDelay),
|
||||||
|
"open_api_v3_aggregation_controller",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.syncHandler = c.sync
|
||||||
|
|
||||||
|
// update each service at least once, also those which are not coming from APIServices, namely local services
|
||||||
|
for _, name := range openAPIAggregationManager.GetAPIServiceNames() {
|
||||||
|
c.queue.AddAfter(name, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts OpenAPI AggregationController
|
||||||
|
func (c *AggregationController) Run(stopCh <-chan struct{}) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer c.queue.ShutDown()
|
||||||
|
|
||||||
|
klog.Info("Starting OpenAPI V3 AggregationController")
|
||||||
|
defer klog.Info("Shutting down OpenAPI V3 AggregationController")
|
||||||
|
|
||||||
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||||
|
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AggregationController) runWorker() {
|
||||||
|
for c.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||||
|
func (c *AggregationController) processNextWorkItem() bool {
|
||||||
|
key, quit := c.queue.Get()
|
||||||
|
defer c.queue.Done(key)
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if aggregator.IsLocalAPIService(key.(string)) {
|
||||||
|
// for local delegation targets that are aggregated once per second, log at
|
||||||
|
// higher level to avoid flooding the log
|
||||||
|
klog.V(6).Infof("OpenAPI AggregationController: Processing item %s", key)
|
||||||
|
} else {
|
||||||
|
klog.V(4).Infof("OpenAPI AggregationController: Processing item %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := c.syncHandler(key.(string))
|
||||||
|
if err == nil {
|
||||||
|
c.queue.Forget(key)
|
||||||
|
} else {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("loading OpenAPI spec for %q failed with: %v", key, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case syncRequeue:
|
||||||
|
if aggregator.IsLocalAPIService(key.(string)) {
|
||||||
|
klog.V(7).Infof("OpenAPI AggregationController: action for local item %s: Requeue after %s.", key, successfulUpdateDelayLocal)
|
||||||
|
c.queue.AddAfter(key, successfulUpdateDelayLocal)
|
||||||
|
} else {
|
||||||
|
klog.V(7).Infof("OpenAPI AggregationController: action for item %s: Requeue.", key)
|
||||||
|
c.queue.AddAfter(key, successfulUpdateDelay)
|
||||||
|
}
|
||||||
|
case syncRequeueRateLimited:
|
||||||
|
klog.Infof("OpenAPI AggregationController: action for item %s: Rate Limited Requeue.", key)
|
||||||
|
c.queue.AddRateLimited(key)
|
||||||
|
case syncNothing:
|
||||||
|
klog.Infof("OpenAPI AggregationController: action for item %s: Nothing (removed from the queue).", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AggregationController) sync(key string) (syncAction, error) {
|
||||||
|
err := c.openAPIAggregationManager.UpdateAPIServiceSpec(key)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return syncRequeueRateLimited, err
|
||||||
|
}
|
||||||
|
return syncRequeue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAPIService adds a new API Service to OpenAPI Aggregation.
|
||||||
|
func (c *AggregationController) AddAPIService(handler http.Handler, apiService *v1.APIService) {
|
||||||
|
if apiService.Spec.Service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.openAPIAggregationManager.AddUpdateAPIService(handler, apiService)
|
||||||
|
c.queue.AddAfter(apiService.Name, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIService updates API Service's info and handler.
|
||||||
|
func (c *AggregationController) UpdateAPIService(handler http.Handler, apiService *v1.APIService) {
|
||||||
|
if apiService.Spec.Service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.openAPIAggregationManager.AddUpdateAPIService(handler, apiService)
|
||||||
|
key := apiService.Name
|
||||||
|
if c.queue.NumRequeues(key) > 0 {
|
||||||
|
// The item has failed before. Remove it from failure queue and
|
||||||
|
// update it in a second
|
||||||
|
c.queue.Forget(key)
|
||||||
|
c.queue.AddAfter(key, time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAPIService removes API Service from OpenAPI Aggregation Controller.
|
||||||
|
func (c *AggregationController) RemoveAPIService(apiServiceName string) {
|
||||||
|
c.openAPIAggregationManager.RemoveAPIServiceSpec(apiServiceName)
|
||||||
|
// This will only remove it if it was failing before. If it was successful, processNextWorkItem will figure it out
|
||||||
|
// and will not add it again to the queue.
|
||||||
|
c.queue.Forget(apiServiceName)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user