Skip to content

Commit f8c4d97

Browse files
committed
add automatic caching for discovery requests, refreshing on a miss (#238)
* add automatic caching for discovery requests, refreshing on a miss * fix base64 encode in python3 * Don't replace ResourceContainer on cache invalidation so that the second attempt succeeds * Use more generic temp directory and path operations (cherry picked from commit c6f2168)
1 parent 51c82d2 commit f8c4d97

File tree

1 file changed

+120
-24
lines changed

1 file changed

+120
-24
lines changed

openshift/dynamic/client.py

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#!/usr/bin/env python
22

3+
import os
34
import sys
45
import copy
56
import json
7+
import base64
8+
import tempfile
69
from functools import partial
7-
from six import PY2
10+
from six import PY2, PY3
811

912
import yaml
1013
from pprint import pformat
@@ -32,6 +35,28 @@
3235
'ResourceField',
3336
]
3437

38+
class CacheEncoder(json.JSONEncoder):
39+
40+
def default(self, o):
41+
return o.to_dict()
42+
43+
def cache_decoder(client):
44+
45+
class CacheDecoder(json.JSONDecoder):
46+
def __init__(self, *args, **kwargs):
47+
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
48+
49+
def object_hook(self, obj):
50+
if '_type' not in obj:
51+
return obj
52+
_type = obj.pop('_type')
53+
if _type == 'Resource':
54+
return Resource(client=client, **obj)
55+
elif _type == 'ResourceList':
56+
return ResourceList(obj['resource'])
57+
return obj
58+
59+
return CacheDecoder
3560

