diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7e937c367ea..6327cde93d6 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -3334,35 +3334,35 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/aggregator", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/generators", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto/validation", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/utils/clock", diff --git a/staging/BUILD b/staging/BUILD index 1f6015e3520..8403046c6fd 100644 --- a/staging/BUILD +++ b/staging/BUILD @@ -92,6 +92,7 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/util/flag:all-srcs", "//staging/src/k8s.io/apiserver/pkg/util/flushwriter:all-srcs", "//staging/src/k8s.io/apiserver/pkg/util/logs:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/util/openapi:all-srcs", "//staging/src/k8s.io/apiserver/pkg/util/proxy:all-srcs", "//staging/src/k8s.io/apiserver/pkg/util/trace:all-srcs", "//staging/src/k8s.io/apiserver/pkg/util/webhook:all-srcs", diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index 374a18f55a7..a90c044928e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -1466,6 +1466,10 @@ "ImportPath": "k8s.io/apiserver/pkg/util/flushwriter", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/util/openapi", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/util/trace", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -2024,23 +2028,23 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/equality", diff --git a/staging/src/k8s.io/apimachinery/Godeps/Godeps.json b/staging/src/k8s.io/apimachinery/Godeps/Godeps.json index 0607ae6e038..02e897967ad 100644 --- a/staging/src/k8s.io/apimachinery/Godeps/Godeps.json +++ b/staging/src/k8s.io/apimachinery/Godeps/Godeps.json @@ -180,7 +180,7 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/staging/src/k8s.io/apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiserver/Godeps/Godeps.json index 138dc9e4c95..32519a909e2 100644 --- a/staging/src/k8s.io/apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiserver/Godeps/Godeps.json @@ -1756,23 +1756,23 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/client-go/discovery", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index 843ae6bd7d6..ebd5e7a761f 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -79,6 +79,9 @@ go_library( "//vendor/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/openapi:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/builder:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/common:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go index 7060eb73897..23d13adc3d3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go @@ -30,6 +30,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/registry/rest" + openapicommon "k8s.io/kube-openapi/pkg/common" ) // APIGroupVersion is a helper for exposing rest.Storage objects as http.Handlers via go-restful @@ -77,6 +78,9 @@ type APIGroupVersion struct { // EnableAPIResponseCompression indicates whether API Responses should support compression // if the client requests it via Accept-Encoding EnableAPIResponseCompression bool + + // OpenAPIConfig lets the individual handlers build a subset of the OpenAPI schema before they are installed. + OpenAPIConfig *openapicommon.Config } // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index bb30cf27326..55871a7c790 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -80,6 +80,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/trace:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 4da38f43b14..942e53483ff 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -39,6 +39,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" + openapiproto "k8s.io/kube-openapi/pkg/util/proto" ) // RequestScope encapsulates common fields across all RESTful handler methods. @@ -55,6 +56,7 @@ type RequestScope struct { UnsafeConvertor runtime.ObjectConvertor TableConvertor rest.TableConvertor + OpenAPISchema openapiproto.Schema Resource schema.GroupVersionResource Kind schema.GroupVersionKind diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 0158d28f611..3edd09dcdf9 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -39,6 +39,8 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/registry/rest" genericfilters "k8s.io/apiserver/pkg/server/filters" + utilopenapi "k8s.io/apiserver/pkg/util/openapi" + openapibuilder "k8s.io/kube-openapi/pkg/builder" ) const ( @@ -495,6 +497,16 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if a.group.MetaGroupVersion != nil { reqScope.MetaGroupVersion = *a.group.MetaGroupVersion } + if a.group.OpenAPIConfig != nil { + openAPIDefinitions, err := openapibuilder.BuildOpenAPIDefinitionsForResource(defaultVersionedObject, a.group.OpenAPIConfig) + if err != nil { + return nil, fmt.Errorf("unable to build openapi definitions for %v: %v", fqKindToRegister, err) + } + reqScope.OpenAPISchema, err = utilopenapi.ToProtoSchema(openAPIDefinitions, fqKindToRegister) + if err != nil { + return nil, fmt.Errorf("unable to get openapi schema for %v: %v", fqKindToRegister, err) + } + } for _, action := range actions { producedObject := storageMeta.ProducesObject(action.Verb) if producedObject == nil { diff --git a/staging/src/k8s.io/apiserver/pkg/server/BUILD b/staging/src/k8s.io/apiserver/pkg/server/BUILD index dcefdb4b746..1dfabf8fa46 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/BUILD @@ -15,6 +15,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go index 10a8ddff6e5..9beba735d33 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go @@ -426,6 +426,7 @@ func (s *GenericAPIServer) newAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupV Admit: s.admissionControl, MinRequestTimeout: s.minRequestTimeout, EnableAPIResponseCompression: s.enableAPIResponseCompression, + OpenAPIConfig: s.openAPIConfig, } } diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go index fbe7235c2c2..8439d8cf75a 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver_test.go @@ -31,6 +31,7 @@ import ( "testing" "time" + openapi "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -78,9 +79,45 @@ func init() { examplev1.AddToScheme(scheme) } +func buildTestOpenAPIDefinition() kubeopenapi.OpenAPIDefinition { + return kubeopenapi.OpenAPIDefinition{ + Schema: openapi.Schema{ + SchemaProps: openapi.SchemaProps{ + Description: "Description", + Properties: map[string]openapi.Schema{}, + }, + VendorExtensible: openapi.VendorExtensible{ + Extensions: openapi.Extensions{ + "x-kubernetes-group-version-kind": []map[string]string{ + { + "group": "", + "version": "v1", + "kind": "Getter", + }, + { + "group": "batch", + "version": "v1", + "kind": "Getter", + }, + { + "group": "extensions", + "version": "v1", + "kind": "Getter", + }, + }, + }, + }, + }, + } +} + func testGetOpenAPIDefinitions(_ kubeopenapi.ReferenceCallback) map[string]kubeopenapi.OpenAPIDefinition { return map[string]kubeopenapi.OpenAPIDefinition{ - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Status": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": {}, + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": buildTestOpenAPIDefinition(), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": {}, } } diff --git a/staging/src/k8s.io/apiserver/pkg/util/openapi/BUILD b/staging/src/k8s.io/apiserver/pkg/util/openapi/BUILD new file mode 100644 index 00000000000..fd9dc89c835 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/openapi/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["proto.go"], + importpath = "k8s.io/apiserver/pkg/util/openapi", + visibility = ["//visibility:public"], + deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", + "//vendor/github.com/googleapis/gnostic/compiler:go_default_library", + "//vendor/gopkg.in/yaml.v2:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto: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"], +) + +go_test( + name = "go_default_test", + srcs = ["proto_test.go"], + embed = [":go_default_library"], + deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go new file mode 100644 index 00000000000..5641d1a141f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto.go @@ -0,0 +1,142 @@ +/* +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 ( + "encoding/json" + "fmt" + + "github.com/go-openapi/spec" + openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" + "github.com/googleapis/gnostic/compiler" + yaml "gopkg.in/yaml.v2" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/util/proto" +) + +const ( + // groupVersionKindExtensionKey is the key used to lookup the + // GroupVersionKind value for an object definition from the + // definition's "extensions" map. + groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" +) + +// ToProtoSchema builds the proto formatted schema from an OpenAPI spec +func ToProtoSchema(openAPIDefinitions *spec.Definitions, gvk schema.GroupVersionKind) (proto.Schema, error) { + openAPISpec := newMinimalValidOpenAPISpec() + openAPISpec.Definitions = *openAPIDefinitions + + specBytes, err := json.MarshalIndent(openAPISpec, " ", " ") + if err != nil { + return nil, err + } + + var info yaml.MapSlice + err = yaml.Unmarshal(specBytes, &info) + if err != nil { + return nil, err + } + + doc, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil)) + if err != nil { + return nil, err + } + + models, err := proto.NewOpenAPIData(doc) + if err != nil { + return nil, err + } + + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if model == nil { + return nil, fmt.Errorf("the ListModels function returned a model that can't be looked-up") + } + gvkList := parseGroupVersionKind(model) + for _, modelGVK := range gvkList { + if modelGVK == gvk { + return model, nil + } + } + } + + return nil, fmt.Errorf("no model found with a %v tag matching %v", groupVersionKindExtensionKey, gvk) +} + +// newMinimalValidOpenAPISpec creates a minimal openapi spec with only the required fields filled in +func newMinimalValidOpenAPISpec() *spec.Swagger { + return &spec.Swagger{ + SwaggerProps: spec.SwaggerProps{ + Swagger: "2.0", + Info: &spec.Info{ + InfoProps: spec.InfoProps{ + Title: "Kubernetes", + Version: "0.0.0", + }, + }, + }, + } +} + +// parseGroupVersionKind gets and parses GroupVersionKind from the extension. Returns empty if it doesn't have one. +func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind { + extensions := s.GetExtensions() + + gvkListResult := []schema.GroupVersionKind{} + + // Get the extensions + gvkExtension, ok := extensions[groupVersionKindExtensionKey] + if !ok { + return []schema.GroupVersionKind{} + } + + // gvk extension must be a list of at least 1 element. + gvkList, ok := gvkExtension.([]interface{}) + if !ok { + return []schema.GroupVersionKind{} + } + + for _, gvk := range gvkList { + // gvk extension list must be a map with group, version, and + // kind fields + gvkMap, ok := gvk.(map[interface{}]interface{}) + if !ok { + continue + } + group, ok := gvkMap["group"].(string) + if !ok { + continue + } + version, ok := gvkMap["version"].(string) + if !ok { + continue + } + kind, ok := gvkMap["kind"].(string) + if !ok { + continue + } + + gvkListResult = append(gvkListResult, schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + } + + return gvkListResult +} diff --git a/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go new file mode 100644 index 00000000000..64421a7ff8f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/openapi/proto_test.go @@ -0,0 +1,77 @@ +/* +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/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/util/proto" +) + +// TestOpenAPIDefinitionsToProtoSchema tests the openapi parser +func TestOpenAPIDefinitionsToProtoSchema(t *testing.T) { + openAPIDefinitions := &spec.Definitions{ + "io.k8s.api.testgroup.v1.Foo": spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Description of Foos", + Properties: map[string]spec.Schema{}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-group-version-kind": []map[string]string{ + { + "group": "testgroup.k8s.io", + "version": "v1", + "kind": "Foo", + }, + }, + }, + }, + }, + } + gvk := schema.GroupVersionKind{ + Group: "testgroup.k8s.io", + Version: "v1", + Kind: "Foo", + } + expectedSchema := &proto.Arbitrary{ + BaseSchema: proto.BaseSchema{ + Description: "Description of Foos", + Extensions: map[string]interface{}{ + "x-kubernetes-group-version-kind": []interface{}{ + map[interface{}]interface{}{ + "group": "testgroup.k8s.io", + "version": "v1", + "kind": "Foo", + }, + }, + }, + Path: proto.NewPath("io.k8s.api.testgroup.v1.Foo"), + }, + } + actualSchema, err := ToProtoSchema(openAPIDefinitions, gvk) + if err != nil { + t.Fatalf("expected ToProtoSchema not to return an error") + } + if !reflect.DeepEqual(expectedSchema, actualSchema) { + t.Fatalf("expected schema:\n%v\nbut got:\n%v", expectedSchema, actualSchema) + } +} diff --git a/staging/src/k8s.io/client-go/Godeps/Godeps.json b/staging/src/k8s.io/client-go/Godeps/Godeps.json index f1337ce7868..f314634d39f 100644 --- a/staging/src/k8s.io/client-go/Godeps/Godeps.json +++ b/staging/src/k8s.io/client-go/Godeps/Godeps.json @@ -588,7 +588,7 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/staging/src/k8s.io/code-generator/Godeps/Godeps.json b/staging/src/k8s.io/code-generator/Godeps/Godeps.json index ad8da9bbe1e..15a35e1fe01 100644 --- a/staging/src/k8s.io/code-generator/Godeps/Godeps.json +++ b/staging/src/k8s.io/code-generator/Godeps/Godeps.json @@ -260,11 +260,11 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/generators", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index 843a6e7082f..f6f37f73e81 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -1138,6 +1138,10 @@ "ImportPath": "k8s.io/apiserver/pkg/util/logs", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/util/openapi", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/util/proxy", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -1672,27 +1676,27 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/aggregator", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index c4b92ce04bf..b3a5c9f4daf 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -1110,6 +1110,10 @@ "ImportPath": "k8s.io/apiserver/pkg/util/logs", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/util/openapi", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/util/trace", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -1636,23 +1640,23 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/staging/src/k8s.io/sample-controller/Godeps/Godeps.json b/staging/src/k8s.io/sample-controller/Godeps/Godeps.json index 520574a0a56..9d2e0acd719 100644 --- a/staging/src/k8s.io/sample-controller/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-controller/Godeps/Godeps.json @@ -1068,7 +1068,7 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", - "Rev": "61db125d227fc9d4e373819a059516f32f7f23c7" + "Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5" } ] } diff --git a/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go b/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go index 78714e8b2ae..e4fe7c62ed9 100644 --- a/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go +++ b/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go @@ -45,6 +45,32 @@ type openAPI struct { // BuildOpenAPISpec builds OpenAPI spec given a list of webservices (containing routes) and common.Config to customize it. func BuildOpenAPISpec(webServices []*restful.WebService, config *common.Config) (*spec.Swagger, error) { + o := newOpenAPI(config) + err := o.buildPaths(webServices) + if err != nil { + return nil, err + } + return o.finalizeSwagger() +} + +// BuildOpenAPIDefinitionsForResource builds a partial OpenAPI spec given a sample object and common.Config to customize it. +func BuildOpenAPIDefinitionsForResource(model interface{}, config *common.Config) (*spec.Definitions, error) { + o := newOpenAPI(config) + // We can discard the return value of toSchema because all we care about is the side effect of calling it. + // All the models created for this resource get added to o.swagger.Definitions + _, err := o.toSchema(model) + if err != nil { + return nil, err + } + swagger, err := o.finalizeSwagger() + if err != nil { + return nil, err + } + return &swagger.Definitions, nil +} + +// newOpenAPI sets up the openAPI object so we can build the spec. +func newOpenAPI(config *common.Config) openAPI { o := openAPI{ config: config, swagger: &spec.Swagger{ @@ -56,16 +82,6 @@ func BuildOpenAPISpec(webServices []*restful.WebService, config *common.Config) }, }, } - - err := o.init(webServices) - if err != nil { - return nil, err - } - - return o.swagger, nil -} - -func (o *openAPI) init(webServices []*restful.WebService) error { if o.config.GetOperationIDAndTags == nil { o.config.GetOperationIDAndTags = func(r *restful.Route) (string, []string, error) { return r.Operation, nil, nil @@ -83,22 +99,25 @@ func (o *openAPI) init(webServices []*restful.WebService) error { if o.config.CommonResponses == nil { o.config.CommonResponses = map[int]spec.Response{} } - err := o.buildPaths(webServices) - if err != nil { - return err - } + return o +} + +// finalizeSwagger is called after the spec is built and returns the final spec. +// NOTE: finalizeSwagger also make changes to the final spec, as specified in the config. +func (o *openAPI) finalizeSwagger() (*spec.Swagger, error) { if o.config.SecurityDefinitions != nil { o.swagger.SecurityDefinitions = *o.config.SecurityDefinitions o.swagger.Security = o.config.DefaultSecurity } if o.config.PostProcessSpec != nil { + var err error o.swagger, err = o.config.PostProcessSpec(o.swagger) if err != nil { - return err + return nil, err } } - return nil + return o.swagger, nil } func getCanonicalizeTypeName(t reflect.Type) string { diff --git a/vendor/k8s.io/kube-openapi/pkg/generators/extension.go b/vendor/k8s.io/kube-openapi/pkg/generators/extension.go index 7f3602408a3..befe38db248 100644 --- a/vendor/k8s.io/kube-openapi/pkg/generators/extension.go +++ b/vendor/k8s.io/kube-openapi/pkg/generators/extension.go @@ -27,18 +27,33 @@ import ( const extensionPrefix = "x-kubernetes-" -// Extension tag to openapi extension -var tagToExtension = map[string]string{ - "patchMergeKey": "x-kubernetes-patch-merge-key", - "patchStrategy": "x-kubernetes-patch-strategy", - "listType": "x-kubernetes-list-type", - "listMapKey": "x-kubernetes-list-map-keys", +// extensionAttributes encapsulates common traits for particular extensions. +type extensionAttributes struct { + xName string + kind types.Kind + allowedValues sets.String } -// Enum values per extension -var allowedExtensionValues = map[string]sets.String{ - "x-kubernetes-patch-strategy": sets.NewString("merge", "retainKeys"), - "x-kubernetes-list-type": sets.NewString("atomic", "set", "map"), +// Extension tag to openapi extension attributes +var tagToExtension = map[string]extensionAttributes{ + "patchMergeKey": extensionAttributes{ + xName: "x-kubernetes-patch-merge-key", + kind: types.Slice, + }, + "patchStrategy": extensionAttributes{ + xName: "x-kubernetes-patch-strategy", + kind: types.Slice, + allowedValues: sets.NewString("merge", "retainKeys"), + }, + "listMapKey": extensionAttributes{ + xName: "x-kubernetes-list-map-keys", + kind: types.Slice, + }, + "listType": extensionAttributes{ + xName: "x-kubernetes-list-type", + kind: types.Slice, + allowedValues: sets.NewString("atomic", "set", "map"), + }, } // Extension encapsulates information necessary to generate an OpenAPI extension. @@ -48,10 +63,25 @@ type extension struct { values []string // Example: [atomic] } +func (e extension) hasAllowedValues() bool { + return tagToExtension[e.idlTag].allowedValues.Len() > 0 +} + +func (e extension) allowedValues() sets.String { + return tagToExtension[e.idlTag].allowedValues +} + +func (e extension) hasKind() bool { + return len(tagToExtension[e.idlTag].kind) > 0 +} + +func (e extension) kind() types.Kind { + return tagToExtension[e.idlTag].kind +} + func (e extension) validateAllowedValues() error { // allowedValues not set means no restrictions on values. - allowedValues, exists := allowedExtensionValues[e.xName] - if !exists { + if !e.hasAllowedValues() { return nil } // Check for missing value. @@ -59,6 +89,7 @@ func (e extension) validateAllowedValues() error { return fmt.Errorf("%s needs a value, none given.", e.idlTag) } // For each extension value, validate that it is allowed. + allowedValues := e.allowedValues() if !allowedValues.HasAll(e.values...) { return fmt.Errorf("%v not allowed for %s. Allowed values: %v", e.values, e.idlTag, allowedValues.List()) @@ -66,6 +97,18 @@ func (e extension) validateAllowedValues() error { return nil } +func (e extension) validateType(kind types.Kind) error { + // If this extension class has no kind, then don't validate the type. + if !e.hasKind() { + return nil + } + if kind != e.kind() { + return fmt.Errorf("tag %s on type %v; only allowed on type %v", + e.idlTag, kind, e.kind()) + } + return nil +} + func (e extension) hasMultipleValues() bool { return len(e.values) > 1 } @@ -82,7 +125,9 @@ func sortedMapKeys(m map[string][]string) []string { return keys } -// Parses comments to return openapi extensions. +// Parses comments to return openapi extensions. Returns a list of +// extensions which parsed correctly, as well as a list of the +// parse errors. Validating extensions is performed separately. // NOTE: Non-empty errors does not mean extensions is empty. func parseExtensions(comments []string) ([]extension, []error) { extensions := []extension{} @@ -108,21 +153,30 @@ func parseExtensions(comments []string) ([]extension, []error) { // Next, generate extensions from "idlTags" (e.g. +listType) tagValues := types.ExtractCommentTags("+", comments) for _, idlTag := range sortedMapKeys(tagValues) { - xName, exists := tagToExtension[idlTag] + xAttrs, exists := tagToExtension[idlTag] if !exists { continue } values := tagValues[idlTag] e := extension{ - idlTag: idlTag, // listType - xName: xName, // x-kubernetes-list-type - values: values, // [atomic] - } - if err := e.validateAllowedValues(); err != nil { - // For now, only log the extension validation errors. - errors = append(errors, err) + idlTag: idlTag, // listType + xName: xAttrs.xName, // x-kubernetes-list-type + values: values, // [atomic] } extensions = append(extensions, e) } return extensions, errors } + +func validateMemberExtensions(extensions []extension, m *types.Member) []error { + errors := []error{} + for _, e := range extensions { + if err := e.validateAllowedValues(); err != nil { + errors = append(errors, err) + } + if err := e.validateType(m.Type.Kind); err != nil { + errors = append(errors, err) + } + } + return errors +} diff --git a/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go b/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go index deeac757197..13e1e0985af 100644 --- a/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go +++ b/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go @@ -432,7 +432,7 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type) error { extensions, errors := parseExtensions(t.CommentLines) // Initially, we will only log struct extension errors. if len(errors) > 0 { - for e := range errors { + for _, e := range errors { glog.V(2).Infof("[%s]: %s\n", t.String(), e) } } @@ -442,17 +442,16 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type) error { } func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type) error { - extensions, errors := parseExtensions(m.CommentLines) + extensions, parseErrors := parseExtensions(m.CommentLines) + validationErrors := validateMemberExtensions(extensions, m) + errors := append(parseErrors, validationErrors...) // Initially, we will only log member extension errors. if len(errors) > 0 { errorPrefix := fmt.Sprintf("[%s] %s:", parent.String(), m.String()) - for e := range errors { + for _, e := range errors { glog.V(2).Infof("%s %s\n", errorPrefix, e) } } - // TODO(seans3): Validate member extensions here. - // Example: listType extension is only on a Slice. - // Example: cross-extension validation - listMapKey only makes sense with listType=map g.emitExtensions(extensions) return nil }