diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 3c7093a4..f1337ce7 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "k8s.io/client-go", - "GoVersion": "go1.9", + "GoVersion": "go1.10", "GodepVersion": "v80", "Packages": [ "./..." @@ -86,6 +86,10 @@ "ImportPath": "github.com/golang/protobuf/ptypes/timestamp", "Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" }, + { + "ImportPath": "github.com/google/btree", + "Rev": "7d79101e329e5a3adf994758c578dab82b90c017" + }, { "ImportPath": "github.com/google/gofuzz", "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" @@ -130,6 +134,14 @@ "ImportPath": "github.com/gophercloud/gophercloud/pagination", "Rev": "781450b3c4fcb4f5182bcc5133adb4b2e4a09d1d" }, + { + "ImportPath": "github.com/gregjones/httpcache", + "Rev": "787624de3eb7bd915c329cba748687a3b22666a6" + }, + { + "ImportPath": "github.com/gregjones/httpcache/diskcache", + "Rev": "787624de3eb7bd915c329cba748687a3b22666a6" + }, { "ImportPath": "github.com/hashicorp/golang-lru", "Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4" @@ -154,6 +166,10 @@ "ImportPath": "github.com/modern-go/reflect2", "Rev": "05fbef0ca5da472bbf96c9322b84a53edc03c9fd" }, + { + "ImportPath": "github.com/peterbourgon/diskv", + "Rev": "5f041e8faa004a95c88a202771f4cc3e991971e6" + }, { "ImportPath": "github.com/pmezard/go-difflib/difflib", "Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d" @@ -252,323 +268,323 @@ }, { "ImportPath": "k8s.io/api/admissionregistration/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/admissionregistration/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/apps/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/apps/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/apps/v1beta2", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/authentication/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/authentication/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/authorization/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/authorization/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/autoscaling/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/autoscaling/v2beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/batch/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/batch/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/batch/v2alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/certificates/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/core/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/events/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/extensions/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/imagepolicy/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/networking/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/policy/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/rbac/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/rbac/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/rbac/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/scheduling/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/scheduling/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/settings/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/storage/v1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/storage/v1alpha1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/api/storage/v1beta1", - "Rev": "fbc8bec270ad675ba2e610dd8f3b70c0f92fd46a" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/equality", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/errors", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/meta", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/resource", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/testing", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/testing/fuzzer", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/api/testing/roundtrip", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/fuzzer", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion/queryparams", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/fields", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/labels", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/schema", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/json", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/protobuf", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/recognizer", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/streaming", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/versioning", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/selection", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/types", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/cache", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/clock", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/diff", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/errors", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/framer", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/httpstream", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/httpstream/spdy", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/intstr", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/json", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/mergepatch", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/net", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/remotecommand", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/runtime", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/sets", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/strategicpatch", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/validation", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/validation/field", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/wait", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/util/yaml", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/version", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/pkg/watch", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/third_party/forked/golang/json", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/third_party/forked/golang/netutil", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/apimachinery/third_party/forked/golang/reflect", - "Rev": "8e510c818b62c1a0a1b738153104e8627916ebeb" + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util/proto", diff --git a/discovery/cached_discovery.go b/discovery/cached_discovery.go new file mode 100644 index 00000000..0cd814ab --- /dev/null +++ b/discovery/cached_discovery.go @@ -0,0 +1,274 @@ +/* +Copyright 2016 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 discovery + +import ( + "errors" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "sync" + "time" + + "github.com/golang/glog" + "github.com/googleapis/gnostic/OpenAPIv2" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/kubernetes/scheme" + restclient "k8s.io/client-go/rest" +) + +// CachedDiscoveryClient implements the functions that discovery server-supported API groups, +// versions and resources. +type CachedDiscoveryClient struct { + delegate DiscoveryInterface + + // cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well. + cacheDirectory string + + // ttl is how long the cache should be considered valid + ttl time.Duration + + // mutex protects the variables below + mutex sync.Mutex + + // ourFiles are all filenames of cache files created by this process + ourFiles map[string]struct{} + // invalidated is true if all cache files should be ignored that are not ours (e.g. after Invalidate() was called) + invalidated bool + // fresh is true if all used cache files were ours + fresh bool +} + +var _ CachedDiscoveryInterface = &CachedDiscoveryClient{} + +// ServerResourcesForGroupVersion returns the supported resources for a group and version. +func (d *CachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { + filename := filepath.Join(d.cacheDirectory, groupVersion, "serverresources.json") + cachedBytes, err := d.getCachedFile(filename) + // don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback. + if err == nil { + cachedResources := &metav1.APIResourceList{} + if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedResources); err == nil { + glog.V(10).Infof("returning cached discovery info from %v", filename) + return cachedResources, nil + } + } + + liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion) + if err != nil { + glog.V(3).Infof("skipped caching discovery info due to %v", err) + return liveResources, err + } + if liveResources == nil || len(liveResources.APIResources) == 0 { + glog.V(3).Infof("skipped caching discovery info, no resources found") + return liveResources, err + } + + if err := d.writeCachedFile(filename, liveResources); err != nil { + glog.V(3).Infof("failed to write cache to %v due to %v", filename, err) + } + + return liveResources, nil +} + +// ServerResources returns the supported resources for all groups and versions. +func (d *CachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) { + return ServerResources(d) +} + +func (d *CachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { + filename := filepath.Join(d.cacheDirectory, "servergroups.json") + cachedBytes, err := d.getCachedFile(filename) + // don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback. + if err == nil { + cachedGroups := &metav1.APIGroupList{} + if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil { + glog.V(10).Infof("returning cached discovery info from %v", filename) + return cachedGroups, nil + } + } + + liveGroups, err := d.delegate.ServerGroups() + if err != nil { + glog.V(3).Infof("skipped caching discovery info due to %v", err) + return liveGroups, err + } + if liveGroups == nil || len(liveGroups.Groups) == 0 { + glog.V(3).Infof("skipped caching discovery info, no groups found") + return liveGroups, err + } + + if err := d.writeCachedFile(filename, liveGroups); err != nil { + glog.V(3).Infof("failed to write cache to %v due to %v", filename, err) + } + + return liveGroups, nil +} + +func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) { + // after invalidation ignore cache files not created by this process + d.mutex.Lock() + _, ourFile := d.ourFiles[filename] + if d.invalidated && !ourFile { + d.mutex.Unlock() + return nil, errors.New("cache invalidated") + } + d.mutex.Unlock() + + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + + if time.Now().After(fileInfo.ModTime().Add(d.ttl)) { + return nil, errors.New("cache expired") + } + + // the cache is present and its valid. Try to read and use it. + cachedBytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + d.mutex.Lock() + defer d.mutex.Unlock() + d.fresh = d.fresh && ourFile + + return cachedBytes, nil +} + +func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error { + if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { + return err + } + + bytes, err := runtime.Encode(scheme.Codecs.LegacyCodec(), obj) + if err != nil { + return err + } + + f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".") + if err != nil { + return err + } + defer os.Remove(f.Name()) + _, err = f.Write(bytes) + if err != nil { + return err + } + + err = os.Chmod(f.Name(), 0755) + if err != nil { + return err + } + + name := f.Name() + err = f.Close() + if err != nil { + return err + } + + // atomic rename + d.mutex.Lock() + defer d.mutex.Unlock() + err = os.Rename(name, filename) + if err == nil { + d.ourFiles[filename] = struct{}{} + } + return err +} + +func (d *CachedDiscoveryClient) RESTClient() restclient.Interface { + return d.delegate.RESTClient() +} + +func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { + return ServerPreferredResources(d) +} + +func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { + return ServerPreferredNamespacedResources(d) +} + +func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) { + return d.delegate.ServerVersion() +} + +func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { + return d.delegate.OpenAPISchema() +} + +func (d *CachedDiscoveryClient) Fresh() bool { + d.mutex.Lock() + defer d.mutex.Unlock() + + return d.fresh +} + +func (d *CachedDiscoveryClient) Invalidate() { + d.mutex.Lock() + defer d.mutex.Unlock() + + d.ourFiles = map[string]struct{}{} + d.fresh = true + d.invalidated = true +} + +// NewCachedDiscoveryClientForConfig creates a new DiscoveryClient for the given config, and wraps +// the created client in a CachedDiscoveryClient. The provided configuration is upddated with a +// custom transport that understands cache responses. +func NewCachedDiscoveryClientForConfig(config *restclient.Config, cacheDirectory string, ttl time.Duration) (*CachedDiscoveryClient, error) { + if len(cacheDirectory) > 0 { + // update the given restconfig with a custom roundtripper that + // understands how to handle cache responses. + wt := config.WrapTransport + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + if wt != nil { + rt = wt(rt) + } + return newCacheRoundTripper(cacheDirectory, rt) + } + } + + discoveryClient, err := NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + + return newCachedDiscoveryClient(discoveryClient, cacheDirectory, ttl), nil +} + +// NewCachedDiscoveryClient creates a new DiscoveryClient. cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well. +func newCachedDiscoveryClient(delegate DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient { + return &CachedDiscoveryClient{ + delegate: delegate, + cacheDirectory: cacheDirectory, + ttl: ttl, + ourFiles: map[string]struct{}{}, + fresh: true, + } +} diff --git a/discovery/cached_discovery_test.go b/discovery/cached_discovery_test.go new file mode 100644 index 00000000..278931c2 --- /dev/null +++ b/discovery/cached_discovery_test.go @@ -0,0 +1,169 @@ +/* +Copyright 2016 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 discovery + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/googleapis/gnostic/OpenAPIv2" + "github.com/stretchr/testify/assert" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest/fake" +) + +func TestCachedDiscoveryClient_Fresh(t *testing.T) { + assert := assert.New(t) + + d, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(d) + + c := fakeDiscoveryClient{} + cdc := newCachedDiscoveryClient(&c, d, 60*time.Second) + assert.True(cdc.Fresh(), "should be fresh after creation") + + cdc.ServerGroups() + assert.True(cdc.Fresh(), "should be fresh after groups call without cache") + assert.Equal(c.groupCalls, 1) + + cdc.ServerGroups() + assert.True(cdc.Fresh(), "should be fresh after another groups call") + assert.Equal(c.groupCalls, 1) + + cdc.ServerResources() + assert.True(cdc.Fresh(), "should be fresh after resources call") + assert.Equal(c.resourceCalls, 1) + + cdc.ServerResources() + assert.True(cdc.Fresh(), "should be fresh after another resources call") + assert.Equal(c.resourceCalls, 1) + + cdc = newCachedDiscoveryClient(&c, d, 60*time.Second) + cdc.ServerGroups() + assert.False(cdc.Fresh(), "should NOT be fresh after recreation with existing groups cache") + assert.Equal(c.groupCalls, 1) + + cdc.ServerResources() + assert.False(cdc.Fresh(), "should NOT be fresh after recreation with existing resources cache") + assert.Equal(c.resourceCalls, 1) + + cdc.Invalidate() + assert.True(cdc.Fresh(), "should be fresh after cache invalidation") + + cdc.ServerResources() + assert.True(cdc.Fresh(), "should ignore existing resources cache after invalidation") + assert.Equal(c.resourceCalls, 2) +} + +func TestNewCachedDiscoveryClient_TTL(t *testing.T) { + assert := assert.New(t) + + d, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(d) + + c := fakeDiscoveryClient{} + cdc := newCachedDiscoveryClient(&c, d, 1*time.Nanosecond) + cdc.ServerGroups() + assert.Equal(c.groupCalls, 1) + + time.Sleep(1 * time.Second) + + cdc.ServerGroups() + assert.Equal(c.groupCalls, 2) +} + +type fakeDiscoveryClient struct { + groupCalls int + resourceCalls int + versionCalls int + openAPICalls int + + serverResourcesHandler func() ([]*metav1.APIResourceList, error) +} + +var _ DiscoveryInterface = &fakeDiscoveryClient{} + +func (c *fakeDiscoveryClient) RESTClient() restclient.Interface { + return &fake.RESTClient{} +} + +func (c *fakeDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { + c.groupCalls = c.groupCalls + 1 + return &metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "a", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "a/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "a/v1", + Version: "v1", + }, + }, + }, + }, nil +} + +func (c *fakeDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { + c.resourceCalls = c.resourceCalls + 1 + if groupVersion == "a/v1" { + return &metav1.APIResourceList{APIResources: []metav1.APIResource{{Name: "widgets", Kind: "Widget"}}}, nil + } + + return nil, errors.NewNotFound(schema.GroupResource{}, "") +} + +func (c *fakeDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) { + c.resourceCalls = c.resourceCalls + 1 + if c.serverResourcesHandler != nil { + return c.serverResourcesHandler() + } + return []*metav1.APIResourceList{}, nil +} + +func (c *fakeDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { + c.resourceCalls = c.resourceCalls + 1 + return nil, nil +} + +func (c *fakeDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { + c.resourceCalls = c.resourceCalls + 1 + return nil, nil +} + +func (c *fakeDiscoveryClient) ServerVersion() (*version.Info, error) { + c.versionCalls = c.versionCalls + 1 + return &version.Info{}, nil +} + +func (c *fakeDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { + c.openAPICalls = c.openAPICalls + 1 + return &openapi_v2.Document{}, nil +} diff --git a/discovery/round_tripper.go b/discovery/round_tripper.go new file mode 100644 index 00000000..2e352b88 --- /dev/null +++ b/discovery/round_tripper.go @@ -0,0 +1,51 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package transport provides a round tripper capable of caching HTTP responses. +package discovery + +import ( + "net/http" + "path/filepath" + + "github.com/gregjones/httpcache" + "github.com/gregjones/httpcache/diskcache" + "github.com/peterbourgon/diskv" +) + +type cacheRoundTripper struct { + rt *httpcache.Transport +} + +// newCacheRoundTripper creates a roundtripper that reads the ETag on +// response headers and send the If-None-Match header on subsequent +// corresponding requests. +func newCacheRoundTripper(cacheDir string, rt http.RoundTripper) http.RoundTripper { + d := diskv.New(diskv.Options{ + BasePath: cacheDir, + TempDir: filepath.Join(cacheDir, ".diskv-temp"), + }) + t := httpcache.NewTransport(diskcache.NewWithDiskv(d)) + t.Transport = rt + + return &cacheRoundTripper{rt: t} +} + +func (rt *cacheRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt.rt.RoundTrip(req) +} + +func (rt *cacheRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt.Transport } diff --git a/discovery/round_tripper_test.go b/discovery/round_tripper_test.go new file mode 100644 index 00000000..b15e2e77 --- /dev/null +++ b/discovery/round_tripper_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discovery + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/url" + "os" + "testing" +) + +// copied from k8s.io/client-go/transport/round_trippers_test.go +type testRoundTripper struct { + Request *http.Request + Response *http.Response + Err error +} + +func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.Request = req + return rt.Response, rt.Err +} + +func TestCacheRoundTripper(t *testing.T) { + rt := &testRoundTripper{} + cacheDir, err := ioutil.TempDir("", "cache-rt") + defer os.RemoveAll(cacheDir) + if err != nil { + t.Fatal(err) + } + cache := newCacheRoundTripper(cacheDir, rt) + + // First call, caches the response + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Host: "localhost"}, + } + rt.Response = &http.Response{ + Header: http.Header{"ETag": []string{`"123456"`}}, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Content"))), + StatusCode: http.StatusOK, + } + resp, err := cache.RoundTrip(req) + if err != nil { + t.Fatal(err) + } + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(content) != "Content" { + t.Errorf(`Expected Body to be "Content", got %q`, string(content)) + } + + // Second call, returns cached response + req = &http.Request{ + Method: http.MethodGet, + URL: &url.URL{Host: "localhost"}, + } + rt.Response = &http.Response{ + StatusCode: http.StatusNotModified, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Other Content"))), + } + + resp, err = cache.RoundTrip(req) + if err != nil { + t.Fatal(err) + } + + // Read body and make sure we have the initial content + content, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + t.Fatal(err) + } + if string(content) != "Content" { + t.Errorf("Invalid content read from cache %q", string(content)) + } +}