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/__init__.py
CHANGED
kubernator/api.py
CHANGED
|
@@ -23,6 +23,7 @@ import os
|
|
|
23
23
|
import platform
|
|
24
24
|
import re
|
|
25
25
|
import sys
|
|
26
|
+
import textwrap
|
|
26
27
|
import traceback
|
|
27
28
|
import urllib.parse
|
|
28
29
|
from collections.abc import Callable
|
|
@@ -38,14 +39,16 @@ from typing import Optional, Union, MutableSequence
|
|
|
38
39
|
|
|
39
40
|
import requests
|
|
40
41
|
import yaml
|
|
41
|
-
from appdirs import user_config_dir
|
|
42
42
|
from diff_match_patch import diff_match_patch
|
|
43
|
+
from gevent import sleep
|
|
43
44
|
from jinja2 import (Environment,
|
|
44
45
|
ChainableUndefined,
|
|
45
46
|
make_logging_undefined,
|
|
46
47
|
Template as JinjaTemplate,
|
|
47
48
|
pass_context)
|
|
48
49
|
from jsonschema import validators
|
|
50
|
+
from platformdirs import user_cache_dir
|
|
51
|
+
from yaml import MarkedYAMLError
|
|
49
52
|
|
|
50
53
|
from kubernator._json_path import jp # noqa: F401
|
|
51
54
|
from kubernator._k8s_client_patches import (URLLIB_HEADERS_PATCH,
|
|
@@ -57,6 +60,98 @@ _CACHE_HEADER_TRANSLATION = {"etag": "if-none-match",
|
|
|
57
60
|
_CACHE_HEADERS = ("etag", "last-modified")
|
|
58
61
|
|
|
59
62
|
|
|
63
|
+
def to_json(obj: Union[dict, list]):
|
|
64
|
+
return json.dumps(obj)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_yaml_str(s: str):
|
|
68
|
+
return yaml.safe_dump(s)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def to_json_yaml_str(obj: Union[dict, list]):
|
|
72
|
+
"""
|
|
73
|
+
Takes `obj`, dumps as json representation, converts json representation to YAML string literal.
|
|
74
|
+
"""
|
|
75
|
+
return to_yaml_str(to_json(obj))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_yaml_str_block(s: str, indent: int = 4, pretty_indent: int = 2):
|
|
79
|
+
"""
|
|
80
|
+
Takes a multiline string, dedents it then indents it `indent` spaces for in-yaml alignment and
|
|
81
|
+
`pretty-indent` spaces for in-block alignment.
|
|
82
|
+
"""
|
|
83
|
+
return (f"|+{pretty_indent}\n" +
|
|
84
|
+
textwrap.indent(textwrap.dedent(s), " " * (indent + pretty_indent)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def to_json_yaml_str_block(obj: Union[str, dict, list], indent: int = 4, pretty_indent=2):
|
|
88
|
+
"""
|
|
89
|
+
Takes an `obj`, serializes it as pretty JSON with `pretty_indent` in-json indentation and then
|
|
90
|
+
passes it to `to_yaml_str_block`.
|
|
91
|
+
"""
|
|
92
|
+
return to_yaml_str_block(json.dumps(obj, indent=pretty_indent), indent=indent, pretty_indent=pretty_indent)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def to_yaml(obj: Union[dict, list], level_indent: int, indent: int):
|
|
96
|
+
s = yaml.safe_dump(obj, indent=indent)
|
|
97
|
+
return "\n" + textwrap.indent(s, " " * level_indent)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TemplateEngine:
|
|
101
|
+
VARIABLE_START_STRING = "{${"
|
|
102
|
+
VARIABLE_END_STRING = "}$}"
|
|
103
|
+
|
|
104
|
+
def __init__(self, logger):
|
|
105
|
+
self.template_failures = 0
|
|
106
|
+
self.templates = {}
|
|
107
|
+
|
|
108
|
+
class CollectingUndefined(ChainableUndefined):
|
|
109
|
+
__slots__ = ()
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
self.template_failures += 1
|
|
113
|
+
return super().__str__()
|
|
114
|
+
|
|
115
|
+
logging_undefined = make_logging_undefined(
|
|
116
|
+
logger=logger,
|
|
117
|
+
base=CollectingUndefined
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@pass_context
|
|
121
|
+
def variable_finalizer(ctx, value):
|
|
122
|
+
normalized_value = str(value)
|
|
123
|
+
if self.VARIABLE_START_STRING in normalized_value and self.VARIABLE_END_STRING in normalized_value:
|
|
124
|
+
value_template_content = sys.intern(normalized_value)
|
|
125
|
+
env: Environment = ctx.environment
|
|
126
|
+
value_template = self.templates.get(value_template_content)
|
|
127
|
+
if not value_template:
|
|
128
|
+
value_template = env.from_string(value_template_content, env.globals)
|
|
129
|
+
self.templates[value_template_content] = value_template
|
|
130
|
+
return value_template.render(ctx.parent)
|
|
131
|
+
|
|
132
|
+
return normalized_value
|
|
133
|
+
|
|
134
|
+
self.env = Environment(variable_start_string=self.VARIABLE_START_STRING,
|
|
135
|
+
variable_end_string=self.VARIABLE_END_STRING,
|
|
136
|
+
autoescape=False,
|
|
137
|
+
finalize=variable_finalizer,
|
|
138
|
+
undefined=logging_undefined,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self.env.filters["to_json"] = to_json
|
|
142
|
+
self.env.filters["to_yaml_str"] = to_yaml_str
|
|
143
|
+
self.env.filters["to_yaml"] = to_yaml
|
|
144
|
+
self.env.filters["to_yaml_str_block"] = to_yaml_str_block
|
|
145
|
+
self.env.filters["to_json_yaml_str_block"] = to_json_yaml_str_block
|
|
146
|
+
self.env.filters["to_json_yaml_str"] = to_json_yaml_str
|
|
147
|
+
|
|
148
|
+
def from_string(self, template):
|
|
149
|
+
return self.env.from_string(template)
|
|
150
|
+
|
|
151
|
+
def failures(self):
|
|
152
|
+
return self.template_failures
|
|
153
|
+
|
|
154
|
+
|
|
60
155
|
def calling_frame_source(depth=2):
|
|
61
156
|
f = traceback.extract_stack(limit=depth + 1)[0]
|
|
62
157
|
return f"file {f.filename}, line {f.lineno} in {f.name}"
|
|
@@ -87,18 +182,33 @@ def scan_dir(logger, path: Path, path_filter: Callable[[os.DirEntry], bool], exc
|
|
|
87
182
|
yield path / f
|
|
88
183
|
|
|
89
184
|
|
|
185
|
+
def parse_yaml_docs(document: str, source=None):
|
|
186
|
+
try:
|
|
187
|
+
return list(d for d in yaml.safe_load_all(document) if d)
|
|
188
|
+
except MarkedYAMLError:
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
|
|
90
192
|
class FileType(Enum):
|
|
91
|
-
|
|
92
|
-
|
|
193
|
+
TEXT = (lambda x: x,)
|
|
194
|
+
BINARY = (lambda x: x,)
|
|
195
|
+
JSON = (json.loads,)
|
|
196
|
+
YAML = (parse_yaml_docs,)
|
|
93
197
|
|
|
94
198
|
def __init__(self, func):
|
|
95
199
|
self.func = func
|
|
96
200
|
|
|
97
201
|
|
|
98
|
-
def _load_file(logger, path: Path, file_type: FileType, source=None
|
|
99
|
-
|
|
202
|
+
def _load_file(logger, path: Path, file_type: FileType, source=None,
|
|
203
|
+
template_engine: Optional[TemplateEngine] = None,
|
|
204
|
+
template_context: Optional[dict] = None) -> Iterable[dict]:
|
|
205
|
+
with open(path, "rb" if file_type == FileType.BINARY else "rt") as f:
|
|
100
206
|
try:
|
|
101
|
-
|
|
207
|
+
if template_engine and not file_type == FileType.BINARY:
|
|
208
|
+
raw_data = template_engine.from_string(f.read()).render(template_context)
|
|
209
|
+
else:
|
|
210
|
+
raw_data = f.read()
|
|
211
|
+
data = file_type.func(raw_data)
|
|
102
212
|
if isinstance(data, GeneratorType):
|
|
103
213
|
data = list(data)
|
|
104
214
|
return data
|
|
@@ -108,27 +218,43 @@ def _load_file(logger, path: Path, file_type: FileType, source=None) -> Iterable
|
|
|
108
218
|
|
|
109
219
|
|
|
110
220
|
def _download_remote_file(url, file_name, cache: dict):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
221
|
+
retry_delay = 0
|
|
222
|
+
while True:
|
|
223
|
+
if retry_delay:
|
|
224
|
+
sleep(retry_delay)
|
|
225
|
+
|
|
226
|
+
with requests.get(url, headers=cache, stream=True) as r:
|
|
227
|
+
if r.status_code == 429:
|
|
228
|
+
if not retry_delay:
|
|
229
|
+
retry_delay = 0.2
|
|
230
|
+
else:
|
|
231
|
+
retry_delay *= 2.0
|
|
232
|
+
if retry_delay > 2.5:
|
|
233
|
+
retry_delay = 2.5
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
r.raise_for_status()
|
|
237
|
+
if r.status_code != 304:
|
|
238
|
+
with open(file_name, "wb") as out:
|
|
239
|
+
for chunk in r.iter_content(chunk_size=65535):
|
|
240
|
+
out.write(chunk)
|
|
241
|
+
return dict(r.headers)
|
|
242
|
+
else:
|
|
243
|
+
return None
|
|
118
244
|
|
|
119
245
|
|
|
120
246
|
def get_app_cache_dir():
|
|
121
|
-
return Path(
|
|
247
|
+
return Path(user_cache_dir("kubernator", "karellen"))
|
|
122
248
|
|
|
123
249
|
|
|
124
250
|
def get_cache_dir(category: str, sub_category: str = None):
|
|
125
|
-
|
|
251
|
+
cache_dir = get_app_cache_dir() / category
|
|
126
252
|
if sub_category:
|
|
127
|
-
|
|
128
|
-
if not
|
|
129
|
-
|
|
253
|
+
cache_dir = cache_dir / sub_category
|
|
254
|
+
if not cache_dir.exists():
|
|
255
|
+
cache_dir.mkdir(parents=True)
|
|
130
256
|
|
|
131
|
-
return
|
|
257
|
+
return cache_dir
|
|
132
258
|
|
|
133
259
|
|
|
134
260
|
def download_remote_file(logger, url: str, category: str = "k8s", sub_category: str = None,
|
|
@@ -174,9 +300,12 @@ def load_remote_file(logger, url, file_type: FileType, category: str = "k8s", su
|
|
|
174
300
|
return _load_file(logger, file_name, file_type, url)
|
|
175
301
|
|
|
176
302
|
|
|
177
|
-
def load_file(logger, path: Path, file_type: FileType, source=None
|
|
303
|
+
def load_file(logger, path: Path, file_type: FileType, source=None,
|
|
304
|
+
template_engine: Optional[TemplateEngine] = None,
|
|
305
|
+
template_context: Optional[dict] = None) -> Iterable[dict]:
|
|
178
306
|
logger.debug("Loading %s using %s", source or path, file_type.name)
|
|
179
|
-
return _load_file(logger, path, file_type
|
|
307
|
+
return _load_file(logger, path, file_type,
|
|
308
|
+
source, template_engine, template_context)
|
|
180
309
|
|
|
181
310
|
|
|
182
311
|
def validator_with_defaults(validator_class):
|
|
@@ -496,53 +625,6 @@ class Globs(MutableSet[Union[str, re.Pattern]]):
|
|
|
496
625
|
return f"Globs[{self._list}]"
|
|
497
626
|
|
|
498
627
|
|
|
499
|
-
class TemplateEngine:
|
|
500
|
-
VARIABLE_START_STRING = "{${"
|
|
501
|
-
VARIABLE_END_STRING = "}$}"
|
|
502
|
-
|
|
503
|
-
def __init__(self, logger):
|
|
504
|
-
self.template_failures = 0
|
|
505
|
-
self.templates = {}
|
|
506
|
-
|
|
507
|
-
class CollectingUndefined(ChainableUndefined):
|
|
508
|
-
__slots__ = ()
|
|
509
|
-
|
|
510
|
-
def __str__(self):
|
|
511
|
-
self.template_failures += 1
|
|
512
|
-
return super().__str__()
|
|
513
|
-
|
|
514
|
-
logging_undefined = make_logging_undefined(
|
|
515
|
-
logger=logger,
|
|
516
|
-
base=CollectingUndefined
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
@pass_context
|
|
520
|
-
def variable_finalizer(ctx, value):
|
|
521
|
-
normalized_value = str(value)
|
|
522
|
-
if self.VARIABLE_START_STRING in normalized_value and self.VARIABLE_END_STRING in normalized_value:
|
|
523
|
-
value_template_content = sys.intern(normalized_value)
|
|
524
|
-
env: Environment = ctx.environment
|
|
525
|
-
value_template = self.templates.get(value_template_content)
|
|
526
|
-
if not value_template:
|
|
527
|
-
value_template = env.from_string(value_template_content, env.globals)
|
|
528
|
-
self.templates[value_template_content] = value_template
|
|
529
|
-
return value_template.render(ctx.parent)
|
|
530
|
-
|
|
531
|
-
return normalized_value
|
|
532
|
-
|
|
533
|
-
self.env = Environment(variable_start_string=self.VARIABLE_START_STRING,
|
|
534
|
-
variable_end_string=self.VARIABLE_END_STRING,
|
|
535
|
-
autoescape=False,
|
|
536
|
-
finalize=variable_finalizer,
|
|
537
|
-
undefined=logging_undefined)
|
|
538
|
-
|
|
539
|
-
def from_string(self, template):
|
|
540
|
-
return self.env.from_string(template)
|
|
541
|
-
|
|
542
|
-
def failures(self):
|
|
543
|
-
return self.template_failures
|
|
544
|
-
|
|
545
|
-
|
|
546
628
|
class Template:
|
|
547
629
|
def __init__(self, name: str, template: JinjaTemplate, defaults: dict = None, path=None, source=None):
|
|
548
630
|
self.name = name
|
|
@@ -747,8 +829,12 @@ class KubernatorPlugin:
|
|
|
747
829
|
def handle_shutdown(self):
|
|
748
830
|
pass
|
|
749
831
|
|
|
832
|
+
def handle_summary(self):
|
|
833
|
+
pass
|
|
834
|
+
|
|
750
835
|
|
|
751
|
-
def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_stderr, disable_patching
|
|
836
|
+
def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_stderr, disable_patching,
|
|
837
|
+
fallback=False):
|
|
752
838
|
cache_dir = get_cache_dir("python")
|
|
753
839
|
package_major_dir = cache_dir / str(package_major)
|
|
754
840
|
package_major_dir_str = str(package_major_dir)
|
|
@@ -760,13 +846,43 @@ def install_python_k8s_client(run, package_major, logger, logger_stdout, logger_
|
|
|
760
846
|
str(package_major), package_major_dir)
|
|
761
847
|
rmtree(package_major_dir)
|
|
762
848
|
|
|
763
|
-
if not package_major_dir.exists():
|
|
849
|
+
if not package_major_dir.exists() or not len(os.listdir(package_major_dir)):
|
|
764
850
|
package_major_dir.mkdir(parents=True, exist_ok=True)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
851
|
+
try:
|
|
852
|
+
run([sys.executable, "-m", "pip", "install", "--no-deps", "--no-input",
|
|
853
|
+
"--root-user-action=ignore", "--break-system-packages", "--disable-pip-version-check",
|
|
854
|
+
"--target", package_major_dir_str, f"kubernetes>={package_major!s}dev0,<{int(package_major) + 1!s}"],
|
|
855
|
+
logger_stdout, logger_stderr)
|
|
856
|
+
except CalledProcessError as e:
|
|
857
|
+
if not fallback and "No matching distribution found for" in e.stderr:
|
|
858
|
+
logger.warning("Kubernetes Client %s (%s) failed to install because the version wasn't found. "
|
|
859
|
+
"Falling back to a client of the previous version - %s",
|
|
860
|
+
str(package_major), package_major_dir, int(package_major) - 1)
|
|
861
|
+
return install_python_k8s_client(run,
|
|
862
|
+
int(package_major) - 1,
|
|
863
|
+
logger,
|
|
864
|
+
logger_stdout,
|
|
865
|
+
logger_stderr,
|
|
866
|
+
disable_patching,
|
|
867
|
+
fallback=True)
|
|
868
|
+
else:
|
|
869
|
+
raise
|
|
768
870
|
|
|
769
871
|
if not patch_indicator.exists() and not disable_patching:
|
|
872
|
+
if not fallback and not len(os.listdir(package_major_dir)):
|
|
873
|
+
# Directory is empty
|
|
874
|
+
logger.warning("Kubernetes Client %s (%s) directory is empty - the client was not installed. "
|
|
875
|
+
"Falling back to a client of the previous version - %s",
|
|
876
|
+
str(package_major), package_major_dir, int(package_major) - 1)
|
|
877
|
+
|
|
878
|
+
return install_python_k8s_client(run,
|
|
879
|
+
int(package_major) - 1,
|
|
880
|
+
logger,
|
|
881
|
+
logger_stdout,
|
|
882
|
+
logger_stderr,
|
|
883
|
+
disable_patching,
|
|
884
|
+
fallback=True)
|
|
885
|
+
|
|
770
886
|
for patch_text, target_file, skip_if_found, min_version, max_version, name in (
|
|
771
887
|
URLLIB_HEADERS_PATCH, CUSTOM_OBJECT_PATCH_23, CUSTOM_OBJECT_PATCH_25):
|
|
772
888
|
patch_target = package_major_dir / target_file
|
kubernator/app.py
CHANGED
|
@@ -29,11 +29,13 @@ from pathlib import Path
|
|
|
29
29
|
from shutil import rmtree
|
|
30
30
|
from typing import Optional, Union
|
|
31
31
|
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
32
34
|
import kubernator
|
|
33
35
|
from kubernator.api import (KubernatorPlugin, Globs, scan_dir, PropertyDict, config_as_dict, config_parent,
|
|
34
36
|
download_remote_file, load_remote_file, Repository, StripNL, jp, get_app_cache_dir,
|
|
35
37
|
get_cache_dir, install_python_k8s_client)
|
|
36
|
-
from kubernator.proc import run, run_capturing_out
|
|
38
|
+
from kubernator.proc import run, run_capturing_out, run_pass_through_capturing
|
|
37
39
|
|
|
38
40
|
TRACE = 5
|
|
39
41
|
|
|
@@ -55,6 +57,11 @@ logging.addLevelName(5, "TRACE")
|
|
|
55
57
|
logging.Logger.trace = trace
|
|
56
58
|
logger = logging.getLogger("kubernator")
|
|
57
59
|
|
|
60
|
+
try:
|
|
61
|
+
del (yaml.resolver.Resolver.yaml_implicit_resolvers["="])
|
|
62
|
+
except KeyError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
58
65
|
|
|
59
66
|
def define_arg_parse():
|
|
60
67
|
parser = argparse.ArgumentParser(prog="kubernator",
|
|
@@ -171,42 +178,49 @@ class App(KubernatorPlugin):
|
|
|
171
178
|
self.register_plugin(self)
|
|
172
179
|
|
|
173
180
|
try:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
try:
|
|
182
|
+
while True:
|
|
183
|
+
cwd = self.next()
|
|
184
|
+
if not cwd:
|
|
185
|
+
logger.debug("No paths left to traverse")
|
|
186
|
+
break
|
|
179
187
|
|
|
180
|
-
|
|
188
|
+
context = self.context
|
|
181
189
|
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
logger.debug("Inspecting directory %s", self._display_path(cwd))
|
|
191
|
+
self._run_handlers(KubernatorPlugin.handle_before_dir, False, context, None, cwd)
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
if (ktor_py := (cwd / ".kubernator.py")).exists():
|
|
194
|
+
self._run_handlers(KubernatorPlugin.handle_before_script, False, context, None, cwd)
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
for h in self.context._plugins:
|
|
197
|
+
h.set_context(context)
|
|
190
198
|
|
|
191
|
-
|
|
199
|
+
self._exec_ktor(ktor_py)
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
for h in self.context._plugins:
|
|
202
|
+
h.set_context(None)
|
|
195
203
|
|
|
196
|
-
|
|
204
|
+
self._run_handlers(KubernatorPlugin.handle_after_script, True, context, None, cwd)
|
|
197
205
|
|
|
198
|
-
|
|
206
|
+
self._run_handlers(KubernatorPlugin.handle_after_dir, True, context, None, cwd)
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
self.context = self._top_dir_context
|
|
209
|
+
context = self.context
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
self._run_handlers(KubernatorPlugin.handle_apply, True, context, None)
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
self._run_handlers(KubernatorPlugin.handle_verify, True, context, None)
|
|
214
|
+
finally:
|
|
215
|
+
self.context = self._top_dir_context
|
|
216
|
+
context = self.context
|
|
217
|
+
self._run_handlers(KubernatorPlugin.handle_shutdown, True, context, None)
|
|
218
|
+
except: # noqa E722
|
|
219
|
+
raise
|
|
220
|
+
else:
|
|
207
221
|
self.context = self._top_dir_context
|
|
208
222
|
context = self.context
|
|
209
|
-
self._run_handlers(KubernatorPlugin.
|
|
223
|
+
self._run_handlers(KubernatorPlugin.handle_summary, True, context, None)
|
|
210
224
|
|
|
211
225
|
def discover_plugins(self):
|
|
212
226
|
importlib.invalidate_caches()
|
|
@@ -334,6 +348,7 @@ class App(KubernatorPlugin):
|
|
|
334
348
|
jp=jp,
|
|
335
349
|
run=self._run,
|
|
336
350
|
run_capturing_out=self._run_capturing_out,
|
|
351
|
+
run_passthrough_capturing=self._run_passthrough_capturing,
|
|
337
352
|
repository=self.repository,
|
|
338
353
|
StripNL=StripNL,
|
|
339
354
|
default_includes=Globs(["*"], True),
|
|
@@ -451,6 +466,9 @@ class App(KubernatorPlugin):
|
|
|
451
466
|
def _run_capturing_out(self, *args, **kwargs):
|
|
452
467
|
return run_capturing_out(*args, **kwargs)
|
|
453
468
|
|
|
469
|
+
def _run_passthrough_capturing(self, *args, **kwargs):
|
|
470
|
+
return run_pass_through_capturing(*args, **kwargs)
|
|
471
|
+
|
|
454
472
|
def __repr__(self):
|
|
455
473
|
return "Kubernator"
|
|
456
474
|
|
|
@@ -479,7 +497,7 @@ def pre_cache_k8s_clients(*versions, disable_patching=False):
|
|
|
479
497
|
for v in versions:
|
|
480
498
|
logger.info("Caching K8S client library ~=v%s.0%s...", v,
|
|
481
499
|
" (no patches)" if disable_patching else "")
|
|
482
|
-
install_python_k8s_client(
|
|
500
|
+
install_python_k8s_client(run_pass_through_capturing, v, logger, stdout_logger, stderr_logger, disable_patching)
|
|
483
501
|
|
|
484
502
|
|
|
485
503
|
def main():
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020 Express Systems USA, Inc
|
|
4
|
+
# Copyright 2025 Karellen, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from shutil import which
|
|
24
|
+
|
|
25
|
+
from kubernator.api import (KubernatorPlugin,
|
|
26
|
+
StripNL
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("kubernator.gke")
|
|
30
|
+
proc_logger = logger.getChild("proc")
|
|
31
|
+
stdout_logger = StripNL(proc_logger.info)
|
|
32
|
+
stderr_logger = StripNL(proc_logger.warning)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GkePlugin(KubernatorPlugin):
|
|
36
|
+
logger = logger
|
|
37
|
+
|
|
38
|
+
_name = "gke"
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.context = None
|
|
42
|
+
self.kubeconfig_dir = tempfile.TemporaryDirectory()
|
|
43
|
+
self.name = None
|
|
44
|
+
self.region = None
|
|
45
|
+
self.project = None
|
|
46
|
+
self.gcloud_file = None
|
|
47
|
+
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
def set_context(self, context):
|
|
51
|
+
self.context = context
|
|
52
|
+
|
|
53
|
+
def register(self, *, name=None, region=None, project=None):
|
|
54
|
+
context = self.context
|
|
55
|
+
|
|
56
|
+
if not name:
|
|
57
|
+
raise ValueError("`name` is required")
|
|
58
|
+
if not region:
|
|
59
|
+
raise ValueError("`region` is required")
|
|
60
|
+
if not project:
|
|
61
|
+
raise ValueError("`project` is required")
|
|
62
|
+
|
|
63
|
+
self.name = name
|
|
64
|
+
self.region = region
|
|
65
|
+
self.project = project
|
|
66
|
+
|
|
67
|
+
# Use current version
|
|
68
|
+
gcloud_file = which("gcloud")
|
|
69
|
+
if not gcloud_file:
|
|
70
|
+
raise RuntimeError("`gcloud` cannot be found")
|
|
71
|
+
logger.debug("Found gcloud in %r", gcloud_file)
|
|
72
|
+
self.gcloud_file = gcloud_file
|
|
73
|
+
|
|
74
|
+
context.app.register_plugin("kubeconfig")
|
|
75
|
+
|
|
76
|
+
context.globals.gke = dict(kubeconfig=str(Path(self.kubeconfig_dir.name) / "config"),
|
|
77
|
+
name=name,
|
|
78
|
+
region=region,
|
|
79
|
+
project=project,
|
|
80
|
+
gcloud_file=gcloud_file
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def handle_init(self):
|
|
84
|
+
context = self.context
|
|
85
|
+
|
|
86
|
+
env = dict(os.environ)
|
|
87
|
+
env["KUBECONFIG"] = str(context.gke.kubeconfig)
|
|
88
|
+
self.context.app.run([context.gke.gcloud_file, "components", "install", "gke-gcloud-auth-plugin"],
|
|
89
|
+
stdout_logger,
|
|
90
|
+
stderr_logger).wait()
|
|
91
|
+
self.context.app.run([context.gke.gcloud_file, "container", "clusters", "get-credentials",
|
|
92
|
+
context.gke.name,
|
|
93
|
+
"--region", context.gke.region,
|
|
94
|
+
"--project", context.gke.project],
|
|
95
|
+
stdout_logger,
|
|
96
|
+
stderr_logger,
|
|
97
|
+
env=env).wait()
|
|
98
|
+
|
|
99
|
+
context.kubeconfig.set(context.gke.kubeconfig)
|
|
100
|
+
|
|
101
|
+
def handle_shutdown(self):
|
|
102
|
+
self.kubeconfig_dir.cleanup()
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
return "GKE Plugin"
|