Merge pull request #71158 from liggitt/revert-openapi-publish

Revert openapi publish
This commit is contained in:
k8s-ci-robot 2018-11-16 18:22:43 -08:00 committed by GitHub
commit 39c8219999
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 40 additions and 1774 deletions

View File

@ -123,7 +123,6 @@
- k8s.io/apiserver
- k8s.io/client-go
- k8s.io/klog
- k8s.io/kube-openapi
- baseImportPath: "./vendor/k8s.io/kube-openapi/"
allowedImports:

View File

@ -50,7 +50,6 @@ filegroup(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/openapi:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/test/integration:all-srcs",

View File

@ -2262,10 +2262,6 @@
"ImportPath": "k8s.io/klog",
"Rev": "8139d8cb77af419532b33dfa7dd09fbc5f1d344f"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/aggregator",
"Rev": "c59034cc13d587f5ef4e85ca0ade0c1866ae8e1d"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "c59034cc13d587f5ef4e85ca0ade0c1866ae8e1d"

View File

@ -36,7 +36,6 @@ go_library(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/openapi:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library",

View File

@ -41,7 +41,6 @@ import (
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
"k8s.io/apiextensions-apiserver/pkg/controller/status"
openapiaggregator "k8s.io/apiextensions-apiserver/pkg/openapi"
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
@ -205,13 +204,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
return nil
})
s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
// create OpenAPI aggregation manager in the last step because only now genericapiserver's the OpenAPI services and spec is available (after PrepareRun).
crdOpenAPIAggregationManager, err := openapiaggregator.NewAggregationManager(s.GenericAPIServer.OpenAPIService, s.GenericAPIServer.OpenAPIVersionedService, s.GenericAPIServer.StaticOpenAPISpec)
if err != nil {
return err
}
go crdController.Run(context.StopCh, crdOpenAPIAggregationManager)
go crdController.Run(context.StopCh)
go namingController.Run(context.StopCh)
go establishingController.Run(context.StopCh)
go finalizingController.Run(5, context.StopCh)

View File

@ -31,15 +31,12 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/discovery"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/openapi"
)
type DiscoveryController struct {
@ -53,8 +50,6 @@ type DiscoveryController struct {
syncFn func(version schema.GroupVersion) error
queue workqueue.RateLimitingInterface
openAPIAggregationManager apiextensionsopenapi.AggregationManager
}
func NewDiscoveryController(crdInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController {
@ -88,7 +83,6 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
if err != nil {
return err
}
apiServiceName := version.Group + "." + version.Version
foundVersion := false
foundGroup := false
for _, crd := range crds {
@ -125,25 +119,6 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
continue
}
foundVersion = true
if c.openAPIAggregationManager != nil && utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
validationSchema, err := getSchemaForVersion(crd, version.Version)
if err != nil {
return err
}
// We aggregate the schema even if it's nil as it maybe a removal of the schema for this CRD,
// and the aggreated OpenAPI spec should reflect this change.
crdspec, etag, err := apiextensionsopenapi.CustomResourceDefinitionOpenAPISpec(&crd.Spec, version.Version, validationSchema)
if err != nil {
return err
}
// Add/update the local API service's spec for the CRD in apiExtensionsServer's
// openAPIAggregationManager
if err := c.openAPIAggregationManager.AddUpdateLocalAPIServiceSpec(apiServiceName, crdspec, etag); err != nil {
return err
}
}
verbs := metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"})
// if we're terminating we don't allow some verbs
@ -189,14 +164,6 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
if !foundGroup {
c.groupHandler.unsetDiscovery(version.Group)
c.versionHandler.unsetDiscovery(version)
if c.openAPIAggregationManager != nil && utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
// Remove the local API service for the CRD in apiExtensionsServer's
// openAPIAggregationManager.
// Note that we don't check if apiServiceName exists in openAPIAggregationManager
// because RemoveAPIServiceSpec properly handles non-existing API service by
// returning no error.
return c.openAPIAggregationManager.RemoveAPIServiceSpec(apiServiceName)
}
return nil
}
@ -213,14 +180,6 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
if !foundVersion {
c.versionHandler.unsetDiscovery(version)
if c.openAPIAggregationManager != nil && utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
// Remove the local API service for the CRD in apiExtensionsServer's
// openAPIAggregationManager.
// Note that we don't check if apiServiceName exists in openAPIAggregationManager
// because RemoveAPIServiceSpec properly handles non-existing API service by
// returning no error.
return c.openAPIAggregationManager.RemoveAPIServiceSpec(apiServiceName)
}
return nil
}
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
@ -236,15 +195,13 @@ func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery)
})
}
func (c *DiscoveryController) Run(stopCh <-chan struct{}, crdOpenAPIAggregationManager apiextensionsopenapi.AggregationManager) {
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
defer klog.Infof("Shutting down DiscoveryController")
klog.Infof("Starting DiscoveryController")
c.openAPIAggregationManager = crdOpenAPIAggregationManager
if !cache.WaitForCacheSync(stopCh, c.crdsSynced) {
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
return

View File

@ -50,17 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche
return nil
}
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema.
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
return ConvertJSONSchemaPropsWithPostProcess(in, out, nil)
}
// PostProcessFunc post-processes one node of a spec.Schema.
type PostProcessFunc func(*spec.Schema) error
// ConvertJSONSchemaPropsWithPostProcess converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
// and run a post process step on each JSONSchemaProps node.
func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, out *spec.Schema, postProcess PostProcessFunc) error {
if in == nil {
return nil
}
@ -95,43 +86,41 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
out.Example = *(in.Example)
}
if in.Enum != nil {
out.Enum = make([]interface{}, len(in.Enum))
for k, v := range in.Enum {
out.Enum[k] = v
}
out.Enum = make([]interface{}, len(in.Enum))
for k, v := range in.Enum {
out.Enum[k] = v
}
if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf, postProcess); err != nil {
if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil {
return err
}
if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf, postProcess); err != nil {
if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil {
return err
}
if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf, postProcess); err != nil {
if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil {
return err
}
if in.Not != nil {
in, out := &in.Not, &out.Not
*out = new(spec.Schema)
if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
var err error
out.Properties, err = convertMapOfJSONSchemaProps(in.Properties, postProcess)
out.Properties, err = convertMapOfJSONSchemaProps(in.Properties)
if err != nil {
return err
}
out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties, postProcess)
out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties)
if err != nil {
return err
}
out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions, postProcess)
out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions)
if err != nil {
return err
}
@ -146,7 +135,7 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
if in.AdditionalProperties != nil {
in, out := &in.AdditionalProperties, &out.AdditionalProperties
*out = new(spec.SchemaOrBool)
if err := convertJSONSchemaPropsorBool(*in, *out, postProcess); err != nil {
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
return err
}
}
@ -154,7 +143,7 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
if in.AdditionalItems != nil {
in, out := &in.AdditionalItems, &out.AdditionalItems
*out = new(spec.SchemaOrBool)
if err := convertJSONSchemaPropsorBool(*in, *out, postProcess); err != nil {
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
return err
}
}
@ -162,7 +151,7 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = new(spec.SchemaOrArray)
if err := convertJSONSchemaPropsOrArray(*in, *out, postProcess); err != nil {
if err := convertJSONSchemaPropsOrArray(*in, *out); err != nil {
return err
}
}
@ -172,7 +161,7 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
*out = make(spec.Dependencies, len(*in))
for key, val := range *in {
newVal := new(spec.SchemaOrStringArray)
if err := convertJSONSchemaPropsOrStringArray(&val, newVal, postProcess); err != nil {
if err := convertJSONSchemaPropsOrStringArray(&val, newVal); err != nil {
return err
}
(*out)[key] = *newVal
@ -185,20 +174,14 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
out.ExternalDocs.URL = in.ExternalDocs.URL
}
if postProcess != nil {
if err := postProcess(out); err != nil {
return err
}
}
return nil
}
func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema, postProcess PostProcessFunc) error {
func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error {
if in != nil {
for _, jsonSchemaProps := range *in {
schema := spec.Schema{}
if err := ConvertJSONSchemaPropsWithPostProcess(&jsonSchemaProps, &schema, postProcess); err != nil {
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return err
}
*out = append(*out, schema)
@ -207,27 +190,25 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s
return nil
}
func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps, postProcess PostProcessFunc) (map[string]spec.Schema, error) {
if in == nil {
return nil, nil
}
func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) {
out := make(map[string]spec.Schema)
for k, jsonSchemaProps := range in {
schema := spec.Schema{}
if err := ConvertJSONSchemaPropsWithPostProcess(&jsonSchemaProps, &schema, postProcess); err != nil {
return nil, err
if len(in) != 0 {
for k, jsonSchemaProps := range in {
schema := spec.Schema{}
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return nil, err
}
out[k] = schema
}
out[k] = schema
}
return out, nil
}
func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray, postProcess PostProcessFunc) error {
func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error {
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
@ -235,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
in, out := &in.JSONSchemas, &out.Schemas
*out = make([]spec.Schema, len(*in))
for i := range *in {
if err := ConvertJSONSchemaPropsWithPostProcess(&(*in)[i], &(*out)[i], postProcess); err != nil {
if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
return err
}
}
@ -243,24 +224,24 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
return nil
}
func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool, postProcess PostProcessFunc) error {
func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error {
out.Allows = in.Allows
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
return nil
}
func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray, postProcess PostProcessFunc) error {
func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error {
out.Property = in.Property
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}

View File

@ -1,49 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"aggregator.go",
"construction.go",
"conversion.go",
"swagger_util.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/openapi",
importpath = "k8s.io/apiextensions-apiserver/pkg/openapi",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/aggregator:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/handler:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["conversion_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -1,232 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"fmt"
"sync"
"github.com/go-openapi/spec"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kube-openapi/pkg/aggregator"
"k8s.io/kube-openapi/pkg/handler"
)
// AggregationManager is the interface between OpenAPI Aggregator service and a controller
// that manages CRD openapi spec aggregation
type AggregationManager interface {
// AddUpdateLocalAPIService allows adding/updating local API service with nil handler and
// nil Spec.Service. This function can be used for local dynamic OpenAPI spec aggregation
// management (e.g. CRD)
AddUpdateLocalAPIServiceSpec(name string, spec *spec.Swagger, etag string) error
RemoveAPIServiceSpec(apiServiceName string) error
}
type specAggregator struct {
// mutex protects all members of this struct.
rwMutex sync.RWMutex
// Map of API Services' OpenAPI specs by their name
openAPISpecs map[string]*openAPISpecInfo
// provided for dynamic OpenAPI spec
openAPIService *handler.OpenAPIService
openAPIVersionedService *handler.OpenAPIService
}
var _ AggregationManager = &specAggregator{}
// NewAggregationManager constructs a specAggregator from input openAPIService, openAPIVersionedService and
// recorded static OpenAPI spec. The function returns an AggregationManager interface.
func NewAggregationManager(openAPIService, openAPIVersionedService *handler.OpenAPIService, staticSpec *spec.Swagger) (AggregationManager, error) {
// openAPIVersionedService and deprecated openAPIService should be initialized together
if (openAPIService == nil) != (openAPIVersionedService == nil) {
return nil, fmt.Errorf("unexpected openapi service initialization error")
}
return &specAggregator{
openAPISpecs: map[string]*openAPISpecInfo{
"initial_static_spec": {
spec: staticSpec,
},
},
openAPIService: openAPIService,
openAPIVersionedService: openAPIVersionedService,
}, nil
}
// openAPISpecInfo is used to store OpenAPI spec with its priority.
// It can be used to sort specs with their priorities.
type openAPISpecInfo struct {
// Name of a registered ApiService
name string
// Specification of this API Service. If null then the spec is not loaded yet.
spec *spec.Swagger
etag string
}
// buildOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err error) {
specs := []openAPISpecInfo{}
for _, specInfo := range s.openAPISpecs {
if specInfo.spec == nil {
continue
}
specs = append(specs, *specInfo)
}
if len(specs) == 0 {
return &spec.Swagger{}, nil
}
for _, specInfo := range specs {
if specToReturn == nil {
specToReturn, err = aggregator.CloneSpec(specInfo.spec)
if err != nil {
return nil, err
}
continue
}
mergeSpecs(specToReturn, specInfo.spec)
}
// Add minimum required keys if missing, to properly serve the OpenAPI spec
// through apiextensions-apiserver HTTP handler. These keys will not be
// aggregated to top-level OpenAPI spec (only paths and definitions will).
// However these keys make the OpenAPI->proto serialization happy.
if specToReturn.Info == nil {
specToReturn.Info = &spec.Info{
InfoProps: spec.InfoProps{
Title: "Kubernetes",
},
}
}
if len(specToReturn.Swagger) == 0 {
specToReturn.Swagger = "2.0"
}
return specToReturn, nil
}
// updateOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) updateOpenAPISpec() error {
if s.openAPIService == nil || s.openAPIVersionedService == nil {
// openAPIVersionedService and deprecated openAPIService should be initialized together
if !(s.openAPIService == nil && s.openAPIVersionedService == nil) {
return fmt.Errorf("unexpected openapi service initialization error")
}
return nil
}
specToServe, err := s.buildOpenAPISpec()
if err != nil {
return err
}
// openAPIService.UpdateSpec and openAPIVersionedService.UpdateSpec read the same swagger spec
// serially and update their local caches separately. Both endpoints will have same spec in
// their caches if the caller is holding proper locks.
err = s.openAPIService.UpdateSpec(specToServe)
if err != nil {
return err
}
return s.openAPIVersionedService.UpdateSpec(specToServe)
}
// tryUpdatingServiceSpecs tries updating openAPISpecs map with specified specInfo, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryUpdatingServiceSpecs(specInfo *openAPISpecInfo) error {
orgSpecInfo, exists := s.openAPISpecs[specInfo.name]
s.openAPISpecs[specInfo.name] = specInfo
if err := s.updateOpenAPISpec(); err != nil {
if exists {
s.openAPISpecs[specInfo.name] = orgSpecInfo
} else {
delete(s.openAPISpecs, specInfo.name)
}
return err
}
return nil
}
// tryDeleteServiceSpecs tries delete specified specInfo from openAPISpecs map, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryDeleteServiceSpecs(apiServiceName string) error {
orgSpecInfo, exists := s.openAPISpecs[apiServiceName]
if !exists {
return nil
}
delete(s.openAPISpecs, apiServiceName)
if err := s.updateOpenAPISpec(); err != nil {
s.openAPISpecs[apiServiceName] = orgSpecInfo
return err
}
return nil
}
// AddUpdateLocalAPIService allows adding/updating local API service with nil handler and
// nil Spec.Service. This function can be used for local dynamic OpenAPI spec aggregation
// management (e.g. CRD)
func (s *specAggregator) AddUpdateLocalAPIServiceSpec(name string, spec *spec.Swagger, etag string) error {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
return s.tryUpdatingServiceSpecs(&openAPISpecInfo{
name: name,
spec: spec,
etag: etag,
})
}
// RemoveAPIServiceSpec removes an api service from OpenAPI aggregation. If it does not exist, no error is returned.
// It is thread safe.
func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) error {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
if _, existingService := s.openAPISpecs[apiServiceName]; !existingService {
return nil
}
return s.tryDeleteServiceSpecs(apiServiceName)
}
// mergeSpecs simply adds source openapi spec to dest and ignores any path/definition
// conflicts because CRD openapi spec should not have conflict
func mergeSpecs(dest, source *spec.Swagger) {
// Paths may be empty, due to [ACL constraints](http://goo.gl/8us55a#securityFiltering).
if source.Paths == nil {
// If Path is nil, none of the model defined in Definitions is used and we
// should not do anything.
// NOTE: this should not happen for CRD specs, because we automatically construct
// the Paths for CRD specs. We use utilruntime.HandleError to log this impossible
// case
utilruntime.HandleError(fmt.Errorf("unexpected CRD spec with empty Path: %v", *source))
return
}
if dest.Paths == nil {
dest.Paths = &spec.Paths{}
}
for k, v := range source.Definitions {
if dest.Definitions == nil {
dest.Definitions = spec.Definitions{}
}
dest.Definitions[k] = v
}
for k, v := range source.Paths.Paths {
// PathItem may be empty, due to [ACL constraints](http://goo.gl/8us55a#securityFiltering).
if dest.Paths.Paths == nil {
dest.Paths.Paths = map[string]spec.PathItem{}
}
dest.Paths.Paths[k] = v
}
}

