kubernator 1.0.14__py3-none-any.whl → 1.0.24.dev20251109010128__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kubernator/__init__.py +1 -1
- kubernator/api.py +189 -73
- kubernator/app.py +43 -25
- kubernator/plugins/gke.py +105 -0
- kubernator/plugins/helm.py +107 -18
- kubernator/plugins/istio.py +150 -37
- kubernator/plugins/k8s.py +158 -59
- kubernator/plugins/k8s_api.py +30 -26
- kubernator/plugins/kops.py +5 -4
- kubernator/plugins/kubectl.py +35 -4
- kubernator/plugins/minikube.py +48 -3
- kubernator/plugins/template.py +2 -2
- kubernator/proc.py +24 -2
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/METADATA +32 -17
- kubernator-1.0.24.dev20251109010128.dist-info/RECORD +31 -0
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/WHEEL +1 -1
- kubernator-1.0.14.dist-info/RECORD +0 -30
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/entry_points.txt +0 -0
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/namespace_packages.txt +0 -0
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/top_level.txt +0 -0
- {kubernator-1.0.14.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/zip-safe +0 -0
kubernator/plugins/k8s.py
CHANGED
|
@@ -30,6 +30,7 @@ from typing import Iterable, Callable, Sequence
|
|
|
30
30
|
|
|
31
31
|
import jsonpatch
|
|
32
32
|
import yaml
|
|
33
|
+
from kubernetes.client import ApiException
|
|
33
34
|
|
|
34
35
|
from kubernator.api import (KubernatorPlugin,
|
|
35
36
|
Globs,
|
|
@@ -38,7 +39,9 @@ from kubernator.api import (KubernatorPlugin,
|
|
|
38
39
|
FileType,
|
|
39
40
|
load_remote_file,
|
|
40
41
|
StripNL,
|
|
41
|
-
install_python_k8s_client
|
|
42
|
+
install_python_k8s_client,
|
|
43
|
+
TemplateEngine,
|
|
44
|
+
sleep)
|
|
42
45
|
from kubernator.merge import extract_merge_instructions, apply_merge_instructions
|
|
43
46
|
from kubernator.plugins.k8s_api import (K8SResourcePluginMixin,
|
|
44
47
|
K8SResource,
|
|
@@ -68,6 +71,34 @@ def final_resource_validator(resources: Sequence[K8SResource],
|
|
|
68
71
|
resource, resource.source)
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
def normalize_pkg_version(v: str):
|
|
75
|
+
v_split = v.split(".")
|
|
76
|
+
rev = v_split[-1]
|
|
77
|
+
if not rev.isdigit():
|
|
78
|
+
new_rev = ""
|
|
79
|
+
for c in rev:
|
|
80
|
+
if not c.isdigit():
|
|
81
|
+
break
|
|
82
|
+
new_rev += c
|
|
83
|
+
v_split[-1] = new_rev
|
|
84
|
+
return tuple(map(int, v_split))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def api_exc_normalize_body(e: "ApiException"):
|
|
88
|
+
if e.headers and "content-type" in e.headers:
|
|
89
|
+
content_type = e.headers["content-type"]
|
|
90
|
+
if content_type == "application/json" or content_type.endswith("+json"):
|
|
91
|
+
e.body = json.loads(e.body)
|
|
92
|
+
elif (content_type in ("application/yaml", "application/x-yaml", "text/yaml",
|
|
93
|
+
"text/x-yaml") or content_type.endswith("+yaml")):
|
|
94
|
+
e.body = yaml.safe_load(e.body)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def api_exc_format_body(e: ApiException):
|
|
98
|
+
if not isinstance(e.body, (str, bytes)):
|
|
99
|
+
e.body = json.dumps(e.body, indent=4)
|
|
100
|
+
|
|
101
|
+
|
|
71
102
|
class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
72
103
|
logger = logger
|
|
73
104
|
|
|
@@ -81,6 +112,9 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
81
112
|
|
|
82
113
|
self._transformers = []
|
|
83
114
|
self._validators = []
|
|
115
|
+
self._manifest_patchers = []
|
|
116
|
+
self._summary = 0, 0, 0
|
|
117
|
+
self._template_engine = TemplateEngine(logger)
|
|
84
118
|
|
|
85
119
|
def set_context(self, context):
|
|
86
120
|
self.context = context
|
|
@@ -104,6 +138,8 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
104
138
|
("apps", "StatefulSet"): K8SPropagationPolicy.ORPHAN,
|
|
105
139
|
("apps", "Deployment"): K8SPropagationPolicy.ORPHAN,
|
|
106
140
|
("storage.k8s.io", "StorageClass"): K8SPropagationPolicy.ORPHAN,
|
|
141
|
+
(None, "Pod"): K8SPropagationPolicy.BACKGROUND,
|
|
142
|
+
("batch", "Job"): K8SPropagationPolicy.ORPHAN,
|
|
107
143
|
},
|
|
108
144
|
default_includes=Globs(["*.yaml", "*.yml"], True),
|
|
109
145
|
default_excludes=Globs([".*"], True),
|
|
@@ -115,12 +151,14 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
115
151
|
add_transformer=self.api_add_transformer,
|
|
116
152
|
remove_transformer=self.api_remove_transformer,
|
|
117
153
|
add_validator=self.api_remove_validator,
|
|
154
|
+
add_manifest_patcher=self.api_add_manifest_patcher,
|
|
118
155
|
get_api_versions=self.get_api_versions,
|
|
119
156
|
create_resource=self.create_resource,
|
|
120
157
|
disable_client_patches=disable_client_patches,
|
|
121
158
|
field_validation=field_validation,
|
|
122
159
|
field_validation_warn_fatal=field_validation_warn_fatal,
|
|
123
160
|
field_validation_warnings=0,
|
|
161
|
+
conflict_retry_delay=0.3,
|
|
124
162
|
_k8s=self,
|
|
125
163
|
)
|
|
126
164
|
context.k8s = dict(default_includes=Globs(context.globals.k8s.default_includes),
|
|
@@ -150,7 +188,7 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
150
188
|
|
|
151
189
|
logger.info("Using Kubernetes client version =~%s.0 for server version %s",
|
|
152
190
|
server_minor, ".".join(k8s.server_version))
|
|
153
|
-
pkg_dir = install_python_k8s_client(self.context.app.
|
|
191
|
+
pkg_dir = install_python_k8s_client(self.context.app.run_passthrough_capturing, server_minor, logger,
|
|
154
192
|
stdout_logger, stderr_logger, k8s.disable_client_patches)
|
|
155
193
|
|
|
156
194
|
modules_to_delete = []
|
|
@@ -183,7 +221,7 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
183
221
|
|
|
184
222
|
k8s.client = self._setup_k8s_client()
|
|
185
223
|
version = client.VersionApi(k8s.client).get_code()
|
|
186
|
-
if "-eks-" in version.git_version:
|
|
224
|
+
if "-eks-" or "-gke" in version.git_version:
|
|
187
225
|
git_version = version.git_version.split("-")[0]
|
|
188
226
|
else:
|
|
189
227
|
git_version = version.git_version
|
|
@@ -192,7 +230,8 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
192
230
|
k8s.server_git_version = git_version
|
|
193
231
|
|
|
194
232
|
logger.info("Found Kubernetes %s on %s", k8s.server_git_version, k8s.client.configuration.host)
|
|
195
|
-
|
|
233
|
+
|
|
234
|
+
K8SResource._k8s_client_version = normalize_pkg_version(pkg_version("kubernetes"))
|
|
196
235
|
K8SResource._k8s_field_validation = k8s.field_validation
|
|
197
236
|
K8SResource._k8s_field_validation_patched = not k8s.disable_client_patches
|
|
198
237
|
K8SResource._logger = self.logger
|
|
@@ -224,7 +263,10 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
224
263
|
display_p = context.app.display_path(p)
|
|
225
264
|
logger.debug("Adding Kubernetes manifest from %s", display_p)
|
|
226
265
|
|
|
227
|
-
manifests = load_file(logger, p, FileType.YAML, display_p
|
|
266
|
+
manifests = load_file(logger, p, FileType.YAML, display_p,
|
|
267
|
+
self._template_engine,
|
|
268
|
+
{"ktor": context}
|
|
269
|
+
)
|
|
228
270
|
|
|
229
271
|
for manifest in manifests:
|
|
230
272
|
if manifest:
|
|
@@ -248,6 +290,7 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
248
290
|
|
|
249
291
|
patch_field_excludes = [re.compile(e) for e in context.globals.k8s.patch_field_excludes]
|
|
250
292
|
dump_results = []
|
|
293
|
+
total_created, total_patched, total_deleted = 0, 0, 0
|
|
251
294
|
for resource in self.resources.values():
|
|
252
295
|
if dump:
|
|
253
296
|
resource_id = {"apiVersion": resource.api_version,
|
|
@@ -280,13 +323,17 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
280
323
|
create_func = partial(resource.create, dry_run=dry_run)
|
|
281
324
|
delete_func = partial(resource.delete, dry_run=dry_run)
|
|
282
325
|
|
|
283
|
-
self._apply_resource(dry_run,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
326
|
+
created, patched, deleted = self._apply_resource(dry_run,
|
|
327
|
+
patch_field_excludes,
|
|
328
|
+
resource,
|
|
329
|
+
patch_func,
|
|
330
|
+
create_func,
|
|
331
|
+
delete_func,
|
|
332
|
+
status_msg)
|
|
333
|
+
|
|
334
|
+
total_created += created
|
|
335
|
+
total_patched += patched
|
|
336
|
+
total_deleted += deleted
|
|
290
337
|
|
|
291
338
|
if ((dump or dry_run) and
|
|
292
339
|
k8s.field_validation_warn_fatal and self.context.globals.k8s.field_validation_warnings):
|
|
@@ -301,6 +348,12 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
301
348
|
indent=4 if file_format == "json-pretty" else None)
|
|
302
349
|
else:
|
|
303
350
|
yaml.safe_dump(dump_results, file)
|
|
351
|
+
else:
|
|
352
|
+
self._summary = total_created, total_patched, total_deleted
|
|
353
|
+
|
|
354
|
+
def handle_summary(self):
|
|
355
|
+
total_created, total_patched, total_deleted = self._summary
|
|
356
|
+
logger.info("Created %d, patched %d, deleted %d resources", total_created, total_patched, total_deleted)
|
|
304
357
|
|
|
305
358
|
def api_load_resources(self, path: Path, file_type: str):
|
|
306
359
|
return self.add_local_resources(path, FileType[file_type.upper()])
|
|
@@ -322,6 +375,10 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
322
375
|
if validator not in self._validators:
|
|
323
376
|
self._validators.append(validator)
|
|
324
377
|
|
|
378
|
+
def api_add_manifest_patcher(self, patcher):
|
|
379
|
+
if patcher not in self._manifest_patchers:
|
|
380
|
+
self._manifest_patchers.append(patcher)
|
|
381
|
+
|
|
325
382
|
def api_remove_transformer(self, transformer):
|
|
326
383
|
if transformer in self._transformers:
|
|
327
384
|
self._transformers.remove(transformer)
|
|
@@ -340,6 +397,17 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
340
397
|
frame = frame.f_back
|
|
341
398
|
return ValueError((msg % args) if args else msg).with_traceback(tb)
|
|
342
399
|
|
|
400
|
+
def _patch_manifest(self,
|
|
401
|
+
manifest: dict,
|
|
402
|
+
resource_description: str):
|
|
403
|
+
for patcher in reversed(self._manifest_patchers):
|
|
404
|
+
logger.debug("Applying patcher %s to %s",
|
|
405
|
+
getattr(patcher, "__name__", patcher),
|
|
406
|
+
resource_description)
|
|
407
|
+
manifest = patcher(manifest, resource_description) or manifest
|
|
408
|
+
|
|
409
|
+
return manifest
|
|
410
|
+
|
|
343
411
|
def _transform_resource(self, resources: Sequence[K8SResource], resource: K8SResource) -> K8SResource:
|
|
344
412
|
for transformer in reversed(self._transformers):
|
|
345
413
|
logger.debug("Applying transformer %s to %s from %s",
|
|
@@ -378,8 +446,8 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
378
446
|
|
|
379
447
|
def handle_400_strict_validation_error(e: ApiException):
|
|
380
448
|
if e.status == 400:
|
|
381
|
-
|
|
382
|
-
|
|
449
|
+
# Assumes the body has been parsed
|
|
450
|
+
status = e.body
|
|
383
451
|
if status["status"] == "Failure":
|
|
384
452
|
if FIELD_VALIDATION_STRICT_MARKER in status["message"]:
|
|
385
453
|
message = status["message"]
|
|
@@ -394,18 +462,31 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
394
462
|
resource, resource.source, status["message"])
|
|
395
463
|
raise e from None
|
|
396
464
|
|
|
397
|
-
def create(exists_ok=False):
|
|
465
|
+
def create(exists_ok=False, wait_for_delete=False):
|
|
398
466
|
logger.info("Creating resource %s%s%s", resource, status_msg,
|
|
399
467
|
" (ignoring existing)" if exists_ok else "")
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
468
|
+
while True:
|
|
469
|
+
try:
|
|
470
|
+
create_func()
|
|
471
|
+
return
|
|
472
|
+
except ApiException as __e:
|
|
473
|
+
api_exc_normalize_body(__e)
|
|
474
|
+
try:
|
|
475
|
+
if exists_ok or wait_for_delete:
|
|
476
|
+
if __e.status == 409:
|
|
477
|
+
status = __e.body
|
|
478
|
+
if status["reason"] == "AlreadyExists":
|
|
479
|
+
if wait_for_delete:
|
|
480
|
+
sleep(self.context.k8s.conflict_retry_delay)
|
|
481
|
+
logger.info("Retry creating resource %s%s%s", resource, status_msg,
|
|
482
|
+
" (ignoring existing)" if exists_ok else "")
|
|
483
|
+
continue
|
|
484
|
+
else:
|
|
485
|
+
return
|
|
486
|
+
raise
|
|
487
|
+
except ApiException as ___e:
|
|
488
|
+
api_exc_format_body(___e)
|
|
489
|
+
raise
|
|
409
490
|
|
|
410
491
|
merge_instrs, normalized_manifest = extract_merge_instructions(resource.manifest, resource)
|
|
411
492
|
if merge_instrs:
|
|
@@ -419,14 +500,20 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
419
500
|
remote_resource = resource.get()
|
|
420
501
|
logger.trace("Current resource %s: %s", resource, remote_resource)
|
|
421
502
|
except ApiException as e:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
503
|
+
api_exc_normalize_body(e)
|
|
504
|
+
try:
|
|
505
|
+
if e.status == 404:
|
|
506
|
+
try:
|
|
507
|
+
create()
|
|
508
|
+
return 1, 0, 0
|
|
509
|
+
except ApiException as e:
|
|
510
|
+
api_exc_normalize_body(e)
|
|
511
|
+
if not handle_400_strict_validation_error(e):
|
|
512
|
+
raise
|
|
513
|
+
else:
|
|
514
|
+
raise
|
|
515
|
+
except ApiException as _e:
|
|
516
|
+
api_exc_format_body(_e)
|
|
430
517
|
raise
|
|
431
518
|
else:
|
|
432
519
|
logger.trace("Attempting to retrieve a normalized patch for resource %s: %s", resource, normalized_manifest)
|
|
@@ -436,34 +523,44 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
436
523
|
dry_run=True,
|
|
437
524
|
force=True)
|
|
438
525
|
except ApiException as e:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
526
|
+
try:
|
|
527
|
+
api_exc_normalize_body(e)
|
|
528
|
+
|
|
529
|
+
if e.status == 422:
|
|
530
|
+
status = e.body
|
|
531
|
+
# Assumes the body has been unmarshalled
|
|
532
|
+
details = status["details"]
|
|
533
|
+
immutable_key = details.get("group"), details["kind"]
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
propagation_policy = self.context.k8s.immutable_changes[immutable_key]
|
|
537
|
+
except KeyError:
|
|
538
|
+
raise e from None
|
|
539
|
+
else:
|
|
540
|
+
for cause in details["causes"]:
|
|
541
|
+
if (
|
|
542
|
+
cause["reason"] == "FieldValueInvalid" and
|
|
543
|
+
"field is immutable" in cause["message"]
|
|
544
|
+
or
|
|
545
|
+
cause["reason"] == "FieldValueForbidden" and
|
|
546
|
+
("Forbidden: updates to" in cause["message"]
|
|
547
|
+
or
|
|
548
|
+
"Forbidden: pod updates" in cause["message"])
|
|
549
|
+
):
|
|
550
|
+
logger.info("Deleting resource %s (cascade %s)%s", resource,
|
|
551
|
+
propagation_policy.policy,
|
|
552
|
+
status_msg)
|
|
553
|
+
delete_func(propagation_policy=propagation_policy)
|
|
554
|
+
create(exists_ok=dry_run, wait_for_delete=not dry_run)
|
|
555
|
+
return 1, 0, 1
|
|
556
|
+
raise
|
|
448
557
|
else:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"Forbidden: updates to" in cause["message"]
|
|
456
|
-
):
|
|
457
|
-
logger.info("Deleting resource %s (cascade %s)%s", resource,
|
|
458
|
-
propagation_policy.policy,
|
|
459
|
-
status_msg)
|
|
460
|
-
delete_func(propagation_policy=propagation_policy)
|
|
461
|
-
create(exists_ok=dry_run)
|
|
462
|
-
return
|
|
463
|
-
raise
|
|
464
|
-
else:
|
|
465
|
-
if not handle_400_strict_validation_error(e):
|
|
466
|
-
raise
|
|
558
|
+
if not handle_400_strict_validation_error(e):
|
|
559
|
+
raise
|
|
560
|
+
except ApiException as _e:
|
|
561
|
+
api_exc_format_body(_e)
|
|
562
|
+
raise
|
|
563
|
+
|
|
467
564
|
else:
|
|
468
565
|
logger.trace("Merged resource %s: %s", resource, merged_resource)
|
|
469
566
|
if merge_instrs:
|
|
@@ -476,8 +573,10 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
476
573
|
if patch:
|
|
477
574
|
logger.info("Patching resource %s%s", resource, status_msg)
|
|
478
575
|
patch_func(patch)
|
|
576
|
+
return 0, 1, 0
|
|
479
577
|
else:
|
|
480
578
|
logger.info("Nothing to patch for resource %s", resource)
|
|
579
|
+
return 0, 0, 0
|
|
481
580
|
|
|
482
581
|
def _filter_resource_patch(self, patch: Iterable[Mapping], excludes: Iterable[re.compile]):
|
|
483
582
|
result = []
|
kubernator/plugins/k8s_api.py
CHANGED
|
@@ -23,19 +23,17 @@ from collections import namedtuple
|
|
|
23
23
|
from collections.abc import Callable, Mapping, MutableMapping, Sequence, Iterable
|
|
24
24
|
from enum import Enum, auto
|
|
25
25
|
from functools import partial
|
|
26
|
-
from io import StringIO
|
|
27
26
|
from pathlib import Path
|
|
28
27
|
from typing import Union, Optional
|
|
29
28
|
|
|
30
29
|
import yaml
|
|
31
30
|
from jsonschema._format import FormatChecker
|
|
32
|
-
from jsonschema.
|
|
33
|
-
from jsonschema._validators import required
|
|
31
|
+
from jsonschema._keywords import required
|
|
34
32
|
from jsonschema.exceptions import ValidationError
|
|
35
|
-
from jsonschema.validators import extend, Draft7Validator
|
|
36
|
-
from openapi_schema_validator import
|
|
33
|
+
from jsonschema.validators import extend, Draft7Validator
|
|
34
|
+
from openapi_schema_validator import OAS31Validator
|
|
37
35
|
|
|
38
|
-
from kubernator.api import load_file, FileType, load_remote_file, calling_frame_source
|
|
36
|
+
from kubernator.api import load_file, FileType, load_remote_file, calling_frame_source, parse_yaml_docs
|
|
39
37
|
|
|
40
38
|
K8S_WARNING_HEADER = re.compile(r'(?:,\s*)?(\d{3})\s+(\S+)\s+"(.+?)(?<!\\)"(?:\s+\"(.+?)(?<!\\)\")?\s*')
|
|
41
39
|
UPPER_FOLLOWED_BY_LOWER_RE = re.compile(r"(.)([A-Z][a-z]+)")
|
|
@@ -89,11 +87,11 @@ def is_integer(instance):
|
|
|
89
87
|
# bool inherits from int, so ensure bools aren't reported as ints
|
|
90
88
|
if isinstance(instance, bool):
|
|
91
89
|
return False
|
|
92
|
-
return isinstance(instance,
|
|
90
|
+
return isinstance(instance, int)
|
|
93
91
|
|
|
94
92
|
|
|
95
93
|
def is_string(instance):
|
|
96
|
-
return isinstance(instance,
|
|
94
|
+
return isinstance(instance, str)
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
def type_validator(validator, data_type, instance, schema):
|
|
@@ -107,7 +105,7 @@ def type_validator(validator, data_type, instance, schema):
|
|
|
107
105
|
yield ValidationError("%r is not of type %s" % (instance, data_type))
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
K8SValidator = extend(
|
|
108
|
+
K8SValidator = extend(OAS31Validator, validators={
|
|
111
109
|
"type": type_validator,
|
|
112
110
|
"required": required
|
|
113
111
|
})
|
|
@@ -117,22 +115,22 @@ k8s_format_checker = FormatChecker()
|
|
|
117
115
|
|
|
118
116
|
@k8s_format_checker.checks("int32")
|
|
119
117
|
def check_int32(value):
|
|
120
|
-
return -2147483648 < value < 2147483647
|
|
118
|
+
return value is not None and (-2147483648 < value < 2147483647)
|
|
121
119
|
|
|
122
120
|
|
|
123
121
|
@k8s_format_checker.checks("int64")
|
|
124
122
|
def check_int64(value):
|
|
125
|
-
return -9223372036854775808 < value < 9223372036854775807
|
|
123
|
+
return value is not None and (-9223372036854775808 < value < 9223372036854775807)
|
|
126
124
|
|
|
127
125
|
|
|
128
126
|
@k8s_format_checker.checks("float")
|
|
129
127
|
def check_float(value):
|
|
130
|
-
return -3.4E+38 < value < +3.4E+38
|
|
128
|
+
return value is not None and (-3.4E+38 < value < +3.4E+38)
|
|
131
129
|
|
|
132
130
|
|
|
133
131
|
@k8s_format_checker.checks("double")
|
|
134
132
|
def check_double(value):
|
|
135
|
-
return -1.7E+308 < value < +1.7E+308
|
|
133
|
+
return value is not None and (-1.7E+308 < value < +1.7E+308)
|
|
136
134
|
|
|
137
135
|
|
|
138
136
|
@k8s_format_checker.checks("byte", ValueError)
|
|
@@ -148,10 +146,6 @@ def check_int_or_string(value):
|
|
|
148
146
|
return check_int32(value) if is_integer(value) else is_string(value)
|
|
149
147
|
|
|
150
148
|
|
|
151
|
-
# def make_api_version(group, version):
|
|
152
|
-
# return f"{group}/{version}" if group else version
|
|
153
|
-
|
|
154
|
-
|
|
155
149
|
def to_group_and_version(api_version):
|
|
156
150
|
group, _, version = api_version.partition("/")
|
|
157
151
|
if not version:
|
|
@@ -529,7 +523,7 @@ class K8SResourcePluginMixin:
|
|
|
529
523
|
source = calling_frame_source()
|
|
530
524
|
|
|
531
525
|
if isinstance(manifests, str):
|
|
532
|
-
manifests = list(
|
|
526
|
+
manifests = list(parse_yaml_docs(manifests, source))
|
|
533
527
|
|
|
534
528
|
if isinstance(manifests, (Mapping, dict)):
|
|
535
529
|
return self.add_resource(manifests, source)
|
|
@@ -562,7 +556,7 @@ class K8SResourcePluginMixin:
|
|
|
562
556
|
source = calling_frame_source()
|
|
563
557
|
|
|
564
558
|
if isinstance(manifests, str):
|
|
565
|
-
manifests = list(
|
|
559
|
+
manifests = list(parse_yaml_docs(manifests, source))
|
|
566
560
|
|
|
567
561
|
if isinstance(manifests, (Mapping, dict)):
|
|
568
562
|
return self.add_crd(manifests, source)
|
|
@@ -619,14 +613,19 @@ class K8SResourcePluginMixin:
|
|
|
619
613
|
|
|
620
614
|
def _create_resource(self, manifest: dict, source: Union[str, Path] = None):
|
|
621
615
|
resource_description = K8SResource.get_manifest_description(manifest, source)
|
|
622
|
-
self.logger.debug("Validating K8S manifest for %s", resource_description)
|
|
623
616
|
|
|
617
|
+
new_manifest = self._patch_manifest(manifest, resource_description)
|
|
618
|
+
if new_manifest != manifest:
|
|
619
|
+
manifest = new_manifest
|
|
620
|
+
resource_description = K8SResource.get_manifest_description(manifest, source)
|
|
621
|
+
|
|
622
|
+
self.logger.debug("Validating K8S manifest for %s", resource_description)
|
|
624
623
|
errors = list(self._validate_resource(manifest, source))
|
|
625
624
|
if errors:
|
|
626
625
|
for error in errors:
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
626
|
+
self.logger.error("Error detected in K8S manifest %s from %s: \n%s",
|
|
627
|
+
resource_description, source or "<unknown>", yaml.safe_dump(manifest, None),
|
|
628
|
+
exc_info=error)
|
|
630
629
|
raise errors[0]
|
|
631
630
|
|
|
632
631
|
rdef = self._get_manifest_rdef(manifest)
|
|
@@ -649,6 +648,11 @@ class K8SResourcePluginMixin:
|
|
|
649
648
|
|
|
650
649
|
return resource
|
|
651
650
|
|
|
651
|
+
def _patch_manifest(self,
|
|
652
|
+
manifest: dict,
|
|
653
|
+
resource_description: str):
|
|
654
|
+
return manifest
|
|
655
|
+
|
|
652
656
|
def _transform_resource(self,
|
|
653
657
|
resources: Sequence[K8SResource],
|
|
654
658
|
resource: K8SResource) -> K8SResource:
|
|
@@ -663,10 +667,8 @@ class K8SResourcePluginMixin:
|
|
|
663
667
|
yield error
|
|
664
668
|
else:
|
|
665
669
|
rdef = error
|
|
666
|
-
# schema = ChainMap(manifest, self.resource_definitions_schema)
|
|
667
670
|
k8s_validator = K8SValidator(rdef.schema,
|
|
668
|
-
format_checker=k8s_format_checker
|
|
669
|
-
resolver=RefResolver.from_schema(self.resource_definitions_schema))
|
|
671
|
+
format_checker=k8s_format_checker)
|
|
670
672
|
yield from k8s_validator.iter_errors(manifest)
|
|
671
673
|
|
|
672
674
|
def _get_manifest_rdef(self, manifest):
|
|
@@ -744,6 +746,8 @@ class K8SResourcePluginMixin:
|
|
|
744
746
|
rdef_paths[path] = actions
|
|
745
747
|
|
|
746
748
|
for k, schema in k8s_def["definitions"].items():
|
|
749
|
+
# This short-circuits the resolution of the references to the top of the document
|
|
750
|
+
schema["definitions"] = k8s_def["definitions"]
|
|
747
751
|
for key in k8s_resource_def_key(schema):
|
|
748
752
|
for rdef in K8SResourceDef.from_manifest(key, schema, self.resource_paths):
|
|
749
753
|
self.resource_definitions[key] = rdef
|
kubernator/plugins/kops.py
CHANGED
|
@@ -30,7 +30,7 @@ from kubernator.api import (KubernatorPlugin, scan_dir,
|
|
|
30
30
|
io_StringIO,
|
|
31
31
|
TemplateEngine,
|
|
32
32
|
StripNL,
|
|
33
|
-
Globs)
|
|
33
|
+
Globs, load_file)
|
|
34
34
|
from kubernator.plugins.k8s_api import K8SResourcePluginMixin
|
|
35
35
|
from kubernator.proc import CalledProcessError
|
|
36
36
|
|
|
@@ -136,10 +136,11 @@ class KopsPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
136
136
|
display_p = context.app.display_path(p)
|
|
137
137
|
logger.debug("Adding Kops resources from %s", display_p)
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
manifests = load_file(logger, p, FileType.YAML, display_p,
|
|
140
|
+
self.template_engine,
|
|
141
|
+
{"ktor": context})
|
|
141
142
|
|
|
142
|
-
self.add_resources(
|
|
143
|
+
self.add_resources(manifests, display_p)
|
|
143
144
|
|
|
144
145
|
def update(self):
|
|
145
146
|
context = self.context
|
kubernator/plugins/kubectl.py
CHANGED
|
@@ -23,6 +23,8 @@ import tempfile
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from shutil import which, copy
|
|
25
25
|
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
26
28
|
from kubernator.api import (KubernatorPlugin,
|
|
27
29
|
prepend_os_path,
|
|
28
30
|
StripNL,
|
|
@@ -89,15 +91,44 @@ class KubectlPlugin(KubernatorPlugin):
|
|
|
89
91
|
context.globals.kubectl = dict(version=version,
|
|
90
92
|
kubectl_file=kubectl_file,
|
|
91
93
|
stanza=self.stanza,
|
|
92
|
-
test=self.test_kubectl
|
|
94
|
+
test=self.test_kubectl,
|
|
95
|
+
run=self.run,
|
|
96
|
+
run_capturing=self.run_capturing,
|
|
97
|
+
get=self.get,
|
|
93
98
|
)
|
|
94
99
|
|
|
95
100
|
context.globals.kubectl.version = context.kubectl.test()
|
|
96
101
|
|
|
102
|
+
def run_capturing(self, *args, **kwargs):
|
|
103
|
+
return self.context.app.run_capturing_out(self.stanza() +
|
|
104
|
+
list(args),
|
|
105
|
+
stderr_logger,
|
|
106
|
+
**kwargs
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def run(self, *args, **kwargs):
|
|
110
|
+
self.context.app.run(self.stanza() +
|
|
111
|
+
list(args),
|
|
112
|
+
stdout_logger,
|
|
113
|
+
stderr_logger,
|
|
114
|
+
**kwargs
|
|
115
|
+
).wait()
|
|
116
|
+
|
|
117
|
+
def get(self, resource_type, resource_name, namespace=None):
|
|
118
|
+
args = ["get", resource_type, resource_name]
|
|
119
|
+
if namespace:
|
|
120
|
+
args += ["-n", namespace]
|
|
121
|
+
args += ["-o", "yaml"]
|
|
122
|
+
|
|
123
|
+
res = list(yaml.safe_load_all(self.context.kubectl.run_capturing(*args)))
|
|
124
|
+
if len(res):
|
|
125
|
+
if len(res) > 1:
|
|
126
|
+
return res
|
|
127
|
+
return res[0]
|
|
128
|
+
return None
|
|
129
|
+
|
|
97
130
|
def test_kubectl(self):
|
|
98
|
-
version_out: str = self.
|
|
99
|
-
["version", "--client=true", "-o", "json"],
|
|
100
|
-
stderr_logger)
|
|
131
|
+
version_out: str = self.run_capturing("version", "--client=true", "-o", "json")
|
|
101
132
|
|
|
102
133
|
version_out_js = json.loads(version_out)
|
|
103
134
|
kubectl_version = version_out_js["clientVersion"]["gitVersion"][1:]
|