k8s.io/client-go@v0.22.2/discovery/cached/memory/memcache.go (about)

     1  /*
     2  Copyright 2017 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 memory
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"sync"
    23  	"syscall"
    24  
    25  	openapi_v2 "github.com/googleapis/gnostic/openapiv2"
    26  
    27  	errorsutil "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    30  	"k8s.io/apimachinery/pkg/version"
    31  	"k8s.io/client-go/discovery"
    32  	restclient "k8s.io/client-go/rest"
    33  )
    34  
    35  type cacheEntry struct {
    36  	resourceList *metav1.APIResourceList
    37  	err          error
    38  }
    39  
    40  // memCacheClient can Invalidate() to stay up-to-date with discovery
    41  // information.
    42  //
    43  // TODO: Switch to a watch interface. Right now it will poll after each
    44  // Invalidate() call.
    45  type memCacheClient struct {
    46  	delegate discovery.DiscoveryInterface
    47  
    48  	lock                   sync.RWMutex
    49  	groupToServerResources map[string]*cacheEntry
    50  	groupList              *metav1.APIGroupList
    51  	cacheValid             bool
    52  }
    53  
    54  // Error Constants
    55  var (
    56  	ErrCacheNotFound = errors.New("not found")
    57  )
    58  
    59  var _ discovery.CachedDiscoveryInterface = &memCacheClient{}
    60  
    61  // isTransientConnectionError checks whether given error is "Connection refused" or
    62  // "Connection reset" error which usually means that apiserver is temporarily
    63  // unavailable.
    64  func isTransientConnectionError(err error) bool {
    65  	var errno syscall.Errno
    66  	if errors.As(err, &errno) {
    67  		return errno == syscall.ECONNREFUSED || errno == syscall.ECONNRESET
    68  	}
    69  	return false
    70  }
    71  
    72  func isTransientError(err error) bool {
    73  	if isTransientConnectionError(err) {
    74  		return true
    75  	}
    76  
    77  	if t, ok := err.(errorsutil.APIStatus); ok && t.Status().Code >= 500 {
    78  		return true
    79  	}
    80  
    81  	return errorsutil.IsTooManyRequests(err)
    82  }
    83  
    84  // ServerResourcesForGroupVersion returns the supported resources for a group and version.
    85  func (d *memCacheClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
    86  	d.lock.Lock()
    87  	defer d.lock.Unlock()
    88  	if !d.cacheValid {
    89  		if err := d.refreshLocked(); err != nil {
    90  			return nil, err
    91  		}
    92  	}
    93  	cachedVal, ok := d.groupToServerResources[groupVersion]
    94  	if !ok {
    95  		return nil, ErrCacheNotFound
    96  	}
    97  
    98  	if cachedVal.err != nil && isTransientError(cachedVal.err) {
    99  		r, err := d.serverResourcesForGroupVersion(groupVersion)
   100  		if err != nil {
   101  			utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", groupVersion, err))
   102  		}
   103  		cachedVal = &cacheEntry{r, err}
   104  		d.groupToServerResources[groupVersion] = cachedVal
   105  	}
   106  
   107  	return cachedVal.resourceList, cachedVal.err
   108  }
   109  
   110  // ServerResources returns the supported resources for all groups and versions.
   111  // Deprecated: use ServerGroupsAndResources instead.
   112  func (d *memCacheClient) ServerResources() ([]*metav1.APIResourceList, error) {
   113  	return discovery.ServerResources(d)
   114  }
   115  
   116  // ServerGroupsAndResources returns the groups and supported resources for all groups and versions.
   117  func (d *memCacheClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   118  	return discovery.ServerGroupsAndResources(d)
   119  }
   120  
   121  func (d *memCacheClient) ServerGroups() (*metav1.APIGroupList, error) {
   122  	d.lock.Lock()
   123  	defer d.lock.Unlock()
   124  	if !d.cacheValid {
   125  		if err := d.refreshLocked(); err != nil {
   126  			return nil, err
   127  		}
   128  	}
   129  	return d.groupList, nil
   130  }
   131  
   132  func (d *memCacheClient) RESTClient() restclient.Interface {
   133  	return d.delegate.RESTClient()
   134  }
   135  
   136  func (d *memCacheClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
   137  	return discovery.ServerPreferredResources(d)
   138  }
   139  
   140  func (d *memCacheClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
   141  	return discovery.ServerPreferredNamespacedResources(d)
   142  }
   143  
   144  func (d *memCacheClient) ServerVersion() (*version.Info, error) {
   145  	return d.delegate.ServerVersion()
   146  }
   147  
   148  func (d *memCacheClient) OpenAPISchema() (*openapi_v2.Document, error) {
   149  	return d.delegate.OpenAPISchema()
   150  }
   151  
   152  func (d *memCacheClient) Fresh() bool {
   153  	d.lock.RLock()
   154  	defer d.lock.RUnlock()
   155  	// Return whether the cache is populated at all. It is still possible that
   156  	// a single entry is missing due to transient errors and the attempt to read
   157  	// that entry will trigger retry.
   158  	return d.cacheValid
   159  }
   160  
   161  // Invalidate enforces that no cached data that is older than the current time
   162  // is used.
   163  func (d *memCacheClient) Invalidate() {
   164  	d.lock.Lock()
   165  	defer d.lock.Unlock()
   166  	d.cacheValid = false
   167  	d.groupToServerResources = nil
   168  	d.groupList = nil
   169  }
   170  
   171  // refreshLocked refreshes the state of cache. The caller must hold d.lock for
   172  // writing.
   173  func (d *memCacheClient) refreshLocked() error {
   174  	// TODO: Could this multiplicative set of calls be replaced by a single call
   175  	// to ServerResources? If it's possible for more than one resulting
   176  	// APIResourceList to have the same GroupVersion, the lists would need merged.
   177  	gl, err := d.delegate.ServerGroups()
   178  	if err != nil || len(gl.Groups) == 0 {
   179  		utilruntime.HandleError(fmt.Errorf("couldn't get current server API group list: %v", err))
   180  		return err
   181  	}
   182  
   183  	wg := &sync.WaitGroup{}
   184  	resultLock := &sync.Mutex{}
   185  	rl := map[string]*cacheEntry{}
   186  	for _, g := range gl.Groups {
   187  		for _, v := range g.Versions {
   188  			gv := v.GroupVersion
   189  			wg.Add(1)
   190  			go func() {
   191  				defer wg.Done()
   192  				defer utilruntime.HandleCrash()
   193  
   194  				r, err := d.serverResourcesForGroupVersion(gv)
   195  				if err != nil {
   196  					utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", gv, err))
   197  				}
   198  
   199  				resultLock.Lock()
   200  				defer resultLock.Unlock()
   201  				rl[gv] = &cacheEntry{r, err}
   202  			}()
   203  		}
   204  	}
   205  	wg.Wait()
   206  
   207  	d.groupToServerResources, d.groupList = rl, gl
   208  	d.cacheValid = true
   209  	return nil
   210  }
   211  
   212  func (d *memCacheClient) serverResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
   213  	r, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
   214  	if err != nil {
   215  		return r, err
   216  	}
   217  	if len(r.APIResources) == 0 {
   218  		return r, fmt.Errorf("Got empty response for: %v", groupVersion)
   219  	}
   220  	return r, nil
   221  }
   222  
   223  // NewMemCacheClient creates a new CachedDiscoveryInterface which caches
   224  // discovery information in memory and will stay up-to-date if Invalidate is
   225  // called with regularity.
   226  //
   227  // NOTE: The client will NOT resort to live lookups on cache misses.
   228  func NewMemCacheClient(delegate discovery.DiscoveryInterface) discovery.CachedDiscoveryInterface {
   229  	return &memCacheClient{
   230  		delegate:               delegate,
   231  		groupToServerResources: map[string]*cacheEntry{},
   232  	}
   233  }