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.
@@ -27,7 +27,7 @@ from shutil import which, copy
27
27
  from typing import Sequence
28
28
 
29
29
  import yaml
30
- from jsonschema import Draft7Validator, draft7_format_checker
30
+ from jsonschema import Draft7Validator
31
31
 
32
32
  from kubernator.api import (KubernatorPlugin, Globs, StripNL,
33
33
  scan_dir,
@@ -37,7 +37,7 @@ from kubernator.api import (KubernatorPlugin, Globs, StripNL,
37
37
  validator_with_defaults,
38
38
  get_golang_os,
39
39
  get_golang_machine,
40
- prepend_os_path
40
+ prepend_os_path, TemplateEngine, get_cache_dir
41
41
  )
42
42
  from kubernator.plugins.k8s_api import K8SResource
43
43
  from kubernator.proc import DEVNULL
@@ -85,12 +85,12 @@ HELM_SCHEMA = {
85
85
  }
86
86
  },
87
87
  "type": "object",
88
- "required": ["repository", "chart", "version", "name", "namespace"]
88
+ "required": ["chart", "name", "namespace"]
89
89
  }
90
90
 
91
91
  Draft7Validator.check_schema(HELM_SCHEMA)
92
92
  HELM_VALIDATOR_CLS = validator_with_defaults(Draft7Validator)
93
- HELM_VALIDATOR = HELM_VALIDATOR_CLS(HELM_SCHEMA, format_checker=draft7_format_checker)
93
+ HELM_VALIDATOR = HELM_VALIDATOR_CLS(HELM_SCHEMA, format_checker=Draft7Validator.FORMAT_CHECKER)
94
94
 
95
95
 
96
96
  class HelmPlugin(KubernatorPlugin):
@@ -102,18 +102,25 @@ class HelmPlugin(KubernatorPlugin):
102
102
  self.context = None
103
103
  self.repositories = set()
104
104
  self.helm_dir = None
105
+ self.template_engine = TemplateEngine(logger)
106
+
107
+ self._repositories_not_populated = True
108
+ self._helm_repositories_file = None
109
+ self._charts_used: dict[str, list[tuple[str, str]]] = {}
105
110
 
106
111
  def set_context(self, context):
107
112
  self.context = context
108
113
 
109
114
  def stanza(self):
110
115
  context = self.context
111
- stanza = [context.helm.helm_file, f"--kubeconfig={context.kubeconfig.kubeconfig}"]
116
+ stanza = [context.helm.helm_file,
117
+ f"--kubeconfig={context.kubeconfig.kubeconfig}",
118
+ f"--repository-config={self._helm_repositories_file}"]
112
119
  if logger.getEffectiveLevel() < logging.INFO:
113
120
  stanza.append("--debug")
114
121
  return stanza
115
122
 
116
- def register(self, version=None):
123
+ def register(self, version=None, check_chart_versions=False):
117
124
  context = self.context
118
125
  context.app.register_plugin("kubeconfig")
119
126
  context.app.register_plugin("k8s")
@@ -130,7 +137,7 @@ class HelmPlugin(KubernatorPlugin):
130
137
  helm_tar = tarfile.open(helm_file_dl)
131
138
  helm_tar.extractall(self.helm_dir.name)
132
139
 
133
- copy(Path(self.helm_dir.name)/f"{get_golang_os()}-{get_golang_machine()}"/"helm", helm_file)
140
+ copy(Path(self.helm_dir.name) / f"{get_golang_os()}-{get_golang_machine()}" / "helm", helm_file)
134
141
 
135
142
  os.chmod(helm_file, 0o500)
136
143
  prepend_os_path(self.helm_dir.name)
@@ -142,6 +149,8 @@ class HelmPlugin(KubernatorPlugin):
142
149
 
143
150
  logger.debug("Found Helm in %r", helm_file)
144
151
 
152
+ helm_dir = get_cache_dir("helm")
153
+ self._helm_repositories_file = Path(helm_dir) / "repositories.yaml"
145
154
  context.globals.helm = dict(default_includes=Globs(["*.helm.yaml", "*.helm.yml"], True),
146
155
  default_excludes=Globs([".*"], True),
147
156
  namespace_transformer=True,
@@ -149,6 +158,7 @@ class HelmPlugin(KubernatorPlugin):
149
158
  stanza=self.stanza,
150
159
  add_helm_template=self.add_helm_template,
151
160
  add_helm=self.add_helm,
161
+ check_chart_versions=check_chart_versions,
152
162
  )
