chartreuse 7.0.0.dev1__tar.gz → 7.0.2__tar.gz

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.
Files changed (24) hide show
  1. {chartreuse-7.0.0.dev1/src/chartreuse.egg-info → chartreuse-7.0.2}/PKG-INFO +1 -1
  2. chartreuse-7.0.2/VERSION +1 -0
  3. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/chartreuse_restore.py +2 -0
  4. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/kubernetes_helper.py +60 -9
  5. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2/src/chartreuse.egg-info}/PKG-INFO +1 -1
  6. chartreuse-7.0.0.dev1/VERSION +0 -1
  7. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/LICENCE.md +0 -0
  8. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/MANIFEST.in +0 -0
  9. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/README.md +0 -0
  10. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/pyproject.toml +0 -0
  11. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/setup.cfg +0 -0
  12. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/__init__.py +0 -0
  13. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/chartreuse.py +0 -0
  14. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/chartreuse_upgrade.py +0 -0
  15. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/config_loader.py +0 -0
  16. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/utils/__init__.py +0 -0
  17. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/utils/alembic_migration_helper.py +0 -0
  18. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse/utils/command.py +0 -0
  19. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/SOURCES.txt +0 -0
  20. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/dependency_links.txt +0 -0
  21. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/entry_points.txt +0 -0
  22. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/requires.txt +0 -0
  23. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/top_level.txt +0 -0
  24. {chartreuse-7.0.0.dev1 → chartreuse-7.0.2}/src/chartreuse.egg-info/zip-safe +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartreuse
3
- Version: 7.0.0.dev1
3
+ Version: 7.0.2
4
4
  Summary: Helper for Alembic migrations within Kubernetes.
5
5
  Author-email: wiremind <dev@wiremind.io>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -0,0 +1 @@
1
+ 7.0.2
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import os
3
3
 
4
+ from .chartreuse import configure_logging
4
5
  from .chartreuse_upgrade import ensure_safe_run
5
6
  from .kubernetes_helper import KubernetesDeploymentManager
6
7
 
@@ -22,6 +23,7 @@ def main() -> None:
22
23
  the annotation set by stop_pods() are touched. If this program fails, the deployment is
23
24
  considered as failed: stranded workers must be visible, not a log line.
24
25
  """
26
+ configure_logging()
25
27
  ensure_safe_run()
26
28
 
27
29
  release_name: str = os.environ["CHARTREUSE_RELEASE_NAME"]
@@ -9,6 +9,7 @@ Depends only on the official kubernetes client.
9
9
  """
10
10
 
11
11
  import functools
12
+ import json
12
13
  import logging
13
14
  import os
14
15
  import pprint
@@ -20,10 +21,18 @@ import kubernetes
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
24
+ # Target-rename prefix used by the pre-7.0.1 HPA disable mechanism. Only the repair path
25
+ # (re_enable_hpa) remains, for HPAs left renamed by an older chartreuse.
26
+ # TODO: remove in 8.0 together with re_enable_hpa.
23
27
  HPA_ID_PREFIX = "wm--disabled--kube"
28
+ # A Pod in a terminal phase holds no resource: it is not a living replica.
29
+ TERMINAL_POD_PHASES = ("Failed", "Succeeded")
24
30
  # Set by stop_pods() on every Deployment it scales down, cleared by start_pods() and
25
31
  # restore_stopped_pods(). Allows telling "stopped by us" apart from "deliberately scaled to 0".
26
32
  STOPPED_ANNOTATION = "wiremind.io/stopped-by"
33
+ # Set by pause_hpa() on the HPA itself; stores the pre-pause spec.behavior as JSON ("" when
34
+ # absent) so resume_hpa() can restore it exactly. Presence of the annotation == "paused by us".
35
+ PAUSED_ANNOTATION = "wiremind.io/pre-pause-scale-behavior"
27
36
 
28
37
 
29
38
  def load_kubernetes_config(use_kubeconfig: bool | None = None) -> None:
@@ -177,7 +186,7 @@ class KubernetesDeploymentManager:
177
186
 
178
187
  current_scale = 0
179
188
  for pod in pod_list:
180
- if pod.status.phase not in ("Failed"):
189
+ if pod.status.phase not in TERMINAL_POD_PHASES:
181
190
  current_scale += 1
182
191
 
183
192
  if current_scale > 0:
@@ -254,14 +263,51 @@ class KubernetesDeploymentManager:
254
263
  return eds_dict
255
264
 
256
265
  @retry_kubernetes_request
257
- def disable_hpa(self, *, deployment_name: str) -> None:
266
+ def pause_hpa(self, *, deployment_name: str) -> None:
267
+ """
268
+ Forbid the Deployment's HPAs from scaling up (spec.behavior.scaleUp.selectPolicy:
269
+ Disabled) so they can't start pods mid-migration. Unlike breaking the scaleTargetRef,
270
+ the HPA stays Healthy: no FailedGetScale events, no Degraded ArgoCD application.
271
+
272
+ The pre-pause spec.behavior is saved in PAUSED_ANNOTATION so resume_hpa() can restore
273
+ it exactly. Idempotent: an already-paused HPA is left alone (never overwrite the
274
+ saved original with a paused state).
275
+ """
258
276
  for hpa in self.get_deployment_hpa(deployment_name=deployment_name):
