k8s.io/client-go@v0.22.2/discovery/cached/disk/cached_discovery.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 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 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 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. 15 */ 16 17 package disk 18 19 import ( 20 "errors" 21 "io/ioutil" 22 "net/http" 23 "os" 24 "path/filepath" 25 "sync" 26 "time" 27 28 openapi_v2 "github.com/googleapis/gnostic/openapiv2" 29 "k8s.io/klog/v2" 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/discovery" 35 "k8s.io/client-go/kubernetes/scheme" 36 restclient "k8s.io/client-go/rest" 37 ) 38 39 // CachedDiscoveryClient implements the functions that discovery server-supported API groups, 40 // versions and resources. 41 type CachedDiscoveryClient struct { 42 delegate discovery.DiscoveryInterface 43 44 // cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well. 45 cacheDirectory string 46 47 // ttl is how long the cache should be considered valid 48 ttl time.Duration 49 50 // mutex protects the variables below 51 mutex sync.Mutex 52 53 // ourFiles are all filenames of cache files created by this process 54 ourFiles map[string]struct{} 55 // invalidated is true if all cache files should be ignored that are not ours (e.g. after Invalidate() was called) 56 invalidated bool 57 // fresh is true if all used cache files were ours 58 fresh bool 59 } 60 61 var _ discovery.CachedDiscoveryInterface = &CachedDiscoveryClient{} 62 63 // ServerResourcesForGroupVersion returns the supported resources for a group and version. 64 func (d *CachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { 65 filename := filepath.Join(d.cacheDirectory, groupVersion, "serverresources.json") 66 cachedBytes, err := d.getCachedFile(filename) 67 // 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 if err == nil { 69 cachedResources := &metav1.APIResourceList{} 70 if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedResources); err == nil { 71 klog.V(10).Infof("returning cached discovery info from %v", filename) 72 return cachedResources, nil 73 } 74 } 75 76 liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion) 77 if err != nil { 78 klog.V(3).Infof("skipped caching discovery info due to %v", err) 79 return liveResources, err 80 } 81 if liveResources == nil || len(liveResources.APIResources) == 0 { 82 klog.V(3).Infof("skipped caching discovery info, no resources found") 83 return liveResources, err 84 } 85 86 if err := d.writeCachedFile(filename, liveResources); err != nil { 87 klog.V(1).Infof("failed to write cache to %v due to %v", filename, err) 88 } 89 90 return liveResources, nil 91 } 92 93 // ServerResources returns the supported resources for all groups and versions. 94 // Deprecated: use ServerGroupsAndResources instead. 95 func (d *CachedDiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) { 96 _, rs, err := discovery.ServerGroupsAndResources(d) 97 return rs, err 98 } 99 100 // ServerGroupsAndResources returns the supported groups and resources for all groups and versions. 101 func (d *CachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 102 return discovery.ServerGroupsAndResources(d) 103 } 104 105 // ServerGroups returns the supported groups, with information like supported versions and the 106 // preferred version. 107 func (d *CachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { 108 filename := filepath.Join(d.cacheDirectory, "servergroups.json") 109 cachedBytes, err := d.getCachedFile(filename) 110 // 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. 111 if err == nil { 112 cachedGroups := &metav1.APIGroupList{} 113 if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil { 114 klog.V(10).Infof("returning cached discovery info from %v", filename) 115 return cachedGroups, nil 116 } 117 } 118 119 liveGroups, err := d.delegate.ServerGroups() 120 if err != nil { 121 klog.V(3).Infof("skipped caching discovery info due to %v", err) 122 return liveGroups, err 123 } 124 if liveGroups == nil || len(liveGroups.Groups) == 0 { 125 klog.V(3).Infof("skipped caching discovery info, no groups found") 126 return liveGroups, err 127 } 128 129 if err := d.writeCachedFile(filename, liveGroups); err != nil { 130 klog.V(1).Infof("failed to write cache to %v due to %v", filename, err) 131 } 132 133 return liveGroups, nil 134 } 135 136 func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) { 137 // after invalidation ignore cache files not created by this process 138 d.mutex.Lock() 139 _, ourFile := d.ourFiles[filename] 140 if d.invalidated && !ourFile { 141 d.mutex.Unlock() 142 return nil, errors.New("cache invalidated") 143 } 144 d.mutex.Unlock() 145 146 file, err := os.Open(filename) 147 if err != nil { 148 return nil, err 149 } 150 defer file.Close() 151 152 fileInfo, err := file.Stat() 153 if err != nil { 154 return nil, err 155 } 156 157 if time.Now().After(fileInfo.ModTime().Add(d.ttl)) { 158 return nil, errors.New("cache expired") 159 } 160 161 // the cache is present and its valid. Try to read and use it. 162 cachedBytes, err := ioutil.ReadAll(file) 163 if err != nil { 164 return nil, err 165 } 166 167 d.mutex.Lock() 168 defer d.mutex.Unlock() 169 d.fresh = d.fresh && ourFile 170 171 return cachedBytes, nil 172 } 173 174 func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error { 175 if err := os.MkdirAll(filepath.Dir(filename), 0750); err != nil { 176 return err 177 } 178 179 bytes, err := runtime.Encode(scheme.Codecs.LegacyCodec(), obj) 180 if err != nil { 181 return err 182 } 183 184 f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".") 185 if err != nil { 186 return err 187 } 188 defer os.Remove(f.Name()) 189 _, err = f.Write(bytes) 190 if err != nil { 191 return err 192 } 193 194 err = os.Chmod(f.Name(), 0660) 195 if err != nil { 196 return err 197 } 198 199 name := f.Name() 200 err = f.Close() 201 if err != nil { 202 return err 203 } 204 205 // atomic rename 206 d.mutex.Lock() 207 defer d.mutex.Unlock() 208 err = os.Rename(name, filename) 209 if err == nil { 210 d.ourFiles[filename] = struct{}{} 211 } 212 return err 213 } 214 215 // RESTClient returns a RESTClient that is used to communicate with API server 216 // by this client implementation. 217 func (d *CachedDiscoveryClient) RESTClient() restclient.Interface { 218 return d.delegate.RESTClient() 219 } 220 221 // ServerPreferredResources returns the supported resources with the version preferred by the 222 // server. 223 func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { 224 return discovery.ServerPreferredResources(d) 225 } 226 227 // ServerPreferredNamespacedResources returns the supported namespaced resources with the 228 // version preferred by the server. 229 func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { 230 return discovery.ServerPreferredNamespacedResources(d) 231 } 232 233 // ServerVersion retrieves and parses the server's version (git version). 234 func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) { 235 return d.delegate.ServerVersion() 236 } 237 238 // OpenAPISchema retrieves and parses the swagger API schema the server supports. 239 func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { 240 return d.delegate.OpenAPISchema() 241 } 242 243 // Fresh is supposed to tell the caller whether or not to retry if the cache 244 // fails to find something (false = retry, true = no need to retry). 245 func (d *CachedDiscoveryClient) Fresh() bool { 246 d.mutex.Lock() 247 defer d.mutex.Unlock() 248 249 return d.fresh 250 } 251 252 // Invalidate enforces that no cached data is used in the future that is older than the current time. 253 func (d *CachedDiscoveryClient) Invalidate() { 254 d.mutex.Lock() 255 defer d.mutex.Unlock() 256 257 d.ourFiles = map[string]struct{}{} 258 d.fresh = true 259 d.invalidated = true 260 } 261 262 // NewCachedDiscoveryClientForConfig creates a new DiscoveryClient for the given config, and wraps 263 // the created client in a CachedDiscoveryClient. The provided configuration is updated with a 264 // custom transport that understands cache responses. 265 // We receive two distinct cache directories for now, in order to preserve old behavior 266 // which makes use of the --cache-dir flag value for storing cache data from the CacheRoundTripper, 267 // and makes use of the hardcoded destination (~/.kube/cache/discovery/...) for storing 268 // CachedDiscoveryClient cache data. If httpCacheDir is empty, the restconfig's transport will not 269 // be updated with a roundtripper that understands cache responses. 270 // If discoveryCacheDir is empty, cached server resource data will be looked up in the current directory. 271 func NewCachedDiscoveryClientForConfig(config *restclient.Config, discoveryCacheDir, httpCacheDir string, ttl time.Duration) (*CachedDiscoveryClient, error) { 272 if len(httpCacheDir) > 0 { 273 // update the given restconfig with a custom roundtripper that 274 // understands how to handle cache responses. 275 config = restclient.CopyConfig(config) 276 config.Wrap(func(rt http.RoundTripper) http.RoundTripper { 277 return newCacheRoundTripper(httpCacheDir, rt) 278 }) 279 } 280 281 discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) 282 if err != nil { 283 return nil, err 284 } 285 286 return newCachedDiscoveryClient(discoveryClient, discoveryCacheDir, ttl), nil 287 } 288 289 // 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. 290 func newCachedDiscoveryClient(delegate discovery.DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient { 291 return &CachedDiscoveryClient{ 292 delegate: delegate, 293 cacheDirectory: cacheDirectory, 294 ttl: ttl, 295 ourFiles: map[string]struct{}{}, 296 fresh: true, 297 } 298 }