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/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.run, server_minor, logger,
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
- status = json.loads(e.body)
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
- try:
427
- create_func()
428
- except ApiException as e:
429
- if exists_ok:
430
- if e.status == 409:
431
- status = json.loads(e.body)
432
- if status["reason"] == "AlreadyExists":
433
- return
434
- 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
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
- if e.status == 404:
449
- try:
450
- create()
451
- return 1, 0, 0
452
- except ApiException as e:
453
- if not handle_400_strict_validation_error(e):
454
- raise
455
-
456
- 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)
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
- if e.status == 422:
467
- status = json.loads(e.body)
468
- details = status["details"]
469
- immutable_key = details["group"], details["kind"]
470
-
471
- try:
472
- propagation_policy = self.context.k8s.immutable_changes[immutable_key]
473
- except KeyError:
474
- 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
475
557
  else:
476
- for cause in details["causes"]:
477
- if (
478
- cause["reason"] == "FieldValueInvalid" and
479
- "field is immutable" in cause["message"]
480
- or
481
- cause["reason"] == "FieldValueForbidden" and
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:
@@ -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:]
@@ -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
- self.cmd("stop", "-o", "json")
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
@@ -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, draft7_format_checker
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=draft7_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):