diff --git a/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery.go b/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery.go index 6a35dcc604c9b..c1b5c816b5646 100644 --- a/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery.go +++ b/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery.go @@ -26,6 +26,7 @@ import ( "time" openapi_v2 "github.com/googleapis/gnostic/openapiv2" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" "k8s.io/klog/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -240,6 +241,15 @@ func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { return d.delegate.OpenAPISchema() } +// OpenAPISchema retrieves and parses the swagger API schema the server supports. +func (d *CachedDiscoveryClient) OpenAPIV3Schema(path, hash string) (*openapi_v3.Document, error) { + return d.delegate.OpenAPIV3Schema(path, hash) +} + +func (d *CachedDiscoveryClient) OpenAPIV3Discovery() (*discovery.OpenAPIV3Discovery, error) { + return d.delegate.OpenAPIV3Discovery() +} + // Fresh is supposed to tell the caller whether or not to retry if the cache // fails to find something (false = retry, true = no need to retry). func (d *CachedDiscoveryClient) Fresh() bool { diff --git a/staging/src/k8s.io/client-go/discovery/cached/memory/memcache.go b/staging/src/k8s.io/client-go/discovery/cached/memory/memcache.go index 9de389fa7e4d3..746139fbb88ad 100644 --- a/staging/src/k8s.io/client-go/discovery/cached/memory/memcache.go +++ b/staging/src/k8s.io/client-go/discovery/cached/memory/memcache.go @@ -23,6 +23,7 @@ import ( "syscall" openapi_v2 "github.com/googleapis/gnostic/openapiv2" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" errorsutil "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -149,6 +150,14 @@ func (d *memCacheClient) OpenAPISchema() (*openapi_v2.Document, error) { return d.delegate.OpenAPISchema() } +func (d *memCacheClient) OpenAPIV3Schema(path, hash string) (*openapi_v3.Document, error) { + return d.delegate.OpenAPIV3Schema(path, hash) +} + +func (d *memCacheClient) OpenAPIV3Discovery() (*discovery.OpenAPIV3Discovery, error) { + return d.delegate.OpenAPIV3Discovery() +} + func (d *memCacheClient) Fresh() bool { d.lock.RLock() defer d.lock.RUnlock() diff --git a/staging/src/k8s.io/client-go/discovery/discovery_client.go b/staging/src/k8s.io/client-go/discovery/discovery_client.go index 50e59c5d85cb6..7374f531fbbd8 100644 --- a/staging/src/k8s.io/client-go/discovery/discovery_client.go +++ b/staging/src/k8s.io/client-go/discovery/discovery_client.go @@ -30,6 +30,7 @@ import ( //nolint:staticcheck // SA1019 Keep using module since it's still being maintained and the api of google.golang.org/protobuf/proto differs "github.com/golang/protobuf/proto" openapi_v2 "github.com/googleapis/gnostic/openapiv2" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -46,7 +47,9 @@ const ( // defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. CustomResourceDefinitions). defaultRetries = 2 // protobuf mime type - mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf" + openAPIV2mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf" + + openAPIV3mimePb = "application/com.github.proto-openapi.spec.v3@v1.0+protobuf" // defaultTimeout is the maximum amount of time per request when no timeout has been set on a RESTClient. // Defaults to 32s in order to have a distinguishable length of time, relative to other timeouts that exist. defaultTimeout = 32 * time.Second @@ -60,6 +63,7 @@ type DiscoveryInterface interface { ServerResourcesInterface ServerVersionInterface OpenAPISchemaInterface + OpenAPIV3SchemaInterface } // CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness. @@ -128,6 +132,11 @@ type OpenAPISchemaInterface interface { OpenAPISchema() (*openapi_v2.Document, error) } +type OpenAPIV3SchemaInterface interface { + OpenAPIV3Discovery() (*OpenAPIV3Discovery, error) + OpenAPIV3Schema(string, string) (*openapi_v3.Document, error) +} + // DiscoveryClient implements the functions that discover server-supported API groups, // versions and resources. type DiscoveryClient struct { @@ -420,9 +429,9 @@ func (d *DiscoveryClient) ServerVersion() (*version.Info, error) { return &info, nil } -// OpenAPISchema fetches the open api schema using a rest client and parses the proto. +// OpenAPISchema fetches the open api v2 schema using a rest client and parses the proto. func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { - data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", mimePb).Do(context.TODO()).Raw() + data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", openAPIV2mimePb).Do(context.TODO()).Raw() if err != nil { if errors.IsForbidden(err) || errors.IsNotFound(err) || errors.IsNotAcceptable(err) { // single endpoint not found/registered in old server, try to fetch old endpoint @@ -443,6 +452,42 @@ func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { return document, nil } + +type OpenAPIV3Discovery struct { + Paths map[string]string +} + + +func (d *DiscoveryClient) OpenAPIV3Discovery() (*OpenAPIV3Discovery, error) { + data, err := d.restClient.Get().AbsPath("/openapi/v3").Do(context.TODO()).Raw() + if err != nil { + return nil, err + } + + foo := &OpenAPIV3Discovery{} + err = json.Unmarshal(data, foo) + if err != nil { + return nil, err + } + + return foo, nil +} + +func (d *DiscoveryClient) OpenAPIV3Schema(path, hash string) (*openapi_v3.Document, error) { + data, err := d.restClient.Get().AbsPath("/openapi/v3", path).Param("hash", hash).SetHeader("Accept", openAPIV3mimePb).Do(context.TODO()).Raw() + if err != nil { + return nil, err + } + document := &openapi_v3.Document{} + err = proto.Unmarshal(data, document) + if err != nil { + return nil, err + } + + return document, nil +} + + // withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns. func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { var result []*metav1.APIResourceList diff --git a/staging/src/k8s.io/client-go/discovery/fake/discovery.go b/staging/src/k8s.io/client-go/discovery/fake/discovery.go index d3835c9fa148a..7147faa8bb5cc 100644 --- a/staging/src/k8s.io/client-go/discovery/fake/discovery.go +++ b/staging/src/k8s.io/client-go/discovery/fake/discovery.go @@ -21,6 +21,7 @@ import ( "net/http" openapi_v2 "github.com/googleapis/gnostic/openapiv2" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,6 +30,8 @@ import ( kubeversion "k8s.io/client-go/pkg/version" restclient "k8s.io/client-go/rest" "k8s.io/client-go/testing" + + "k8s.io/client-go/discovery" ) // FakeDiscovery implements discovery.DiscoveryInterface and sometimes calls testing.Fake.Invoke with an action, @@ -161,6 +164,14 @@ func (c *FakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) { return &openapi_v2.Document{}, nil } +func (d *FakeDiscovery) OpenAPIV3Schema(path, hash string) (*openapi_v3.Document, error) { + return &openapi_v3.Document{}, nil +} + +func (d *FakeDiscovery) OpenAPIV3Discovery() (*discovery.OpenAPIV3Discovery, error) { + return nil, nil +} + // RESTClient returns a RESTClient that is used to communicate with API server // by this client implementation. func (c *FakeDiscovery) RESTClient() restclient.Interface { diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator/downloader.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator/downloader.go index b91f9e64aa3f3..d08776f078928 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator/downloader.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator/downloader.go @@ -45,7 +45,7 @@ func (s *Downloader) handlerWithUser(handler http.Handler, info user.Info) http. // gvList is a struct for the response of the /openapi/v3 endpoint to unmarshal into type gvList struct { - Paths []string `json:"Paths"` + Paths map[string]string `json:"Paths"` } // SpecETag is a OpenAPI v3 spec and etag pair for the endpoint of each OpenAPI group/version @@ -81,7 +81,7 @@ func (s *Downloader) Download(handler http.Handler, etagList map[string]string) if err := json.Unmarshal(writer.data, &groups); err != nil { return nil, err } - for _, path := range groups.Paths { + for path, _ := range groups.Paths { reqPath := fmt.Sprintf("/openapi/v3/%s", path) req, err := http.NewRequest("GET", reqPath, nil) if err != nil { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go b/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go index 0ae86b1daf514..d3defde6ff457 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go @@ -101,6 +101,7 @@ func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { if err != nil { return err } + return nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go index 9235b4b0ab3aa..995b53d2a24b3 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go @@ -26,6 +26,7 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/validation" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" ) // Factory provides abstractions that allow the Kubectl command to be extended across multiple types @@ -66,4 +67,10 @@ type Factory interface { OpenAPISchema() (openapi.Resources, error) // OpenAPIGetter returns a getter for the openapi schema document OpenAPIGetter() discovery.OpenAPISchemaInterface + + // OpenAPIV3Discovery returns the list of paths for OpenAPI V3 documents + OpenAPIV3Discovery() (*discovery.OpenAPIV3Discovery, error) + + // OpenAPIV3GroupVersionSchema returns the OpenAPI V3 schema for the corresponding group version + OpenAPIV3GroupVersionSchema(path, hash string) (*openapi_v3.Document, error) } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go index 2b9074cbac83d..0d6cd60241487 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go @@ -34,6 +34,7 @@ import ( "k8s.io/kubectl/pkg/util/openapi" openapivalidation "k8s.io/kubectl/pkg/util/openapi/validation" "k8s.io/kubectl/pkg/validation" + openapi_v3 "github.com/googleapis/gnostic/openapiv3" ) type factoryImpl struct { @@ -186,3 +187,23 @@ func (f *factoryImpl) OpenAPIGetter() discovery.OpenAPISchemaInterface { return f.openAPIGetter } + +func (f *factoryImpl) OpenAPIV3Discovery() (*discovery.OpenAPIV3Discovery, error) { + discovery, err := f.clientGetter.ToDiscoveryClient() + if err != nil { + return nil, err + } + + foo, err := discovery.OpenAPIV3Discovery() + return foo, err +} + +func (f *factoryImpl) OpenAPIV3GroupVersionSchema(path, hash string) (*openapi_v3.Document, error) { + discovery, err := f.clientGetter.ToDiscoveryClient() + if err != nil { + return nil, err + } + + schema, err := discovery.OpenAPIV3Schema(path, hash) + return schema, err +} diff --git a/vendor/k8s.io/kube-openapi/pkg/handler3/handler.go b/vendor/k8s.io/kube-openapi/pkg/handler3/handler.go index aa32908236415..a4822cb0240bd 100644 --- a/vendor/k8s.io/kube-openapi/pkg/handler3/handler.go +++ b/vendor/k8s.io/kube-openapi/pkg/handler3/handler.go @@ -37,6 +37,11 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" ) +type OpenAPIV3Discovery struct { + Paths map[string]string +} + + const ( jsonExt = ".json" @@ -83,6 +88,14 @@ func computeETag(data []byte) string { return fmt.Sprintf("\"%X\"", sha512.Sum512(data)) } +// unquoteETag removes the "" quotes around a generated ETag +func unquoteETag(etag string) string { + if len(etag) < 2 { + return etag + } + return etag[1:len(etag)-1] +} + // NewOpenAPIService builds an OpenAPIService starting with the given spec. func NewOpenAPIService(spec *spec.Swagger) (*OpenAPIService, error) { o := &OpenAPIService{} @@ -101,10 +114,11 @@ func (o *OpenAPIService) getGroupBytes() ([]byte, error) { } sort.Strings(keys) - group := make(map[string][]string) - group["Paths"] = keys - - j, err := json.Marshal(group) + discovery := &OpenAPIV3Discovery{Paths: make(map[string]string)} + for k, v := range o.v3Schema { + discovery.Paths[k] = "/openapi/v3/" + k + "?hash=" + unquoteETag(v.specBytesETag) + } + j, err := json.Marshal(discovery) if err != nil { return nil, err } @@ -121,7 +135,7 @@ func (o *OpenAPIService) getSingleGroupBytes(getType string, group string) ([]by if getType == subTypeJSON { return v.specBytes, v.specBytesETag, v.lastModified, nil } else if getType == subTypeProtobuf { - return v.specPb, v.specPbETag, v.lastModified, nil + return v.specPb, v.specBytesETag, v.lastModified, nil } return nil, "", time.Now(), fmt.Errorf("Invalid accept clause %s", getType) } @@ -168,6 +182,18 @@ func (o *OpenAPIService) HandleDiscovery(w http.ResponseWriter, r *http.Request) http.ServeContent(w, r, "/openapi/v3", time.Now(), bytes.NewReader(data)) } +func ApplyCacheBusting(w http.ResponseWriter, r *http.Request, etag string) { + hash := r.URL.Query().Get("hash") + if hash != "" { + if "\"" + hash + "\"" == etag { + w.Header().Set("Cache-Control", "public, immutable") + w.Header().Set("Expires", time.Now().AddDate(5, 0, 0).Format(time.RFC1123)) + } else { + http.Redirect(w, r, r.URL.Path + "?hash=" + unquoteETag(etag), 301) + } + } +} + func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Request) { url := strings.SplitAfterN(r.URL.Path, "/", 4) group := url[3] @@ -204,6 +230,7 @@ func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Reque return } w.Header().Set("Etag", etag) + ApplyCacheBusting(w, r, etag) http.ServeContent(w, r, "", lastModified, bytes.NewReader(data)) return }