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 }