|
| 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