153
163
 
154
164
  def handle_init(self):
@@ -180,11 +190,46 @@ class HelmPlugin(KubernatorPlugin):
180
190
  display_p = context.app.display_path(p)
181
191
  logger.debug("Adding Helm template from %s", display_p)
182
192
 
183
- helm_templates = load_file(logger, p, FileType.YAML, display_p)
193
+ helm_templates = load_file(logger, p, FileType.YAML, display_p,
194
+ self.template_engine,
195
+ {"ktor": context})
184
196
 
185
197
  for helm_template in helm_templates:
186
198
  self._add_helm(helm_template, display_p)
187
199
 
200
+ def handle_summary(self):
201
+ context = self.context
202
+ if context.helm.check_chart_versions:
203
+ logger.info("Checking Helm chart versions")
204
+ all_chart_versions = json.loads(self.context.app.run_capturing_out(self.stanza() +
205
+ ["search", "repo",
206
+ "-l", "-o", "json",
207
+ ],
208
+ stderr_logger,
209
+ ))
210
+
211
+ def chart_version_part_normalizer(x):
212
+ return int(x) if x.isnumeric() else x
213
+
214
+ def chart_version_normalizer(x):
215
+ return tuple(map(chart_version_part_normalizer, x.split(".")))
216
+
217
+ charts_versions = {}
218
+ for chart_version in all_chart_versions:
219
+ charts_versions.setdefault(chart_version["name"], []).append(
220
+ chart_version_normalizer(chart_version["version"]))
221
+
222
+ for chart, version_source in self._charts_used.items():
223
+ chart_versions = sorted(charts_versions[chart])
224
+ for version, source in version_source:
225
+ if chart_versions[-1] > chart_version_normalizer(version):
226
+ logger.warning("Chart %s is version %s while the latest is %s (defined in %s)",
227
+ chart.split("/")[1],
228
+ version,
229
+ ".".join(map(str, chart_versions[-1])),
230
+ source,
231
+ )
232
+
188
233
  def add_helm_template(self, template):
189
234
  return self._add_helm(template, calling_frame_source())
190
235
 
@@ -201,6 +246,23 @@ class HelmPlugin(KubernatorPlugin):
201
246
  return self._internal_add_helm(source, **{k.replace("-", "_"): v for k, v in template.items()})
202
247
 
203
248
  def _add_repository(self, repository: str):
249
+ def _update_repositories():
250
+ if self.repositories:
251
+ self.context.app.run(self.stanza() + ["repo", "update"],
252
+ stdout_logger,
253
+ stderr_logger).wait()
254
+
255
+ if self._repositories_not_populated:
256
+ preexisting_repositories = json.loads(
257
+ self.context.app.run_capturing_out(self.stanza() + ["repo", "list", "-o", "json"], stderr_logger))
258
+ for pr in preexisting_repositories:
259
+ repo_name = pr["name"]
260
+ repo_url = pr["url"]
261
+ logger.debug("Recording pre-existing repository %s mapping %s", repo_name, repo_url)
262
+ self.repositories.add(repo_name)
263
+ _update_repositories()
264
+ self._repositories_not_populated = False
265
+
204
266
  repository_hash = sha256(repository.encode("UTF-8")).hexdigest()
205
267
  logger.debug("Repository %s mapping to %s", repository, repository_hash)
206
268
  if repository_hash not in self.repositories:
@@ -208,24 +270,52 @@ class HelmPlugin(KubernatorPlugin):
208
270
  self.context.app.run(self.stanza() + ["repo", "add", repository_hash, repository],
209
271
  stdout_logger,
210
272
  stderr_logger).wait()
211
- self.context.app.run(self.stanza() + ["repo", "update"],
212
- stdout_logger,
213
- stderr_logger).wait()
273
+ _update_repositories()
214
274
  self.repositories.add(repository_hash)
215
275
 
216
276
  return repository_hash
217
277
 
218
- def _internal_add_helm(self, source, *, repository, chart, version, name, namespace, include_crds,
219
- values=None, values_file=None):
278
+ def _internal_add_helm(self, source, *, chart, name, namespace, include_crds,
279
+ values=None, values_file=None, repository=None, version=None):
220
280
  if values and values_file:
221
281
  raise RuntimeError(f"In {source} either values or values file may be specified, but not both")
222
282
 
