From 0a058820021f1801bd3ad0daba6aff322a21ea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20de=20Saint=20Martin?= Date: Mon, 5 Oct 2026 18:12:03 +0200 Subject: [PATCH] fix: do not count Succeeded Pods as living replicas is_deployment_stopped counted a Pod in phase Succeeded as a living replica, so a Deployment holding one was never seen as stopped and the migration waited for it. The terminal-phase test also read ("Failed"), a string, so `not in` was a substring test instead of a membership test. Same fix as wiremind-kubernetes 7.7.2, which this helper was ported from. --- CHANGELOG.md | 8 +++++++ VERSION | 2 +- src/chartreuse/kubernetes_helper.py | 4 +++- .../unit_tests/test_kubernetes_helper.py | 24 +++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355bca6..9474ce0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v7.0.2 (2026-10-05) + +### Fix +- `is_deployment_stopped` no longer counts a Pod in phase `Succeeded` (`Completed`) as a living + replica. A Deployment holding one was never seen as stopped, so the migration waited until an + operator deleted the Pod by hand. Terminal phases are now a real tuple, `("Failed", "Succeeded")`, + instead of the string `("Failed")`, which made `not in` a substring test. + ## v7.0.1 (2026-07-09) ### Fix diff --git a/VERSION b/VERSION index 9fe9ff9..a8907c0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.0.1 +7.0.2 diff --git a/src/chartreuse/kubernetes_helper.py b/src/chartreuse/kubernetes_helper.py index 1f636ad..4ff4139 100644 --- a/src/chartreuse/kubernetes_helper.py +++ b/src/chartreuse/kubernetes_helper.py @@ -25,6 +25,8 @@ # (re_enable_hpa) remains, for HPAs left renamed by an older chartreuse. # TODO: remove in 8.0 together with re_enable_hpa. HPA_ID_PREFIX = "wm--disabled--kube" +# A Pod in a terminal phase holds no resource: it is not a living replica. +TERMINAL_POD_PHASES = ("Failed", "Succeeded") # Set by stop_pods() on every Deployment it scales down, cleared by start_pods() and # restore_stopped_pods(). Allows telling "stopped by us" apart from "deliberately scaled to 0". STOPPED_ANNOTATION = "wiremind.io/stopped-by" @@ -184,7 +186,7 @@ def is_deployment_stopped(self, deployment_name: str) -> bool: current_scale = 0 for pod in pod_list: - if pod.status.phase not in ("Failed"): + if pod.status.phase not in TERMINAL_POD_PHASES: current_scale += 1 if current_scale > 0: diff --git a/src/chartreuse/tests/unit_tests/test_kubernetes_helper.py b/src/chartreuse/tests/unit_tests/test_kubernetes_helper.py index f931525..93beecb 100644 --- a/src/chartreuse/tests/unit_tests/test_kubernetes_helper.py +++ b/src/chartreuse/tests/unit_tests/test_kubernetes_helper.py @@ -2,6 +2,7 @@ import unittest import kubernetes +import pytest from pytest_mock import MockerFixture from chartreuse.kubernetes_helper import PAUSED_ANNOTATION, STOPPED_ANNOTATION, KubernetesDeploymentManager @@ -275,3 +276,26 @@ def test_resume_hpa_restores_original_behavior(mocker: MockerFixture) -> None: body={"metadata": {"annotations": {PAUSED_ANNOTATION: None}}, "spec": {"behavior": None}}, ), ] + + +@pytest.mark.parametrize( + "phase, expected_stopped", + [ + ("Failed", True), + ("Succeeded", True), + ("Running", False), + ("Pending", False), + ], +) +def test_is_deployment_stopped(mocker: MockerFixture, phase: str, expected_stopped: bool) -> None: + """ + Test that we only consider Pods in a non-terminal phase as living Pods. + + A Failed (like evicted) or Succeeded (Completed) Pod holds no resource. + """ + kdm = _make_kdm(mocker) + pod = mocker.MagicMock() + pod.status.phase = phase + mocker.patch.object(kdm, "_get_pods_from_deployment", return_value=[pod]) + + assert kdm.is_deployment_stopped("worker") is expected_stopped