Skip to content

Commit e05be60

Browse files
Rodrigo MenezesLawouach
authored andcommitted
Add statefulset_is_fully_available and statefulset_is_not_fully_available
Signed-off-by: Rodrigo Menezes <rodrigomenezes@pinterest.com>
1 parent 06f683a commit e05be60

4 files changed

Lines changed: 214 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
## [Unreleased][]
44

5+
[Unreleased]: https://github.com/chaostoolkit/chaostoolkit-kubernetes/compare/0.22.0...HEAD
6+
7+
### Added
8+
59
- Add namespace to all probes and actions with backward compatibility [#39][39]
10+
- Added `statefulset_is_fully_available` and `statefulset_is_not_fully_available` [#86][86].
611

712
[39]: https://github.com/chaostoolkit/chaostoolkit-kubernetes/issues/39
8-
9-
[Unreleased]: https://github.com/chaostoolkit/chaostoolkit-kubernetes/compare/0.22.0...HEAD
13+
[86]: https://github.com/chaostoolkit/chaostoolkit-kubernetes/pull/86
1014

1115
### Changed
1216

chaosk8s/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def load_exported_activities() -> List[DiscoveredActivities]:
142142
activities.extend(discover_actions("chaosk8s.service.actions"))
143143
activities.extend(discover_actions("chaosk8s.service.probes"))
144144
activities.extend(discover_actions("chaosk8s.statefulset.actions"))
145+
activities.extend(discover_probes("chaosk8s.statefulset.probes"))
145146
return activities
146147

147148

chaosk8s/statefulset/probes.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# -*- coding: utf-8 -*-
2+
from typing import Union
3+
4+
import urllib3
5+
from chaoslib.exceptions import ActivityFailed
6+
from chaoslib.types import Secrets
7+
from functools import partial
8+
from kubernetes import client, watch
9+
from logzero import logger
10+
11+
from chaosk8s import create_k8s_api_client
12+
13+
__all__ = [
14+
"statefulset_fully_available",
15+
"statefulset_not_fully_available"
16+
]
17+
18+
19+
def _statefulset_readiness_has_state(name: str, ready: bool,
20+
ns: str = "default",
21+
label_selector: str = None,
22+
timeout: int = 30,
23+
secrets: Secrets = None):
24+
"""
25+
Check wether if the given statefulSet state is ready or not
26+
according to the ready paramter.
27+
If the state is not reached after `timeout` seconds, a
28+
:exc:`chaoslib.exceptions.ActivityFailed` exception is raised.
29+
"""
30+
field_selector = "metadata.name={name}".format(name=name)
31+
api = create_k8s_api_client(secrets)
32+
v1 = client.AppsV1Api(api)
33+
w = watch.Watch()
34+
timeout = int(timeout)
35+
36+
if label_selector is None:
37+
watch_events = partial(w.stream, v1.list_namespaced_stateful_set,
38+
namespace=ns,
39+
field_selector=field_selector,
40+
_request_timeout=timeout)
41+
else:
42+
label_selector = label_selector.format(name=name)
43+
watch_events = partial(w.stream, v1.list_namespaced_stateful_set,
44+
namespace=ns,
45+
field_selector=field_selector,
46+
label_selector=label_selector,
47+
_request_timeout=timeout)
48+
49+
try:
50+
logger.debug("Watching events for {t}s".format(t=timeout))
51+
for event in watch_events():
52+
statefulset = event['object']
53+
status = statefulset.status
54+
spec = statefulset.spec
55+
56+
logger.debug(
57+
"StatefulSet '{p}' {t}: "
58+
"Ready Replicas {r} - "
59+
"Unavailable Replicas {u} - "
60+
"Desired Replicas {a}".format(
61+
p=statefulset.metadata.name, t=event["type"],
62+
r=status.ready_replicas,
63+
a=spec.replicas,
64+
u=status.unavailable_replicas))
65+
66+
readiness = status.ready_replicas == spec.replicas
67+
if ready == readiness:
68+
w.stop()
69+
return True
70+
71+
except urllib3.exceptions.ReadTimeoutError:
72+
logger.debug("Timed out!")
73+
return False
74+
75+
76+
def statefulset_not_fully_available(name: str, ns: str = "default",
77+
label_selector: str = None,
78+
timeout: int = 30,
79+
secrets: Secrets = None):
80+
"""
81+
Wait until the statefulSet gets into an intermediate state where not all
82+
expected replicas are available. Once this state is reached, return `True`.
83+
If the state is not reached after `timeout` seconds, a
84+
:exc:`chaoslib.exceptions.ActivityFailed` exception is raised.
85+
"""
86+
if _statefulset_readiness_has_state(
87+
name,
88+
False,
89+
ns,
90+
label_selector,
91+
timeout,
92+
secrets,
93+
):
94+
return True
95+
else:
96+
raise ActivityFailed(
97+
"microservice '{name}' failed to stop running within {t}s".format(
98+
name=name, t=timeout))
99+
100+
101+
def statefulset_fully_available(name: str, ns: str = "default",
102+
label_selector: str = None,
103+
timeout: int = 30,
104+
secrets: Secrets = None):
105+
"""
106+
Wait until all the statefulSet expected replicas are available.
107+
Once this state is reached, return `True`.
108+
If the state is not reached after `timeout` seconds, a
109+
:exc:`chaoslib.exceptions.ActivityFailed` exception is raised.
110+
"""
111+
if _statefulset_readiness_has_state(
112+
name,
113+
True,
114+
ns,
115+
label_selector,
116+
timeout,
117+
secrets,
118+
):
119+
return True
120+
else:
121+
raise ActivityFailed(
122+
"microservice '{name}' failed to recover within {t}s".format(
123+
name=name, t=timeout))

tests/test_probes.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
microservice_available_and_healthy, microservice_is_not_available, \
1313
service_endpoint_is_initialized, deployment_is_not_fully_available, \
1414
deployment_is_fully_available, read_microservices_logs
15+
from chaosk8s.statefulset.probes import statefulset_fully_available, \
16+
statefulset_not_fully_available
1517

1618

1719
@patch('chaosk8s.has_local_config_file', autospec=True)
@@ -223,6 +225,88 @@ def test_deployment_is_not_fully_available_when_it_should(cl, client,
223225
str(excinfo.value)
224226

225227

228+
@patch('chaosk8s.has_local_config_file', autospec=True)
229+
@patch('chaosk8s.statefulset.probes.watch', autospec=True)
230+
@patch('chaosk8s.statefulset.probes.client', autospec=True)
231+
@patch('chaosk8s.client')
232+
def test_statefulset_not_fully_available(cl, client, watch, has_conf):
233+
has_conf.return_value = False
234+
statefulset = MagicMock()
235+
statefulset.spec.replicas = 2
236+
statefulset.status.ready_replicas = 1
237+
238+
watcher = MagicMock()
239+
watcher.stream = MagicMock()
240+
watcher.stream.side_effect = [[{"object": statefulset, "type": "ADDED"}]]
241+
watch.Watch.return_value = watcher
242+
243+
assert statefulset_not_fully_available("mysvc") is True
244+
245+
246+
@patch('chaosk8s.has_local_config_file', autospec=True)
247+
@patch('chaosk8s.statefulset.probes.watch', autospec=True)
248+
@patch('chaosk8s.statefulset.probes.client', autospec=True)
249+
@patch('chaosk8s.client')
250+
def test_statefulset_fully_available_when_it_should_not(cl, client,
251+
watch, has_conf):
252+
has_conf.return_value = False
253+
statefulset = MagicMock()
254+
statefulset.spec.replicas = 2
255+
statefulset.status.ready_replicas = 2
256+
257+
watcher = MagicMock()
258+
watcher.stream = MagicMock()
259+
watcher.stream.side_effect = urllib3.exceptions.ReadTimeoutError(
260+
None, None, None)
261+
watch.Watch.return_value = watcher
262+
263+
with pytest.raises(ActivityFailed) as excinfo:
264+
statefulset_not_fully_available("mysvc")
265+
assert "microservice 'mysvc' failed to stop running within" in \
266+
str(excinfo.value)
267+
268+
269+
@patch('chaosk8s.has_local_config_file', autospec=True)
270+
@patch('chaosk8s.statefulset.probes.watch', autospec=True)
271+
@patch('chaosk8s.statefulset.probes.client', autospec=True)
272+
@patch('chaosk8s.client')
273+
def test_statefulset_fully_available(cl, client, watch, has_conf):
274+
has_conf.return_value = False
275+
statefulset = MagicMock()
276+
statefulset.spec.replicas = 2
277+
statefulset.status.ready_replicas = 2
278+
279+
watcher = MagicMock()
280+
watcher.stream = MagicMock()
281+
watcher.stream.side_effect = [[{"object": statefulset, "type": "ADDED"}]]
282+
watch.Watch.return_value = watcher
283+
284+
assert statefulset_fully_available("mysvc") is True
285+
286+
287+
@patch('chaosk8s.has_local_config_file', autospec=True)
288+
@patch('chaosk8s.statefulset.probes.watch', autospec=True)
289+
@patch('chaosk8s.statefulset.probes.client', autospec=True)
290+
@patch('chaosk8s.client')
291+
def test_statefulset_not_fully_available_when_it_should(cl, client,
292+
watch, has_conf):
293+
has_conf.return_value = False
294+
statefulset = MagicMock()
295+
statefulset.spec.replicas = 2
296+
statefulset.status.ready_replicas = 1
297+
298+
watcher = MagicMock()
299+
watcher.stream = MagicMock()
300+
watcher.stream.side_effect = urllib3.exceptions.ReadTimeoutError(
301+
None, None, None)
302+
watch.Watch.return_value = watcher
303+
304+
with pytest.raises(ActivityFailed) as excinfo:
305+
statefulset_fully_available("mysvc")
306+
assert "microservice 'mysvc' failed to recover within" in \
307+
str(excinfo.value)
308+
309+
226310
@patch('chaosk8s.has_local_config_file', autospec=True)
227311
@patch('chaosk8s.pod.probes.client', autospec=True)
228312
@patch('chaosk8s.client')

0 commit comments

Comments
 (0)