283
+ if (repository and chart and chart.startswith("oci://") or
284
+ not repository and chart and not chart.startswith("oci://")):
285
+ raise RuntimeError(
286
+ f"In {source} either repository must be specified or OCI-chart must be used, but not both")
287
+
288
+ if not version and repository:
289
+ raise RuntimeError(f"In {source} version must be specified unless OCI-chart is used")
290
+
223
291
  if values_file:
224
292
  values_file = Path(values_file)
225
293
  if not values_file.is_absolute():
226
294
  values_file = self.context.app.cwd / values_file
295
+ values = list(load_file(logger, values_file, FileType.YAML,
296
+ template_engine=self.template_engine,
297
+ template_context={"ktor": self.context,
298
+ "helm": {"chart": chart,
299
+ "name": name,
300
+ "namespace": namespace,
301
+ "include_crds": include_crds,
302
+ "repository": repository,
303
+ "version": version,
304
+ }}))
305
+ values = values[0] if values else {}
306
+
307
+ version_spec = []
308
+ if repository:
309
+ repository_hash = self._add_repository(repository)
310
+ chart_name = f"{repository_hash}/{chart}"
311
+ if version:
312
+ self._charts_used.setdefault(chart_name, []).append((version, source))
313
+ else:
314
+ chart_name = chart
315
+
316
+ if version:
317
+ version_spec = ["--version", version]
227
318
 
228
- repository_hash = self._add_repository(repository)
229
319
  stdin = DEVNULL
230
320
 
231
321
  if values:
@@ -237,14 +327,13 @@ class HelmPlugin(KubernatorPlugin):
237
327
  resources = self.context.app.run_capturing_out(self.stanza() +
238
328
  ["template",
239
329
  name,
240
- f"{repository_hash}/{chart}",
241
- "--version", version,
330
+ chart_name,
242
331
  "-n", namespace,
243
332
  "-a", ",".join(self.context.k8s.get_api_versions())
244
333
  ] +
334
+ version_spec +
245
335
  (["--include-crds"] if include_crds else []) +
246
- (["-f", values_file] if values_file else []) +
247
- (["-f", "-"] if values else []),
336
+ ["-f", "-"],
248
337
  stderr_logger,
249
338
  stdin=stdin,
250
339
  )
@@ -25,6 +25,7 @@ from pathlib import Path
25
25
  from shutil import which
26
26
 
27
27
  import yaml
28
+
28
29
  from kubernator.api import (KubernatorPlugin, scan_dir,
29
30
  TemplateEngine,
30
31
  load_remote_file,
@@ -33,7 +34,8 @@ from kubernator.api import (KubernatorPlugin, scan_dir,
33
34
  Globs,
34
35
  get_golang_os,
35
36
  get_golang_machine,
36
- prepend_os_path, jp)
37
+ prepend_os_path, jp, load_file)
38
+ from kubernator.plugins.k8s import api_exc_normalize_body, api_exc_format_body
37
39
  from kubernator.plugins.k8s_api import K8SResourcePluginMixin
38
40
 
39
41
  logger = logging.getLogger("kubernator.istio")
@@ -53,6 +55,9 @@ class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
53
55
  self.client_version = None
54
56
  self.server_version = None
55
57
  self.provision_operator = False
58
+ self.install = False
59
+ self.upgrade = False
60
+ self.upgrade_from_operator = False
56
61
  self.template_engine = TemplateEngine(logger)
57
62
 
58
63
  self.istioctl_dir = None
@@ -133,25 +138,41 @@ class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
133
138
  context = self.context
134
139
 
135
140
  version, version_out_js = self.test_istioctl()
136
- self.client_version = tuple(version.split("."))
137
- mesh_versions = set(tuple(m.value.split(".")) for m in MESH_PILOT_JP.find(version_out_js))
141
+ self.client_version = tuple(map(int, version.split(".")))
142
+ mesh_versions = set(tuple(map(int, m.value.split("."))) for m in MESH_PILOT_JP.find(version_out_js))
138
143
 
139
144
  if mesh_versions:
140
145
  self.server_version = max(mesh_versions)
141
146
 
142
147
  if not self.server_version:
143
148
  logger.info("No Istio mesh has been found and it'll be created")
149
+ self.install = True
144
150
  self.provision_operator = True
145
- elif self.server_version < self.client_version:
146
- logger.info("Istio client is version %s while server is up to %s - operator will be upgraded",
147
- ".".join(self.client_version),
148
- ".".join(self.server_version))
151
+ elif self.server_version != self.client_version:
152
+ logger.info("Istio client is version %s while server is up to %s - up/downgrade will be performed",
153
+ ".".join(map(str, self.client_version)),
154
+ ".".join(map(str, self.server_version)))
155
+ self.upgrade = True
149
156
  self.provision_operator = True
