Merge pull request #59293 from roycaihw/openapi_endpoint

Automatic merge from submit-queue (batch tested with PRs 60011, 59256, 59293, 60328, 60367). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Serve OpenAPI spec with single /openapi/v2 endpoint

**What this PR does / why we need it**:
We are deprecating format-separated endpoints (`/swagger.json`, `/swagger-2.0.0.json`, `/swagger-2.0.0.pb-v1`, `/swagger-2.0.0.pb-v1.gz`) for OpenAPI spec, and switching 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

Requested format is specified by setting HTTP headers

header | possible values
-- | --
Accept | `application/json`, `application/com.github.proto-openapi.spec.v2@v1.0+protobuf`
Accept-Encoding | `gzip`

This PR changes dynamic_client (and kubectl as a result) to use the new endpoint. The old endpoints will remain in 1.10 and 1.11, and get removed in 1.12. 

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

**Release note**:

```release-note
action required: Deprecate format-separated endpoints for OpenAPI spec. Please use single `/openapi/v2` endpoint instead.
```

/sig api-machinery
This commit is contained in:
Kubernetes Submit Queue 2018-02-26 23:47:53 -08:00 committed by GitHub
commit d6153194d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 297 additions and 63 deletions

16
Godeps/Godeps.json generated
View File

@ -3265,35 +3265,35 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/aggregator",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/generators",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto/validation",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/utils/clock",

View File

@ -176,6 +176,7 @@ func ClusterRoles() []rbac.ClusterRole {
// do not expand this pattern for openapi discovery docs
// move to a single openapi endpoint that takes accept/accept-encoding headers
"/swagger.json", "/swagger-2.0.0.pb-v1",
"/openapi", "/openapi/*",
"/api", "/api/*",
"/apis", "/apis/*",
).RuleOrDie(),

View File

