blob: df69d6a1930bd04b65c50fd773916a08463709cd [file] [log] [blame]
sslobodrd046be82019-01-16 10:02:22 -05001/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package discovery
18
19import (
20 "errors"
21 "io/ioutil"
22 "net/http"
23 "os"
24 "path/filepath"
25 "sync"
26 "time"
27
28 "github.com/googleapis/gnostic/OpenAPIv2"
29 "k8s.io/klog"
30
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"
36)
37
38// CachedDiscoveryClient implements the functions that discovery server-supported API groups,
39// versions and resources.
40type CachedDiscoveryClient struct {
41 delegate DiscoveryInterface
42
43 // cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
44 cacheDirectory string
45
46 // ttl is how long the cache should be considered valid
47 ttl time.Duration
48
49 // mutex protects the variables below
50 mutex sync.Mutex
51
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)
55 invalidated bool
56 // fresh is true if all used cache files were ours
57 fresh bool
58}
59
60var _ CachedDiscoveryInterface = &CachedDiscoveryClient{}
61
62// ServerResourcesForGroupVersion returns the supported resources for a group and version.
63func (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.
67 if err == nil {
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
72 }
73 }
74
75 liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
76 if err != nil {
77 klog.V(3).Infof("skipped caching discovery info due to %v", err)
78 return liveResources, err
79 }
80 if liveResources == nil || len(liveResources.APIResources) == 0 {
81 klog.V(3).Infof("skipped caching discovery info, no resources found")
82 return liveResources, err
83 }
84
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)
87 }
88
89 return liveResources, nil
90}
91
92// ServerResources returns the supported resources for all groups and versions.
93func (d *CachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) {
94 return ServerResources(d)
95}
96
97// ServerGroups returns the supported groups, with information like supported versions and the
98// preferred version.
99func (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.
103 if err == nil {
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
108 }
109 }
110
111 liveGroups, err := d.delegate.ServerGroups()
112 if err != nil {
113 klog.V(3).Infof("skipped caching discovery info due to %v", err)
114 return liveGroups, err
115 }
116 if liveGroups == nil || len(liveGroups.Groups) == 0 {
117 klog.V(3).Infof("skipped caching discovery info, no groups found")
118 return liveGroups, err
119 }
120
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)
123 }
124
125 return liveGroups, nil
126}
127
128func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) {
129 // after invalidation ignore cache files not created by this process
130 d.mutex.Lock()
131 _, ourFile := d.ourFiles[filename]
132 if d.invalidated && !ourFile {
133 d.mutex.Unlock()
134 return nil, errors.New("cache invalidated")
135 }
136 d.mutex.Unlock()
137
138 file, err := os.Open(filename)
139 if err != nil {
140 return nil, err
141 }
142 defer file.Close()
143
144 fileInfo, err := file.Stat()
145 if err != nil {
146 return nil, err
147 }
148
149 if time.Now().After(fileInfo.ModTime().Add(d.ttl)) {
150 return nil, errors.New("cache expired")
151 }
152
153 // the cache is present and its valid. Try to read and use it.
154 cachedBytes, err := ioutil.ReadAll(file)
155 if err != nil {
156 return nil, err
157 }
158
159 d.mutex.Lock()
160 defer d.mutex.Unlock()
161 d.fresh = d.fresh && ourFile
162
163 return cachedBytes, nil
164}
165
166func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error {
167 if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
168 return err
169 }
170
171 bytes, err := runtime.Encode(scheme.Codecs.LegacyCodec(), obj)
172 if err != nil {
173 return err
174 }
175
176 f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".")
177 if err != nil {
178 return err
179 }
180 defer os.Remove(f.Name())
181 _, err = f.Write(bytes)
182 if err != nil {
183 return err
184 }
185
186 err = os.Chmod(f.Name(), 0755)
187 if err != nil {
188 return err
189 }
190
191 name := f.Name()
192 err = f.Close()
193 if err != nil {
194 return err
195 }
196
197 // atomic rename
198 d.mutex.Lock()
199 defer d.mutex.Unlock()
200 err = os.Rename(name, filename)
201 if err == nil {
202 d.ourFiles[filename] = struct{}{}
203 }
204 return err
205}
206
207// RESTClient returns a RESTClient that is used to communicate with API server
208// by this client implementation.
209func (d *CachedDiscoveryClient) RESTClient() restclient.Interface {
210 return d.delegate.RESTClient()
211}
212
213// ServerPreferredResources returns the supported resources with the version preferred by the
214// server.
215func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
216 return ServerPreferredResources(d)
217}
218
219// ServerPreferredNamespacedResources returns the supported namespaced resources with the
220// version preferred by the server.
221func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
222 return ServerPreferredNamespacedResources(d)
223}
224
225// ServerVersion retrieves and parses the server's version (git version).
226func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) {
227 return d.delegate.ServerVersion()
228}
229
230// OpenAPISchema retrieves and parses the swagger API schema the server supports.
231func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
232 return d.delegate.OpenAPISchema()
233}
234
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).
237func (d *CachedDiscoveryClient) Fresh() bool {
238 d.mutex.Lock()
239 defer d.mutex.Unlock()
240
241 return d.fresh
242}
243
244// Invalidate enforces that no cached data is used in the future that is older than the current time.
245func (d *CachedDiscoveryClient) Invalidate() {
246 d.mutex.Lock()
247 defer d.mutex.Unlock()
248
249 d.ourFiles = map[string]struct{}{}
250 d.fresh = true
251 d.invalidated = true
252}
253
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.
265func 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 {
271 if wt != nil {
272 rt = wt(rt)
273 }
274 return newCacheRoundTripper(httpCacheDir, rt)
275 }
276 }
277
278 discoveryClient, err := NewDiscoveryClientForConfig(config)
279 if err != nil {
280 return nil, err
281 }
282
283 return newCachedDiscoveryClient(discoveryClient, discoveryCacheDir, ttl), nil
284}
285
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.
287func newCachedDiscoveryClient(delegate DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
288 return &CachedDiscoveryClient{
289 delegate: delegate,
290 cacheDirectory: cacheDirectory,
291 ttl: ttl,
292 ourFiles: map[string]struct{}{},
293 fresh: true,
294 }
295}