add force deletion option to pod disruption scenario (#1544)

* feat: add force pod deletion option to pod disruption scenario

Add a `force` boolean config option (default: false) that controls
whether pods are killed gracefully or forcefully. When force is true,
grace_period_seconds=0 is passed to delete_pod(), causing immediate
termination without waiting for the pod's terminationGracePeriodSeconds.

Works with both serial and parallel execution modes.

Depends on: krkn-lib feat/force-pod-delete branch

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: ddjain <darjain@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address PR review - validate force config, bump krkn-lib, add tests

- Add type validation for 'force' config to reject non-boolean values
- Bump krkn-lib to 6.1.3 (includes grace_period_seconds in delete_pod)
- Add unit tests for force deletion in serial and parallel modes

Signed-off-by: ddjain <darjain@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: ddjain <darjain@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Darshan Jain
2026-08-12 19:57:43 +05:30
committed by GitHub
co-authored by Cursor
parent bced12a265
commit e86ac5b802
4 changed files with 80 additions and 8 deletions
@@ -30,6 +30,9 @@ class InputParams:
self.node_label_selector = config["node_label_selector"] if "node_label_selector" in config else ""
self.node_names = config["node_names"] if "node_names" in config else []
self.exclude_label = config["exclude_label"] if "exclude_label" in config else ""
self.force = config["force"] if "force" in config else False
if not isinstance(self.force, bool):
raise ValueError(f"Invalid value '{self.force}' for 'force' in config. Must be a boolean (true/false).")
namespace_pattern: str
krkn_pod_recovery_time: int
@@ -41,4 +44,5 @@ class InputParams:
name_pattern: str
node_label_selector: str
node_names: list
exclude_label: str
exclude_label: str
force: bool
@@ -250,11 +250,15 @@ class PodDisruptionScenarioPlugin(AbstractScenarioPlugin):
pods_to_kill.append(pod)
if config.execution == "parallel":
self._delete_pods_parallel(pods_to_kill, kubecli)
self._delete_pods_parallel(pods_to_kill, kubecli, config.force)
else:
for pod in pods_to_kill:
logging.info(f'Deleting pod {pod[0]}')
kubecli.delete_pod(pod[0], pod[1])
if config.force:
logging.info(f'Force deleting pod {pod[0]} (grace_period_seconds=0)')
kubecli.delete_pod(pod[0], pod[1], grace_period_seconds=0)
else:
logging.info(f'Gracefully deleting pod {pod[0]}')
kubecli.delete_pod(pod[0], pod[1])
return_val = self.wait_for_pods(config.label_selector,config.name_pattern,config.namespace_pattern, pods_count, config.duration, config.timeout, kubecli, config.node_label_selector, config.node_names)
except Exception as e:
@@ -284,14 +288,18 @@ class PodDisruptionScenarioPlugin(AbstractScenarioPlugin):
return 0
def _delete_pods_parallel(self, pods: list, kubecli: KrknKubernetes):
def _delete_pods_parallel(self, pods: list, kubecli: KrknKubernetes, force: bool = False):
"""Delete pods concurrently using a thread pool to avoid unbounded threads."""
error_queue = queue.Queue()
def _delete(pod):
try:
logging.info(f'[parallel] Deleting pod {pod[0]}')
kubecli.delete_pod(pod[0], pod[1])
if force:
logging.info(f'[parallel] Force deleting pod {pod[0]} (grace_period_seconds=0)')
kubecli.delete_pod(pod[0], pod[1], grace_period_seconds=0)
else:
logging.info(f'[parallel] Gracefully deleting pod {pod[0]}')
kubecli.delete_pod(pod[0], pod[1])
except Exception as exc:
error_queue.put(exc)
+1 -1
View File
@@ -22,7 +22,7 @@ reportlab>=4.0
cbor2<5.7.0 # Pinned by arcaflow-plugin-sdk
lxml==6.1.0
kubernetes>=35.0.0,<36.0.0
krkn-lib==6.1.2
krkn-lib==6.1.3
numpy==1.26.4
pandas==2.2.0
openshift-client==1.0.21
@@ -170,6 +170,66 @@ class TestKillingPodsMode(unittest.TestCase):
# Only pod2 should be deleted; pod1 is excluded
self.kubecli.delete_pod.assert_called_once_with("pod2", "ns1")
# --- force deletion tests ---
def test_force_defaults_to_false(self):
"""force defaults to False when not specified in config."""
params = InputParams({"kill": 1})
self.assertFalse(params.force)
def test_force_invalid_type_raises_value_error(self):
"""force raises ValueError when given a non-boolean value like a string."""
with self.assertRaises(ValueError) as context:
InputParams({"kill": 1, "force": "false"})
self.assertIn("Must be a boolean", str(context.exception))
def test_serial_mode_force_passes_grace_period_zero(self):
"""Serial mode with force=True calls delete_pod with grace_period_seconds=0."""
config = InputParams({"kill": 2, "execution": "serial", "force": True})
self.plugin.get_pods.return_value = [("pod1", "ns1"), ("pod2", "ns1")]
result = self.plugin.killing_pods(config, self.kubecli)
self.assertEqual(result, 0)
self.assertEqual(self.kubecli.delete_pod.call_count, 2)
self.kubecli.delete_pod.assert_any_call("pod1", "ns1", grace_period_seconds=0)
self.kubecli.delete_pod.assert_any_call("pod2", "ns1", grace_period_seconds=0)
def test_serial_mode_graceful_no_grace_period_kwarg(self):
"""Serial mode with force=False (default) calls delete_pod without grace_period_seconds."""
config = InputParams({"kill": 1, "execution": "serial", "force": False})
self.plugin.get_pods.return_value = [("pod1", "ns1")]
result = self.plugin.killing_pods(config, self.kubecli)
self.assertEqual(result, 0)
self.kubecli.delete_pod.assert_called_once_with("pod1", "ns1")
def test_parallel_mode_force_passes_grace_period_zero(self):
"""Parallel mode with force=True calls delete_pod with grace_period_seconds=0."""
config = InputParams({"kill": 2, "execution": "parallel", "force": True})
self.plugin.get_pods.return_value = [("pod1", "ns1"), ("pod2", "ns1")]
result = self.plugin.killing_pods(config, self.kubecli)
self.assertEqual(result, 0)
self.assertEqual(self.kubecli.delete_pod.call_count, 2)
self.kubecli.delete_pod.assert_any_call("pod1", "ns1", grace_period_seconds=0)
self.kubecli.delete_pod.assert_any_call("pod2", "ns1", grace_period_seconds=0)
def test_parallel_mode_graceful_no_grace_period_kwarg(self):
"""Parallel mode with force=False (default) calls delete_pod without grace_period_seconds."""
config = InputParams({"kill": 2, "execution": "parallel", "force": False})
self.plugin.get_pods.return_value = [("pod1", "ns1"), ("pod2", "ns1")]
result = self.plugin.killing_pods(config, self.kubecli)
self.assertEqual(result, 0)
self.assertEqual(self.kubecli.delete_pod.call_count, 2)
self.kubecli.delete_pod.assert_any_call("pod1", "ns1")
self.kubecli.delete_pod.assert_any_call("pod2", "ns1")
if __name__ == "__main__":
unittest.main()