@ -635,6 +635,8 @@ items:
- /apis
- /apis/*
- /healthz
- /openapi
- /openapi/*
- /swagger-2.0.0.pb-v1
- /swagger.json
- /swaggerapi

View File

@ -1984,23 +1984,23 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/equality",

View File

@ -172,7 +172,7 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -1720,23 +1720,23 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/client-go/discovery",

View File

@ -17,7 +17,7 @@ limitations under the License.
package routes
import (
"github.com/emicklei/go-restful"
restful "github.com/emicklei/go-restful"
"github.com/golang/glog"
"k8s.io/apiserver/pkg/server/mux"
@ -32,8 +32,15 @@ type OpenAPI struct {
// 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.
_, err := handler.BuildAndRegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux)
if err != nil {
glog.Fatalf("Failed to register open api spec for root: %v", err)
}
_, err = handler.BuildAndRegisterOpenAPIVersionedService("/openapi/v2", c.RegisteredWebServices(), oa.Config, mux)
if err != nil {
glog.Fatalf("Failed to register versioned open api spec for root: %v", err)
}
}

View File

@ -576,7 +576,7 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -36,8 +36,12 @@ import (
restclient "k8s.io/client-go/rest"
)
// defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. ThirdPartyResources).
const defaultRetries = 2
const (
// defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. ThirdPartyResources).
defaultRetries = 2
// protobuf mime type
mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf"
)
// DiscoveryInterface holds the methods that discover server-supported API groups,
// versions and resources.
@ -329,9 +333,18 @@ func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
// OpenAPISchema fetches the open api schema using a rest client and parses the proto.
func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
data, err := d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1").Do().Raw()
data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", mimePb).Do().Raw()
if err != nil {
return nil, err
if errors.IsForbidden(err) || errors.IsNotFound(err) {
// single endpoint not found/registered in old server, try to fetch old endpoint
// TODO(roycaihw): remove this in 1.11
data, err = d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1").Do().Raw()
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
document := &openapi_v2.Document{}
err = proto.Unmarshal(data, document)

View File

@ -326,9 +326,14 @@ var returnedOpenAPI = openapi_v2.Document{
},
}
func openapiSchemaFakeServer() (*httptest.Server, error) {
func openapiSchemaDeprecatedFakeServer() (*httptest.Server, error) {
var sErr error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// old server returns 403 on new endpoint request
if req.URL.Path == "/openapi/v2" {
w.WriteHeader(http.StatusForbidden)
return
}
if req.URL.Path != "/swagger-2.0.0.pb-v1" {
sErr = fmt.Errorf("Unexpected url %v", req.URL)
}
@ -349,6 +354,33 @@ func openapiSchemaFakeServer() (*httptest.Server, error) {
return server, sErr
}
func openapiSchemaFakeServer() (*httptest.Server, error) {
var sErr error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/openapi/v2" {
sErr = fmt.Errorf("Unexpected url %v", req.URL)
}
if req.Method != "GET" {
sErr = fmt.Errorf("Unexpected method %v", req.Method)
}
decipherableFormat := req.Header.Get("Accept")
if decipherableFormat != "application/com.github.proto-openapi.spec.v2@v1.0+protobuf" {
sErr = fmt.Errorf("Unexpected accept mime type %v", decipherableFormat)
}
mime.AddExtensionType(".pb-v1", "application/com.github.googleapis.gnostic.OpenAPIv2@68f4ded+protobuf")
output, err := proto.Marshal(&returnedOpenAPI)
if err != nil {
sErr = err
return
}
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
return server, sErr
}
func TestGetOpenAPISchema(t *testing.T) {
server, err := openapiSchemaFakeServer()
if err != nil {
@ -366,6 +398,23 @@ func TestGetOpenAPISchema(t *testing.T) {
}
}
func TestGetOpenAPISchemaFallback(t *testing.T) {
server, err := openapiSchemaDeprecatedFakeServer()
if err != nil {
t.Errorf("unexpected error starting fake server: %v", err)
}
defer server.Close()
client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL})
got, err := client.OpenAPISchema()
if err != nil {
t.Fatalf("unexpected error getting openapi: %v", err)
}
if e, a := returnedOpenAPI, *got; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
func TestServerPreferredResources(t *testing.T) {
stable := metav1.APIResourceList{
GroupVersion: "v1",

View File

@ -260,11 +260,11 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/generators",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -1628,27 +1628,27 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/aggregator",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -23,7 +23,7 @@ import (
"sync"
"time"
"github.com/emicklei/go-restful"
restful "github.com/emicklei/go-restful"
"github.com/go-openapi/spec"
"k8s.io/apiserver/pkg/server"
@ -51,7 +51,8 @@ type specAggregator struct {
openAPISpecs map[string]*openAPISpecInfo
// provided for dynamic OpenAPI spec
openAPIService *handler.OpenAPIService
openAPIService *handler.OpenAPIService
openAPIVersionedService *handler.OpenAPIService
}
var _ AggregationManager = &specAggregator{}
@ -109,11 +110,19 @@ func BuildAndRegisterAggregator(downloader *Downloader, delegationTarget server.
}
// Install handler
// 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.
s.openAPIService, err = handler.RegisterOpenAPIService(
specToServe, "/swagger.json", pathHandler)
if err != nil {
return nil, err
}
s.openAPIVersionedService, err = handler.RegisterOpenAPIVersionedService(
specToServe, "/openapi/v2", pathHandler)
if err != nil {
return nil, err
}
return s, nil
}
@ -207,14 +216,25 @@ func (s *specAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err err
// 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 {
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
}
return s.openAPIService.UpdateSpec(specToServe)
// 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

View File

@ -93,6 +93,32 @@ func (h handlerTest) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write(h.data)
}
type handlerDeprecatedTest struct {
etag string
data []byte
}
var _ http.Handler = handlerDeprecatedTest{}
func (h handlerDeprecatedTest) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// old server returns 403 on new endpoint
if r.URL.Path == "/openapi/v2" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(h.etag) > 0 {
w.Header().Add("Etag", h.etag)
}
ifNoneMatches := r.Header["If-None-Match"]
for _, match := range ifNoneMatches {
if match == h.etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
w.Write(h.data)
}
func assertDownloadedSpec(actualSpec *spec.Swagger, actualEtag string, err error,
expectedSpecID string, expectedEtag string) error {
if err != nil {
@ -132,4 +158,9 @@ func TestDownloadOpenAPISpec(t *testing.T) {
actualSpec, actualEtag, _, err = s.Download(
handlerTest{data: []byte("{\"id\": \"test\"}"), etag: "etag_test1"}, "etag_test2")
assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "test", "etag_test1"))
// Test old server fallback path
actualSpec, actualEtag, _, err = s.Download(handlerDeprecatedTest{data: []byte("{\"id\": \"test\"}")}, "")
assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "test", "\"6E8F849B434D4B98A569B9D7718876E9-356ECAB19D7FBE1336BABB1E70F8F3025050DE218BE78256BE81620681CFC9A268508E542B8B55974E17B2184BBFC8FFFAA577E51BE195D32B3CA2547818ABE4\""))
}

View File

@ -99,10 +99,11 @@ func (s *Downloader) Download(handler http.Handler, etag string) (returnSpec *sp
handler = request.WithRequestContext(handler, s.contextMapper)
handler = http.TimeoutHandler(handler, specDownloadTimeout, "request timed out")
req, err := http.NewRequest("GET", "/swagger.json", nil)
req, err := http.NewRequest("GET", "/openapi/v2", nil)
if err != nil {
return nil, "", 0, err
}
req.Header.Add("Accept", "application/json")
// Only pass eTag if it is not generated locally
if len(etag) > 0 && !strings.HasPrefix(etag, locallyGeneratedEtagPrefix) {
@ -112,6 +113,23 @@ func (s *Downloader) Download(handler http.Handler, etag string) (returnSpec *sp
writer := newInMemoryResponseWriter()
handler.ServeHTTP(writer, req)
// single endpoint not found/registered in old server, try to fetch old endpoint
// TODO(roycaihw): remove this in 1.11
if writer.respCode == http.StatusForbidden || writer.respCode == http.StatusNotFound {
req, err = http.NewRequest("GET", "/swagger.json", nil)
if err != nil {
return nil, "", 0, err
}
// Only pass eTag if it is not generated locally
if len(etag) > 0 && !strings.HasPrefix(etag, locallyGeneratedEtagPrefix) {
req.Header.Add("If-None-Match", etag)
}
writer = newInMemoryResponseWriter()
handler.ServeHTTP(writer, req)
}
switch writer.respCode {
case http.StatusNotModified:
if len(etag) == 0 {

View File

@ -1592,23 +1592,23 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/builder",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -900,7 +900,7 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "a07b7bbb58e7fdc5144f8d7046331d29fc9ad3b3"
"Rev": "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
}
]
}

View File

@ -58,8 +58,10 @@ func (s *referenceWalker) walkRef(ref spec.Ref) spec.Ref {
// We do not support external references yet.
if !s.alreadyVisited[refStr] && strings.HasPrefix(refStr, definitionPrefix) {
s.alreadyVisited[refStr] = true
def := s.root.Definitions[refStr[len(definitionPrefix):]]
k := refStr[len(definitionPrefix):]
def := s.root.Definitions[k]
s.walkSchema(&def)
s.root.Definitions[k] = def
}
return s.walkRefCallback(ref)
}
@ -69,23 +71,26 @@ func (s *referenceWalker) walkSchema(schema *spec.Schema) {
return
}
schema.Ref = s.walkRef(schema.Ref)
for _, v := range schema.Definitions {
for k, v := range schema.Definitions {
s.walkSchema(&v)
schema.Definitions[k] = v
}
for _, v := range schema.Properties {
for k, v := range schema.Properties {
s.walkSchema(&v)
schema.Properties[k] = v
}
for _, v := range schema.PatternProperties {
for k, v := range schema.PatternProperties {
s.walkSchema(&v)
schema.PatternProperties[k] = v
}
for _, v := range schema.AllOf {
s.walkSchema(&v)
for i, _ := range schema.AllOf {
s.walkSchema(&schema.AllOf[i])
}
for _, v := range schema.AnyOf {
s.walkSchema(&v)
for i, _ := range schema.AnyOf {
s.walkSchema(&schema.AnyOf[i])
}
for _, v := range schema.OneOf {
s.walkSchema(&v)
for i, _ := range schema.OneOf {
s.walkSchema(&schema.OneOf[i])
}
if schema.Not != nil {
s.walkSchema(schema.Not)
@ -100,8 +105,8 @@ func (s *referenceWalker) walkSchema(schema *spec.Schema) {
if schema.Items.Schema != nil {
s.walkSchema(schema.Items.Schema)
}
for _, v := range schema.Items.Schemas {
s.walkSchema(&v)
for i, _ := range schema.Items.Schemas {
s.walkSchema(&schema.Items.Schemas[i])
}
}
}
@ -291,15 +296,29 @@ func mergeSpecs(dest, source *spec.Swagger, renameModelConflicts, ignorePathConf
from, to string
}
renames := []Rename{}
OUTERLOOP:
for k, v := range source.Definitions {
if usedNames[k] {
v2, found := dest.Definitions[k]
// Reuse model iff they are exactly the same.
// Reuse model if they are exactly the same.
if found && reflect.DeepEqual(v, v2) {
continue
}
i := 2
newName := fmt.Sprintf("%s_v%d", k, i)
// Reuse previously renamed model if one exists
var newName string
i := 1
for found {
i++
newName = fmt.Sprintf("%s_v%d", k, i)
v2, found = dest.Definitions[newName]
if found && reflect.DeepEqual(v, v2) {
renames = append(renames, Rename{from: k, to: newName})
continue OUTERLOOP
}
}
_, foundInSource := source.Definitions[newName]
for usedNames[newName] || foundInSource {
i++

View File

@ -634,6 +634,8 @@ func (g openAPITypeWriter) generateSliceProperty(t *types.Type) error {
return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType)
case types.Struct:
g.generateReferenceProperty(elemType)
case types.Slice, types.Array:
g.generateSliceProperty(elemType)
default:
return fmt.Errorf("slice Element kind %v is not supported in %v", elemType.Kind, t)
}

View File

@ -6,6 +6,7 @@ go_library(
importpath = "k8s.io/kube-openapi/pkg/handler",
visibility = ["//visibility:public"],
deps = [
"//vendor/bitbucket.org/ww/goautoneg:go_default_library",
"//vendor/github.com/NYTimes/gziphandler:go_default_library",
"//vendor/github.com/emicklei/go-restful:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",

View File

@ -22,18 +22,21 @@ import (
"crypto/sha512"
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
"mime"
"net/http"
"strings"
"sync"
"time"
"bitbucket.org/ww/goautoneg"
yaml "gopkg.in/yaml.v2"
"github.com/NYTimes/gziphandler"
"github.com/emicklei/go-restful"
restful "github.com/emicklei/go-restful"
"github.com/go-openapi/spec"
"github.com/golang/protobuf/proto"
"github.com/googleapis/gnostic/OpenAPIv2"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
"github.com/googleapis/gnostic/compiler"
"k8s.io/kube-openapi/pkg/builder"
@ -77,6 +80,10 @@ func computeETag(data []byte) string {
return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
}
// 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.
//
// BuildAndRegisterOpenAPIService builds the spec and registers a handler to provides access to it.
// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIService.
func BuildAndRegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
@ -87,6 +94,10 @@ func BuildAndRegisterOpenAPIService(servePath string, webServices []*restful.Web
return RegisterOpenAPIService(spec, servePath, handler)
}
// 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.
//
// RegisterOpenAPIService registers a handler to provides access to provided swagger spec.
// Note: servePath should end with ".json" as the RegisterOpenAPIService assume it is serving a
// json file and will also serve .pb and .gz files.
@ -202,3 +213,63 @@ func toGzip(data []byte) []byte {
zw.Close()
return buf.Bytes()
}
// RegisterOpenAPIVersionedService registers a handler to provides access to provided swagger spec.
func RegisterOpenAPIVersionedService(openapiSpec *spec.Swagger, servePath string, handler common.PathHandler) (*OpenAPIService, error) {
o := OpenAPIService{}
if err := o.UpdateSpec(openapiSpec); err != nil {
return nil, err
}
accepted := []struct {
Type string
SubType string
GetDataAndETag func() ([]byte, string, time.Time)
}{
{"application", "json", o.getSwaggerBytes},
{"application", "com.github.proto-openapi.spec.v2@v1.0+protobuf", o.getSwaggerPbBytes},
}
handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
decipherableFormats := r.Header.Get("Accept")
if decipherableFormats == "" {
decipherableFormats = "*/*"
}
clauses := goautoneg.ParseAccept(decipherableFormats)
w.Header().Add("Vary", "Accept")
for _, clause := range clauses {
for _, accepts := range accepted {
if clause.Type != accepts.Type && clause.Type != "*" {
continue
}
if clause.SubType != accepts.SubType && clause.SubType != "*" {
continue
}
// serve the first matching media type in the sorted clause list
data, etag, lastModified := accepts.GetDataAndETag()
w.Header().Set("Etag", etag)
// ServeContent will take care of caching using eTag.
http.ServeContent(w, r, servePath, lastModified, bytes.NewReader(data))
return
}
}
// Return 406 for not acceptable format
w.WriteHeader(406)
return
}),
))
return &o, nil
}
// BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provides access to it.
// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
spec, err := builder.BuildOpenAPISpec(webServices, config)
if err != nil {
return nil, err
}
return RegisterOpenAPIVersionedService(spec, servePath, handler)
}