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  }