View File

@ -1,371 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
// ResourceKind determines the scope of an API object: if it's the parent resource,
// scale subresource or status subresource.
type ResourceKind string
const (
// Resource specifies an object of custom resource kind
Resource ResourceKind = "Resource"
// Scale specifies an object of custom resource's scale subresource kind
Scale ResourceKind = "Scale"
// Status specifies an object of custom resource's status subresource kind
Status ResourceKind = "Status"
scaleSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.Scale"
statusSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status"
listMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
patchSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
)
// SwaggerConstructor takes in CRD OpenAPI schema and CustomResourceDefinitionSpec, and
// constructs the OpenAPI swagger that an apiserver serves.
type SwaggerConstructor struct {
// schema is the CRD's OpenAPI v2 schema
schema *spec.Schema
status, scale bool
group string
version string
kind string
listKind string
plural string
scope apiextensions.ResourceScope
}
// NewSwaggerConstructor creates a new SwaggerConstructor using the CRD OpenAPI schema
// and CustomResourceDefinitionSpec
func NewSwaggerConstructor(schema *spec.Schema, crdSpec *apiextensions.CustomResourceDefinitionSpec, version string) (*SwaggerConstructor, error) {
ret := &SwaggerConstructor{
schema: schema,
group: crdSpec.Group,
version: version,
kind: crdSpec.Names.Kind,
listKind: crdSpec.Names.ListKind,
plural: crdSpec.Names.Plural,
scope: crdSpec.Scope,
}
sub, err := getSubresourcesForVersion(crdSpec, version)
if err != nil {
return nil, err
}
if sub != nil {
ret.status = sub.Status != nil
ret.scale = sub.Scale != nil
}
return ret, nil
}
// ConstructCRDOpenAPISpec constructs the complete OpenAPI swagger (spec).
func (c *SwaggerConstructor) ConstructCRDOpenAPISpec() *spec.Swagger {
basePath := fmt.Sprintf("/apis/%s/%s/%s", c.group, c.version, c.plural)
if c.scope == apiextensions.NamespaceScoped {
basePath = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", c.group, c.version, c.plural)
}
model := fmt.Sprintf("%s.%s.%s", c.group, c.version, c.kind)
listModel := fmt.Sprintf("%s.%s.%s", c.group, c.version, c.listKind)
var schema spec.Schema
if c.schema != nil {
schema = *c.schema
}
ret := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Paths: &spec.Paths{
Paths: map[string]spec.PathItem{
basePath: {
PathItemProps: spec.PathItemProps{
Get: c.listOperation(),
Post: c.createOperation(),
Delete: c.deleteCollectionOperation(),
Parameters: pathParameters(),
},
},
fmt.Sprintf("%s/{name}", basePath): {
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Resource),
Put: c.replaceOperation(Resource),
Delete: c.deleteOperation(),
Patch: c.patchOperation(Resource),
Parameters: pathParameters(),
},
},
},
},
Definitions: spec.Definitions{
model: schema,
listModel: *c.listSchema(),
},
},
}
if c.status {
ret.SwaggerProps.Paths.Paths[fmt.Sprintf("%s/{name}/status", basePath)] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Status),
Put: c.replaceOperation(Status),
Patch: c.patchOperation(Status),
Parameters: pathParameters(),
},
}
}
if c.scale {
ret.SwaggerProps.Paths.Paths[fmt.Sprintf("%s/{name}/scale", basePath)] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Scale),
Put: c.replaceOperation(Scale),
Patch: c.patchOperation(Scale),
Parameters: pathParameters(),
},
}
// TODO(roycaihw): this is a hack to let apiExtension apiserver and generic kube-apiserver
// to have the same io.k8s.api.autoscaling.v1.Scale definition, so that aggregator server won't
// detect name conflict and create a duplicate io.k8s.api.autoscaling.v1.Scale_V2 schema
// when aggregating the openapi spec. It would be better if apiExtension apiserver serves
// identical definition through the same code path (using routes) as generic kube-apiserver.
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.Scale"] = *scaleSchema()
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.ScaleSpec"] = *scaleSpecSchema()
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.ScaleStatus"] = *scaleStatusSchema()
}
return ret
}
// baseOperation initializes a base operation that all operations build upon
func (c *SwaggerConstructor) baseOperation(kind ResourceKind, action string) *spec.Operation {
op := spec.NewOperation(c.operationID(kind, action)).
WithConsumes(
"application/json",
"application/yaml",
).
WithProduces(
"application/json",
"application/yaml",
).
WithTags(fmt.Sprintf("%s_%s", c.group, c.version)).
RespondsWith(401, unauthorizedResponse())
op.Schemes = []string{"https"}
op.AddExtension("x-kubernetes-action", action)
// Add x-kubernetes-group-version-kind extension
// For CRD scale subresource, the x-kubernetes-group-version-kind is autoscaling.v1.Scale
switch kind {
case Scale:
op.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": "autoscaling",
"kind": "Scale",
"version": "v1",
},
})
default:
op.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": c.group,
"kind": c.kind,
"version": c.version,
},
})
}
return op
}
// listOperation constructs a list operation for a CRD
func (c *SwaggerConstructor) listOperation() *spec.Operation {
op := c.baseOperation(Resource, "list").
WithDescription(fmt.Sprintf("list or watch objects of kind %s", c.kind)).
RespondsWith(200, okResponse(fmt.Sprintf("#/definitions/%s.%s.%s", c.group, c.version, c.listKind)))
return addCollectionOperationParameters(op)
}
// createOperation constructs a create operation for a CRD
func (c *SwaggerConstructor) createOperation() *spec.Operation {
ref := c.constructSchemaRef(Resource)
return c.baseOperation(Resource, "create").
WithDescription(fmt.Sprintf("create a %s", c.kind)).
RespondsWith(200, okResponse(ref)).
RespondsWith(201, createdResponse(ref)).
RespondsWith(202, acceptedResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(ref)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// deleteOperation constructs a delete operation for a CRD
func (c *SwaggerConstructor) deleteOperation() *spec.Operation {
op := c.baseOperation(Resource, "delete").
WithDescription(fmt.Sprintf("delete a %s", c.kind)).
RespondsWith(200, okResponse(statusSchemaRef)).
RespondsWith(202, acceptedResponse(statusSchemaRef))
return addDeleteOperationParameters(op)
}
// deleteCollectionOperation constructs a deletecollection operation for a CRD
func (c *SwaggerConstructor) deleteCollectionOperation() *spec.Operation {
op := c.baseOperation(Resource, "deletecollection").
WithDescription(fmt.Sprintf("delete collection of %s", c.kind))
return addCollectionOperationParameters(op)
}
// readOperation constructs a read operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) readOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "read"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, action)).
RespondsWith(200, okResponse(ref))
}
// replaceOperation constructs a replace operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) replaceOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "replace"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, action)).
RespondsWith(200, okResponse(ref)).
RespondsWith(201, createdResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(ref)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// patchOperation constructs a patch operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) patchOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "patch"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, "partially update")).
RespondsWith(200, okResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(patchSchemaRef)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// listSchema constructs the OpenAPI schema for a list of CRD objects
func (c *SwaggerConstructor) listSchema() *spec.Schema {
ref := c.constructSchemaRef(Resource)
s := new(spec.Schema).
WithDescription(fmt.Sprintf("%s is a list of %s", c.listKind, c.kind)).
WithRequired("items").
SetProperty("apiVersion", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["apiVersion"])).
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(ref)).
WithDescription(fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md", c.plural))).
SetProperty("kind", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["kind"])).
SetProperty("metadata", *spec.RefSchema(listMetaSchemaRef).
WithDescription(swaggerListDescriptions["metadata"]))
s.AddExtension("x-kubernetes-group-version-kind", map[string]string{
"group": c.group,
"kind": c.listKind,
"version": c.version,
})
return s
}
// operationID generates the ID for an operation
func (c *SwaggerConstructor) operationID(kind ResourceKind, action string) string {
var collectionTemplate, namespacedTemplate, subresourceTemplate string
if action == "deletecollection" {
action = "delete"
collectionTemplate = "Collection"
}
if c.scope == apiextensions.NamespaceScoped {
namespacedTemplate = "Namespaced"
}
switch kind {
case Status:
subresourceTemplate = "Status"
case Scale:
subresourceTemplate = "Scale"
}
return fmt.Sprintf("%s%s%s%s%s%s%s", action, strings.Title(c.group), strings.Title(c.version), collectionTemplate, namespacedTemplate, c.kind, subresourceTemplate)
}
// constructSchemaRef generates a reference to an object schema, based on the ResourceKind
// used by an operation
func (c *SwaggerConstructor) constructSchemaRef(kind ResourceKind) string {
var ref string
switch kind {
case Scale:
ref = scaleSchemaRef
default:
ref = fmt.Sprintf("#/definitions/%s.%s.%s", c.group, c.version, c.kind)
}
return ref
}
// constructDescription generates a description for READ, REPLACE and PATCH operations, based on
// the ResourceKind used by the operation
func (c *SwaggerConstructor) constructDescription(kind ResourceKind, action string) string {
var descriptionTemplate string
switch kind {
case Status:
descriptionTemplate = "status of "
case Scale:
descriptionTemplate = "scale of "
}
return fmt.Sprintf("%s %sthe specified %s", action, descriptionTemplate, c.kind)
}
// hasPerVersionSubresources returns true if a CRD spec uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// getSubresourcesForVersion returns the subresources for given version in given CRD spec.
func getSubresourcesForVersion(spec *apiextensions.CustomResourceDefinitionSpec, version string) (*apiextensions.CustomResourceSubresources, error) {
if !hasPerVersionSubresources(spec.Versions) {
return spec.Subresources, nil
}
if spec.Subresources != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinitionSpec version %s: top-level and per-version subresources must be mutual exclusive", version)
}
for _, v := range spec.Versions {
if version == v.Name {
return v.Subresources, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinitionSpec", version)
}

View File

@ -1,45 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
)
// ConvertJSONSchemaPropsToOpenAPIv2Schema converts our internal OpenAPI v3 schema
// (*apiextensions.JSONSchemaProps) to an OpenAPI v2 schema (*spec.Schema).
// NOTE: we use versioned type (v1beta1) here so that we can properly marshal the object
// using the JSON tags
func ConvertJSONSchemaPropsToOpenAPIv2Schema(in *apiextensions.JSONSchemaProps) (*spec.Schema, error) {
if in == nil {
return nil, nil
}
out := new(spec.Schema)
validation.ConvertJSONSchemaPropsWithPostProcess(in, out, func(p *spec.Schema) error {
// Remove unsupported fields in OpenAPI v2
p.OneOf = nil
p.AnyOf = nil
p.Not = nil
return nil
})
return out, nil
}

View File

@ -1,528 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"reflect"
"testing"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/util/diff"
)
func Test_ConvertJSONSchemaPropsToOpenAPIv2Schema(t *testing.T) {
testStr := "test"
testStr2 := "test2"
testFloat64 := float64(6.4)
testInt64 := int64(64)
testApiextensionsJSON := apiextensions.JSON(testStr)
tests := []struct {
name string
in *apiextensions.JSONSchemaProps
expected *spec.Schema
}{
{
name: "id",
in: &apiextensions.JSONSchemaProps{
ID: testStr,
},
expected: new(spec.Schema).
WithID(testStr),
},
{
name: "$schema",
in: &apiextensions.JSONSchemaProps{
Schema: "test",
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Schema: "test",
},
},
},
{
name: "$ref",
in: &apiextensions.JSONSchemaProps{
Ref: &testStr,
},
expected: spec.RefSchema(testStr),
},
{
name: "description",
in: &apiextensions.JSONSchemaProps{
Description: testStr,
},
expected: new(spec.Schema).
WithDescription(testStr),
},
{
name: "type and format",
in: &apiextensions.JSONSchemaProps{
Type: testStr,
Format: testStr2,
},
expected: new(spec.Schema).
Typed(testStr, testStr2),
},
{
name: "title",
in: &apiextensions.JSONSchemaProps{
Title: testStr,
},
expected: new(spec.Schema).
WithTitle(testStr),
},
{
name: "default",
in: &apiextensions.JSONSchemaProps{
Default: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithDefault(testStr),
},
{
name: "maximum and exclusiveMaximum",
in: &apiextensions.JSONSchemaProps{
Maximum: &testFloat64,
ExclusiveMaximum: true,
},
expected: new(spec.Schema).
WithMaximum(testFloat64, true),
},
{
name: "minimum and exclusiveMinimum",
in: &apiextensions.JSONSchemaProps{
Minimum: &testFloat64,
ExclusiveMinimum: true,
},
expected: new(spec.Schema).
WithMinimum(testFloat64, true),
},
{
name: "maxLength",
in: &apiextensions.JSONSchemaProps{
MaxLength: &testInt64,
},
expected: new(spec.Schema).
WithMaxLength(testInt64),
},
{
name: "minLength",
in: &apiextensions.JSONSchemaProps{
MinLength: &testInt64,
},
expected: new(spec.Schema).
WithMinLength(testInt64),
},
{
name: "pattern",
in: &apiextensions.JSONSchemaProps{
Pattern: testStr,
},
expected: new(spec.Schema).
WithPattern(testStr),
},
{
name: "maxItems",
in: &apiextensions.JSONSchemaProps{
MaxItems: &testInt64,
},
expected: new(spec.Schema).
WithMaxItems(testInt64),
},
{
name: "minItems",
in: &apiextensions.JSONSchemaProps{
MinItems: &testInt64,
},
expected: new(spec.Schema).
WithMinItems(testInt64),
},
{
name: "uniqueItems",
in: &apiextensions.JSONSchemaProps{
UniqueItems: true,
},
expected: new(spec.Schema).
UniqueValues(),
},
{
name: "multipleOf",
in: &apiextensions.JSONSchemaProps{
MultipleOf: &testFloat64,
},
expected: new(spec.Schema).
WithMultipleOf(testFloat64),
},
{
name: "enum",
in: &apiextensions.JSONSchemaProps{
Enum: []apiextensions.JSON{apiextensions.JSON(testStr), apiextensions.JSON(testStr2)},
},
expected: new(spec.Schema).
WithEnum(testStr, testStr2),
},
{
name: "maxProperties",
in: &apiextensions.JSONSchemaProps{
MaxProperties: &testInt64,
},
expected: new(spec.Schema).
WithMaxProperties(testInt64),
},
{
name: "minProperties",
in: &apiextensions.JSONSchemaProps{
MinProperties: &testInt64,
},
expected: new(spec.Schema).
WithMinProperties(testInt64),
},
{
name: "required",
in: &apiextensions.JSONSchemaProps{
Required: []string{testStr, testStr2},
},
expected: new(spec.Schema).
WithRequired(testStr, testStr2),
},
{
name: "items single props",
in: &apiextensions.JSONSchemaProps{
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Items: &spec.SchemaOrArray{
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "items array props",
in: &apiextensions.JSONSchemaProps{
Items: &apiextensions.JSONSchemaPropsOrArray{
JSONSchemas: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Items: &spec.SchemaOrArray{
Schemas: []spec.Schema{
*spec.BooleanProperty(),
*spec.StringProperty(),
},
},
},
},
},
{
name: "allOf",
in: &apiextensions.JSONSchemaProps{
AllOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema).
WithAllOf(*spec.BooleanProperty(), *spec.StringProperty()),
},
{
name: "oneOf",
in: &apiextensions.JSONSchemaProps{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// OneOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "anyOf",
in: &apiextensions.JSONSchemaProps{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// AnyOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "not",
in: &apiextensions.JSONSchemaProps{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// Not: spec.BooleanProperty(),
// },
// },
},
{
name: "nested logic",
in: &apiextensions.JSONSchemaProps{
AllOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
AnyOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
OneOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
Not: &apiextensions.JSONSchemaProps{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
},
expected: new(spec.Schema).
WithAllOf(spec.Schema{}, spec.Schema{}, spec.Schema{}, *spec.StringProperty()),
},
{
name: "properties",
in: &apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expected: new(spec.Schema).
SetProperty(testStr, *spec.BooleanProperty()),
},
{
name: "additionalProperties",
in: &apiextensions.JSONSchemaProps{
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
Allows: true,
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "patternProperties",
in: &apiextensions.JSONSchemaProps{
PatternProperties: map[string]apiextensions.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
PatternProperties: map[string]spec.Schema{
testStr: *spec.BooleanProperty(),
},
},
},
},
{
name: "dependencies schema",
in: &apiextensions.JSONSchemaProps{
Dependencies: apiextensions.JSONSchemaDependencies{
testStr: apiextensions.JSONSchemaPropsOrStringArray{
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Dependencies: spec.Dependencies{
testStr: spec.SchemaOrStringArray{
Schema: spec.BooleanProperty(),
},
},
},
},
},
{
name: "dependencies string array",
in: &apiextensions.JSONSchemaProps{
Dependencies: apiextensions.JSONSchemaDependencies{
testStr: apiextensions.JSONSchemaPropsOrStringArray{
Property: []string{testStr2},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Dependencies: spec.Dependencies{
testStr: spec.SchemaOrStringArray{
Property: []string{testStr2},
},
},
},
},
},
{
name: "additionalItems",
in: &apiextensions.JSONSchemaProps{
AdditionalItems: &apiextensions.JSONSchemaPropsOrBool{
Allows: true,
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalItems: &spec.SchemaOrBool{
Allows: true,
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "definitions",
in: &apiextensions.JSONSchemaProps{
Definitions: apiextensions.JSONSchemaDefinitions{
testStr: apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Definitions: spec.Definitions{
testStr: *spec.BooleanProperty(),
},
},
},
},
{
name: "externalDocs",
in: &apiextensions.JSONSchemaProps{
ExternalDocs: &apiextensions.ExternalDocumentation{
Description: testStr,
URL: testStr2,
},
},
expected: new(spec.Schema).
WithExternalDocs(testStr, testStr2),
},
{
name: "example",
in: &apiextensions.JSONSchemaProps{
Example: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithExample(testStr),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
out, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(test.in)
if err != nil {
t.Fatalf("unexpected error in converting openapi schema: %v", err)
}
if !reflect.DeepEqual(*out, *test.expected) {
t.Errorf("unexpected result:\n want=%v\n got=%v\n\n%s", *test.expected, *out, diff.ObjectDiff(*test.expected, *out))
}
})
}
}

View File

@ -1,227 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"crypto/sha512"
"encoding/json"
"fmt"
"github.com/go-openapi/spec"
autoscalingv1 "k8s.io/api/autoscaling/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
deleteOptionsSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions"
objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
scaleSpecSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.ScaleSpec"
scaleStatusSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.ScaleStatus"
)
var swaggerTypeMetaDescriptions = metav1.TypeMeta{}.SwaggerDoc()
var swaggerDeleteOptionsDescriptions = metav1.DeleteOptions{}.SwaggerDoc()
var swaggerListDescriptions = metav1.List{}.SwaggerDoc()
var swaggerListOptionsDescriptions = metav1.ListOptions{}.SwaggerDoc()
var swaggerScaleDescriptions = autoscalingv1.Scale{}.SwaggerDoc()
var swaggerScaleSpecDescriptions = autoscalingv1.ScaleSpec{}.SwaggerDoc()
var swaggerScaleStatusDescriptions = autoscalingv1.ScaleStatus{}.SwaggerDoc()
// calcSwaggerEtag calculates an etag of the OpenAPI swagger (spec)
func calcSwaggerEtag(openAPISpec *spec.Swagger) (string, error) {
specBytes, err := json.MarshalIndent(openAPISpec, " ", " ")
if err != nil {
return "", err
}
return fmt.Sprintf("\"%X\"", sha512.Sum512(specBytes)), nil
}
// pathParameters constructs the Parameter used by all paths in the CRD swagger (spec)
func pathParameters() []spec.Parameter {
return []spec.Parameter{
*spec.QueryParam("pretty").
Typed("string", "").
UniqueValues().
WithDescription("If 'true', then the output is pretty printed."),
}
}
// addDeleteOperationParameters add the body&query parameters used by a delete operation
func addDeleteOperationParameters(op *spec.Operation) *spec.Operation {
return op.
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(deleteOptionsSchemaRef)}}).
Named("body").
WithLocation("body").
AsRequired()).
AddParam(spec.QueryParam("dryRun").
Typed("string", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["dryRun"])).
AddParam(spec.QueryParam("gracePeriodSeconds").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["gracePeriodSeconds"])).
AddParam(spec.QueryParam("orphanDependents").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["orphanDependents"])).
AddParam(spec.QueryParam("propagationPolicy").
Typed("string", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["propagationPolicy"]))
}
// addCollectionOperationParameters adds the query parameters used by list and deletecollection
// operations
func addCollectionOperationParameters(op *spec.Operation) *spec.Operation {
return op.
AddParam(spec.QueryParam("continue").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["continue"])).
AddParam(spec.QueryParam("fieldSelector").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["fieldSelector"])).
AddParam(spec.QueryParam("includeUninitialized").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["includeUninitialized"])).
AddParam(spec.QueryParam("labelSelector").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["labelSelector"])).
AddParam(spec.QueryParam("limit").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["limit"])).
AddParam(spec.QueryParam("resourceVersion").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["resourceVersion"])).
AddParam(spec.QueryParam("timeoutSeconds").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["timeoutSeconds"])).
AddParam(spec.QueryParam("watch").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["watch"]))
}
// okResponse constructs a 200 OK response with the input object schema reference
func okResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("OK").
WithSchema(spec.RefSchema(ref))
}
// createdResponse constructs a 201 Created response with the input object schema reference
func createdResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("Created").
WithSchema(spec.RefSchema(ref))
}
// acceptedResponse constructs a 202 Accepted response with the input object schema reference
func acceptedResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("Accepted").
WithSchema(spec.RefSchema(ref))
}
// unauthorizedResponse constructs a 401 Unauthorized response
func unauthorizedResponse() *spec.Response {
return spec.NewResponse().
WithDescription("Unauthorized")
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.Scale objects
// TODO(roycaihw): this is a hack to let apiExtension apiserver and generic kube-apiserver
// to have the same io.k8s.api.autoscaling.v1.Scale definition, so that aggregator server won't
// detect name conflict and create a duplicate io.k8s.api.autoscaling.v1.Scale_V2 schema
// when aggregating the openapi spec. It would be better if apiExtension apiserver serves
// identical definition through the same code path (using routes) as generic kube-apiserver.
func scaleSchema() *spec.Schema {
s := new(spec.Schema).
WithDescription(swaggerScaleDescriptions[""]).
SetProperty("apiVersion", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["apiVersion"])).
SetProperty("kind", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["kind"])).
SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
WithDescription(swaggerScaleDescriptions["metadata"])).
SetProperty("spec", *spec.RefSchema(scaleSpecSchemaRef).
WithDescription(swaggerScaleDescriptions["spec"])).
SetProperty("status", *spec.RefSchema(scaleStatusSchemaRef).
WithDescription(swaggerScaleDescriptions["status"]))
s.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": "autoscaling",
"kind": "Scale",
"version": "v1",
},
})
return s
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.ScaleSpec objects
func scaleSpecSchema() *spec.Schema {
return new(spec.Schema).
WithDescription(swaggerScaleSpecDescriptions[""]).
SetProperty("replicas", *spec.Int32Property().
WithDescription(swaggerScaleSpecDescriptions["replicas"]))
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.ScaleStatus objects
func scaleStatusSchema() *spec.Schema {
return new(spec.Schema).
WithDescription(swaggerScaleStatusDescriptions[""]).
WithRequired("replicas").
SetProperty("replicas", *spec.Int32Property().
WithDescription(swaggerScaleStatusDescriptions["replicas"])).
SetProperty("selector", *spec.StringProperty().
WithDescription(swaggerScaleStatusDescriptions["selector"]))
}
// CustomResourceDefinitionOpenAPISpec constructs the OpenAPI spec (swagger) and calculates
// etag for a given CustomResourceDefinitionSpec.
// NOTE: in apiserver we general operates on internal types. We are using versioned (v1beta1)
// validation schema here because we need the json tags to properly marshal the object to
// JSON.
func CustomResourceDefinitionOpenAPISpec(crdSpec *apiextensions.CustomResourceDefinitionSpec, version string, validationSchema *apiextensions.CustomResourceValidation) (*spec.Swagger, string, error) {
schema := &spec.Schema{}
if validationSchema != nil && validationSchema.OpenAPIV3Schema != nil {
var err error
schema, err = ConvertJSONSchemaPropsToOpenAPIv2Schema(validationSchema.OpenAPIV3Schema)
if err != nil {
return nil, "", err
}
}
crdSwaggerConstructor, err := NewSwaggerConstructor(schema, crdSpec, version)
if err != nil {
return nil, "", err
}
crdOpenAPISpec := crdSwaggerConstructor.ConstructCRDOpenAPISpec()
etag, err := calcSwaggerEtag(crdOpenAPISpec)
if err != nil {
return nil, "", err
}
return crdOpenAPISpec, etag, nil
}

View File

@ -116,7 +116,6 @@ go_library(
"//vendor/k8s.io/klog:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/builder:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/common:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/handler:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
],

View File

@ -25,8 +25,7 @@ import (
"time"
systemd "github.com/coreos/go-systemd/daemon"
swagger "github.com/emicklei/go-restful-swagger12"
"github.com/go-openapi/spec"
"github.com/emicklei/go-restful-swagger12"
"k8s.io/klog"
"k8s.io/apimachinery/pkg/api/meta"
@ -48,7 +47,6 @@ import (
restclient "k8s.io/client-go/rest"
openapibuilder "k8s.io/kube-openapi/pkg/builder"
openapicommon "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/handler"
openapiutil "k8s.io/kube-openapi/pkg/util"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
)
@ -126,11 +124,6 @@ type GenericAPIServer struct {
swaggerConfig *swagger.Config
openAPIConfig *openapicommon.Config
// Expose the registered OpenAPI Services and built static OpenAPI spec if openAPIConfig is non-nil
OpenAPIService *handler.OpenAPIService // for endpoint /swagger.json
OpenAPIVersionedService *handler.OpenAPIService // for endpoint /openapi/v2
StaticOpenAPISpec *spec.Swagger
// PostStartHooks are each called after the server has started listening, in a separate go func for each
// with no guarantee of ordering between them. The map key is a name used for error reporting.
// It may kill the process with a panic if it wishes to by returning an error.
@ -247,7 +240,7 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
routes.Swagger{Config: s.swaggerConfig}.Install(s.Handler.GoRestfulContainer)
}
if s.openAPIConfig != nil {
s.OpenAPIService, s.OpenAPIVersionedService, s.StaticOpenAPISpec = routes.OpenAPI{
routes.OpenAPI{
Config: s.openAPIConfig,
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
}

View File

@ -32,10 +32,8 @@ go_library(
"//vendor/github.com/elazarl/go-bindata-assetfs:go_default_library",
"//vendor/github.com/emicklei/go-restful:go_default_library",
"//vendor/github.com/emicklei/go-restful-swagger12:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/builder:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/common:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/handler:go_default_library",
],

View File

@ -18,11 +18,9 @@ package routes
import (
restful "github.com/emicklei/go-restful"
"github.com/go-openapi/spec"
"k8s.io/klog"
"k8s.io/apiserver/pkg/server/mux"
"k8s.io/kube-openapi/pkg/builder"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/handler"
)
@ -32,27 +30,17 @@ type OpenAPI struct {
Config *common.Config
}
// Install adds the SwaggerUI webservice to the given mux. This function returns
// the built static OpenAPI spec and the registered OpenAPI services to allow
// further OpenAPI spec aggregation.
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) (openAPIService, openAPIVersionedService *handler.OpenAPIService, spec *spec.Swagger) {
var err error
// Record the static OpenAPI spec to allow further OpenAPI spec aggregation
// with this static spec on the registered OpenAPI services
spec, err = builder.BuildOpenAPISpec(c.RegisteredWebServices(), oa.Config)
if err != nil {
klog.Fatalf("Failed to build open api spec for root: %v", err)
}
// Install adds the SwaggerUI webservice to the given mux.
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) {
// NOTE: [DEPRECATION] We will announce deprecation for format-separated endpoints for OpenAPI spec,
// and switch to a single /openapi/v2 endpoint in Kubernetes 1.10. The design doc and deprecation process
// are tracked at: https://docs.google.com/document/d/19lEqE9lc4yHJ3WJAJxS_G7TcORIJXGHyq3wpwcH28nU.
openAPIService, err = handler.RegisterOpenAPIService(spec, "/swagger.json", mux)
_, err := handler.BuildAndRegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux)
if err != nil {
klog.Fatalf("Failed to register open api spec for root: %v", err)
}
openAPIVersionedService, err = handler.RegisterOpenAPIVersionedService(spec, "/openapi/v2", mux)
_, err = handler.BuildAndRegisterOpenAPIVersionedService("/openapi/v2", c.RegisteredWebServices(), oa.Config, mux)
if err != nil {
klog.Fatalf("Failed to register versioned open api spec for root: %v", err)
}
return openAPIService, openAPIVersionedService, spec
}

View File

@ -53,19 +53,12 @@ type specAggregator struct {
// provided for dynamic OpenAPI spec
openAPIService *handler.OpenAPIService
openAPIVersionedService *handler.OpenAPIService
// initialized is set to be true at the end of startup. All local specs
// must be registered before initialized is set, we panic otherwise.
initialized bool
}
var _ AggregationManager = &specAggregator{}
// This function is not thread safe as it only being called on startup.
func (s *specAggregator) addLocalSpec(spec *spec.Swagger, localHandler http.Handler, name, etag string) {
if s.initialized {
panic("Local spec must not be added after startup")
}
localAPIService := apiregistration.APIService{}
localAPIService.Name = name
s.openAPISpecs[name] = &openAPISpecInfo{
@ -76,17 +69,6 @@ func (s *specAggregator) addLocalSpec(spec *spec.Swagger, localHandler http.Hand
}
}
// GetAPIServicesName returns the names of APIServices recorded in specAggregator.openAPISpecs.
// 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 {
names := make([]string, len(s.openAPISpecs))
for key := range s.openAPISpecs {
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, webServices []*restful.WebService,
config *common.Config, pathHandler common.PathHandler) (AggregationManager, error) {
@ -142,9 +124,6 @@ func BuildAndRegisterAggregator(downloader *Downloader, delegationTarget server.
return nil, err
}
// We set initialized to be true to forbid any future local spec addition
s.initialized = true
return s, nil
}

View File

@ -49,9 +49,6 @@ type AggregationManager interface {
UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error
RemoveAPIServiceSpec(apiServiceName string) error
GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool)
// GetAPIServicesName returns the names of APIServices recorded in AggregationManager.
GetAPIServiceNames() []string
}
// AggregationController periodically check for changes in OpenAPI specs of APIServices and update/remove
@ -75,18 +72,6 @@ func NewAggregationController(downloader *Downloader, openAPIAggregationManager
}
c.syncHandler = c.sync
// During initialization, openAPIAggregationManager only has record of local APIServices. There must be
// no aggregated APIService recorded, because aggregated APIServices only get added to openAPIAggregationManager
// by calling AggregationController.AddAPIService or AggregationController.UpdateAPIService after the
// controller is initialized.
// Here we add delegation target API services to queue, to periodically sync dynamic OpenAPI spec from
// delegation target.
// NOTE: openAPIAggregationManager.GetAPIServiceNames() will also return the APIService of non-name spec
// for aggregator, which has no http.Handler. The first time sync (when popping off from queue) for
// this APIService will be a no-op, and the controller will drop the APIService from queue.
for _, name := range openAPIAggregationManager.GetAPIServiceNames() {
c.queue.AddAfter(name, time.Second)
}
return c
}

View File

@ -17,22 +17,16 @@ limitations under the License.
package apimachinery
import (
"fmt"
"strings"
"time"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
utilversion "k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/test/e2e/framework"
. "github.com/onsi/ginkgo"
)
var crdVersion = utilversion.MustParseSemantic("v1.7.0")
var crdOpenAPIVersion = utilversion.MustParseSemantic("v1.13.0")
var _ = SIGDescribe("CustomResourceDefinition resources", func() {
@ -73,58 +67,5 @@ var _ = SIGDescribe("CustomResourceDefinition resources", func() {
}
}()
})
It("has OpenAPI spec served with CRD Validation chema", func() {
framework.SkipUnlessServerVersionGTE(crdOpenAPIVersion, f.ClientSet.Discovery())
config, err := framework.LoadConfig()
if err != nil {
framework.Failf("failed to load config: %v", err)
}
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
framework.Failf("failed to initialize apiExtensionClient: %v", err)
}
randomDefinition := fixtures.NewRandomNameCustomResourceDefinition(v1beta1.ClusterScoped)
//create CRD and waits for the resource to be recognized and available.
randomDefinition, err = fixtures.CreateNewCustomResourceDefinition(randomDefinition, apiExtensionClient, f.DynamicClient)
if err != nil {
framework.Failf("failed to create CustomResourceDefinition: %v", err)
}
// TODO(roycaihw): think about tweaking feature gates in e2e test (is it possible/easy
// to do so?) and have CRD use top-level/per-version schema
// Also need to test NamespaceScoped CRDs
// We use a wait.Poll block here because the kube-aggregator openapi
// controller takes time to rotate the queue and resync apiextensions-apiserver's spec
if err := wait.Poll(5*time.Second, 120*time.Second, func() (bool, error) {
data, err := f.ClientSet.CoreV1().RESTClient().Get().
AbsPath("/swagger.json").
DoRaw()
if err != nil {
return false, err
}
// TODO(roycaihw): verify more Paths and List Definitions, also for multiple versions
baseDefinition := fmt.Sprintf("%s.%s.%s", randomDefinition.Spec.Group, randomDefinition.Spec.Version, randomDefinition.Spec.Names.Kind)
basePath := fmt.Sprintf("/apis/%s/%s/%s", randomDefinition.Spec.Group, randomDefinition.Spec.Version, randomDefinition.Spec.Names.Plural)
return strings.Contains(string(data), basePath) &&
strings.Contains(string(data), baseDefinition), nil
}); err != nil {
framework.Failf("failed to wait for apiserver to serve openapi spec for registered CRD: %v", err)
}
defer func() {
err = fixtures.DeleteCustomResourceDefinition(randomDefinition, apiExtensionClient)
if err != nil {
framework.Failf("failed to delete CustomResourceDefinition: %v", err)
}
}()
})
})
})