150
157
 
158
+ if self.client_version >= (1, 24, 0):
159
+ # No more operator in 1.24.0+
160
+ self.provision_operator = False
161
+
162
+ if self.upgrade and (self.client_version >= (1, 24, 0) > self.server_version):
163
+ self.upgrade_from_operator = True
164
+
165
+ if self.upgrade and (self.client_version < (1, 24, 0) <= self.server_version):
166
+ raise ValueError(f"Unable to downgrade Istio from {self.server_version} to {self.client_version}")
167
+
151
168
  # Register Istio-related CRDs with K8S
169
+ if self.client_version >= (1, 24, 0):
170
+ crd_path = "manifests/charts/base/files/crd-all.gen.yaml"
171
+ else:
172
+ crd_path = "manifests/charts/base/crds/crd-all.gen.yaml"
152
173
  self.context.k8s.load_remote_crds(
153
- f"https://raw.githubusercontent.com/istio/istio/{'.'.join(self.client_version)}/"
154
- "manifests/charts/base/crds/crd-all.gen.yaml", "yaml")
174
+ f"https://raw.githubusercontent.com/istio/istio/{'.'.join(map(str, self.client_version))}/{crd_path}",
175
+ "yaml")
155
176
 
156
177
  # This plugin only deals with Istio Operator, so only load that stuff
157
178
  self.resource_definitions_schema = load_remote_file(logger,
@@ -161,8 +182,10 @@ class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
161
182
  FileType.JSON)
162
183
  self._populate_resource_definitions()
163
184
 
164
- self.add_remote_crds(f"https://raw.githubusercontent.com/istio/istio/{'.'.join(self.client_version)}/"
165
- f"manifests/charts/istio-operator/crds/crd-operator.yaml", FileType.YAML)
185
+ crd_operator_version = (1, 23, 4) if self.client_version >= (1, 24, 0) else self.client_version
186
+ self.add_remote_crds(
187
+ f"https://raw.githubusercontent.com/istio/istio/{'.'.join(map(str, crd_operator_version))}/"
188
+ f"manifests/charts/istio-operator/crds/crd-operator.yaml", FileType.YAML)
166
189
 
167
190
  # Exclude Istio YAMLs from K8S resource loading
168
191
  context.k8s.default_excludes.add("*.istio.yaml")
@@ -189,10 +212,11 @@ class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
189
212
  display_p = context.app.display_path(p)
190
213
  logger.info("Adding Istio Operator from %s", display_p)
191
214
 
192
- with open(p, "rt") as file:
193
- template = self.template_engine.from_string(file.read())
215
+ manifests = load_file(logger, p, FileType.YAML, display_p,
216
+ self.template_engine,
217
+ {"ktor": context})
194
218
 
195
- self.add_resources(template.render({"ktor": context}), display_p)
219
+ self.add_resources(manifests, display_p)
196
220
 
197
221
  def handle_apply(self):
198
222
  context = self.context
@@ -211,41 +235,130 @@ class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
211
235
  context.app.run(context.istio.stanza() + ["validate", "-f", operators_file.name],
212
236
  stdout_logger, stderr_logger).wait()
213
237
 
214
- self._operator_init(operators_file, True)
215
-
216
- if not context.app.args.dry_run:
217
- self._operator_init(operators_file, False)
218
-
219
- def _operator_init(self, operators_file, dry_run):
238
+ dry_run = context.app.args.dry_run
239
+
240
+ if self.provision_operator:
241
+ self._create_istio_system_ns(True)
242
+ self._operator_init(operators_file, True)
243
+
244
+ if not dry_run:
245
+ self._create_istio_system_ns(False)
246
+ self._operator_init(operators_file, False)
247
+ elif self.install:
248
+ self._install(operators_file, True)
249
+
250
+ if not dry_run:
251
+ self._install(operators_file, False)
252
+ elif self.upgrade:
253
+ def _upgrade(dry_run):
254
+ if self.upgrade_from_operator:
255
+ # delete deployment -n istio-system istio-operator
256
+ self._delete_resource_internal({"apiVersion": "apps/v1",
257
+ "kind": "Deployment",
258
+ "metadata":
259
+ {
260
+ "namespace": "istio-system",
261
+ "name": "istio-operator"
262
+ }
263
+ }, dry_run, True)
264
+ self._upgrade(operators_file, dry_run)
265
+
266
+ _upgrade(True)
267
+ if not dry_run:
268
+ _upgrade(False)
269
+
270
+ def _delete_resource_internal(self, manifest, dry_run=True, missing_ok=False):
220
271
  from kubernetes import client
