diff --git a/krkn/scenario_plugins/pod_disruption/models/models.py b/krkn/scenario_plugins/pod_disruption/models/models.py index 5ffb3fe1..c06923b0 100644 --- a/krkn/scenario_plugins/pod_disruption/models/models.py +++ b/krkn/scenario_plugins/pod_disruption/models/models.py @@ -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 \ No newline at end of file + exclude_label: str + force: bool \ No newline at end of file diff --git a/krkn/scenario_plugins/pod_disruption/pod_disruption_scenario_plugin.py b/krkn/scenario_plugins/pod_disruption/pod_disruption_scenario_plugin.py index 8830bc30..a3e7c8dc 100644 --- a/krkn/scenario_plugins/pod_disruption/pod_disruption_scenario_plugin.py +++ b/krkn/scenario_plugins/pod_disruption/pod_disruption_scenario_plugin.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 69d9f511..df9c18cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/test_pod_disruption_scenario_plugin.py b/tests/test_pod_disruption_scenario_plugin.py index 61f2f682..3253e995 100644 --- a/tests/test_pod_disruption_scenario_plugin.py +++ b/tests/test_pod_disruption_scenario_plugin.py @@ -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()