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/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.run, server_minor, logger,
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
- K8SResource._k8s_client_version = tuple(map(int, pkg_version("kubernetes").split(".")))
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
- patch_field_excludes,
285
- resource,
286
- patch_func,
287
- create_func,
288
- delete_func,
289
- status_msg)
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
- status = json.loads(e.body)
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
- try:
401
- create_func()
402
- except ApiException as e:
403
- if exists_ok:
404
- if e.status == 409:
405
- status = json.loads(e.body)
406
- if status["reason"] == "AlreadyExists":
407
- return
408
- raise
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
- if e.status == 404:
423
- try:
424
- create()
425
- except ApiException as e:
426
- if not handle_400_strict_validation_error(e):
427
- raise
428
-
429
- else:
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
- if e.status == 422:
440
- status = json.loads(e.body)
441
- details = status["details"]
442
- immutable_key = details["group"], details["kind"]
443
-
444
- try:
445
- propagation_policy = self.context.k8s.immutable_changes[immutable_key]
446
- except KeyError:
447
- raise e from None
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
- for cause in details["causes"]:
450
- if (
451
- cause["reason"] == "FieldValueInvalid" and
452
- "field is immutable" in cause["message"]
453
- or
454
- cause["reason"] == "FieldValueForbidden" and
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 = []
@@ -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._types import int_types, str_types
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, RefResolver
36
- from openapi_schema_validator import OAS30Validator
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, int_types)
90
+ return isinstance(instance, int)
93
91
 
94
92
 
95
93
  def is_string(instance):
96
- return isinstance(instance, str_types)
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(OAS30Validator, validators={
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(yaml.safe_load_all(StringIO(manifests)))
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(yaml.safe_load_all(StringIO(manifests)))
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
- if source:
628
- self.logger.error("Error detected in K8S manifest %s from %s",
629
- resource_description, source, exc_info=error)
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
@@ -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
- with open(p, "rt") as file:
140
- template = self.template_engine.from_string(file.read())
139
+ manifests = load_file(logger, p, FileType.YAML, display_p,
140
+ self.template_engine,
141
+ {"ktor": context})
141
142
 
142
- self.add_resources(template.render({"ktor": context}), display_p)
143
+ self.add_resources(manifests, display_p)
143
144
 
144
145
  def update(self):
145
146
  context = self.context
@@ -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.context.app.run_capturing_out(self.stanza() +
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:]