2 Copyright 2016 The Kubernetes Authors.
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
28 "github.com/googleapis/gnostic/OpenAPIv2"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/version"
34 "k8s.io/client-go/kubernetes/scheme"
35 restclient "k8s.io/client-go/rest"
38 // CachedDiscoveryClient implements the functions that discovery server-supported API groups,
39 // versions and resources.
40 type CachedDiscoveryClient struct {
41 delegate DiscoveryInterface
43 // cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
46 // ttl is how long the cache should be considered valid
49 // mutex protects the variables below
52 // ourFiles are all filenames of cache files created by this process
53 ourFiles map[string]struct{}
54 // invalidated is true if all cache files should be ignored that are not ours (e.g. after Invalidate() was called)
56 // fresh is true if all used cache files were ours
60 var _ CachedDiscoveryInterface = &CachedDiscoveryClient{}
62 // ServerResourcesForGroupVersion returns the supported resources for a group and version.
63 func (d *CachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
64 filename := filepath.Join(d.cacheDirectory, groupVersion, "serverresources.json")
65 cachedBytes, err := d.getCachedFile(filename)
66 // 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.
68 cachedResources := &metav1.APIResourceList{}
69 if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedResources); err == nil {
70 klog.V(10).Infof("returning cached discovery info from %v", filename)
71 return cachedResources, nil
75 liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
77 klog.V(3).Infof("skipped caching discovery info due to %v", err)
78 return liveResources, err
80 if liveResources == nil || len(liveResources.APIResources) == 0 {
81 klog.V(3).Infof("skipped caching discovery info, no resources found")
82 return liveResources, err
85 if err := d.writeCachedFile(filename, liveResources); err != nil {
86 klog.V(1).Infof("failed to write cache to %v due to %v", filename, err)
89 return liveResources, nil
92 // ServerResources returns the supported resources for all groups and versions.
93 func (d *CachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) {
94 return ServerResources(d)
97 // ServerGroups returns the supported groups, with information like supported versions and the
99 func (d *CachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
100 filename := filepath.Join(d.cacheDirectory, "servergroups.json")
101 cachedBytes, err := d.getCachedFile(filename)
102 // 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.
104 cachedGroups := &metav1.APIGroupList{}
105 if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil {
106 klog.V(10).Infof("returning cached discovery info from %v", filename)
107 return cachedGroups, nil
111 liveGroups, err := d.delegate.ServerGroups()
113 klog.V(3).Infof("skipped caching discovery info due to %v", err)
114 return liveGroups, err
116 if liveGroups == nil || len(liveGroups.Groups) == 0 {
117 klog.V(3).Infof("skipped caching discovery info, no groups found")
118 return liveGroups, err
121 if err := d.writeCachedFile(filename, liveGroups); err != nil {
122 klog.V(1).Infof("failed to write cache to %v due to %v", filename, err)
125 return liveGroups, nil
128 func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) {
129 // after invalidation ignore cache files not created by this process
131 _, ourFile := d.ourFiles[filename]
132 if d.invalidated && !ourFile {
134 return nil, errors.New("cache invalidated")
138 file, err := os.Open(filename)
144 fileInfo, err := file.Stat()
149 if time.Now().After(fileInfo.ModTime().Add(d.ttl)) {
150 return nil, errors.New("cache expired")
153 // the cache is present and its valid. Try to read and use it.
154 cachedBytes, err := ioutil.ReadAll(file)
160 defer d.mutex.Unlock()
161 d.fresh = d.fresh && ourFile
163 return cachedBytes, nil
166 func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error {
167 if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
171 bytes, err := runtime.Encode(scheme.Codecs.LegacyCodec(), obj)
176 f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".")
180 defer os.Remove(f.Name())
181 _, err = f.Write(bytes)
186 err = os.Chmod(f.Name(), 0755)
199 defer d.mutex.Unlock()
200 err = os.Rename(name, filename)
202 d.ourFiles[filename] = struct{}{}
207 // RESTClient returns a RESTClient that is used to communicate with API server
208 // by this client implementation.
209 func (d *CachedDiscoveryClient) RESTClient() restclient.Interface {
210 return d.delegate.RESTClient()
213 // ServerPreferredResources returns the supported resources with the version preferred by the
215 func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
216 return ServerPreferredResources(d)
219 // ServerPreferredNamespacedResources returns the supported namespaced resources with the
220 // version preferred by the server.
221 func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
222 return ServerPreferredNamespacedResources(d)
225 // ServerVersion retrieves and parses the server's version (git version).
226 func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) {
227 return d.delegate.ServerVersion()
230 // OpenAPISchema retrieves and parses the swagger API schema the server supports.
231 func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
232 return d.delegate.OpenAPISchema()
235 // Fresh is supposed to tell the caller whether or not to retry if the cache
236 // fails to find something (false = retry, true = no need to retry).
237 func (d *CachedDiscoveryClient) Fresh() bool {
239 defer d.mutex.Unlock()
244 // Invalidate enforces that no cached data is used in the future that is older than the current time.
245 func (d *CachedDiscoveryClient) Invalidate() {
247 defer d.mutex.Unlock()
249 d.ourFiles = map[string]struct{}{}
254 // NewCachedDiscoveryClientForConfig creates a new DiscoveryClient for the given config, and wraps
255 // the created client in a CachedDiscoveryClient. The provided configuration is updated with a
256 // custom transport that understands cache responses.
257 // We receive two distinct cache directories for now, in order to preserve old behavior
258 // which makes use of the --cache-dir flag value for storing cache data from the CacheRoundTripper,
259 // and makes use of the hardcoded destination (~/.kube/cache/discovery/...) for storing
260 // CachedDiscoveryClient cache data. If httpCacheDir is empty, the restconfig's transport will not
261 // be updated with a roundtripper that understands cache responses.
262 // If discoveryCacheDir is empty, cached server resource data will be looked up in the current directory.
263 // TODO(juanvallejo): the value of "--cache-dir" should be honored. Consolidate discoveryCacheDir with httpCacheDir
264 // so that server resources and http-cache data are stored in the same location, provided via config flags.
265 func NewCachedDiscoveryClientForConfig(config *restclient.Config, discoveryCacheDir, httpCacheDir string, ttl time.Duration) (*CachedDiscoveryClient, error) {
266 if len(httpCacheDir) > 0 {
267 // update the given restconfig with a custom roundtripper that
268 // understands how to handle cache responses.
269 wt := config.WrapTransport
270 config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
274 return newCacheRoundTripper(httpCacheDir, rt)
278 discoveryClient, err := NewDiscoveryClientForConfig(config)
283 return newCachedDiscoveryClient(discoveryClient, discoveryCacheDir, ttl), nil
286 // 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.
287 func newCachedDiscoveryClient(delegate DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
288 return &CachedDiscoveryClient{
290 cacheDirectory: cacheDirectory,
292 ourFiles: map[string]struct{}{},