259
- # Tell the hpa to manage a non-existing Deployment
260
- hpa.spec.scale_target_ref.name = f"{HPA_ID_PREFIX}-{deployment_name}"
261
- self.patch_deployment_hpa(hpa_name=hpa.metadata.name, body=hpa)
277
+ if PAUSED_ANNOTATION in (hpa.metadata.annotations or {}):
278
+ continue
279
+ original_behavior = self.client_autoscalingv2_api.api_client.sanitize_for_serialization(hpa.spec.behavior)
280
+ saved = json.dumps(original_behavior) if original_behavior else ""
281
+ body = {
282
+ "metadata": {"annotations": {PAUSED_ANNOTATION: saved}},
283
+ "spec": {"behavior": {"scaleUp": {"selectPolicy": "Disabled"}}},
284
+ }
285
+ self.patch_deployment_hpa(hpa_name=hpa.metadata.name, body=body)
286
+
287
+ @retry_kubernetes_request
288
+ def resume_hpa(self, *, deployment_name: str) -> None:
289
+ """
290
+ Undo pause_hpa(): restore the exact pre-pause spec.behavior (or clear it when there
291
+ was none) and drop PAUSED_ANNOTATION. No-op on HPAs that don't carry the annotation,
292
+ so it is always safe to call.
293
+ """
294
+ for hpa in self.get_deployment_hpa(deployment_name=deployment_name):
295
+ saved = (hpa.metadata.annotations or {}).get(PAUSED_ANNOTATION)
296
+ if saved is None:
297
+ continue
298
+ body = {
299
+ "metadata": {"annotations": {PAUSED_ANNOTATION: None}},
300
+ "spec": {"behavior": json.loads(saved) if saved else None},
301
+ }
302
+ self.patch_deployment_hpa(hpa_name=hpa.metadata.name, body=body)
262
303
 
263
304
  @retry_kubernetes_request
264
305
  def re_enable_hpa(self, *, deployment_name: str) -> None:
306
+ """
307
+ Repair an HPA left pointing at a wm--disabled--kube- target by a pre-7.0.1 chartreuse
308
+ whose run crashed before re-enabling. 7.0.1+ never creates such renames.
309
+ TODO: remove in 8.0.
310
+ """
265
311
  for hpa in self.get_deployment_hpa(deployment_name=f"{HPA_ID_PREFIX}-{deployment_name}"):
266
312
  hpa.spec.scale_target_ref.name = deployment_name
267
313
  self.patch_deployment_hpa(hpa_name=hpa.metadata.name, body=hpa)
@@ -278,9 +324,9 @@ class KubernetesDeploymentManager:
278
324
  """
279
325
  for deployment_name in deployment_dict:
280
326
  self.annotate_deployment(deployment_name, {STOPPED_ANNOTATION: self.release_name})
327
+ self.pause_hpa(deployment_name=deployment_name)
281
328
  for _ in range(self.SCALE_DOWN_MAX_WAIT_TIME):
282
329
  for deployment_name in deployment_dict:
283
- self.disable_hpa(deployment_name=deployment_name)
284
330
  self.scale_down_deployment(deployment_name)
285
331
  if self._are_deployments_stopped(deployment_dict):
286
332
  break
@@ -324,6 +370,7 @@ class KubernetesDeploymentManager:
324
370
  if len(priority_dict):
325
371
  scaled = True
326
372
  for name, expected_scale in priority_dict.items():
373
+ self.resume_hpa(deployment_name=name)
327
374
  self.re_enable_hpa(deployment_name=name)
328
375
  self.scale_up_deployment(name, expected_scale)
329
376
  self.annotate_deployment(name, {STOPPED_ANNOTATION: None})
@@ -336,8 +383,11 @@ class KubernetesDeploymentManager:
336
383
  """
337
384
  Restore Deployments that stop_pods() scaled down and that nothing scaled back up since.
338
385
 
339
- Only Deployments still carrying STOPPED_ANNOTATION are touched, so a Deployment
340
- deliberately scaled to zero outside of stop_pods() is left alone.
386
+ Only the replica restore is gated on STOPPED_ANNOTATION, so a Deployment deliberately
387
+ scaled to zero outside of stop_pods() is left alone. HPA resume/repair on the other
388
+ hand runs for EVERY tracked Deployment: a pause left behind by a crashed run is not
389
+ self-healing (ArgoCD ignores the live-only spec.behavior field), and resuming an
390
+ unpaused HPA is a no-op.
341
391
 
342
392
  Meant to run after the deployment that follows a stop_pods() without start_pods()
343
393
  (CHARTREUSE_UPGRADE_BEFORE_DEPLOYMENT): that deployment restores spec.replicas of
@@ -351,6 +401,8 @@ class KubernetesDeploymentManager:
351
401
  restored: list[str] = []
352
402
  for priority in sorted(expected_deployment_scale_dict):
353
403
  for name, expected_scale in expected_deployment_scale_dict[priority].items():
404
+ self.resume_hpa(deployment_name=name)
405
+ self.re_enable_hpa(deployment_name=name)
354
406
  try:
355
407
  deployment = self.client_appsv1_api.read_namespaced_deployment(name, self.namespace)
356
408
  except kubernetes.client.rest.ApiException as e:
@@ -359,7 +411,6 @@ class KubernetesDeploymentManager:
359
411
  raise
360
412
  if STOPPED_ANNOTATION not in (deployment.metadata.annotations or {}):
361
413
  continue
362
- self.re_enable_hpa(deployment_name=name)
363
414
  if not deployment.spec.replicas:
364
415
  target_scale = expected_scale
365
416
  if target_scale < 1 and any(self.get_deployment_hpa(deployment_name=name)):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartreuse
3
- Version: 7.0.0.dev1
3
+ Version: 7.0.2
4
4
  Summary: Helper for Alembic migrations within Kubernetes.
5
5
  Author-email: wiremind <dev@wiremind.io>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -1 +0,0 @@
1
- 7.0.0.dev1
File without changes
File without changes
File without changes
File without changes