Skip to content
This repository was archived by the owner on Mar 13, 2022. It is now read-only.

Commit 4b8e89f

Browse files
authored
Merge pull request #56 from fabianvf/dynamic-client
Dynamic Client
2 parents ec31e05 + 53c4cb2 commit 4b8e89f

File tree

6 files changed

+1584
-0
lines changed

6 files changed

+1584
-0
lines changed

dynamic/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2019 The Kubernetes Authors.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
from .client import * # NOQA

dynamic/client.py

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2019 The Kubernetes Authors.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import six
18+
import json
19+
20+
from kubernetes import watch
21+
from kubernetes.client.rest import ApiException
22+
23+
from .discovery import EagerDiscoverer, LazyDiscoverer
24+
from .exceptions import api_exception, KubernetesValidateMissing
25+
from .resource import Resource, ResourceList, Subresource, ResourceInstance, ResourceField
26+
27+
try:
28+
import kubernetes_validate
29+
HAS_KUBERNETES_VALIDATE = True
30+
except ImportError:
31+
HAS_KUBERNETES_VALIDATE = False
32+
33+
try:
34+
from kubernetes_validate.utils import VersionNotSupportedError
35+
except ImportError:
36+
class VersionNotSupportedError(NotImplementedError):
37+
pass
38+
39+
__all__ = [
40+
'DynamicClient',
41+
'ResourceInstance',
42+
'Resource',
43+
'ResourceList',
44+
'Subresource',
45+
'EagerDiscoverer',
46+
'LazyDiscoverer',
47+
'ResourceField',
48+
]
49+
50+
51+
def meta_request(func):
52+
""" Handles parsing response structure and translating API Exceptions """
53+
def inner(self, *args, **kwargs):
54+
serialize_response = kwargs.pop('serialize', True)
55+
serializer = kwargs.pop('serializer', ResourceInstance)
56+
try:
57+
resp = func(self, *args, **kwargs)
58+
except ApiException as e:
59+
raise api_exception(e)
60+
if serialize_response:
61+
try:
62+
if six.PY2:
63+
return serializer(self, json.loads(resp.data))
64+
return serializer(self, json.loads(resp.data.decode('utf8')))
65+
except ValueError:
66+
if six.PY2:
67+
return resp.data
68+
return resp.data.decode('utf8')
69+
return resp
70+
71+
return inner
72+
73+
74+
class DynamicClient(object):
75+
""" A kubernetes client that dynamically discovers and interacts with
76+
the kubernetes API
77+
"""
78+
79+
def __init__(self, client, cache_file=None, discoverer=None):
80+
# Setting default here to delay evaluation of LazyDiscoverer class
81+
# until constructor is called
82+
discoverer = discoverer or LazyDiscoverer
83+
84+
self.client = client
85+
self.configuration = client.configuration
86+
self.__discoverer = discoverer(self, cache_file)
87+
88+
@property
89+
def resources(self):
90+
return self.__discoverer
91+
92+
@property
93+
def version(self):
94+
return self.__discoverer.version
95+
96+
def ensure_namespace(self, resource, namespace, body):
97+
namespace = namespace or body.get('metadata', {}).get('namespace')
98+
if not namespace:
99+
raise ValueError("Namespace is required for {}.{}".format(resource.group_version, resource.kind))
100+
return namespace
101+
102+
def serialize_body(self, body):
103+
if hasattr(body, 'to_dict'):
104+
return body.to_dict()
105+
return body or {}
106+
107+
def get(self, resource, name=None, namespace=None, **kwargs):
108+
path = resource.path(name=name, namespace=namespace)
109+
return self.request('get', path, **kwargs)
110+
111+
def create(self, resource, body=None, namespace=None, **kwargs):
112+
body = self.serialize_body(body)
113+
if resource.namespaced:
114+
namespace = self.ensure_namespace(resource, namespace, body)
115+
path = resource.path(namespace=namespace)
116+
return self.request('post', path, body=body, **kwargs)
117+
118+
def delete(self, resource, name=None, namespace=None, body=None, label_selector=None, field_selector=None, **kwargs):
119+
if not (name or label_selector or field_selector):
120+
raise ValueError("At least one of name|label_selector|field_selector is required")
121+
if resource.namespaced and not (label_selector or field_selector or namespace):
122+
raise ValueError("At least one of namespace|label_selector|field_selector is required")
123+
path = resource.path(name=name, namespace=namespace)
124+
return self.request('delete', path, body=body, label_selector=label_selector, field_selector=field_selector, **kwargs)
125+
126+
def replace(self, resource, body=None, name=None, namespace=None, **kwargs):
127+
body = self.serialize_body(body)
128+
name = name or body.get('metadata', {}).get('name')
129+
if not name:
130+
raise ValueError("name is required to replace {}.{}".format(resource.group_version, resource.kind))
131+
if resource.namespaced:
132+
namespace = self.ensure_namespace(resource, namespace, body)
133+
path = resource.path(name=name, namespace=namespace)
134+
return self.request('put', path, body=body, **kwargs)
135+
136+
def patch(self, resource, body=None, name=None, namespace=None, **kwargs):
137+
body = self.serialize_body(body)
138+
name = name or body.get('metadata', {}).get('name')
139+
if not name:
140+
raise ValueError("name is required to patch {}.{}".format(resource.group_version, resource.kind))
141+
if resource.namespaced:
142+
namespace = self.ensure_namespace(resource, namespace, body)
143+
144+
content_type = kwargs.pop('content_type', 'application/strategic-merge-patch+json')
145+
path = resource.path(name=name, namespace=namespace)
146+
147+
return self.request('patch', path, body=body, content_type=content_type, **kwargs)
148+
149+
def watch(self, resource, namespace=None, name=None, label_selector=None, field_selector=None, resource_version=None, timeout=None):
150+
"""
151+
Stream events for a resource from the Kubernetes API
152+
153+
:param resource: The API resource object that will be used to query the API
154+
:param namespace: The namespace to query
155+
:param name: The name of the resource instance to query
156+
:param label_selector: The label selector with which to filter results
157+
:param field_selector: The field selector with which to filter results
158+
:param resource_version: The version with which to filter results. Only events with
159+
a resource_version greater than this value will be returned
160+
:param timeout: The amount of time in seconds to wait before terminating the stream
161+
162+
:return: Event object with these keys:
163+
'type': The type of event such as "ADDED", "DELETED", etc.
164+
'raw_object': a dict representing the watched object.
165+
'object': A ResourceInstance wrapping raw_object.
166+
167+
Example:
168+
client = DynamicClient(k8s_client)
169+
v1_pods = client.resources.get(api_version='v1', kind='Pod')
170+
171+
for e in v1_pods.watch(resource_version=0, namespace=default, timeout=5):
172+
print(e['type'])
173+
print(e['object'].metadata)
174+
"""
175+
watcher = watch.Watch()
176+
for event in watcher.stream(
177+
resource.get,
178+
namespace=namespace,
179+
name=name,
180+
field_selector=field_selector,
181+
label_selector=label_selector,
182+
resource_version=resource_version,
183+
serialize=False,
184+
timeout_seconds=timeout
185+
):
186+
event['object'] = ResourceInstance(resource, event['object'])
187+
yield event
188+
189+
@meta_request
190+
def request(self, method, path, body=None, **params):
191+
if not path.startswith('/'):
192+
path = '/' + path
193+
194+
path_params = params.get('path_params', {})
195+
query_params = params.get('query_params', [])
196+
if params.get('pretty') is not None:
197+
query_params.append(('pretty', params['pretty']))
198+
if params.get('_continue') is not None:
199+
query_params.append(('continue', params['_continue']))
200+
if params.get('include_uninitialized') is not None:
201+
query_params.append(('includeUninitialized', params['include_uninitialized']))
202+
if params.get('field_selector') is not None:
203+
query_params.append(('fieldSelector', params['field_selector']))
204+
if params.get('label_selector') is not None:
205+
query_params.append(('labelSelector', params['label_selector']))
206+
if params.get('limit') is not None:
207+
query_params.append(('limit', params['limit']))
208+
if params.get('resource_version') is not None:
209+
query_params.append(('resourceVersion', params['resource_version']))
210+
if params.get('timeout_seconds') is not None:
211+
query_params.append(('timeoutSeconds', params['timeout_seconds']))
212+
if params.get('watch') is not None:
213+
query_params.append(('watch', params['watch']))
214+
if params.get('grace_period_seconds') is not None:
215+
query_params.append(('gracePeriodSeconds', params['grace_period_seconds']))
216+
if params.get('propagation_policy') is not None:
217+
query_params.append(('propagationPolicy', params['propagation_policy']))
218+
if params.get('orphan_dependents') is not None:
219+
query_params.append(('orphanDependents', params['orphan_dependents']))
220+
221+
header_params = params.get('header_params', {})
222+
form_params = []
223+
local_var_files = {}
224+
# HTTP header `Accept`
225+
header_params['Accept'] = self.client.select_header_accept([
226+
'application/json',
227+
'application/yaml',
228+
])
229+
230+
# HTTP header `Content-Type`
231+
if params.get('content_type'):
232+
header_params['Content-Type'] = params['content_type']
233+
else:
234+
header_params['Content-Type'] = self.client.select_header_content_type(['*/*'])
235+
236+
# Authentication setting
237+
auth_settings = ['BearerToken']
238+
239+
return self.client.call_api(
240+
path,
241+
method.upper(),
242+
path_params,
243+
query_params,
244+
header_params,
245+
body=body,
246+
post_params=form_params,
247+
async_req=params.get('async_req'),
248+
files=local_var_files,
249+
auth_settings=auth_settings,
250+
_preload_content=False,
251+
_return_http_data_only=params.get('_return_http_data_only', True)
252+
)
253+
254+
def validate(self, definition, version=None, strict=False):
255+
"""validate checks a kubernetes resource definition
256+
257+
Args:
258+
definition (dict): resource definition
259+
version (str): version of kubernetes to validate against
260+
strict (bool): whether unexpected additional properties should be considered errors
261+
262+
Returns:
263+
warnings (list), errors (list): warnings are missing validations, errors are validation failures
264+
"""
265+
if not HAS_KUBERNETES_VALIDATE:
266+
raise KubernetesValidateMissing()
267+
268+
errors = list()
269+
warnings = list()
270+
try:
271+
if version is None:
272+
try:
273+
version = self.version['kubernetes']['gitVersion']
274+
except KeyError:
275+
version = kubernetes_validate.latest_version()
276+
kubernetes_validate.validate(definition, version, strict)
277+
except kubernetes_validate.utils.ValidationError as e:
278+
errors.append("resource definition validation error at %s: %s" % ('.'.join([str(item) for item in e.path]), e.message)) # noqa: B306
279+
except VersionNotSupportedError:
280+
errors.append("Kubernetes version %s is not supported by kubernetes-validate" % version)
281+
except kubernetes_validate.utils.SchemaNotFoundError as e:
282+
warnings.append("Could not find schema for object kind %s with API version %s in Kubernetes version %s (possibly Custom Resource?)" %
283+
(e.kind, e.api_version, e.version))
284+
return warnings, errors

0 commit comments

Comments
 (0)