221
272
  from kubernetes.client.rest import ApiException
222
273
 
223
274
  context = self.context
275
+ k8s_client = context.k8s.client
224
276
 
225
- status_details = " (dry run)" if dry_run else ""
277
+ res = self._create_resource(manifest)
278
+ res.rdef.populate_api(client, k8s_client)
279
+ try:
280
+ res.delete(dry_run=dry_run)
281
+ except ApiException as e:
282
+ api_exc_normalize_body(e)
283
+ try:
284
+ skip = False
285
+ if e.status == 404 and missing_ok:
286
+ skip = True
287
+ if not skip:
288
+ raise
289
+ except ApiException as e:
290
+ api_exc_format_body(e)
291
+ raise
292
+
293
+ return res
294
+
295
+ def _create_resource_internal(self, manifest, dry_run=True, exists_ok=False):
296
+ from kubernetes import client
297
+ from kubernetes.client.rest import ApiException
226
298
 
299
+ context = self.context
227
300
  k8s_client = context.k8s.client
228
- logger.info("Creating istio-system namespace%s", status_details)
229
- istio_system = self.add_resource({"apiVersion": "v1",
230
- "kind": "Namespace",
231
- "metadata": {
232
- "labels": {
233
- "istio-injection": "disabled"
234
- },
235
- "name": "istio-system"
236
- }})
237
- istio_system.rdef.populate_api(client, k8s_client)
301
+
302
+ res = self._create_resource(manifest)
303
+ res.rdef.populate_api(client, k8s_client)
238
304
  try:
239
- istio_system.create(dry_run=dry_run)
305
+ res.create(dry_run=dry_run)
240
306
  except ApiException as e:
241
307
  skip = False
242
- if e.status == 409:
243
- status = json.loads(e.body)
244
- if status["reason"] == "AlreadyExists":
245
- skip = True
246
- if not skip:
308
+ api_exc_normalize_body(e)
309
+ try:
310
+ if e.status == 409:
311
+ status = e.body
312
+ if status["reason"] == "AlreadyExists" and exists_ok:
313
+ skip = True
314
+ if not skip:
315
+ raise
316
+ except ApiException as e:
317
+ api_exc_format_body(e)
247
318
  raise
248
319
 
320
+ return res
321
+
322
+ def _install(self, operators_file, dry_run):
323
+ context = self.context
324
+ status_details = " (dry run)" if dry_run else ""
325
+
326
+ logger.info("Running Istio install%s", status_details)
327
+ istio_install_cmd = context.istio.stanza() + ["install", "-f", operators_file.name, "-y", "--verify"]
328
+ context.app.run(istio_install_cmd + (["--dry-run"] if dry_run else []),
329
+ stdout_logger,
330
+ stderr_logger).wait()
331
+
332
+ def _upgrade(self, operators_file, dry_run):
333
+ context = self.context
334
+ status_details = " (dry run)" if dry_run else ""
335
+
336
+ logger.info("Running Istio upgrade%s", status_details)
337
+ istio_upgrade_cmd = context.istio.stanza() + ["upgrade", "-f", operators_file.name, "-y", "--verify"]
338
+ context.app.run(istio_upgrade_cmd + (["--dry-run"] if dry_run else []),
339
+ stdout_logger,
340
+ stderr_logger).wait()
341
+
342
+ def _create_istio_system_ns(self, dry_run):
343
+ status_details = " (dry run)" if dry_run else ""
344
+ logger.info("Creating istio-system namespace%s", status_details)
345
+ self._create_resource_internal({"apiVersion": "v1",
346
+ "kind": "Namespace",
347
+ "metadata": {
348
+ "labels": {
349
+ "istio-injection": "disabled"
350
+ },
351
+ "name": "istio-system"
352
+ }
353
+ },
354
+ dry_run=dry_run,
355
+ exists_ok=True
356
+ )
357
+
358
+ def _operator_init(self, operators_file, dry_run):
359
+ context = self.context
360
+ status_details = " (dry run)" if dry_run else ""
361
+
249
362
  logger.info("Running Istio operator init%s", status_details)
250
363
  istio_operator_init = context.istio.stanza() + ["operator", "init", "-f", operators_file.name]
251
364
  context.app.run(istio_operator_init + (["--dry-run"] if dry_run else []),