feat: storage I/O throttle scenario (cgroups v1/v2) for PVC-backed workloads (#1296)

* feat(storage-throttle): add storage throttle scenario and tests

Consolidate the storage-throttle implementation, scenario configs, CI v2 coverage, and krkn-lib 6.0.10 dependency update into a single signed commit for cleaner PR history.

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

* adding need dco auto review (#1329)

Signed-off-by: Paige Patton <prubenda@redhat.com>
Signed-off-by: ddjain <darjain@redhat.com>

* fix: start_klusterlet_scenario action calls start instead of stop (#1324)

The start_klusterlet_scenario branch in inject_managedcluster_scenario
was calling stop_klusterlet_scenario on the scenarios object instead of
start_klusterlet_scenario. Any user configuring this action would stop
the klusterlet (scale to 0) instead of starting it (scale to 3).

Fixes #1323

Signed-off-by: v0idheaven <dahiyavarun2007@gmail.com>
Co-authored-by: Paige Patton <64206430+paigerube14@users.noreply.github.com>
Signed-off-by: ddjain <darjain@redhat.com>

* adding links to issue of completed roadmap items (#1328)

Signed-off-by: Paige Patton <prubenda@redhat.com>
Signed-off-by: ddjain <darjain@redhat.com>

* container scenario template image update (#1342)

Signed-off-by: Paige Patton <prubenda@redhat.com>
Signed-off-by: ddjain <darjain@redhat.com>

---------

Signed-off-by: ddjain <darjain@redhat.com>
Signed-off-by: Paige Patton <prubenda@redhat.com>
Signed-off-by: v0idheaven <dahiyavarun2007@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Paige Patton <64206430+paigerube14@users.noreply.github.com>
Co-authored-by: Varun Yadav <dahiyavarun2007@gmail.com>
This commit is contained in:
Darshan Jain
2026-05-19 00:24:55 +05:30
committed by GitHub
co-authored by Paige Patton Cursor Varun Yadav
parent 3391ff2453
commit eb2efa84ea
13 changed files with 2311 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@ markers =
functional: marks a test as a functional test (deselect with '-m "not functional"')
pod_disruption: marks a test as a pod disruption scenario test
application_outage: marks a test as an application outage scenario test
storage_throttle: marks a test as a storage throttle scenario test
no_workload: skip workload deployment for this test (e.g. negative tests)
order: set test order (pytest-order)
junit_family = xunit2
@@ -0,0 +1,39 @@
# PVC + Deployment for storage throttle integration test.
# Namespace is patched at deploy time by the test framework.
# Uses the default StorageClass (e.g. local-path on KinD).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: krkn-throttle-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 128Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: krkn-throttle-target
spec:
replicas: 1
selector:
matchLabels:
app: krkn-throttle-target
template:
metadata:
labels:
app: krkn-throttle-target
spec:
containers:
- name: app
image: nginx:alpine
command: ["/bin/sh", "-c", "trap : TERM INT; sleep infinity & wait"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: krkn-throttle-pvc
@@ -0,0 +1,13 @@
# Base storage_throttle scenario. Tests load this and patch namespace, pvc_name, etc.
storage_throttle_scenario:
pvc_name: krkn-throttle-pvc
pod_name: ""
namespace: default
mount_path: /data
throttle_type: bandwidth
read_bps: 1Mi
write_bps: 512Ki
read_iops: 100
write_iops: 50
duration: 15
image: quay.io/krkn-chaos/krkn:tools
@@ -0,0 +1,139 @@
"""
Functional test for storage throttle scenario (cgroup I/O throttle on PVC-backed volume).
Deploys a PVC + Deployment in an ephemeral namespace, runs the storage_throttle
scenario, and verifies:
- Krkn exits 0 (throttle applied and removed cleanly)
- Target pods survive (running and ready after scenario)
- Negative cases: bad namespace and invalid throttle_type fail gracefully
Follows the CI/tests_v2 BaseScenarioTest pattern.
"""
import pytest
from lib.base import BaseScenarioTest, READINESS_TIMEOUT
from lib.utils import (
assert_all_pods_running_and_ready,
assert_kraken_failure,
assert_kraken_success,
assert_pod_count_unchanged,
get_pods_list,
)
@pytest.mark.functional
@pytest.mark.storage_throttle
class TestStorageThrottle(BaseScenarioTest):
"""Storage throttle scenario: apply I/O cgroup limits on a PVC mount and verify recovery."""
WORKLOAD_MANIFEST = "CI/tests_v2/scenarios/storage_throttle/resource.yaml"
WORKLOAD_IS_PATH = True
LABEL_SELECTOR = "app=krkn-throttle-target"
SCENARIO_NAME = "storage_throttle"
SCENARIO_TYPE = "storage_throttle_scenarios"
NAMESPACE_KEY_PATH = ["storage_throttle_scenario", "namespace"]
NAMESPACE_IS_REGEX = False
OVERRIDES_KEY_PATH = ["storage_throttle_scenario"]
@pytest.mark.order(1)
def test_bandwidth_throttle_and_recovery(self, wait_for_pods_running):
"""Bandwidth throttle: apply read/write bps limits, verify Krkn success and pod recovery."""
ns = self.ns
before = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
result = self.run_scenario(self.tmp_path, ns, overrides={
"throttle_type": "bandwidth",
"read_bps": "1Mi",
"write_bps": "512Ki",
"duration": 15,
})
assert_kraken_success(result, context=f"bandwidth namespace={ns}", tmp_path=self.tmp_path)
wait_for_pods_running(ns, self.LABEL_SELECTOR, timeout=READINESS_TIMEOUT)
after = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
assert_pod_count_unchanged(before, after, namespace=ns)
assert_all_pods_running_and_ready(after, namespace=ns)
@pytest.mark.order(2)
def test_iops_throttle_and_recovery(self, wait_for_pods_running):
"""IOPS throttle: apply read/write iops limits, verify Krkn success and pod recovery."""
ns = self.ns
before = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
result = self.run_scenario(
self.tmp_path, ns,
overrides={
"throttle_type": "iops",
"read_iops": 50,
"write_iops": 25,
"duration": 15,
},
config_filename="test_iops_config.yaml",
)
assert_kraken_success(result, context=f"iops namespace={ns}", tmp_path=self.tmp_path)
wait_for_pods_running(ns, self.LABEL_SELECTOR, timeout=READINESS_TIMEOUT)
after = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
assert_pod_count_unchanged(before, after, namespace=ns)
assert_all_pods_running_and_ready(after, namespace=ns)
@pytest.mark.order(3)
def test_both_throttle_and_recovery(self, wait_for_pods_running):
"""Combined throttle: apply both bps and iops limits, verify Krkn success and pod recovery."""
ns = self.ns
before = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
result = self.run_scenario(
self.tmp_path, ns,
overrides={
"throttle_type": "both",
"read_bps": "1Mi",
"write_bps": "512Ki",
"read_iops": 50,
"write_iops": 25,
"duration": 15,
},
config_filename="test_both_config.yaml",
)
assert_kraken_success(result, context=f"both namespace={ns}", tmp_path=self.tmp_path)
wait_for_pods_running(ns, self.LABEL_SELECTOR, timeout=READINESS_TIMEOUT)
after = get_pods_list(self.k8s_core, ns, self.LABEL_SELECTOR)
assert_pod_count_unchanged(before, after, namespace=ns)
assert_all_pods_running_and_ready(after, namespace=ns)
@pytest.mark.no_workload
def test_bad_namespace_fails(self):
"""Scenario targeting non-existent namespace causes Krkn to exit non-zero."""
scenario = self.load_and_patch_scenario(
self.repo_root, "nonexistent-namespace-xyz-99999",
pod_name="nonexistent-pod",
)
scenario_path = self.write_scenario(self.tmp_path, scenario, suffix="_bad_ns")
config_path = self.build_config(
self.SCENARIO_TYPE, str(scenario_path),
filename="storage_throttle_bad_ns_config.yaml",
)
result = self.run_kraken(config_path)
assert_kraken_failure(
result, context=f"bad namespace test", tmp_path=self.tmp_path,
)
@pytest.mark.no_workload
def test_invalid_throttle_type_fails(self):
"""Invalid throttle_type causes Krkn to exit non-zero."""
scenario = self.load_and_patch_scenario(
self.repo_root, self.ns,
throttle_type="invalid_type",
pod_name="doesnt-matter",
)
scenario_path = self.write_scenario(self.tmp_path, scenario, suffix="_bad_type")
config_path = self.build_config(
self.SCENARIO_TYPE, str(scenario_path),
filename="storage_throttle_bad_type_config.yaml",
)
result = self.run_kraken(config_path)
assert_kraken_failure(
result, context=f"invalid throttle_type test", tmp_path=self.tmp_path,
)