blob: aca46546e62d71bb7d816be27533c815443c5bfd [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
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/golang/glog"
29 "github.com/googleapis/gnostic/OpenAPIv2"
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 glog.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 glog.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 glog.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 glog.V(3).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
97func (d *CachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
98 filename := filepath.Join(d.cacheDirectory, "servergroups.json")
99 cachedBytes, err := d.getCachedFile(filename)
100 // 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.
101 if err == nil {
102 cachedGroups := &metav1.APIGroupList{}
103 if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil {
104 glog.V(10).Infof("returning cached discovery info from %v", filename)
105 return cachedGroups, nil
106 }
107 }
108
109 liveGroups, err := d.delegate.ServerGroups()
110 if err != nil {
111 glog.V(3).Infof("skipped caching discovery info due to %v", err)
112 return liveGroups, err
113 }
114 if liveGroups == nil || len(liveGroups.Groups) == 0 {
115 glog.V(3).Infof("skipped caching discovery info, no groups found")
116 return liveGroups, err
117 }
118
119 if err := d.writeCachedFile(filename, liveGroups); err != nil {
120 glog.V(3).Infof("failed to write cache to %v due to %v", filename, err)
121 }
122
123 return liveGroups, nil
124}
125
126func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) {
127 // after invalidation ignore cache files not created by this process
128 d.mutex.Lock()
129 _, ourFile := d.ourFiles[filename]
130 if d.invalidated && !ourFile {
131 d.mutex.Unlock()
132 return nil, errors.New("cache invalidated")
133 }
134 d.mutex.Unlock()
135
136 file, err := os.Open(filename)
137 if err != nil {
138 return nil, err
139 }
140 defer file.Close()
141
142 fileInfo, err := file.Stat()
143 if err != nil {
144 return nil, err
145 }
146
147 if time.Now().After(fileInfo.ModTime().Add(d.ttl)) {
148 return nil, errors.New("cache expired")
149 }
150
151 // the cache is present and its valid. Try to read and use it.
152 cachedBytes, err := ioutil.ReadAll(file)
153 if err != nil {
154 return nil, err
155 }
156
157 d.mutex.Lock()
158 defer d.mutex.Unlock()
159 d.fresh = d.fresh && ourFile
160
161 return cachedBytes, nil
162}
163
164func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error {
165 if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
166 return err
167 }
168
169 bytes, err := runtime.Encode(scheme.Codecs.LegacyCodec(), obj)
170 if err != nil {
171 return err
172 }
173
174 f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".")
175 if err != nil {
176 return err
177 }
178 defer os.Remove(f.Name())
179 _, err = f.Write(bytes)
180 if err != nil {
181 return err
182 }
183
184 err = os.Chmod(f.Name(), 0755)
185 if err != nil {
186 return err
187 }
188
189 name := f.Name()
190 err = f.Close()
191 if err != nil {
192 return err
193 }
194
195 // atomic rename
196 d.mutex.Lock()
197 defer d.mutex.Unlock()
198 err = os.Rename(name, filename)
199 if err == nil {
200 d.ourFiles[filename] = struct{}{}
201 }
202 return err
203}
204
205func (d *CachedDiscoveryClient) RESTClient() restclient.Interface {
206 return d.delegate.RESTClient()
207}
208
209func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
210 return ServerPreferredResources(d)
211}
212
213func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
214 return ServerPreferredNamespacedResources(d)
215}
216
217func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) {
218 return d.delegate.ServerVersion()
219}
220
221func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
222 return d.delegate.OpenAPISchema()
223}
224
225func (d *CachedDiscoveryClient) Fresh() bool {
226 d.mutex.Lock()
227 defer d.mutex.Unlock()
228
229 return d.fresh
230}
231
232func (d *CachedDiscoveryClient) Invalidate() {
233 d.mutex.Lock()
234 defer d.mutex.Unlock()
235
236 d.ourFiles = map[string]struct{}{}
237 d.fresh = true
238 d.invalidated = true
239}
240
241// NewCachedDiscoveryClientForConfig creates a new DiscoveryClient for the given config, and wraps
242// the created client in a CachedDiscoveryClient. The provided configuration is updated with a
243// custom transport that understands cache responses.
244// We receive two distinct cache directories for now, in order to preserve old behavior
245// which makes use of the --cache-dir flag value for storing cache data from the CacheRoundTripper,
246// and makes use of the hardcoded destination (~/.kube/cache/discovery/...) for storing
247// CachedDiscoveryClient cache data. If httpCacheDir is empty, the restconfig's transport will not
248// be updated with a roundtripper that understands cache responses.
249// If discoveryCacheDir is empty, cached server resource data will be looked up in the current directory.
250// TODO(juanvallejo): the value of "--cache-dir" should be honored. Consolidate discoveryCacheDir with httpCacheDir
251// so that server resources and http-cache data are stored in the same location, provided via config flags.
252func NewCachedDiscoveryClientForConfig(config *restclient.Config, discoveryCacheDir, httpCacheDir string, ttl time.Duration) (*CachedDiscoveryClient, error) {
253 if len(httpCacheDir) > 0 {
254 // update the given restconfig with a custom roundtripper that
255 // understands how to handle cache responses.
256 wt := config.WrapTransport
257 config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
258 if wt != nil {
259 rt = wt(rt)
260 }
261 return newCacheRoundTripper(httpCacheDir, rt)
262 }
263 }
264
265 discoveryClient, err := NewDiscoveryClientForConfig(config)
266 if err != nil {
267 return nil, err
268 }
269
270 return newCachedDiscoveryClient(discoveryClient, discoveryCacheDir, ttl), nil
271}
272
273// 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.
274func newCachedDiscoveryClient(delegate DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
275 return &CachedDiscoveryClient{
276 delegate: delegate,
277 cacheDirectory: cacheDirectory,
278 ttl: ttl,
279 ourFiles: map[string]struct{}{},
280 fresh: true,
281 }
282}