krkn-lib 5.1.10__py3-none-any.whl → 6.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- krkn_lib/aws_tests/__init__.py +1 -1
- krkn_lib/elastic/krkn_elastic.py +3 -1
- krkn_lib/k8s/krkn_kubernetes.py +408 -20
- krkn_lib/k8s/pod_monitor/__init__.py +1 -2
- krkn_lib/k8s/pod_monitor/pod_monitor.py +146 -56
- krkn_lib/k8s/templates/snapshot.j2 +10 -0
- krkn_lib/models/elastic/models.py +24 -1
- krkn_lib/models/k8s/models.py +1 -1
- krkn_lib/models/pod_monitor/models.py +2 -2
- krkn_lib/models/telemetry/models.py +9 -0
- krkn_lib/ocp/krkn_openshift.py +4 -4
- krkn_lib/prometheus/krkn_prometheus.py +1 -1
- krkn_lib/telemetry/k8s/krkn_telemetry_kubernetes.py +1 -1
- krkn_lib/telemetry/ocp/krkn_telemetry_openshift.py +1 -1
- krkn_lib/tests/base_test.py +16 -3
- krkn_lib/tests/test_krkn_elastic_models.py +23 -4
- krkn_lib/tests/test_krkn_kubernetes_check.py +3 -2
- krkn_lib/tests/test_krkn_kubernetes_create.py +5 -3
- krkn_lib/tests/test_krkn_kubernetes_delete.py +3 -2
- krkn_lib/tests/test_krkn_kubernetes_get.py +5 -4
- krkn_lib/tests/test_krkn_kubernetes_misc.py +3 -3
- krkn_lib/tests/test_krkn_kubernetes_models.py +1 -1
- krkn_lib/tests/test_krkn_kubernetes_pods_monitor_models.py +3 -4
- krkn_lib/tests/test_krkn_kubernetes_virt.py +735 -0
- krkn_lib/tests/test_krkn_openshift.py +571 -48
- krkn_lib/tests/test_krkn_telemetry_kubernetes.py +848 -0
- krkn_lib/tests/test_safe_logger.py +496 -0
- krkn_lib/tests/test_utils.py +4 -5
- krkn_lib/utils/functions.py +4 -3
- krkn_lib/version/version.py +5 -2
- {krkn_lib-5.1.10.dist-info → krkn_lib-6.0.0.dist-info}/METADATA +7 -10
- {krkn_lib-5.1.10.dist-info → krkn_lib-6.0.0.dist-info}/RECORD +34 -30
- {krkn_lib-5.1.10.dist-info → krkn_lib-6.0.0.dist-info}/WHEEL +1 -1
- {krkn_lib-5.1.10.dist-info/licenses → krkn_lib-6.0.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive unit tests for KrknTelemetryKubernetes class.
|
|
3
|
+
|
|
4
|
+
This test suite uses mocks to test all methods without requiring
|
|
5
|
+
actual Kubernetes clusters or external services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from queue import Queue
|
|
13
|
+
from unittest.mock import Mock, patch
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from krkn_lib.k8s import KrknKubernetes
|
|
18
|
+
from krkn_lib.models.krkn import ChaosRunAlert, ChaosRunAlertSummary
|
|
19
|
+
from krkn_lib.models.telemetry import ChaosRunTelemetry, ScenarioTelemetry
|
|
20
|
+
from krkn_lib.telemetry.k8s.krkn_telemetry_kubernetes import (
|
|
21
|
+
KrknTelemetryKubernetes,
|
|
22
|
+
)
|
|
23
|
+
from krkn_lib.utils.safe_logger import SafeLogger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestKrknTelemetryKubernetesInit(unittest.TestCase):
|
|
27
|
+
"""Test initialization and basic getter methods."""
|
|
28
|
+
|
|
29
|
+
def setUp(self):
|
|
30
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
31
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
32
|
+
self.telemetry_config = {
|
|
33
|
+
"enabled": True,
|
|
34
|
+
"api_url": "https://test-api.com",
|
|
35
|
+
"username": "test_user",
|
|
36
|
+
"password": "test_pass",
|
|
37
|
+
"telemetry_group": "test_group",
|
|
38
|
+
}
|
|
39
|
+
self.request_id = "test-request-123"
|
|
40
|
+
|
|
41
|
+
def test_init_with_config(self):
|
|
42
|
+
"""Test initialization with telemetry config."""
|
|
43
|
+
telemetry = KrknTelemetryKubernetes(
|
|
44
|
+
safe_logger=self.mock_logger,
|
|
45
|
+
lib_kubernetes=self.mock_kubecli,
|
|
46
|
+
krkn_telemetry_config=self.telemetry_config,
|
|
47
|
+
telemetry_request_id=self.request_id,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.assertEqual(telemetry.get_lib_kubernetes(), self.mock_kubecli)
|
|
51
|
+
self.assertEqual(
|
|
52
|
+
telemetry.get_telemetry_config(), self.telemetry_config
|
|
53
|
+
)
|
|
54
|
+
self.assertEqual(telemetry.get_telemetry_request_id(), self.request_id)
|
|
55
|
+
|
|
56
|
+
def test_init_without_config(self):
|
|
57
|
+
"""Test initialization without telemetry config."""
|
|
58
|
+
telemetry = KrknTelemetryKubernetes(
|
|
59
|
+
safe_logger=self.mock_logger,
|
|
60
|
+
lib_kubernetes=self.mock_kubecli,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.assertEqual(telemetry.get_telemetry_config(), {})
|
|
64
|
+
self.assertEqual(telemetry.get_telemetry_request_id(), "")
|
|
65
|
+
|
|
66
|
+
def test_default_telemetry_group(self):
|
|
67
|
+
"""Test default telemetry group value."""
|
|
68
|
+
telemetry = KrknTelemetryKubernetes(
|
|
69
|
+
safe_logger=self.mock_logger,
|
|
70
|
+
lib_kubernetes=self.mock_kubecli,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(telemetry.default_telemetry_group, "default")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestCollectClusterMetadata(unittest.TestCase):
|
|
77
|
+
"""Test collect_cluster_metadata method."""
|
|
78
|
+
|
|
79
|
+
def setUp(self):
|
|
80
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
81
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
82
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
83
|
+
safe_logger=self.mock_logger,
|
|
84
|
+
lib_kubernetes=self.mock_kubecli,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@patch("krkn_lib.utils.get_ci_job_url")
|
|
88
|
+
def test_collect_cluster_metadata_success(self, mock_get_ci_url):
|
|
89
|
+
"""Test successful collection of cluster metadata."""
|
|
90
|
+
# Setup mocks
|
|
91
|
+
mock_get_ci_url.return_value = "https://ci.example.com/job/123"
|
|
92
|
+
|
|
93
|
+
mock_obj_count = {"Deployment": 10, "Pod": 50, "Secret": 5}
|
|
94
|
+
self.mock_kubecli.get_all_kubernetes_object_count.return_value = (
|
|
95
|
+
mock_obj_count
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mock_node_info = Mock(count=3, instance_type="m5.large")
|
|
99
|
+
mock_taints = ["NoSchedule"]
|
|
100
|
+
self.mock_kubecli.get_nodes_infos.return_value = (
|
|
101
|
+
[mock_node_info],
|
|
102
|
+
mock_taints,
|
|
103
|
+
)
|
|
104
|
+
self.mock_kubecli.get_version.return_value = "v1.28.0"
|
|
105
|
+
|
|
106
|
+
# Create telemetry with successful scenario
|
|
107
|
+
chaos_telemetry = ChaosRunTelemetry()
|
|
108
|
+
scenario1 = ScenarioTelemetry()
|
|
109
|
+
scenario1.exit_status = 0
|
|
110
|
+
chaos_telemetry.scenarios.append(scenario1)
|
|
111
|
+
|
|
112
|
+
# Execute
|
|
113
|
+
self.telemetry.collect_cluster_metadata(chaos_telemetry)
|
|
114
|
+
|
|
115
|
+
# Assertions
|
|
116
|
+
self.assertEqual(
|
|
117
|
+
chaos_telemetry.kubernetes_objects_count, mock_obj_count
|
|
118
|
+
)
|
|
119
|
+
self.assertEqual(chaos_telemetry.node_summary_infos, [mock_node_info])
|
|
120
|
+
self.assertEqual(chaos_telemetry.cluster_version, "v1.28.0")
|
|
121
|
+
self.assertEqual(chaos_telemetry.major_version, "1.28")
|
|
122
|
+
self.assertEqual(chaos_telemetry.node_taints, mock_taints)
|
|
123
|
+
self.assertEqual(
|
|
124
|
+
chaos_telemetry.build_url, "https://ci.example.com/job/123"
|
|
125
|
+
)
|
|
126
|
+
self.assertEqual(chaos_telemetry.total_node_count, 3)
|
|
127
|
+
self.assertTrue(chaos_telemetry.job_status) # No failed scenarios
|
|
128
|
+
|
|
129
|
+
self.mock_logger.info.assert_called()
|
|
130
|
+
|
|
131
|
+
@patch("krkn_lib.utils.get_ci_job_url")
|
|
132
|
+
def test_collect_cluster_metadata_with_failed_scenario(
|
|
133
|
+
self, mock_get_ci_url
|
|
134
|
+
):
|
|
135
|
+
"""Test metadata collection when scenario failed."""
|
|
136
|
+
mock_get_ci_url.return_value = None
|
|
137
|
+
self.mock_kubecli.get_all_kubernetes_object_count.return_value = {}
|
|
138
|
+
self.mock_kubecli.get_nodes_infos.return_value = ([], [])
|
|
139
|
+
self.mock_kubecli.get_version.return_value = "v1.27.5"
|
|
140
|
+
|
|
141
|
+
chaos_telemetry = ChaosRunTelemetry()
|
|
142
|
+
scenario1 = ScenarioTelemetry()
|
|
143
|
+
scenario1.exit_status = 1 # Failed scenario
|
|
144
|
+
chaos_telemetry.scenarios.append(scenario1)
|
|
145
|
+
|
|
146
|
+
self.telemetry.collect_cluster_metadata(chaos_telemetry)
|
|
147
|
+
|
|
148
|
+
self.assertFalse(chaos_telemetry.job_status) # Should be False
|
|
149
|
+
|
|
150
|
+
@patch("krkn_lib.utils.get_ci_job_url")
|
|
151
|
+
def test_collect_cluster_metadata_multiple_nodes(self, mock_get_ci_url):
|
|
152
|
+
"""Test with multiple node types."""
|
|
153
|
+
mock_get_ci_url.return_value = None
|
|
154
|
+
self.mock_kubecli.get_all_kubernetes_object_count.return_value = {}
|
|
155
|
+
|
|
156
|
+
node_info1 = Mock(count=3)
|
|
157
|
+
node_info2 = Mock(count=5)
|
|
158
|
+
self.mock_kubecli.get_nodes_infos.return_value = (
|
|
159
|
+
[node_info1, node_info2],
|
|
160
|
+
[],
|
|
161
|
+
)
|
|
162
|
+
self.mock_kubecli.get_version.return_value = "v1.29.1"
|
|
163
|
+
|
|
164
|
+
chaos_telemetry = ChaosRunTelemetry()
|
|
165
|
+
self.telemetry.collect_cluster_metadata(chaos_telemetry)
|
|
166
|
+
|
|
167
|
+
self.assertEqual(chaos_telemetry.total_node_count, 8) # 3 + 5
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestSendTelemetry(unittest.TestCase):
|
|
171
|
+
"""Test send_telemetry method."""
|
|
172
|
+
|
|
173
|
+
def setUp(self):
|
|
174
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
175
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
176
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
177
|
+
safe_logger=self.mock_logger,
|
|
178
|
+
lib_kubernetes=self.mock_kubecli,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@patch("requests.post")
|
|
182
|
+
def test_send_telemetry_success(self, mock_post):
|
|
183
|
+
"""Test successful telemetry send."""
|
|
184
|
+
# Setup
|
|
185
|
+
mock_response = Mock()
|
|
186
|
+
mock_response.status_code = 200
|
|
187
|
+
mock_post.return_value = mock_response
|
|
188
|
+
|
|
189
|
+
telemetry_config = {
|
|
190
|
+
"enabled": True,
|
|
191
|
+
"api_url": "https://test-api.com",
|
|
192
|
+
"username": "testuser",
|
|
193
|
+
"password": "testpass",
|
|
194
|
+
"telemetry_group": "testgroup",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
chaos_telemetry = ChaosRunTelemetry()
|
|
198
|
+
uuid = "test-uuid-123"
|
|
199
|
+
|
|
200
|
+
# Execute
|
|
201
|
+
result = self.telemetry.send_telemetry(
|
|
202
|
+
telemetry_config, uuid, chaos_telemetry
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Assertions
|
|
206
|
+
self.assertIsNotNone(result)
|
|
207
|
+
mock_post.assert_called_once()
|
|
208
|
+
call_kwargs = mock_post.call_args[1]
|
|
209
|
+
self.assertEqual(call_kwargs["url"], "https://test-api.com/telemetry")
|
|
210
|
+
self.assertEqual(call_kwargs["auth"], ("testuser", "testpass"))
|
|
211
|
+
self.assertEqual(call_kwargs["params"]["request_id"], uuid)
|
|
212
|
+
self.assertEqual(call_kwargs["params"]["telemetry_group"], "testgroup")
|
|
213
|
+
|
|
214
|
+
self.mock_logger.info.assert_called_with(
|
|
215
|
+
"successfully sent telemetry data"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@patch("requests.post")
|
|
219
|
+
def test_send_telemetry_default_group(self, mock_post):
|
|
220
|
+
"""Test telemetry send with default group."""
|
|
221
|
+
mock_response = Mock()
|
|
222
|
+
mock_response.status_code = 200
|
|
223
|
+
mock_post.return_value = mock_response
|
|
224
|
+
|
|
225
|
+
telemetry_config = {
|
|
226
|
+
"enabled": True,
|
|
227
|
+
"api_url": "https://test-api.com",
|
|
228
|
+
"username": "testuser",
|
|
229
|
+
"password": "testpass",
|
|
230
|
+
# telemetry_group not specified
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.telemetry.send_telemetry(
|
|
234
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
call_kwargs = mock_post.call_args[1]
|
|
238
|
+
self.assertEqual(call_kwargs["params"]["telemetry_group"], "default")
|
|
239
|
+
|
|
240
|
+
def test_send_telemetry_disabled(self):
|
|
241
|
+
"""Test when telemetry is disabled."""
|
|
242
|
+
telemetry_config = {"enabled": False}
|
|
243
|
+
|
|
244
|
+
result = self.telemetry.send_telemetry(
|
|
245
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.assertIsNone(result)
|
|
249
|
+
|
|
250
|
+
def test_send_telemetry_missing_api_url(self):
|
|
251
|
+
"""Test with missing api_url."""
|
|
252
|
+
telemetry_config = {
|
|
253
|
+
"enabled": True,
|
|
254
|
+
"username": "testuser",
|
|
255
|
+
"password": "testpass",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
with self.assertRaises(Exception) as context:
|
|
259
|
+
self.telemetry.send_telemetry(
|
|
260
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.assertIn("api_url is missing", str(context.exception))
|
|
264
|
+
|
|
265
|
+
def test_send_telemetry_missing_username(self):
|
|
266
|
+
"""Test with missing username."""
|
|
267
|
+
telemetry_config = {
|
|
268
|
+
"enabled": True,
|
|
269
|
+
"api_url": "https://test-api.com",
|
|
270
|
+
"password": "testpass",
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
with self.assertRaises(Exception) as context:
|
|
274
|
+
self.telemetry.send_telemetry(
|
|
275
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self.assertIn("username is missing", str(context.exception))
|
|
279
|
+
|
|
280
|
+
def test_send_telemetry_missing_password(self):
|
|
281
|
+
"""Test with missing password."""
|
|
282
|
+
telemetry_config = {
|
|
283
|
+
"enabled": True,
|
|
284
|
+
"api_url": "https://test-api.com",
|
|
285
|
+
"username": "testuser",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
with self.assertRaises(Exception) as context:
|
|
289
|
+
self.telemetry.send_telemetry(
|
|
290
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
self.assertIn("password is missing", str(context.exception))
|
|
294
|
+
|
|
295
|
+
def test_send_telemetry_multiple_missing_fields(self):
|
|
296
|
+
"""Test with multiple missing required fields."""
|
|
297
|
+
telemetry_config = {"enabled": True}
|
|
298
|
+
|
|
299
|
+
with self.assertRaises(Exception) as context:
|
|
300
|
+
self.telemetry.send_telemetry(
|
|
301
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
exception_str = str(context.exception)
|
|
305
|
+
self.assertIn("api_url is missing", exception_str)
|
|
306
|
+
self.assertIn("username is missing", exception_str)
|
|
307
|
+
self.assertIn("password is missing", exception_str)
|
|
308
|
+
|
|
309
|
+
@patch("requests.post")
|
|
310
|
+
def test_send_telemetry_http_error(self, mock_post):
|
|
311
|
+
"""Test handling of HTTP errors."""
|
|
312
|
+
mock_response = Mock()
|
|
313
|
+
mock_response.status_code = 500
|
|
314
|
+
mock_response.content = b"Internal Server Error"
|
|
315
|
+
mock_post.return_value = mock_response
|
|
316
|
+
|
|
317
|
+
telemetry_config = {
|
|
318
|
+
"enabled": True,
|
|
319
|
+
"api_url": "https://test-api.com",
|
|
320
|
+
"username": "testuser",
|
|
321
|
+
"password": "testpass",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
with self.assertRaises(Exception) as context:
|
|
325
|
+
self.telemetry.send_telemetry(
|
|
326
|
+
telemetry_config, "test-uuid", ChaosRunTelemetry()
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
self.assertIn("failed to send telemetry", str(context.exception))
|
|
330
|
+
self.assertIn("500", str(context.exception))
|
|
331
|
+
self.mock_logger.warning.assert_called()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestSetParametersBase64(unittest.TestCase):
|
|
335
|
+
"""Test set_parameters_base64 method."""
|
|
336
|
+
|
|
337
|
+
def setUp(self):
|
|
338
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
339
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
340
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
341
|
+
safe_logger=self.mock_logger,
|
|
342
|
+
lib_kubernetes=self.mock_kubecli,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def test_set_parameters_base64_success(self):
|
|
346
|
+
"""Test successful base64 encoding of scenario file."""
|
|
347
|
+
scenario_yaml = {
|
|
348
|
+
"scenario": "pod_kill",
|
|
349
|
+
"kubeconfig": "/path/to/kubeconfig",
|
|
350
|
+
"param1": "value1",
|
|
351
|
+
}
|
|
352
|
+
yaml_content = yaml.safe_dump(scenario_yaml)
|
|
353
|
+
|
|
354
|
+
with tempfile.NamedTemporaryFile(
|
|
355
|
+
mode="w", delete=False, suffix=".yaml"
|
|
356
|
+
) as f:
|
|
357
|
+
f.write(yaml_content)
|
|
358
|
+
f.flush()
|
|
359
|
+
temp_path = f.name
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
scenario_telemetry = ScenarioTelemetry()
|
|
363
|
+
result = self.telemetry.set_parameters_base64(
|
|
364
|
+
scenario_telemetry, temp_path
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Verify base64 was set
|
|
368
|
+
self.assertIsNotNone(scenario_telemetry.parameters_base64)
|
|
369
|
+
|
|
370
|
+
# Decode and verify
|
|
371
|
+
decoded = base64.b64decode(
|
|
372
|
+
scenario_telemetry.parameters_base64.encode()
|
|
373
|
+
).decode()
|
|
374
|
+
decoded_yaml = yaml.safe_load(decoded)
|
|
375
|
+
|
|
376
|
+
self.assertEqual(decoded_yaml["scenario"], "pod_kill")
|
|
377
|
+
self.assertEqual(decoded_yaml["kubeconfig"], "anonymized")
|
|
378
|
+
self.assertEqual(decoded_yaml["param1"], "value1")
|
|
379
|
+
self.assertIsInstance(result, dict)
|
|
380
|
+
finally:
|
|
381
|
+
os.unlink(temp_path)
|
|
382
|
+
|
|
383
|
+
def test_set_parameters_base64_file_not_found(self):
|
|
384
|
+
"""Test with non-existent file."""
|
|
385
|
+
scenario_telemetry = ScenarioTelemetry()
|
|
386
|
+
|
|
387
|
+
with self.assertRaises(Exception) as context:
|
|
388
|
+
self.telemetry.set_parameters_base64(
|
|
389
|
+
scenario_telemetry, "/nonexistent/file.yaml"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
self.assertIn("scenario file not found", str(context.exception))
|
|
393
|
+
|
|
394
|
+
def test_set_parameters_base64_invalid_yaml(self):
|
|
395
|
+
"""Test with invalid YAML content."""
|
|
396
|
+
with tempfile.NamedTemporaryFile(
|
|
397
|
+
mode="w", delete=False, suffix=".yaml"
|
|
398
|
+
) as f:
|
|
399
|
+
f.write("invalid: yaml: content:\n - this is\n bad yaml")
|
|
400
|
+
f.flush()
|
|
401
|
+
temp_path = f.name
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
scenario_telemetry = ScenarioTelemetry()
|
|
405
|
+
|
|
406
|
+
with self.assertRaises(Exception) as context:
|
|
407
|
+
self.telemetry.set_parameters_base64(
|
|
408
|
+
scenario_telemetry, temp_path
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# The exception comes from yaml processing
|
|
412
|
+
self.assertIn("telemetry:", str(context.exception))
|
|
413
|
+
finally:
|
|
414
|
+
os.unlink(temp_path)
|
|
415
|
+
|
|
416
|
+
def test_set_parameters_base64_nested_kubeconfig(self):
|
|
417
|
+
"""Test anonymization of nested kubeconfig."""
|
|
418
|
+
scenario_yaml = {
|
|
419
|
+
"input_list": [
|
|
420
|
+
{"scenario": "pod_kill", "kubeconfig": "/path/to/config"},
|
|
421
|
+
{"scenario": "network", "kubeconfig": "/another/config"},
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
yaml_content = yaml.safe_dump(scenario_yaml)
|
|
425
|
+
|
|
426
|
+
with tempfile.NamedTemporaryFile(
|
|
427
|
+
mode="w", delete=False, suffix=".yaml"
|
|
428
|
+
) as f:
|
|
429
|
+
f.write(yaml_content)
|
|
430
|
+
f.flush()
|
|
431
|
+
temp_path = f.name
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
scenario_telemetry = ScenarioTelemetry()
|
|
435
|
+
self.telemetry.set_parameters_base64(scenario_telemetry, temp_path)
|
|
436
|
+
|
|
437
|
+
decoded = base64.b64decode(
|
|
438
|
+
scenario_telemetry.parameters_base64.encode()
|
|
439
|
+
).decode()
|
|
440
|
+
decoded_yaml = yaml.safe_load(decoded)
|
|
441
|
+
|
|
442
|
+
# All kubeconfig fields should be anonymized
|
|
443
|
+
self.assertEqual(
|
|
444
|
+
decoded_yaml["input_list"][0]["kubeconfig"], "anonymized"
|
|
445
|
+
)
|
|
446
|
+
self.assertEqual(
|
|
447
|
+
decoded_yaml["input_list"][1]["kubeconfig"], "anonymized"
|
|
448
|
+
)
|
|
449
|
+
finally:
|
|
450
|
+
os.unlink(temp_path)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class TestGetBucketUrlForFilename(unittest.TestCase):
|
|
454
|
+
"""Test get_bucket_url_for_filename method."""
|
|
455
|
+
|
|
456
|
+
def setUp(self):
|
|
457
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
458
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
459
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
460
|
+
safe_logger=self.mock_logger,
|
|
461
|
+
lib_kubernetes=self.mock_kubecli,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
@patch("requests.get")
|
|
465
|
+
def test_get_bucket_url_success(self, mock_get):
|
|
466
|
+
"""Test successful retrieval of presigned URL."""
|
|
467
|
+
mock_response = Mock()
|
|
468
|
+
mock_response.status_code = 200
|
|
469
|
+
mock_response.content = (
|
|
470
|
+
b"https://s3.amazonaws.com/bucket/file?presigned=true"
|
|
471
|
+
)
|
|
472
|
+
mock_get.return_value = mock_response
|
|
473
|
+
|
|
474
|
+
url = self.telemetry.get_bucket_url_for_filename(
|
|
475
|
+
api_url="https://api.test.com/presigned-url",
|
|
476
|
+
bucket_folder="test/folder",
|
|
477
|
+
remote_filename="test-file.tar",
|
|
478
|
+
username="testuser",
|
|
479
|
+
password="testpass",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
self.assertEqual(
|
|
483
|
+
url, "https://s3.amazonaws.com/bucket/file?presigned=true"
|
|
484
|
+
)
|
|
485
|
+
mock_get.assert_called_once()
|
|
486
|
+
|
|
487
|
+
@patch("requests.get")
|
|
488
|
+
def test_get_bucket_url_http_error(self, mock_get):
|
|
489
|
+
"""Test handling of HTTP errors."""
|
|
490
|
+
mock_response = Mock()
|
|
491
|
+
mock_response.status_code = 403
|
|
492
|
+
mock_get.return_value = mock_response
|
|
493
|
+
|
|
494
|
+
with self.assertRaises(Exception) as context:
|
|
495
|
+
self.telemetry.get_bucket_url_for_filename(
|
|
496
|
+
api_url="https://api.test.com/presigned-url",
|
|
497
|
+
bucket_folder="test/folder",
|
|
498
|
+
remote_filename="test-file.tar",
|
|
499
|
+
username="testuser",
|
|
500
|
+
password="testpass",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
self.assertIn("impossible to get upload url", str(context.exception))
|
|
504
|
+
self.assertIn("403", str(context.exception))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class TestPutFileToUrl(unittest.TestCase):
|
|
508
|
+
"""Test put_file_to_url method."""
|
|
509
|
+
|
|
510
|
+
def setUp(self):
|
|
511
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
512
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
513
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
514
|
+
safe_logger=self.mock_logger,
|
|
515
|
+
lib_kubernetes=self.mock_kubecli,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
@patch("requests.put")
|
|
519
|
+
def test_put_file_success(self, mock_put):
|
|
520
|
+
"""Test successful file upload."""
|
|
521
|
+
mock_response = Mock()
|
|
522
|
+
mock_response.status_code = 200
|
|
523
|
+
mock_put.return_value = mock_response
|
|
524
|
+
|
|
525
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
526
|
+
f.write(b"test content")
|
|
527
|
+
f.flush()
|
|
528
|
+
temp_path = f.name
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
self.telemetry.put_file_to_url(
|
|
532
|
+
"https://s3.test.com/file", temp_path
|
|
533
|
+
)
|
|
534
|
+
mock_put.assert_called_once()
|
|
535
|
+
finally:
|
|
536
|
+
os.unlink(temp_path)
|
|
537
|
+
|
|
538
|
+
@patch("requests.put")
|
|
539
|
+
def test_put_file_http_error(self, mock_put):
|
|
540
|
+
"""Test handling of HTTP errors."""
|
|
541
|
+
mock_response = Mock()
|
|
542
|
+
mock_response.status_code = 403
|
|
543
|
+
mock_put.return_value = mock_response
|
|
544
|
+
|
|
545
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
546
|
+
f.write(b"test content")
|
|
547
|
+
f.flush()
|
|
548
|
+
temp_path = f.name
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
with self.assertRaises(Exception) as context:
|
|
552
|
+
self.telemetry.put_file_to_url(
|
|
553
|
+
"https://s3.test.com/file", temp_path
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
self.assertIn(
|
|
557
|
+
"failed to send archive to s3", str(context.exception)
|
|
558
|
+
)
|
|
559
|
+
self.assertIn("403", str(context.exception))
|
|
560
|
+
finally:
|
|
561
|
+
os.unlink(temp_path)
|
|
562
|
+
|
|
563
|
+
@patch("requests.put")
|
|
564
|
+
def test_put_file_connection_error(self, mock_put):
|
|
565
|
+
"""Test handling of connection errors."""
|
|
566
|
+
mock_put.side_effect = Exception("Connection refused")
|
|
567
|
+
|
|
568
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
569
|
+
f.write(b"test content")
|
|
570
|
+
f.flush()
|
|
571
|
+
temp_path = f.name
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
with self.assertRaises(Exception) as context:
|
|
575
|
+
self.telemetry.put_file_to_url(
|
|
576
|
+
"https://s3.test.com/file", temp_path
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
self.assertIn("Connection refused", str(context.exception))
|
|
580
|
+
finally:
|
|
581
|
+
os.unlink(temp_path)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class TestPutCriticalAlerts(unittest.TestCase):
|
|
585
|
+
"""Test put_critical_alerts method."""
|
|
586
|
+
|
|
587
|
+
def setUp(self):
|
|
588
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
589
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
590
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
591
|
+
safe_logger=self.mock_logger,
|
|
592
|
+
lib_kubernetes=self.mock_kubecli,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def test_put_alerts_empty_summary(self):
|
|
596
|
+
"""Test with empty alert summary."""
|
|
597
|
+
telemetry_config = {
|
|
598
|
+
"events_backup": True,
|
|
599
|
+
"api_url": "https://test-api.com",
|
|
600
|
+
"username": "testuser",
|
|
601
|
+
"password": "testpass",
|
|
602
|
+
"max_retries": 3,
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
empty_summary = ChaosRunAlertSummary()
|
|
606
|
+
|
|
607
|
+
# Should return early without error
|
|
608
|
+
self.telemetry.put_critical_alerts(
|
|
609
|
+
"test-id", telemetry_config, empty_summary
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
self.mock_logger.info.assert_called_with(
|
|
613
|
+
"no alerts collected during the run, skipping"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def test_put_alerts_none_summary(self):
|
|
617
|
+
"""Test with None summary."""
|
|
618
|
+
telemetry_config = {
|
|
619
|
+
"events_backup": True,
|
|
620
|
+
"api_url": "https://test-api.com",
|
|
621
|
+
"username": "testuser",
|
|
622
|
+
"password": "testpass",
|
|
623
|
+
"max_retries": 3,
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
self.telemetry.put_critical_alerts("test-id", telemetry_config, None)
|
|
627
|
+
|
|
628
|
+
self.mock_logger.info.assert_called_with(
|
|
629
|
+
"no alerts collected during the run, skipping"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
def test_put_alerts_missing_config_fields(self):
|
|
633
|
+
"""Test with missing required config fields."""
|
|
634
|
+
telemetry_config = {"events_backup": True}
|
|
635
|
+
|
|
636
|
+
summary = ChaosRunAlertSummary()
|
|
637
|
+
alert = ChaosRunAlert("test", "firing", "default", "critical")
|
|
638
|
+
summary.chaos_alerts.append(alert)
|
|
639
|
+
|
|
640
|
+
with self.assertRaises(Exception) as context:
|
|
641
|
+
self.telemetry.put_critical_alerts(
|
|
642
|
+
"test-id", telemetry_config, summary
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
exception_str = str(context.exception)
|
|
646
|
+
self.assertIn("api_url is missing", exception_str)
|
|
647
|
+
self.assertIn("username is missing", exception_str)
|
|
648
|
+
self.assertIn("password is missing", exception_str)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class TestGenerateUrlAndPutToS3Worker(unittest.TestCase):
|
|
652
|
+
"""Test generate_url_and_put_to_s3_worker method."""
|
|
653
|
+
|
|
654
|
+
def setUp(self):
|
|
655
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
656
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
657
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
658
|
+
safe_logger=self.mock_logger,
|
|
659
|
+
lib_kubernetes=self.mock_kubecli,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
@patch.object(KrknTelemetryKubernetes, "put_file_to_url")
|
|
663
|
+
@patch.object(KrknTelemetryKubernetes, "get_bucket_url_for_filename")
|
|
664
|
+
def test_worker_success(self, mock_get_url, mock_put_file):
|
|
665
|
+
"""Test successful worker execution."""
|
|
666
|
+
mock_get_url.return_value = "https://s3.test.com/file"
|
|
667
|
+
|
|
668
|
+
queue = Queue()
|
|
669
|
+
uploaded_files = []
|
|
670
|
+
|
|
671
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
672
|
+
f.write(b"test")
|
|
673
|
+
f.flush()
|
|
674
|
+
temp_path = f.name
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
queue.put((1, temp_path, 0))
|
|
678
|
+
|
|
679
|
+
self.telemetry.generate_url_and_put_to_s3_worker(
|
|
680
|
+
queue=queue,
|
|
681
|
+
queue_size=1,
|
|
682
|
+
request_id="test-id",
|
|
683
|
+
telemetry_group="default",
|
|
684
|
+
api_url="https://api.test.com/presigned-url",
|
|
685
|
+
username="testuser",
|
|
686
|
+
password="testpass",
|
|
687
|
+
thread_number=0,
|
|
688
|
+
uploaded_file_list=uploaded_files,
|
|
689
|
+
max_retries=3,
|
|
690
|
+
remote_file_prefix="test-",
|
|
691
|
+
remote_file_extension=".tar",
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Worker should have processed the file
|
|
695
|
+
self.assertTrue(queue.empty())
|
|
696
|
+
self.assertEqual(len(uploaded_files), 1)
|
|
697
|
+
mock_get_url.assert_called_once()
|
|
698
|
+
mock_put_file.assert_called_once()
|
|
699
|
+
except FileNotFoundError:
|
|
700
|
+
# File was deleted by worker, which is expected
|
|
701
|
+
pass
|
|
702
|
+
|
|
703
|
+
@patch.object(KrknTelemetryKubernetes, "put_file_to_url")
|
|
704
|
+
@patch.object(KrknTelemetryKubernetes, "get_bucket_url_for_filename")
|
|
705
|
+
@patch("time.sleep")
|
|
706
|
+
def test_worker_retry_on_failure(
|
|
707
|
+
self, mock_sleep, mock_get_url, mock_put_file
|
|
708
|
+
):
|
|
709
|
+
"""Test worker retry logic on failure."""
|
|
710
|
+
mock_get_url.return_value = "https://s3.test.com/file"
|
|
711
|
+
mock_put_file.side_effect = [Exception("Upload failed"), None]
|
|
712
|
+
|
|
713
|
+
queue = Queue()
|
|
714
|
+
uploaded_files = []
|
|
715
|
+
|
|
716
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
717
|
+
f.write(b"test")
|
|
718
|
+
f.flush()
|
|
719
|
+
temp_path = f.name
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
queue.put((1, temp_path, 0))
|
|
723
|
+
|
|
724
|
+
self.telemetry.generate_url_and_put_to_s3_worker(
|
|
725
|
+
queue=queue,
|
|
726
|
+
queue_size=1,
|
|
727
|
+
request_id="test-id",
|
|
728
|
+
telemetry_group="default",
|
|
729
|
+
api_url="https://api.test.com/presigned-url",
|
|
730
|
+
username="testuser",
|
|
731
|
+
password="testpass",
|
|
732
|
+
thread_number=0,
|
|
733
|
+
uploaded_file_list=uploaded_files,
|
|
734
|
+
max_retries=3,
|
|
735
|
+
remote_file_prefix="test-",
|
|
736
|
+
remote_file_extension=".tar",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Should have retried
|
|
740
|
+
self.mock_logger.warning.assert_called()
|
|
741
|
+
mock_sleep.assert_called()
|
|
742
|
+
except FileNotFoundError:
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class TestGetPrometheusPodData(unittest.TestCase):
|
|
747
|
+
"""Test get_prometheus_pod_data method."""
|
|
748
|
+
|
|
749
|
+
def setUp(self):
|
|
750
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
751
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
752
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
753
|
+
safe_logger=self.mock_logger,
|
|
754
|
+
lib_kubernetes=self.mock_kubecli,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def test_prometheus_backup_disabled(self):
|
|
758
|
+
"""Test when prometheus backup is disabled."""
|
|
759
|
+
# Need to provide all required config even when disabled
|
|
760
|
+
# because validation happens before the disabled check
|
|
761
|
+
telemetry_config = {
|
|
762
|
+
"prometheus_backup": False,
|
|
763
|
+
"full_prometheus_backup": True,
|
|
764
|
+
"backup_threads": 4,
|
|
765
|
+
"api_url": "https://test-api.com",
|
|
766
|
+
"username": "testuser",
|
|
767
|
+
"password": "testpass",
|
|
768
|
+
"archive_path": "/tmp",
|
|
769
|
+
"archive_size": 100,
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
result = self.telemetry.get_prometheus_pod_data(
|
|
773
|
+
telemetry_config=telemetry_config,
|
|
774
|
+
request_id="test-id",
|
|
775
|
+
prometheus_pod_name="prometheus-0",
|
|
776
|
+
prometheus_container_name="prometheus",
|
|
777
|
+
prometheus_namespace="monitoring",
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
self.assertEqual(result, list[(int, str)]())
|
|
781
|
+
|
|
782
|
+
def test_prometheus_backup_missing_config(self):
|
|
783
|
+
"""Test with missing required config."""
|
|
784
|
+
telemetry_config = {"prometheus_backup": True}
|
|
785
|
+
|
|
786
|
+
with self.assertRaises(Exception) as context:
|
|
787
|
+
self.telemetry.get_prometheus_pod_data(
|
|
788
|
+
telemetry_config=telemetry_config,
|
|
789
|
+
request_id="test-id",
|
|
790
|
+
prometheus_pod_name="prometheus-0",
|
|
791
|
+
prometheus_container_name="prometheus",
|
|
792
|
+
prometheus_namespace="monitoring",
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
exception_str = str(context.exception)
|
|
796
|
+
self.assertIn("full_prometheus_backup flag is missing", exception_str)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class TestPutPrometheusData(unittest.TestCase):
|
|
800
|
+
"""Test put_prometheus_data method."""
|
|
801
|
+
|
|
802
|
+
def setUp(self):
|
|
803
|
+
self.mock_logger = Mock(spec=SafeLogger)
|
|
804
|
+
self.mock_kubecli = Mock(spec=KrknKubernetes)
|
|
805
|
+
self.telemetry = KrknTelemetryKubernetes(
|
|
806
|
+
safe_logger=self.mock_logger,
|
|
807
|
+
lib_kubernetes=self.mock_kubecli,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
def test_prometheus_backup_disabled(self):
|
|
811
|
+
"""Test when prometheus backup is disabled."""
|
|
812
|
+
# Need to provide all required config even when disabled
|
|
813
|
+
# because validation happens before the disabled check
|
|
814
|
+
telemetry_config = {
|
|
815
|
+
"prometheus_backup": False,
|
|
816
|
+
"backup_threads": 4,
|
|
817
|
+
"api_url": "https://test-api.com",
|
|
818
|
+
"username": "testuser",
|
|
819
|
+
"password": "testpass",
|
|
820
|
+
"max_retries": 3,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
# Should return early without error
|
|
824
|
+
result = self.telemetry.put_prometheus_data(
|
|
825
|
+
telemetry_config=telemetry_config,
|
|
826
|
+
archive_volumes=[],
|
|
827
|
+
request_id="test-id",
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
self.assertIsNone(result)
|
|
831
|
+
|
|
832
|
+
def test_put_prometheus_data_missing_config(self):
|
|
833
|
+
"""Test with missing required config."""
|
|
834
|
+
telemetry_config = {"prometheus_backup": True}
|
|
835
|
+
|
|
836
|
+
with self.assertRaises(Exception) as context:
|
|
837
|
+
self.telemetry.put_prometheus_data(
|
|
838
|
+
telemetry_config=telemetry_config,
|
|
839
|
+
archive_volumes=[(0, "test.tar.b64")],
|
|
840
|
+
request_id="test-id",
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
exception_str = str(context.exception)
|
|
844
|
+
self.assertIn("backup_threads is missing", exception_str)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
if __name__ == "__main__":
|
|
848
|
+
unittest.main()
|