kubernator 1.0.16__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 +180 -67
- kubernator/app.py +7 -3
- kubernator/plugins/gke.py +105 -0
- kubernator/plugins/helm.py +107 -18
- kubernator/plugins/istio.py +150 -37
- kubernator/plugins/k8s.py +122 -52
- 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.16.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.16.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/WHEEL +1 -1
- kubernator-1.0.16.dist-info/RECORD +0 -30
- {kubernator-1.0.16.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/entry_points.txt +0 -0
- {kubernator-1.0.16.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/namespace_packages.txt +0 -0
- {kubernator-1.0.16.dist-info → kubernator-1.0.24.dev20251109010128.dist-info}/top_level.txt +0 -0
- {kubernator-1.0.16.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,
|
|
@@ -81,6 +84,21 @@ def normalize_pkg_version(v: str):
|
|
|
81
84
|
return tuple(map(int, v_split))
|
|
82
85
|
|
|
83
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
|
+
|
|
84
102
|
class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
85
103
|
logger = logger
|
|
86
104
|
|
|
@@ -94,7 +112,9 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
94
112
|
|
|
95
113
|
self._transformers = []
|
|
96
114
|
self._validators = []
|
|
115
|
+
self._manifest_patchers = []
|
|
97
116
|
self._summary = 0, 0, 0
|
|
117
|
+
self._template_engine = TemplateEngine(logger)
|
|
98
118
|
|
|
99
119
|
def set_context(self, context):
|
|
100
120
|
self.context = context
|
|
@@ -118,6 +138,8 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
118
138
|
("apps", "StatefulSet"): K8SPropagationPolicy.ORPHAN,
|
|
119
139
|
("apps", "Deployment"): K8SPropagationPolicy.ORPHAN,
|
|
120
140
|
("storage.k8s.io", "StorageClass"): K8SPropagationPolicy.ORPHAN,
|
|
141
|
+
(None, "Pod"): K8SPropagationPolicy.BACKGROUND,
|
|
142
|
+
("batch", "Job"): K8SPropagationPolicy.ORPHAN,
|
|
121
143
|
},
|
|
122
144
|
default_includes=Globs(["*.yaml", "*.yml"], True),
|
|
123
145
|
default_excludes=Globs([".*"], True),
|
|
@@ -129,12 +151,14 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
129
151
|
add_transformer=self.api_add_transformer,
|
|
130
152
|
remove_transformer=self.api_remove_transformer,
|
|
131
153
|
add_validator=self.api_remove_validator,
|
|
154
|
+
add_manifest_patcher=self.api_add_manifest_patcher,
|
|
132
155
|
get_api_versions=self.get_api_versions,
|
|
133
156
|
create_resource=self.create_resource,
|
|
134
157
|
disable_client_patches=disable_client_patches,
|
|
135
158
|
field_validation=field_validation,
|
|
136
159
|
field_validation_warn_fatal=field_validation_warn_fatal,
|
|
137
160
|
field_validation_warnings=0,
|
|
161
|
+
conflict_retry_delay=0.3,
|
|
138
162
|
_k8s=self,
|
|
139
163
|
)
|
|
140
164
|
context.k8s = dict(default_includes=Globs(context.globals.k8s.default_includes),
|
|
@@ -164,7 +188,7 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
164
188
|
|
|
165
189
|
logger.info("Using Kubernetes client version =~%s.0 for server version %s",
|
|
166
190
|
server_minor, ".".join(k8s.server_version))
|
|
167
|
-
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,
|
|
168
192
|
stdout_logger, stderr_logger, k8s.disable_client_patches)
|
|
169
193
|
|
|
170
194
|
modules_to_delete = []
|
|
@@ -197,7 +221,7 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
197
221
|
|
|
198
222
|
k8s.client = self._setup_k8s_client()
|
|
199
223
|
version = client.VersionApi(k8s.client).get_code()
|
|
200
|
-
if "-eks-" in version.git_version:
|
|
224
|
+
if "-eks-" or "-gke" in version.git_version:
|
|
201
225
|
git_version = version.git_version.split("-")[0]
|
|
202
226
|
else:
|
|
203
227
|
git_version = version.git_version
|
|
@@ -239,7 +263,10 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
239
263
|
display_p = context.app.display_path(p)
|
|
240
264
|
logger.debug("Adding Kubernetes manifest from %s", display_p)
|
|
241
265
|
|
|
242
|
-
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
|
+
)
|
|
243
270
|
|
|
244
271
|
for manifest in manifests:
|
|
245
272
|
if manifest:
|
|
@@ -348,6 +375,10 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
348
375
|
if validator not in self._validators:
|
|
349
376
|
self._validators.append(validator)
|
|
350
377
|
|
|
378
|
+
def api_add_manifest_patcher(self, patcher):
|
|
379
|
+
if patcher not in self._manifest_patchers:
|
|
380
|
+
self._manifest_patchers.append(patcher)
|
|
381
|
+
|
|
351
382
|
def api_remove_transformer(self, transformer):
|
|
352
383
|
if transformer in self._transformers:
|
|
353
384
|
self._transformers.remove(transformer)
|
|
@@ -366,6 +397,17 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
366
397
|
frame = frame.f_back
|
|
367
398
|
return ValueError((msg % args) if args else msg).with_traceback(tb)
|
|
368
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
|
+
|
|
369
411
|
def _transform_resource(self, resources: Sequence[K8SResource], resource: K8SResource) -> K8SResource:
|
|
370
412
|
for transformer in reversed(self._transformers):
|
|
371
413
|
logger.debug("Applying transformer %s to %s from %s",
|
|
@@ -404,8 +446,8 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
404
446
|
|
|
405
447
|
def handle_400_strict_validation_error(e: ApiException):
|
|
406
448
|
if e.status == 400:
|
|
407
|
-
|
|
408
|
-
|
|
449
|
+
# Assumes the body has been parsed
|
|
450
|
+
status = e.body
|
|
409
451
|
if status["status"] == "Failure":
|
|
410
452
|
if FIELD_VALIDATION_STRICT_MARKER in status["message"]:
|
|
411
453
|
message = status["message"]
|
|
@@ -420,18 +462,31 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
420
462
|
resource, resource.source, status["message"])
|
|
421
463
|
raise e from None
|
|
422
464
|
|
|
423
|
-
def create(exists_ok=False):
|
|
465
|
+
def create(exists_ok=False, wait_for_delete=False):
|
|
424
466
|
logger.info("Creating resource %s%s%s", resource, status_msg,
|
|
425
467
|
" (ignoring existing)" if exists_ok else "")
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
435
490
|
|
|
436
491
|
merge_instrs, normalized_manifest = extract_merge_instructions(resource.manifest, resource)
|
|
437
492
|
if merge_instrs:
|
|
@@ -445,15 +500,20 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
445
500
|
remote_resource = resource.get()
|
|
446
501
|
logger.trace("Current resource %s: %s", resource, remote_resource)
|
|
447
502
|
except ApiException as e:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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)
|
|
457
517
|
raise
|
|
458
518
|
else:
|
|
459
519
|
logger.trace("Attempting to retrieve a normalized patch for resource %s: %s", resource, normalized_manifest)
|
|
@@ -463,34 +523,44 @@ class KubernetesPlugin(KubernatorPlugin, K8SResourcePluginMixin):
|
|
|
463
523
|
dry_run=True,
|
|
464
524
|
force=True)
|
|
465
525
|
except ApiException as e:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
475
557
|
else:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
"Forbidden: updates to" in cause["message"]
|
|
483
|
-
):
|
|
484
|
-
logger.info("Deleting resource %s (cascade %s)%s", resource,
|
|
485
|
-
propagation_policy.policy,
|
|
486
|
-
status_msg)
|
|
487
|
-
delete_func(propagation_policy=propagation_policy)
|
|
488
|
-
create(exists_ok=dry_run)
|
|
489
|
-
return 1, 0, 1
|
|
490
|
-
raise
|
|
491
|
-
else:
|
|
492
|
-
if not handle_400_strict_validation_error(e):
|
|
493
|
-
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
|
+
|
|
494
564
|
else:
|
|
495
565
|
logger.trace("Merged resource %s: %s", resource, merged_resource)
|
|
496
566
|
if merge_instrs:
|
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:]
|
kubernator/plugins/minikube.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# See the License for the specific language governing permissions and
|
|
16
16
|
# limitations under the License.
|
|
17
17
|
#
|
|
18
|
-
|
|
18
|
+
import json
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
21
|
import tempfile
|
|
@@ -35,6 +35,8 @@ proc_logger = logger.getChild("proc")
|
|
|
35
35
|
stdout_logger = StripNL(proc_logger.info)
|
|
36
36
|
stderr_logger = StripNL(proc_logger.warning)
|
|
37
37
|
|
|
38
|
+
MINIKUBE_MAX_VERSION_LEGACY = "1.36.0"
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
class MinikubePlugin(KubernatorPlugin):
|
|
40
42
|
logger = logger
|
|
@@ -89,7 +91,7 @@ class MinikubePlugin(KubernatorPlugin):
|
|
|
89
91
|
|
|
90
92
|
def register(self, minikube_version=None, profile="default", k8s_version=None,
|
|
91
93
|
keep_running=False, start_fresh=False,
|
|
92
|
-
nodes=1, driver=None, cpus="no-limit", extra_args=None):
|
|
94
|
+
nodes=1, driver=None, cpus="no-limit", extra_args=None, extra_addons=None):
|
|
93
95
|
context = self.context
|
|
94
96
|
|
|
95
97
|
context.app.register_plugin("kubeconfig")
|
|
@@ -99,9 +101,17 @@ class MinikubePlugin(KubernatorPlugin):
|
|
|
99
101
|
logger.critical(msg)
|
|
100
102
|
raise RuntimeError(msg)
|
|
101
103
|
|
|
104
|
+
k8s_version_tuple = tuple(map(int, k8s_version.split(".")))
|
|
105
|
+
|
|
102
106
|
if not minikube_version:
|
|
103
107
|
minikube_version = self.get_latest_minikube_version()
|
|
104
108
|
logger.info("No minikube version is specified, latest is %s", minikube_version)
|
|
109
|
+
if k8s_version_tuple < (1, 28, 0):
|
|
110
|
+
logger.info("While latest minikube version is %s, "
|
|
111
|
+
"the requested K8S version %s requires %s or earlier - choosing %s",
|
|
112
|
+
minikube_version, k8s_version, MINIKUBE_MAX_VERSION_LEGACY,
|
|
113
|
+
MINIKUBE_MAX_VERSION_LEGACY)
|
|
114
|
+
minikube_version = MINIKUBE_MAX_VERSION_LEGACY
|
|
105
115
|
|
|
106
116
|
minikube_dl_file, _ = context.app.download_remote_file(logger,
|
|
107
117
|
f"https://github.com/kubernetes/minikube/releases"
|
|
@@ -156,12 +166,14 @@ class MinikubePlugin(KubernatorPlugin):
|
|
|
156
166
|
minikube_file=str(minikube_file),
|
|
157
167
|
profile=profile,
|
|
158
168
|
k8s_version=k8s_version,
|
|
169
|
+
k8s_version_tuple=k8s_version_tuple,
|
|
159
170
|
start_fresh=start_fresh,
|
|
160
171
|
keep_running=keep_running,
|
|
161
172
|
nodes=nodes,
|
|
162
173
|
driver=driver,
|
|
163
174
|
cpus=cpus,
|
|
164
175
|
extra_args=extra_args or [],
|
|
176
|
+
extra_addons=extra_addons or [],
|
|
165
177
|
kubeconfig=str(self.kubeconfig_dir / "config"),
|
|
166
178
|
cmd=self.cmd,
|
|
167
179
|
cmd_out=self.cmd_out
|
|
@@ -192,6 +204,16 @@ class MinikubePlugin(KubernatorPlugin):
|
|
|
192
204
|
"--wait", "apiserver",
|
|
193
205
|
"--nodes", str(minikube.nodes)]
|
|
194
206
|
|
|
207
|
+
addons = []
|
|
208
|
+
if minikube.k8s_version_tuple >= (1, 28):
|
|
209
|
+
addons += ["volumesnapshots", "csi-hostpath-driver"]
|
|
210
|
+
|
|
211
|
+
if minikube.extra_addons:
|
|
212
|
+
addons += minikube.extra_addons
|
|
213
|
+
|
|
214
|
+
if addons:
|
|
215
|
+
args += ["--addons", ",".join(addons)]
|
|
216
|
+
|
|
195
217
|
if minikube.driver == "docker":
|
|
196
218
|
args.extend(["--cpus", str(minikube.cpus)])
|
|
197
219
|
|
|
@@ -202,11 +224,34 @@ class MinikubePlugin(KubernatorPlugin):
|
|
|
202
224
|
logger.info("Updating minikube profile %r context", minikube.profile)
|
|
203
225
|
self.cmd("update-context")
|
|
204
226
|
|
|
227
|
+
if minikube.k8s_version_tuple >= (1, 28):
|
|
228
|
+
logger.info("Disabling old storage addons")
|
|
229
|
+
self.cmd("addons", "disable", "storage-provisioner")
|
|
230
|
+
self.cmd("addons", "disable", "default-storageclass")
|
|
231
|
+
|
|
232
|
+
logger.info("Running initialization scripts for profile %r", minikube.profile)
|
|
233
|
+
self.context.app.register_plugin("kubectl", version=minikube.k8s_version)
|
|
234
|
+
if minikube.k8s_version_tuple >= (1, 28):
|
|
235
|
+
storage_class = self.context.kubectl.get("storageclass", "csi-hostpath-sc")
|
|
236
|
+
self.context.kubectl.run("delete", "storageclass", "csi-hostpath-sc")
|
|
237
|
+
storage_class["metadata"]["annotations"]["storageclass.kubernetes.io/is-default-class"] = "true"
|
|
238
|
+
storage_class["volumeBindingMode"] = "WaitForFirstConsumer"
|
|
239
|
+
|
|
240
|
+
def write_stdin():
|
|
241
|
+
return json.dumps(storage_class)
|
|
242
|
+
|
|
243
|
+
self.context.kubectl.run("create", "-f", "-", stdin=write_stdin)
|
|
244
|
+
|
|
205
245
|
def minikube_stop(self):
|
|
206
246
|
minikube = self.context.minikube
|
|
207
247
|
if self.minikube_is_running():
|
|
208
248
|
logger.info("Shutting down minikube profile %r...", minikube.profile)
|
|
209
|
-
|
|
249
|
+
try:
|
|
250
|
+
self.cmd("stop", "-o", "json")
|
|
251
|
+
except CalledProcessError as e:
|
|
252
|
+
# Workaround for minikube 1.35.0 https://github.com/kubernetes/minikube/issues/20302
|
|
253
|
+
if e.returncode != 82:
|
|
254
|
+
raise
|
|
210
255
|
|
|
211
256
|
def minikube_delete(self):
|
|
212
257
|
minikube = self.context.minikube
|
kubernator/plugins/template.py
CHANGED
|
@@ -20,7 +20,7 @@ import logging
|
|
|
20
20
|
from collections.abc import Mapping
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
-
from jsonschema import Draft7Validator
|
|
23
|
+
from jsonschema import Draft7Validator
|
|
24
24
|
|
|
25
25
|
from kubernator.api import (KubernatorPlugin, Globs, scan_dir, load_file, FileType, calling_frame_source,
|
|
26
26
|
validator_with_defaults, TemplateEngine, Template)
|
|
@@ -86,7 +86,7 @@ TEMPLATE_SCHEMA = {
|
|
|
86
86
|
|
|
87
87
|
Draft7Validator.check_schema(TEMPLATE_SCHEMA)
|
|
88
88
|
TEMPLATE_VALIDATOR_CLS: type[Draft7Validator] = validator_with_defaults(Draft7Validator)
|
|
89
|
-
TEMPLATE_VALIDATOR: Draft7Validator = TEMPLATE_VALIDATOR_CLS(TEMPLATE_SCHEMA, format_checker=
|
|
89
|
+
TEMPLATE_VALIDATOR: Draft7Validator = TEMPLATE_VALIDATOR_CLS(TEMPLATE_SCHEMA, format_checker=Draft7Validator.FORMAT_CHECKER)
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
class TemplatePlugin(KubernatorPlugin):
|