View File

@ -66,7 +66,6 @@ go_test(
"//test/integration/framework:go_default_library",
"//test/utils:go_default_library",
"//vendor/github.com/evanphx/json-patch:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/sigs.k8s.io/yaml:go_default_library",
] + select({
"@io_bazel_rules_go//go/platform:android": [

View File

@ -18,12 +18,9 @@ package master
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/go-openapi/spec"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
networkingv1 "k8s.io/api/networking/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
@ -275,91 +272,6 @@ func TestCRD(t *testing.T) {
}
}
func TestCRDOpenAPI(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.Initializers, true)()
result := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
defer result.TearDownFn()
kubeclient, err := kubernetes.NewForConfig(result.ClientConfig)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
apiextensionsclient, err := apiextensionsclientset.NewForConfig(result.ClientConfig)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
t.Logf("Trying to create a custom resource without conflict")
crd := &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "foos.cr.bar.com",
},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "cr.bar.com",
Version: "v1",
Scope: apiextensionsv1beta1.NamespaceScoped,
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "foos",
Kind: "Foo",
},
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"foo": {Type: "string"},
},
},
},
},
}
etcd.CreateTestCRDs(t, apiextensionsclient, false, crd)
waitForSpec := func(expectedType string) {
t.Logf(`Waiting for {properties: {"foo": {"type":"%s"}}} to show up in schema`, expectedType)
lastMsg := ""
if err := wait.PollImmediate(500*time.Millisecond, 120*time.Second, func() (bool, error) {
lastMsg = ""
bs, err := kubeclient.RESTClient().Get().AbsPath("openapi", "v2").DoRaw()
if err != nil {
return false, err
}
spec := spec.Swagger{}
if err := json.Unmarshal(bs, &spec); err != nil {
return false, err
}
if spec.SwaggerProps.Paths == nil {
lastMsg = "spec.SwaggerProps.Paths is nil"
return false, nil
}
d, ok := spec.SwaggerProps.Definitions["cr.bar.com.v1.Foo"]
if !ok {
lastMsg = `spec.SwaggerProps.Definitions["cr.bar.com.v1.Foo"] not found`
return false, nil
}
p, ok := d.Properties["foo"]
if !ok {
lastMsg = `spec.SwaggerProps.Definitions["cr.bar.com.v1.Foo"].Properties["foo"] not found`
return false, nil
}
if !p.Type.Contains(expectedType) {
lastMsg = fmt.Sprintf(`spec.SwaggerProps.Definitions["cr.bar.com.v1.Foo"].Properties["foo"].Type should be %q, but got: %q`, expectedType, p.Type)
return false, nil
}
return true, nil
}); err != nil {
t.Fatalf("Failed to see %s OpenAPI spec in discovery: %v, last message: %s", crd.Name, err, lastMsg)
}
}
waitForSpec("string")
crd, err = apiextensionsclient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
prop := crd.Spec.Validation.OpenAPIV3Schema.Properties["foo"]
prop.Type = "boolean"
crd.Spec.Validation.OpenAPIV3Schema.Properties["foo"] = prop
if _, err = apiextensionsclient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd); err != nil {
t.Fatal(err)
}
waitForSpec("boolean")
}
type Foo struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`