3661
def meta_request(func):
3762
""" Handles parsing response structure and translating API Exceptions """
@@ -66,18 +91,45 @@ class DynamicClient(object):
6691
the kubernetes API
6792
"""
6893

69-
def __init__(self, client):
94+
def __init__(self, client, cache_file=None):
7095
self.client = client
7196
self.configuration = client.configuration
97+
default_cache_id = self.configuration.host
98+
if PY3:
99+
default_cache_id = default_cache_id.encode('utf-8')
100+
default_cachefile_name = 'osrcp-{0}.json'.format(base64.b64encode(default_cache_id).decode('utf-8'))
101+
self.__resources = ResourceContainer({}, client=self)
102+
self.__cache_file = cache_file or os.path.join(tempfile.gettempdir(), default_cachefile_name)
103+
self.__init_cache()
104+
105+
def __init_cache(self, refresh=False):
106+
if refresh or not os.path.exists(self.__cache_file):
107+
self.__cache = {}
108+
refresh = True
109+
else:
110+
with open(self.__cache_file, 'r') as f:
111+
self.__cache = json.load(f, cls=cache_decoder(self))
72112
self._load_server_info()
73-
self.__resources = ResourceContainer(self.parse_api_groups())
113+
self.__resources.update(self.parse_api_groups())
114+
115+
if refresh:
116+
self.__write_cache()
117+
118+
def __write_cache(self):
119+
with open(self.__cache_file, 'w') as f:
120+
json.dump(self.__cache, f, cls=CacheEncoder)
121+
122+
def invalidate_cache(self):
123+
self.__init_cache(refresh=True)
74124

75125
def _load_server_info(self):
76-
self.__version = {'kubernetes': load_json(self.request('get', '/version'))}
77-
try:
78-
self.__version['openshift'] = load_json(self.request('get', '/version/openshift'))
79-
except ApiException:
80-
pass
126+
if not self.__cache.get('version'):
127+
self.__cache['version'] = {'kubernetes': load_json(self.request('get', '/version'))}
128+
try:
129+
self.__cache['version']['openshift'] = load_json(self.request('get', '/version/openshift'))
130+
except ApiException:
131+
pass
132+
self.__version = self.__cache['version']
81133

82134
@property
83135
def resources(self):
@@ -102,20 +154,22 @@ def default_groups(self):
102154

103155
def parse_api_groups(self):
104156
""" Discovers all API groups present in the cluster """
105-
prefix = 'apis'
106-
groups_response = load_json(self.request('GET', '/{}'.format(prefix)))['groups']
107-
108-
groups = self.default_groups()
109-
groups[prefix] = {}
110-
111-
for group in groups_response:
112-
new_group = {}
113-
for version_raw in group['versions']:
114-
version = version_raw['version']
115-
preferred = version_raw == group['preferredVersion']
116-
new_group[version] = self.get_resources_for_api_version(prefix, group['name'], version, preferred)
117-
groups[prefix][group['name']] = new_group
118-
return groups
157+
if not self.__cache.get('resources'):
158+
prefix = 'apis'
159+
groups_response = load_json(self.request('GET', '/{}'.format(prefix)))['groups']
160+
161+
groups = self.default_groups()
162+
groups[prefix] = {}
163+
164+
for group in groups_response:
165+
new_group = {}
166+
for version_raw in group['versions']:
167+
version = version_raw['version']
168+
preferred = version_raw == group['preferredVersion']
169+
new_group[version] = self.get_resources_for_api_version(prefix, group['name'], version, preferred)
170+
groups[prefix][group['name']] = new_group
171+
self.__cache['resources'] = groups
172+
return self.__cache['resources']
119173

120174
def get_resources_for_api_version(self, prefix, group, version, preferred):
121175
""" returns a dictionary of resources associated with provided groupVersion"""
@@ -369,6 +423,24 @@ def __init__(self, prefix=None, group=None, api_version=None, kind=None,
369423

370424
self.extra_args = kwargs
371425

426+
def to_dict(self):
427+
return {
428+
'_type': 'Resource',
429+
'prefix': self.prefix,
430+
'group': self.group,
431+
'api_version': self.api_version,
432+
'kind': self.kind,
433+
'namespaced': self.namespaced,
434+
'verbs': self.verbs,
435+
'name': self.name,
436+
'preferred': self.preferred,
437+
'singular_name': self.singular_name,
438+
'short_names': self.short_names,
439+
'categories': self.categories,
440+
'subresources': {k: sr.to_dict() for k, sr in self.subresources.items()},
441+
'extra_args': self.extra_args,
442+
}
443+
372444
@property
373445
def group_version(self):
374446
if self.group:
@@ -455,6 +527,12 @@ def patch(self, *args, **kwargs):
455527
def __getattr__(self, name):
456528
return getattr(self.resource, name)
457529

530+
def to_dict(self):
531+
return {
532+
'_type': 'ResourceList',
533+
'resource': self.resource.to_dict(),
534+
}
535+
458536

459537
class Subresource(Resource):
460538
""" Represents a subresource of an API resource. This generally includes operations
@@ -498,13 +576,27 @@ def urls(self):
498576
def __getattr__(self, name):
499577
return partial(getattr(self.parent.client, name), self)
500578

579+
def to_dict(self):
580+
return {
581+
'kind': self.kind,
582+
'name': self.name,
583+
'subresource': self.subresource,
584+
'namespaced': self.namespaced,
585+
'verbs': self.verbs,
586+
'extra_args': self.extra_args,
587+
}
588+
501589

502590
class ResourceContainer(object):
503591
""" A convenient container for storing discovered API resources. Allows
504592
easy searching and retrieval of specific resources
505593
"""
506594

507-
def __init__(self, resources):
595+
def __init__(self, resources, client=None):
596+
self.__resources = resources
597+
self.__client = client
598+
599+
def update(self, resources):
508600
self.__resources = resources
509601

510602
@property
@@ -544,7 +636,11 @@ def search(self, **kwargs):
544636
545637
The arbitrary arguments can be any valid attribute for an openshift.dynamic.Resource object
546638
"""
547-
return self.__search(self.__build_search(**kwargs), self.__resources)
639+
results = self.__search(self.__build_search(**kwargs), self.__resources)
640+
if not results:
641+
self.__client.invalidate_cache()
642+
results = self.__search(self.__build_search(**kwargs), self.__resources)
643+
return results
548644

549645
def __build_search(self, kind=None, api_version=None, prefix=None, **kwargs):
550646
if api_version and '/' in api_version:

0 commit comments

Comments
 (0)