mirror of
https://github.com/krkn-chaos/krkn.git
synced 2026-08-25 09:27:36 +00:00
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:
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user