kubectl-mcp-server 1.19.2__py3-none-any.whl → 1.21.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.
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/METADATA +26 -18
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/RECORD +13 -9
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/mcp_server.py +5 -1
- kubectl_mcp_tool/tools/__init__.py +4 -0
- kubectl_mcp_tool/tools/kind.py +1723 -0
- kubectl_mcp_tool/tools/vind.py +744 -0
- tests/test_kind.py +1206 -0
- tests/test_vind.py +512 -0
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.19.2.dist-info → kubectl_mcp_server-1.21.0.dist-info}/top_level.txt +0 -0
tests/test_kind.py
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for kind (Kubernetes IN Docker) tools.
|
|
3
|
+
|
|
4
|
+
This module tests the kind local cluster management toolset.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import json
|
|
9
|
+
from unittest.mock import patch, MagicMock
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestKindHelpers:
|
|
14
|
+
"""Tests for kind helper functions."""
|
|
15
|
+
|
|
16
|
+
@pytest.mark.unit
|
|
17
|
+
def test_kind_module_imports(self):
|
|
18
|
+
"""Test that kind module can be imported."""
|
|
19
|
+
from kubectl_mcp_tool.tools.kind import (
|
|
20
|
+
register_kind_tools,
|
|
21
|
+
_kind_available,
|
|
22
|
+
_get_kind_version,
|
|
23
|
+
_run_kind,
|
|
24
|
+
_run_docker,
|
|
25
|
+
kind_detect,
|
|
26
|
+
kind_version,
|
|
27
|
+
kind_list_clusters,
|
|
28
|
+
kind_get_nodes,
|
|
29
|
+
kind_get_kubeconfig,
|
|
30
|
+
kind_export_logs,
|
|
31
|
+
kind_create_cluster,
|
|
32
|
+
kind_delete_cluster,
|
|
33
|
+
kind_delete_all_clusters,
|
|
34
|
+
kind_load_image,
|
|
35
|
+
kind_load_image_archive,
|
|
36
|
+
kind_build_node_image,
|
|
37
|
+
kind_cluster_info,
|
|
38
|
+
kind_node_labels,
|
|
39
|
+
kind_config_validate,
|
|
40
|
+
kind_config_generate,
|
|
41
|
+
kind_config_show,
|
|
42
|
+
kind_available_images,
|
|
43
|
+
kind_registry_create,
|
|
44
|
+
kind_registry_connect,
|
|
45
|
+
kind_registry_status,
|
|
46
|
+
kind_node_exec,
|
|
47
|
+
kind_node_logs,
|
|
48
|
+
kind_node_inspect,
|
|
49
|
+
kind_node_restart,
|
|
50
|
+
kind_network_inspect,
|
|
51
|
+
kind_port_mappings,
|
|
52
|
+
kind_ingress_setup,
|
|
53
|
+
kind_cluster_status,
|
|
54
|
+
kind_images_list,
|
|
55
|
+
kind_provider_info,
|
|
56
|
+
)
|
|
57
|
+
assert callable(register_kind_tools)
|
|
58
|
+
assert callable(_kind_available)
|
|
59
|
+
assert callable(_get_kind_version)
|
|
60
|
+
assert callable(_run_kind)
|
|
61
|
+
assert callable(_run_docker)
|
|
62
|
+
assert callable(kind_detect)
|
|
63
|
+
assert callable(kind_version)
|
|
64
|
+
assert callable(kind_list_clusters)
|
|
65
|
+
assert callable(kind_get_nodes)
|
|
66
|
+
assert callable(kind_get_kubeconfig)
|
|
67
|
+
assert callable(kind_export_logs)
|
|
68
|
+
assert callable(kind_create_cluster)
|
|
69
|
+
assert callable(kind_delete_cluster)
|
|
70
|
+
assert callable(kind_delete_all_clusters)
|
|
71
|
+
assert callable(kind_load_image)
|
|
72
|
+
assert callable(kind_load_image_archive)
|
|
73
|
+
assert callable(kind_build_node_image)
|
|
74
|
+
assert callable(kind_cluster_info)
|
|
75
|
+
assert callable(kind_node_labels)
|
|
76
|
+
assert callable(kind_config_validate)
|
|
77
|
+
assert callable(kind_config_generate)
|
|
78
|
+
assert callable(kind_config_show)
|
|
79
|
+
assert callable(kind_available_images)
|
|
80
|
+
assert callable(kind_registry_create)
|
|
81
|
+
assert callable(kind_registry_connect)
|
|
82
|
+
assert callable(kind_registry_status)
|
|
83
|
+
assert callable(kind_node_exec)
|
|
84
|
+
assert callable(kind_node_logs)
|
|
85
|
+
assert callable(kind_node_inspect)
|
|
86
|
+
assert callable(kind_node_restart)
|
|
87
|
+
assert callable(kind_network_inspect)
|
|
88
|
+
assert callable(kind_port_mappings)
|
|
89
|
+
assert callable(kind_ingress_setup)
|
|
90
|
+
assert callable(kind_cluster_status)
|
|
91
|
+
assert callable(kind_images_list)
|
|
92
|
+
assert callable(kind_provider_info)
|
|
93
|
+
|
|
94
|
+
@pytest.mark.unit
|
|
95
|
+
def test_kind_available_when_installed(self):
|
|
96
|
+
"""Test _kind_available returns True when CLI is installed."""
|
|
97
|
+
from kubectl_mcp_tool.tools.kind import _kind_available
|
|
98
|
+
|
|
99
|
+
with patch("subprocess.run") as mock_run:
|
|
100
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
101
|
+
result = _kind_available()
|
|
102
|
+
assert result is True
|
|
103
|
+
|
|
104
|
+
@pytest.mark.unit
|
|
105
|
+
def test_kind_available_when_not_installed(self):
|
|
106
|
+
"""Test _kind_available returns False when CLI is not installed."""
|
|
107
|
+
from kubectl_mcp_tool.tools.kind import _kind_available
|
|
108
|
+
|
|
109
|
+
with patch("subprocess.run") as mock_run:
|
|
110
|
+
mock_run.side_effect = FileNotFoundError()
|
|
111
|
+
result = _kind_available()
|
|
112
|
+
assert result is False
|
|
113
|
+
|
|
114
|
+
@pytest.mark.unit
|
|
115
|
+
def test_get_kind_version(self):
|
|
116
|
+
"""Test _get_kind_version extracts version correctly."""
|
|
117
|
+
from kubectl_mcp_tool.tools.kind import _get_kind_version
|
|
118
|
+
|
|
119
|
+
with patch("subprocess.run") as mock_run:
|
|
120
|
+
mock_run.return_value = MagicMock(
|
|
121
|
+
returncode=0,
|
|
122
|
+
stdout="kind v0.23.0 go1.21.0 darwin/arm64"
|
|
123
|
+
)
|
|
124
|
+
result = _get_kind_version()
|
|
125
|
+
assert result == "v0.23.0"
|
|
126
|
+
|
|
127
|
+
@pytest.mark.unit
|
|
128
|
+
def test_get_kind_version_not_installed(self):
|
|
129
|
+
"""Test _get_kind_version returns None when not installed."""
|
|
130
|
+
from kubectl_mcp_tool.tools.kind import _get_kind_version
|
|
131
|
+
|
|
132
|
+
with patch("subprocess.run") as mock_run:
|
|
133
|
+
mock_run.side_effect = FileNotFoundError()
|
|
134
|
+
result = _get_kind_version()
|
|
135
|
+
assert result is None
|
|
136
|
+
|
|
137
|
+
@pytest.mark.unit
|
|
138
|
+
def test_run_kind_not_available(self):
|
|
139
|
+
"""Test _run_kind returns error when CLI not available."""
|
|
140
|
+
from kubectl_mcp_tool.tools.kind import _run_kind
|
|
141
|
+
|
|
142
|
+
with patch("subprocess.run") as mock_run:
|
|
143
|
+
mock_run.side_effect = FileNotFoundError()
|
|
144
|
+
result = _run_kind(["get", "clusters"])
|
|
145
|
+
assert result["success"] is False
|
|
146
|
+
assert "not available" in result["error"]
|
|
147
|
+
|
|
148
|
+
@pytest.mark.unit
|
|
149
|
+
def test_run_kind_success(self):
|
|
150
|
+
"""Test _run_kind returns success on successful command."""
|
|
151
|
+
from kubectl_mcp_tool.tools.kind import _run_kind
|
|
152
|
+
|
|
153
|
+
with patch("subprocess.run") as mock_run:
|
|
154
|
+
mock_run.return_value = MagicMock(
|
|
155
|
+
returncode=0,
|
|
156
|
+
stdout="test-cluster",
|
|
157
|
+
stderr=""
|
|
158
|
+
)
|
|
159
|
+
result = _run_kind(["get", "clusters"])
|
|
160
|
+
assert result["success"] is True
|
|
161
|
+
assert result["output"] == "test-cluster"
|
|
162
|
+
|
|
163
|
+
@pytest.mark.unit
|
|
164
|
+
def test_run_kind_timeout(self):
|
|
165
|
+
"""Test _run_kind handles timeout."""
|
|
166
|
+
from kubectl_mcp_tool.tools.kind import _run_kind
|
|
167
|
+
|
|
168
|
+
with patch("subprocess.run") as mock_run:
|
|
169
|
+
mock_run.side_effect = [
|
|
170
|
+
MagicMock(returncode=0),
|
|
171
|
+
subprocess.TimeoutExpired(cmd="kind", timeout=300)
|
|
172
|
+
]
|
|
173
|
+
result = _run_kind(["create", "cluster"])
|
|
174
|
+
assert result["success"] is False
|
|
175
|
+
assert "timed out" in result["error"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestKindDetect:
|
|
179
|
+
"""Tests for kind_detect function."""
|
|
180
|
+
|
|
181
|
+
@pytest.mark.unit
|
|
182
|
+
def test_kind_detect_installed(self):
|
|
183
|
+
"""Test kind_detect when kind is installed."""
|
|
184
|
+
from kubectl_mcp_tool.tools.kind import kind_detect
|
|
185
|
+
|
|
186
|
+
with patch("subprocess.run") as mock_run:
|
|
187
|
+
mock_run.return_value = MagicMock(
|
|
188
|
+
returncode=0,
|
|
189
|
+
stdout="kind v0.23.0 go1.21.0 darwin/arm64"
|
|
190
|
+
)
|
|
191
|
+
result = kind_detect()
|
|
192
|
+
assert result["installed"] is True
|
|
193
|
+
assert result["cli_available"] is True
|
|
194
|
+
assert result["version"] == "v0.23.0"
|
|
195
|
+
assert result["install_instructions"] is None
|
|
196
|
+
|
|
197
|
+
@pytest.mark.unit
|
|
198
|
+
def test_kind_detect_not_installed(self):
|
|
199
|
+
"""Test kind_detect when kind is not installed."""
|
|
200
|
+
from kubectl_mcp_tool.tools.kind import kind_detect
|
|
201
|
+
|
|
202
|
+
with patch("subprocess.run") as mock_run:
|
|
203
|
+
mock_run.side_effect = FileNotFoundError()
|
|
204
|
+
result = kind_detect()
|
|
205
|
+
assert result["installed"] is False
|
|
206
|
+
assert result["cli_available"] is False
|
|
207
|
+
assert result["version"] is None
|
|
208
|
+
assert result["install_instructions"] is not None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TestKindListClusters:
|
|
212
|
+
"""Tests for kind_list_clusters function."""
|
|
213
|
+
|
|
214
|
+
@pytest.mark.unit
|
|
215
|
+
def test_kind_list_clusters_success(self):
|
|
216
|
+
"""Test kind_list_clusters returns cluster list."""
|
|
217
|
+
from kubectl_mcp_tool.tools.kind import kind_list_clusters
|
|
218
|
+
|
|
219
|
+
with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
|
|
220
|
+
with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
|
|
221
|
+
mock_run.return_value = MagicMock(
|
|
222
|
+
returncode=0,
|
|
223
|
+
stdout="dev-cluster\ntest-cluster",
|
|
224
|
+
stderr=""
|
|
225
|
+
)
|
|
226
|
+
result = kind_list_clusters()
|
|
227
|
+
assert result["success"] is True
|
|
228
|
+
assert result["total"] == 2
|
|
229
|
+
assert "dev-cluster" in result["clusters"]
|
|
230
|
+
assert "test-cluster" in result["clusters"]
|
|
231
|
+
|
|
232
|
+
@pytest.mark.unit
|
|
233
|
+
def test_kind_list_clusters_empty(self):
|
|
234
|
+
"""Test kind_list_clusters returns empty list."""
|
|
235
|
+
from kubectl_mcp_tool.tools.kind import kind_list_clusters
|
|
236
|
+
|
|
237
|
+
with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
|
|
238
|
+
with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
|
|
239
|
+
mock_run.return_value = MagicMock(
|
|
240
|
+
returncode=0,
|
|
241
|
+
stdout="",
|
|
242
|
+
stderr=""
|
|
243
|
+
)
|
|
244
|
+
result = kind_list_clusters()
|
|
245
|
+
assert result["success"] is True
|
|
246
|
+
assert result["total"] == 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestKindGetNodes:
|
|
250
|
+
"""Tests for kind_get_nodes function."""
|
|
251
|
+
|
|
252
|
+
@pytest.mark.unit
|
|
253
|
+
def test_kind_get_nodes_success(self):
|
|
254
|
+
"""Test kind_get_nodes returns node list."""
|
|
255
|
+
from kubectl_mcp_tool.tools.kind import kind_get_nodes
|
|
256
|
+
|
|
257
|
+
with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
|
|
258
|
+
with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
|
|
259
|
+
mock_run.return_value = MagicMock(
|
|
260
|
+
returncode=0,
|
|
261
|
+
stdout="kind-control-plane\nkind-worker\nkind-worker2",
|
|
262
|
+
stderr=""
|
|
263
|
+
)
|
|
264
|
+
result = kind_get_nodes(name="kind")
|
|
265
|
+
assert result["success"] is True
|
|
266
|
+
assert result["total"] == 3
|
|
267
|
+
assert "kind-control-plane" in result["nodes"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestKindCreateCluster:
|
|
271
|
+
"""Tests for kind_create_cluster function."""
|
|
272
|
+
|
|
273
|
+
@pytest.mark.unit
|
|
274
|
+
def test_kind_create_cluster_basic(self):
|
|
275
|
+
"""Test kind_create_cluster with basic options."""
|
|
276
|
+
from kubectl_mcp_tool.tools.kind import kind_create_cluster
|
|
277
|
+
|
|
278
|
+
with patch("subprocess.run") as mock_run:
|
|
279
|
+
mock_run.return_value = MagicMock(
|
|
280
|
+
returncode=0,
|
|
281
|
+
stdout="Creating cluster \"test\" ...",
|
|
282
|
+
stderr=""
|
|
283
|
+
)
|
|
284
|
+
result = kind_create_cluster(name="test")
|
|
285
|
+
assert result["success"] is True
|
|
286
|
+
assert "created" in result["message"].lower()
|
|
287
|
+
|
|
288
|
+
@pytest.mark.unit
|
|
289
|
+
def test_kind_create_cluster_with_options(self):
|
|
290
|
+
"""Test kind_create_cluster with all options."""
|
|
291
|
+
from kubectl_mcp_tool.tools.kind import kind_create_cluster
|
|
292
|
+
|
|
293
|
+
with patch("subprocess.run") as mock_run:
|
|
294
|
+
mock_run.return_value = MagicMock(
|
|
295
|
+
returncode=0,
|
|
296
|
+
stdout="Creating cluster \"prod\" ...",
|
|
297
|
+
stderr=""
|
|
298
|
+
)
|
|
299
|
+
result = kind_create_cluster(
|
|
300
|
+
name="prod",
|
|
301
|
+
image="kindest/node:v1.29.0",
|
|
302
|
+
wait="10m"
|
|
303
|
+
)
|
|
304
|
+
assert result["success"] is True
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestKindDeleteCluster:
|
|
308
|
+
"""Tests for kind_delete_cluster function."""
|
|
309
|
+
|
|
310
|
+
@pytest.mark.unit
|
|
311
|
+
def test_kind_delete_cluster_success(self):
|
|
312
|
+
"""Test kind_delete_cluster deletes cluster."""
|
|
313
|
+
from kubectl_mcp_tool.tools.kind import kind_delete_cluster
|
|
314
|
+
|
|
315
|
+
with patch("subprocess.run") as mock_run:
|
|
316
|
+
mock_run.return_value = MagicMock(
|
|
317
|
+
returncode=0,
|
|
318
|
+
stdout="Deleting cluster \"test\" ...",
|
|
319
|
+
stderr=""
|
|
320
|
+
)
|
|
321
|
+
result = kind_delete_cluster(name="test")
|
|
322
|
+
assert result["success"] is True
|
|
323
|
+
assert "deleted" in result["message"].lower()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class TestKindDeleteAllClusters:
|
|
327
|
+
"""Tests for kind_delete_all_clusters function."""
|
|
328
|
+
|
|
329
|
+
@pytest.mark.unit
|
|
330
|
+
def test_kind_delete_all_clusters_success(self):
|
|
331
|
+
"""Test kind_delete_all_clusters deletes all clusters."""
|
|
332
|
+
from kubectl_mcp_tool.tools.kind import kind_delete_all_clusters
|
|
333
|
+
|
|
334
|
+
with patch("subprocess.run") as mock_run:
|
|
335
|
+
mock_run.return_value = MagicMock(
|
|
336
|
+
returncode=0,
|
|
337
|
+
stdout="Deleted clusters: [\"test1\" \"test2\"]",
|
|
338
|
+
stderr=""
|
|
339
|
+
)
|
|
340
|
+
result = kind_delete_all_clusters()
|
|
341
|
+
assert result["success"] is True
|
|
342
|
+
assert "deleted" in result["message"].lower()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class TestKindLoadImage:
|
|
346
|
+
"""Tests for kind_load_image function."""
|
|
347
|
+
|
|
348
|
+
@pytest.mark.unit
|
|
349
|
+
def test_kind_load_image_success(self):
|
|
350
|
+
"""Test kind_load_image loads images."""
|
|
351
|
+
from kubectl_mcp_tool.tools.kind import kind_load_image
|
|
352
|
+
|
|
353
|
+
with patch("subprocess.run") as mock_run:
|
|
354
|
+
mock_run.return_value = MagicMock(
|
|
355
|
+
returncode=0,
|
|
356
|
+
stdout="Image loaded",
|
|
357
|
+
stderr=""
|
|
358
|
+
)
|
|
359
|
+
result = kind_load_image(images=["myapp:latest"], name="test")
|
|
360
|
+
assert result["success"] is True
|
|
361
|
+
assert result["images"] == ["myapp:latest"]
|
|
362
|
+
|
|
363
|
+
@pytest.mark.unit
|
|
364
|
+
def test_kind_load_image_no_images(self):
|
|
365
|
+
"""Test kind_load_image with no images."""
|
|
366
|
+
from kubectl_mcp_tool.tools.kind import kind_load_image
|
|
367
|
+
|
|
368
|
+
result = kind_load_image(images=[], name="test")
|
|
369
|
+
assert result["success"] is False
|
|
370
|
+
assert "no images" in result["error"].lower()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class TestKindLoadImageArchive:
|
|
374
|
+
"""Tests for kind_load_image_archive function."""
|
|
375
|
+
|
|
376
|
+
@pytest.mark.unit
|
|
377
|
+
def test_kind_load_image_archive_file_not_found(self):
|
|
378
|
+
"""Test kind_load_image_archive with missing file."""
|
|
379
|
+
from kubectl_mcp_tool.tools.kind import kind_load_image_archive
|
|
380
|
+
|
|
381
|
+
result = kind_load_image_archive(archive="/nonexistent/file.tar", name="test")
|
|
382
|
+
assert result["success"] is False
|
|
383
|
+
assert "not found" in result["error"].lower()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class TestKindToolsRegistration:
|
|
387
|
+
"""Tests for kind tools registration."""
|
|
388
|
+
|
|
389
|
+
@pytest.mark.unit
|
|
390
|
+
def test_kind_tools_import(self):
|
|
391
|
+
"""Test that kind tools can be imported."""
|
|
392
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
393
|
+
assert callable(register_kind_tools)
|
|
394
|
+
|
|
395
|
+
@pytest.mark.unit
|
|
396
|
+
@pytest.mark.asyncio
|
|
397
|
+
async def test_kind_tools_register(self, mock_all_kubernetes_apis):
|
|
398
|
+
"""Test that kind tools register correctly."""
|
|
399
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
400
|
+
|
|
401
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
402
|
+
server = MCPServer(name="test")
|
|
403
|
+
|
|
404
|
+
tools = await server.server.list_tools()
|
|
405
|
+
tool_names = {t.name for t in tools}
|
|
406
|
+
|
|
407
|
+
kind_tools = [
|
|
408
|
+
"kind_detect_tool",
|
|
409
|
+
"kind_version_tool",
|
|
410
|
+
"kind_list_clusters_tool",
|
|
411
|
+
"kind_get_nodes_tool",
|
|
412
|
+
"kind_get_kubeconfig_tool",
|
|
413
|
+
"kind_export_logs_tool",
|
|
414
|
+
"kind_cluster_info_tool",
|
|
415
|
+
"kind_node_labels_tool",
|
|
416
|
+
"kind_create_cluster_tool",
|
|
417
|
+
"kind_delete_cluster_tool",
|
|
418
|
+
"kind_delete_all_clusters_tool",
|
|
419
|
+
"kind_load_image_tool",
|
|
420
|
+
"kind_load_image_archive_tool",
|
|
421
|
+
"kind_build_node_image_tool",
|
|
422
|
+
"kind_set_kubeconfig_tool",
|
|
423
|
+
"kind_config_validate_tool",
|
|
424
|
+
"kind_config_generate_tool",
|
|
425
|
+
"kind_config_show_tool",
|
|
426
|
+
"kind_available_images_tool",
|
|
427
|
+
"kind_registry_create_tool",
|
|
428
|
+
"kind_registry_connect_tool",
|
|
429
|
+
"kind_registry_status_tool",
|
|
430
|
+
"kind_node_exec_tool",
|
|
431
|
+
"kind_node_logs_tool",
|
|
432
|
+
"kind_node_inspect_tool",
|
|
433
|
+
"kind_node_restart_tool",
|
|
434
|
+
"kind_network_inspect_tool",
|
|
435
|
+
"kind_port_mappings_tool",
|
|
436
|
+
"kind_ingress_setup_tool",
|
|
437
|
+
"kind_cluster_status_tool",
|
|
438
|
+
"kind_images_list_tool",
|
|
439
|
+
"kind_provider_info_tool",
|
|
440
|
+
]
|
|
441
|
+
for tool in kind_tools:
|
|
442
|
+
assert tool in tool_names, f"kind tool '{tool}' not registered"
|
|
443
|
+
|
|
444
|
+
@pytest.mark.unit
|
|
445
|
+
@pytest.mark.asyncio
|
|
446
|
+
async def test_kind_tool_count(self, mock_all_kubernetes_apis):
|
|
447
|
+
"""Test that correct number of kind tools are registered."""
|
|
448
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
449
|
+
|
|
450
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
451
|
+
server = MCPServer(name="test")
|
|
452
|
+
|
|
453
|
+
tools = await server.server.list_tools()
|
|
454
|
+
tool_names = {t.name for t in tools}
|
|
455
|
+
kind_tools = [name for name in tool_names if name.startswith("kind_")]
|
|
456
|
+
assert len(kind_tools) == 32, f"Expected 32 kind tools, got {len(kind_tools)}: {kind_tools}"
|
|
457
|
+
|
|
458
|
+
@pytest.mark.unit
|
|
459
|
+
def test_kind_non_destructive_mode(self, mock_all_kubernetes_apis):
|
|
460
|
+
"""Test that kind write operations are blocked in non-destructive mode."""
|
|
461
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
462
|
+
|
|
463
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
464
|
+
server = MCPServer(name="test", disable_destructive=True)
|
|
465
|
+
|
|
466
|
+
assert server.non_destructive is True
|
|
467
|
+
|
|
468
|
+
@pytest.mark.unit
|
|
469
|
+
@pytest.mark.asyncio
|
|
470
|
+
async def test_kind_tools_have_descriptions(self, mock_all_kubernetes_apis):
|
|
471
|
+
"""Test that all kind tools have descriptions."""
|
|
472
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
473
|
+
|
|
474
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
475
|
+
server = MCPServer(name="test")
|
|
476
|
+
|
|
477
|
+
tools = await server.server.list_tools()
|
|
478
|
+
kind_tools = [t for t in tools if t.name.startswith("kind_")]
|
|
479
|
+
tools_without_description = [
|
|
480
|
+
t.name for t in kind_tools
|
|
481
|
+
if not t.description or len(t.description.strip()) == 0
|
|
482
|
+
]
|
|
483
|
+
assert not tools_without_description, f"kind tools without descriptions: {tools_without_description}"
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class TestKindNonDestructiveBlocking:
|
|
487
|
+
"""Tests for non-destructive mode blocking of kind write operations."""
|
|
488
|
+
|
|
489
|
+
@pytest.mark.unit
|
|
490
|
+
@pytest.mark.asyncio
|
|
491
|
+
async def test_create_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
492
|
+
"""Test that kind_create_cluster_tool is blocked in non-destructive mode."""
|
|
493
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
from fastmcp import FastMCP
|
|
497
|
+
except ImportError:
|
|
498
|
+
from mcp.server.fastmcp import FastMCP
|
|
499
|
+
|
|
500
|
+
mcp = FastMCP(name="test")
|
|
501
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
502
|
+
|
|
503
|
+
tool = await mcp.get_tool("kind_create_cluster_tool")
|
|
504
|
+
result = tool.fn(name="test")
|
|
505
|
+
result_dict = json.loads(result)
|
|
506
|
+
assert result_dict["success"] is False
|
|
507
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
508
|
+
|
|
509
|
+
@pytest.mark.unit
|
|
510
|
+
@pytest.mark.asyncio
|
|
511
|
+
async def test_delete_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
512
|
+
"""Test that kind_delete_cluster_tool is blocked in non-destructive mode."""
|
|
513
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
from fastmcp import FastMCP
|
|
517
|
+
except ImportError:
|
|
518
|
+
from mcp.server.fastmcp import FastMCP
|
|
519
|
+
|
|
520
|
+
mcp = FastMCP(name="test")
|
|
521
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
522
|
+
|
|
523
|
+
tool = await mcp.get_tool("kind_delete_cluster_tool")
|
|
524
|
+
result = tool.fn(name="test")
|
|
525
|
+
result_dict = json.loads(result)
|
|
526
|
+
assert result_dict["success"] is False
|
|
527
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
528
|
+
|
|
529
|
+
@pytest.mark.unit
|
|
530
|
+
@pytest.mark.asyncio
|
|
531
|
+
async def test_delete_all_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
532
|
+
"""Test that kind_delete_all_clusters_tool is blocked in non-destructive mode."""
|
|
533
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
from fastmcp import FastMCP
|
|
537
|
+
except ImportError:
|
|
538
|
+
from mcp.server.fastmcp import FastMCP
|
|
539
|
+
|
|
540
|
+
mcp = FastMCP(name="test")
|
|
541
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
542
|
+
|
|
543
|
+
tool = await mcp.get_tool("kind_delete_all_clusters_tool")
|
|
544
|
+
result = tool.fn()
|
|
545
|
+
result_dict = json.loads(result)
|
|
546
|
+
assert result_dict["success"] is False
|
|
547
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
548
|
+
|
|
549
|
+
@pytest.mark.unit
|
|
550
|
+
@pytest.mark.asyncio
|
|
551
|
+
async def test_load_image_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
552
|
+
"""Test that kind_load_image_tool is blocked in non-destructive mode."""
|
|
553
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
from fastmcp import FastMCP
|
|
557
|
+
except ImportError:
|
|
558
|
+
from mcp.server.fastmcp import FastMCP
|
|
559
|
+
|
|
560
|
+
mcp = FastMCP(name="test")
|
|
561
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
562
|
+
|
|
563
|
+
tool = await mcp.get_tool("kind_load_image_tool")
|
|
564
|
+
result = tool.fn(images="myapp:latest", name="test")
|
|
565
|
+
result_dict = json.loads(result)
|
|
566
|
+
assert result_dict["success"] is False
|
|
567
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
568
|
+
|
|
569
|
+
@pytest.mark.unit
|
|
570
|
+
@pytest.mark.asyncio
|
|
571
|
+
async def test_read_operations_allowed_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
572
|
+
"""Test that read operations work in non-destructive mode."""
|
|
573
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
from fastmcp import FastMCP
|
|
577
|
+
except ImportError:
|
|
578
|
+
from mcp.server.fastmcp import FastMCP
|
|
579
|
+
|
|
580
|
+
mcp = FastMCP(name="test")
|
|
581
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
582
|
+
|
|
583
|
+
tool = await mcp.get_tool("kind_detect_tool")
|
|
584
|
+
with patch("subprocess.run") as mock_run:
|
|
585
|
+
mock_run.side_effect = FileNotFoundError()
|
|
586
|
+
result = tool.fn()
|
|
587
|
+
result_dict = json.loads(result)
|
|
588
|
+
assert "installed" in result_dict
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class TestKindClusterInfo:
|
|
592
|
+
"""Tests for kind_cluster_info function."""
|
|
593
|
+
|
|
594
|
+
@pytest.mark.unit
|
|
595
|
+
def test_kind_cluster_info_cluster_not_found(self):
|
|
596
|
+
"""Test kind_cluster_info when cluster not found."""
|
|
597
|
+
from kubectl_mcp_tool.tools.kind import kind_cluster_info
|
|
598
|
+
|
|
599
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
|
|
600
|
+
mock_list.return_value = {
|
|
601
|
+
"success": True,
|
|
602
|
+
"clusters": ["other-cluster"]
|
|
603
|
+
}
|
|
604
|
+
result = kind_cluster_info(name="nonexistent")
|
|
605
|
+
assert result["success"] is False
|
|
606
|
+
assert "not found" in result["error"].lower()
|
|
607
|
+
|
|
608
|
+
@pytest.mark.unit
|
|
609
|
+
def test_kind_cluster_info_success(self):
|
|
610
|
+
"""Test kind_cluster_info returns cluster info."""
|
|
611
|
+
from kubectl_mcp_tool.tools.kind import kind_cluster_info
|
|
612
|
+
|
|
613
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
|
|
614
|
+
mock_list.return_value = {
|
|
615
|
+
"success": True,
|
|
616
|
+
"clusters": ["test-cluster"]
|
|
617
|
+
}
|
|
618
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
|
|
619
|
+
mock_nodes.return_value = {
|
|
620
|
+
"success": True,
|
|
621
|
+
"nodes": ["test-cluster-control-plane"],
|
|
622
|
+
"total": 1
|
|
623
|
+
}
|
|
624
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_get_kubeconfig") as mock_kubeconfig:
|
|
625
|
+
mock_kubeconfig.return_value = {
|
|
626
|
+
"success": True,
|
|
627
|
+
"kubeconfig": "apiVersion: v1\n..."
|
|
628
|
+
}
|
|
629
|
+
result = kind_cluster_info(name="test-cluster")
|
|
630
|
+
assert result["success"] is True
|
|
631
|
+
assert result["cluster"] == "test-cluster"
|
|
632
|
+
assert result["node_count"] == 1
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class TestKindConfigValidate:
|
|
636
|
+
"""Tests for kind_config_validate function."""
|
|
637
|
+
|
|
638
|
+
@pytest.mark.unit
|
|
639
|
+
def test_kind_config_validate_file_not_found(self):
|
|
640
|
+
"""Test kind_config_validate with missing file."""
|
|
641
|
+
from kubectl_mcp_tool.tools.kind import kind_config_validate
|
|
642
|
+
|
|
643
|
+
result = kind_config_validate("/nonexistent/config.yaml")
|
|
644
|
+
assert result["success"] is False
|
|
645
|
+
assert "not found" in result["error"].lower()
|
|
646
|
+
|
|
647
|
+
@pytest.mark.unit
|
|
648
|
+
def test_kind_config_validate_valid_config(self, tmp_path):
|
|
649
|
+
"""Test kind_config_validate with valid config."""
|
|
650
|
+
from kubectl_mcp_tool.tools.kind import kind_config_validate
|
|
651
|
+
|
|
652
|
+
config_file = tmp_path / "kind.yaml"
|
|
653
|
+
config_file.write_text("""
|
|
654
|
+
kind: Cluster
|
|
655
|
+
apiVersion: kind.x-k8s.io/v1alpha4
|
|
656
|
+
nodes:
|
|
657
|
+
- role: control-plane
|
|
658
|
+
- role: worker
|
|
659
|
+
""")
|
|
660
|
+
result = kind_config_validate(str(config_file))
|
|
661
|
+
assert result["success"] is True
|
|
662
|
+
assert result["valid"] is True
|
|
663
|
+
assert result["config_summary"]["control_planes"] == 1
|
|
664
|
+
assert result["config_summary"]["workers"] == 1
|
|
665
|
+
|
|
666
|
+
@pytest.mark.unit
|
|
667
|
+
def test_kind_config_validate_invalid_kind(self, tmp_path):
|
|
668
|
+
"""Test kind_config_validate with invalid kind field."""
|
|
669
|
+
from kubectl_mcp_tool.tools.kind import kind_config_validate
|
|
670
|
+
|
|
671
|
+
config_file = tmp_path / "kind.yaml"
|
|
672
|
+
config_file.write_text("""
|
|
673
|
+
kind: Invalid
|
|
674
|
+
apiVersion: kind.x-k8s.io/v1alpha4
|
|
675
|
+
""")
|
|
676
|
+
result = kind_config_validate(str(config_file))
|
|
677
|
+
assert result["success"] is False
|
|
678
|
+
assert len(result["errors"]) > 0
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class TestKindConfigGenerate:
|
|
682
|
+
"""Tests for kind_config_generate function."""
|
|
683
|
+
|
|
684
|
+
@pytest.mark.unit
|
|
685
|
+
def test_kind_config_generate_default(self):
|
|
686
|
+
"""Test kind_config_generate with default options."""
|
|
687
|
+
from kubectl_mcp_tool.tools.kind import kind_config_generate
|
|
688
|
+
|
|
689
|
+
result = kind_config_generate()
|
|
690
|
+
assert result["success"] is True
|
|
691
|
+
assert "config" in result
|
|
692
|
+
assert "kind: Cluster" in result["config"]
|
|
693
|
+
assert result["summary"]["control_planes"] == 1
|
|
694
|
+
assert result["summary"]["workers"] == 0
|
|
695
|
+
|
|
696
|
+
@pytest.mark.unit
|
|
697
|
+
def test_kind_config_generate_multi_node(self):
|
|
698
|
+
"""Test kind_config_generate with multiple nodes."""
|
|
699
|
+
from kubectl_mcp_tool.tools.kind import kind_config_generate
|
|
700
|
+
|
|
701
|
+
result = kind_config_generate(workers=2, control_planes=1)
|
|
702
|
+
assert result["success"] is True
|
|
703
|
+
assert result["summary"]["total_nodes"] == 3
|
|
704
|
+
assert result["summary"]["workers"] == 2
|
|
705
|
+
|
|
706
|
+
@pytest.mark.unit
|
|
707
|
+
def test_kind_config_generate_with_ingress(self):
|
|
708
|
+
"""Test kind_config_generate with ingress enabled."""
|
|
709
|
+
from kubectl_mcp_tool.tools.kind import kind_config_generate
|
|
710
|
+
|
|
711
|
+
result = kind_config_generate(ingress=True)
|
|
712
|
+
assert result["success"] is True
|
|
713
|
+
assert result["summary"]["features"]["ingress"] is True
|
|
714
|
+
assert "extraPortMappings" in result["config"]
|
|
715
|
+
|
|
716
|
+
@pytest.mark.unit
|
|
717
|
+
def test_kind_config_generate_with_registry(self):
|
|
718
|
+
"""Test kind_config_generate with registry enabled."""
|
|
719
|
+
from kubectl_mcp_tool.tools.kind import kind_config_generate
|
|
720
|
+
|
|
721
|
+
result = kind_config_generate(registry=True)
|
|
722
|
+
assert result["success"] is True
|
|
723
|
+
assert result["summary"]["features"]["registry"] is True
|
|
724
|
+
assert "containerdConfigPatches" in result["config"]
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class TestKindAvailableImages:
|
|
728
|
+
"""Tests for kind_available_images function."""
|
|
729
|
+
|
|
730
|
+
@pytest.mark.unit
|
|
731
|
+
def test_kind_available_images(self):
|
|
732
|
+
"""Test kind_available_images returns image list."""
|
|
733
|
+
from kubectl_mcp_tool.tools.kind import kind_available_images
|
|
734
|
+
|
|
735
|
+
result = kind_available_images()
|
|
736
|
+
assert result["success"] is True
|
|
737
|
+
assert "images" in result
|
|
738
|
+
assert len(result["images"]) > 0
|
|
739
|
+
assert result["latest"] is not None
|
|
740
|
+
assert "kindest/node" in result["latest"]
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class TestKindRegistryCreate:
|
|
744
|
+
"""Tests for kind_registry_create function."""
|
|
745
|
+
|
|
746
|
+
@pytest.mark.unit
|
|
747
|
+
def test_kind_registry_create_already_exists(self):
|
|
748
|
+
"""Test kind_registry_create when registry exists."""
|
|
749
|
+
from kubectl_mcp_tool.tools.kind import kind_registry_create
|
|
750
|
+
|
|
751
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
752
|
+
mock_docker.return_value = {"success": True, "output": "container_id"}
|
|
753
|
+
result = kind_registry_create()
|
|
754
|
+
assert result["success"] is True
|
|
755
|
+
assert "already exists" in result["message"]
|
|
756
|
+
|
|
757
|
+
@pytest.mark.unit
|
|
758
|
+
def test_kind_registry_create_new(self):
|
|
759
|
+
"""Test kind_registry_create creates new registry."""
|
|
760
|
+
from kubectl_mcp_tool.tools.kind import kind_registry_create
|
|
761
|
+
|
|
762
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
763
|
+
mock_docker.side_effect = [
|
|
764
|
+
{"success": True, "output": ""},
|
|
765
|
+
{"success": True, "output": ""},
|
|
766
|
+
{"success": True, "output": ""},
|
|
767
|
+
{"success": True, "output": ""}
|
|
768
|
+
]
|
|
769
|
+
result = kind_registry_create()
|
|
770
|
+
assert result["success"] is True
|
|
771
|
+
assert result["port"] == 5001
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class TestKindRegistryStatus:
|
|
775
|
+
"""Tests for kind_registry_status function."""
|
|
776
|
+
|
|
777
|
+
@pytest.mark.unit
|
|
778
|
+
def test_kind_registry_status_not_found(self):
|
|
779
|
+
"""Test kind_registry_status when registry not found."""
|
|
780
|
+
from kubectl_mcp_tool.tools.kind import kind_registry_status
|
|
781
|
+
|
|
782
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
783
|
+
mock_docker.return_value = {"success": False, "error": "not found"}
|
|
784
|
+
result = kind_registry_status()
|
|
785
|
+
assert result["success"] is False
|
|
786
|
+
assert result["installed"] is False
|
|
787
|
+
|
|
788
|
+
@pytest.mark.unit
|
|
789
|
+
def test_kind_registry_status_running(self):
|
|
790
|
+
"""Test kind_registry_status when registry is running."""
|
|
791
|
+
from kubectl_mcp_tool.tools.kind import kind_registry_status
|
|
792
|
+
|
|
793
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
794
|
+
mock_docker.return_value = {
|
|
795
|
+
"success": True,
|
|
796
|
+
"output": json.dumps({
|
|
797
|
+
"State": {"Running": True, "Status": "running"},
|
|
798
|
+
"NetworkSettings": {
|
|
799
|
+
"Ports": {"5000/tcp": [{"HostPort": "5001"}]},
|
|
800
|
+
"Networks": {"kind": {}, "bridge": {}}
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
}
|
|
804
|
+
result = kind_registry_status()
|
|
805
|
+
assert result["success"] is True
|
|
806
|
+
assert result["running"] is True
|
|
807
|
+
assert result["connected_to_kind"] is True
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
class TestKindNodeExec:
|
|
811
|
+
"""Tests for kind_node_exec function."""
|
|
812
|
+
|
|
813
|
+
@pytest.mark.unit
|
|
814
|
+
def test_kind_node_exec_missing_node(self):
|
|
815
|
+
"""Test kind_node_exec with missing node."""
|
|
816
|
+
from kubectl_mcp_tool.tools.kind import kind_node_exec
|
|
817
|
+
|
|
818
|
+
result = kind_node_exec(node="", command="ls")
|
|
819
|
+
assert result["success"] is False
|
|
820
|
+
assert "required" in result["error"].lower()
|
|
821
|
+
|
|
822
|
+
@pytest.mark.unit
|
|
823
|
+
def test_kind_node_exec_missing_command(self):
|
|
824
|
+
"""Test kind_node_exec with missing command."""
|
|
825
|
+
from kubectl_mcp_tool.tools.kind import kind_node_exec
|
|
826
|
+
|
|
827
|
+
result = kind_node_exec(node="kind-control-plane", command="")
|
|
828
|
+
assert result["success"] is False
|
|
829
|
+
assert "required" in result["error"].lower()
|
|
830
|
+
|
|
831
|
+
@pytest.mark.unit
|
|
832
|
+
def test_kind_node_exec_success(self):
|
|
833
|
+
"""Test kind_node_exec succeeds."""
|
|
834
|
+
from kubectl_mcp_tool.tools.kind import kind_node_exec
|
|
835
|
+
|
|
836
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
|
|
837
|
+
mock_nodes.return_value = {
|
|
838
|
+
"success": True,
|
|
839
|
+
"nodes": ["kind-control-plane"]
|
|
840
|
+
}
|
|
841
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
842
|
+
mock_docker.return_value = {"success": True, "output": "output"}
|
|
843
|
+
result = kind_node_exec(node="kind-control-plane", command="ls")
|
|
844
|
+
assert result["success"] is True
|
|
845
|
+
assert result["output"] == "output"
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
class TestKindNodeLogs:
|
|
849
|
+
"""Tests for kind_node_logs function."""
|
|
850
|
+
|
|
851
|
+
@pytest.mark.unit
|
|
852
|
+
def test_kind_node_logs_missing_node(self):
|
|
853
|
+
"""Test kind_node_logs with missing node."""
|
|
854
|
+
from kubectl_mcp_tool.tools.kind import kind_node_logs
|
|
855
|
+
|
|
856
|
+
result = kind_node_logs(node="")
|
|
857
|
+
assert result["success"] is False
|
|
858
|
+
assert "required" in result["error"].lower()
|
|
859
|
+
|
|
860
|
+
@pytest.mark.unit
|
|
861
|
+
def test_kind_node_logs_success(self):
|
|
862
|
+
"""Test kind_node_logs succeeds."""
|
|
863
|
+
from kubectl_mcp_tool.tools.kind import kind_node_logs
|
|
864
|
+
|
|
865
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
866
|
+
mock_docker.return_value = {"success": True, "output": "log output"}
|
|
867
|
+
result = kind_node_logs(node="kind-control-plane")
|
|
868
|
+
assert result["success"] is True
|
|
869
|
+
assert "logs" in result
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
class TestKindNodeInspect:
|
|
873
|
+
"""Tests for kind_node_inspect function."""
|
|
874
|
+
|
|
875
|
+
@pytest.mark.unit
|
|
876
|
+
def test_kind_node_inspect_missing_node(self):
|
|
877
|
+
"""Test kind_node_inspect with missing node."""
|
|
878
|
+
from kubectl_mcp_tool.tools.kind import kind_node_inspect
|
|
879
|
+
|
|
880
|
+
result = kind_node_inspect(node="")
|
|
881
|
+
assert result["success"] is False
|
|
882
|
+
assert "required" in result["error"].lower()
|
|
883
|
+
|
|
884
|
+
@pytest.mark.unit
|
|
885
|
+
def test_kind_node_inspect_success(self):
|
|
886
|
+
"""Test kind_node_inspect succeeds."""
|
|
887
|
+
from kubectl_mcp_tool.tools.kind import kind_node_inspect
|
|
888
|
+
|
|
889
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
890
|
+
mock_docker.return_value = {
|
|
891
|
+
"success": True,
|
|
892
|
+
"output": json.dumps({
|
|
893
|
+
"State": {"Running": True, "Status": "running", "StartedAt": "2024-01-01", "Pid": 1234},
|
|
894
|
+
"Config": {"Image": "kindest/node:v1.29.0", "Labels": {}},
|
|
895
|
+
"NetworkSettings": {"IPAddress": "172.18.0.2", "Networks": {"kind": {}}},
|
|
896
|
+
"HostConfig": {"PortBindings": {}},
|
|
897
|
+
"Mounts": []
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
result = kind_node_inspect(node="kind-control-plane")
|
|
901
|
+
assert result["success"] is True
|
|
902
|
+
assert result["state"]["running"] is True
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class TestKindNodeRestart:
|
|
906
|
+
"""Tests for kind_node_restart function."""
|
|
907
|
+
|
|
908
|
+
@pytest.mark.unit
|
|
909
|
+
def test_kind_node_restart_missing_node(self):
|
|
910
|
+
"""Test kind_node_restart with missing node."""
|
|
911
|
+
from kubectl_mcp_tool.tools.kind import kind_node_restart
|
|
912
|
+
|
|
913
|
+
result = kind_node_restart(node="")
|
|
914
|
+
assert result["success"] is False
|
|
915
|
+
assert "required" in result["error"].lower()
|
|
916
|
+
|
|
917
|
+
@pytest.mark.unit
|
|
918
|
+
def test_kind_node_restart_success(self):
|
|
919
|
+
"""Test kind_node_restart succeeds."""
|
|
920
|
+
from kubectl_mcp_tool.tools.kind import kind_node_restart
|
|
921
|
+
|
|
922
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
923
|
+
mock_docker.return_value = {"success": True, "output": ""}
|
|
924
|
+
result = kind_node_restart(node="kind-control-plane")
|
|
925
|
+
assert result["success"] is True
|
|
926
|
+
assert "restarted" in result["message"].lower()
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class TestKindNetworkInspect:
|
|
930
|
+
"""Tests for kind_network_inspect function."""
|
|
931
|
+
|
|
932
|
+
@pytest.mark.unit
|
|
933
|
+
def test_kind_network_inspect_not_found(self):
|
|
934
|
+
"""Test kind_network_inspect when network not found."""
|
|
935
|
+
from kubectl_mcp_tool.tools.kind import kind_network_inspect
|
|
936
|
+
|
|
937
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
938
|
+
mock_docker.return_value = {"success": False, "error": "not found"}
|
|
939
|
+
result = kind_network_inspect()
|
|
940
|
+
assert result["success"] is False
|
|
941
|
+
|
|
942
|
+
@pytest.mark.unit
|
|
943
|
+
def test_kind_network_inspect_success(self):
|
|
944
|
+
"""Test kind_network_inspect succeeds."""
|
|
945
|
+
from kubectl_mcp_tool.tools.kind import kind_network_inspect
|
|
946
|
+
|
|
947
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
948
|
+
mock_docker.return_value = {
|
|
949
|
+
"success": True,
|
|
950
|
+
"output": json.dumps([{
|
|
951
|
+
"Name": "kind",
|
|
952
|
+
"Driver": "bridge",
|
|
953
|
+
"Scope": "local",
|
|
954
|
+
"IPAM": {"Config": [{"Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1"}]},
|
|
955
|
+
"Containers": {
|
|
956
|
+
"abc123": {"Name": "kind-control-plane", "IPv4Address": "172.18.0.2/16", "MacAddress": "aa:bb:cc"}
|
|
957
|
+
}
|
|
958
|
+
}])
|
|
959
|
+
}
|
|
960
|
+
result = kind_network_inspect()
|
|
961
|
+
assert result["success"] is True
|
|
962
|
+
assert result["subnet"] == "172.18.0.0/16"
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
class TestKindPortMappings:
|
|
966
|
+
"""Tests for kind_port_mappings function."""
|
|
967
|
+
|
|
968
|
+
@pytest.mark.unit
|
|
969
|
+
def test_kind_port_mappings_success(self):
|
|
970
|
+
"""Test kind_port_mappings returns mappings."""
|
|
971
|
+
from kubectl_mcp_tool.tools.kind import kind_port_mappings
|
|
972
|
+
|
|
973
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
|
|
974
|
+
mock_nodes.return_value = {
|
|
975
|
+
"success": True,
|
|
976
|
+
"nodes": ["kind-control-plane"]
|
|
977
|
+
}
|
|
978
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
979
|
+
mock_docker.return_value = {
|
|
980
|
+
"success": True,
|
|
981
|
+
"output": json.dumps({
|
|
982
|
+
"80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "80"}]
|
|
983
|
+
})
|
|
984
|
+
}
|
|
985
|
+
result = kind_port_mappings()
|
|
986
|
+
assert result["success"] is True
|
|
987
|
+
assert result["has_mappings"] is True
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
class TestKindIngressSetup:
|
|
991
|
+
"""Tests for kind_ingress_setup function."""
|
|
992
|
+
|
|
993
|
+
@pytest.mark.unit
|
|
994
|
+
def test_kind_ingress_setup_invalid_type(self):
|
|
995
|
+
"""Test kind_ingress_setup with invalid type."""
|
|
996
|
+
from kubectl_mcp_tool.tools.kind import kind_ingress_setup
|
|
997
|
+
|
|
998
|
+
result = kind_ingress_setup(ingress_type="invalid")
|
|
999
|
+
assert result["success"] is False
|
|
1000
|
+
assert "unsupported" in result["error"].lower()
|
|
1001
|
+
|
|
1002
|
+
@pytest.mark.unit
|
|
1003
|
+
def test_kind_ingress_setup_cluster_not_found(self):
|
|
1004
|
+
"""Test kind_ingress_setup when cluster not found."""
|
|
1005
|
+
from kubectl_mcp_tool.tools.kind import kind_ingress_setup
|
|
1006
|
+
|
|
1007
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
|
|
1008
|
+
mock_list.return_value = {"success": True, "clusters": []}
|
|
1009
|
+
result = kind_ingress_setup(cluster="nonexistent")
|
|
1010
|
+
assert result["success"] is False
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
class TestKindClusterStatus:
|
|
1014
|
+
"""Tests for kind_cluster_status function."""
|
|
1015
|
+
|
|
1016
|
+
@pytest.mark.unit
|
|
1017
|
+
def test_kind_cluster_status_not_found(self):
|
|
1018
|
+
"""Test kind_cluster_status when cluster not found."""
|
|
1019
|
+
from kubectl_mcp_tool.tools.kind import kind_cluster_status
|
|
1020
|
+
|
|
1021
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
|
|
1022
|
+
mock_list.return_value = {"success": True, "clusters": []}
|
|
1023
|
+
result = kind_cluster_status(name="nonexistent")
|
|
1024
|
+
assert result["success"] is False
|
|
1025
|
+
assert "not found" in result["error"].lower()
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
class TestKindImagesList:
|
|
1029
|
+
"""Tests for kind_images_list function."""
|
|
1030
|
+
|
|
1031
|
+
@pytest.mark.unit
|
|
1032
|
+
def test_kind_images_list_no_nodes(self):
|
|
1033
|
+
"""Test kind_images_list when no nodes."""
|
|
1034
|
+
from kubectl_mcp_tool.tools.kind import kind_images_list
|
|
1035
|
+
|
|
1036
|
+
with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
|
|
1037
|
+
mock_nodes.return_value = {"success": True, "nodes": []}
|
|
1038
|
+
result = kind_images_list()
|
|
1039
|
+
assert result["success"] is False
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
class TestKindProviderInfo:
|
|
1043
|
+
"""Tests for kind_provider_info function."""
|
|
1044
|
+
|
|
1045
|
+
@pytest.mark.unit
|
|
1046
|
+
def test_kind_provider_info_success(self):
|
|
1047
|
+
"""Test kind_provider_info returns provider details."""
|
|
1048
|
+
from kubectl_mcp_tool.tools.kind import kind_provider_info
|
|
1049
|
+
|
|
1050
|
+
with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
|
|
1051
|
+
mock_docker.return_value = {
|
|
1052
|
+
"success": True,
|
|
1053
|
+
"output": json.dumps({
|
|
1054
|
+
"Client": {"Version": "24.0.0", "ApiVersion": "1.43", "Os": "darwin", "Arch": "arm64"},
|
|
1055
|
+
"Server": {"Version": "24.0.0"}
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
result = kind_provider_info()
|
|
1059
|
+
assert result["success"] is True
|
|
1060
|
+
assert result["provider"] == "docker"
|
|
1061
|
+
assert result["client_version"] == "24.0.0"
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
class TestRunDocker:
|
|
1065
|
+
"""Tests for _run_docker helper function."""
|
|
1066
|
+
|
|
1067
|
+
@pytest.mark.unit
|
|
1068
|
+
def test_run_docker_success(self):
|
|
1069
|
+
"""Test _run_docker returns success."""
|
|
1070
|
+
from kubectl_mcp_tool.tools.kind import _run_docker
|
|
1071
|
+
|
|
1072
|
+
with patch("subprocess.run") as mock_run:
|
|
1073
|
+
mock_run.return_value = MagicMock(
|
|
1074
|
+
returncode=0,
|
|
1075
|
+
stdout="output",
|
|
1076
|
+
stderr=""
|
|
1077
|
+
)
|
|
1078
|
+
result = _run_docker(["ps"])
|
|
1079
|
+
assert result["success"] is True
|
|
1080
|
+
assert result["output"] == "output"
|
|
1081
|
+
|
|
1082
|
+
@pytest.mark.unit
|
|
1083
|
+
def test_run_docker_not_available(self):
|
|
1084
|
+
"""Test _run_docker when Docker not available."""
|
|
1085
|
+
from kubectl_mcp_tool.tools.kind import _run_docker
|
|
1086
|
+
|
|
1087
|
+
with patch("subprocess.run") as mock_run:
|
|
1088
|
+
mock_run.side_effect = FileNotFoundError()
|
|
1089
|
+
result = _run_docker(["ps"])
|
|
1090
|
+
assert result["success"] is False
|
|
1091
|
+
assert "not available" in result["error"].lower()
|
|
1092
|
+
|
|
1093
|
+
@pytest.mark.unit
|
|
1094
|
+
def test_run_docker_timeout(self):
|
|
1095
|
+
"""Test _run_docker handles timeout."""
|
|
1096
|
+
from kubectl_mcp_tool.tools.kind import _run_docker
|
|
1097
|
+
|
|
1098
|
+
with patch("subprocess.run") as mock_run:
|
|
1099
|
+
mock_run.side_effect = subprocess.TimeoutExpired(cmd="docker", timeout=60)
|
|
1100
|
+
result = _run_docker(["ps"])
|
|
1101
|
+
assert result["success"] is False
|
|
1102
|
+
assert "timed out" in result["error"]
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class TestNewKindNonDestructiveBlocking:
|
|
1106
|
+
"""Tests for non-destructive mode blocking of new kind write operations."""
|
|
1107
|
+
|
|
1108
|
+
@pytest.mark.unit
|
|
1109
|
+
@pytest.mark.asyncio
|
|
1110
|
+
async def test_registry_create_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
1111
|
+
"""Test that kind_registry_create_tool is blocked in non-destructive mode."""
|
|
1112
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
1113
|
+
|
|
1114
|
+
try:
|
|
1115
|
+
from fastmcp import FastMCP
|
|
1116
|
+
except ImportError:
|
|
1117
|
+
from mcp.server.fastmcp import FastMCP
|
|
1118
|
+
|
|
1119
|
+
mcp = FastMCP(name="test")
|
|
1120
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
1121
|
+
|
|
1122
|
+
tool = await mcp.get_tool("kind_registry_create_tool")
|
|
1123
|
+
result = tool.fn()
|
|
1124
|
+
result_dict = json.loads(result)
|
|
1125
|
+
assert result_dict["success"] is False
|
|
1126
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
1127
|
+
|
|
1128
|
+
@pytest.mark.unit
|
|
1129
|
+
@pytest.mark.asyncio
|
|
1130
|
+
async def test_node_exec_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
1131
|
+
"""Test that kind_node_exec_tool is blocked in non-destructive mode."""
|
|
1132
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
1133
|
+
|
|
1134
|
+
try:
|
|
1135
|
+
from fastmcp import FastMCP
|
|
1136
|
+
except ImportError:
|
|
1137
|
+
from mcp.server.fastmcp import FastMCP
|
|
1138
|
+
|
|
1139
|
+
mcp = FastMCP(name="test")
|
|
1140
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
1141
|
+
|
|
1142
|
+
tool = await mcp.get_tool("kind_node_exec_tool")
|
|
1143
|
+
result = tool.fn(node="test", command="ls")
|
|
1144
|
+
result_dict = json.loads(result)
|
|
1145
|
+
assert result_dict["success"] is False
|
|
1146
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
1147
|
+
|
|
1148
|
+
@pytest.mark.unit
|
|
1149
|
+
@pytest.mark.asyncio
|
|
1150
|
+
async def test_node_restart_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
1151
|
+
"""Test that kind_node_restart_tool is blocked in non-destructive mode."""
|
|
1152
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
1153
|
+
|
|
1154
|
+
try:
|
|
1155
|
+
from fastmcp import FastMCP
|
|
1156
|
+
except ImportError:
|
|
1157
|
+
from mcp.server.fastmcp import FastMCP
|
|
1158
|
+
|
|
1159
|
+
mcp = FastMCP(name="test")
|
|
1160
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
1161
|
+
|
|
1162
|
+
tool = await mcp.get_tool("kind_node_restart_tool")
|
|
1163
|
+
result = tool.fn(node="test")
|
|
1164
|
+
result_dict = json.loads(result)
|
|
1165
|
+
assert result_dict["success"] is False
|
|
1166
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
1167
|
+
|
|
1168
|
+
@pytest.mark.unit
|
|
1169
|
+
@pytest.mark.asyncio
|
|
1170
|
+
async def test_ingress_setup_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
1171
|
+
"""Test that kind_ingress_setup_tool is blocked in non-destructive mode."""
|
|
1172
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
1173
|
+
|
|
1174
|
+
try:
|
|
1175
|
+
from fastmcp import FastMCP
|
|
1176
|
+
except ImportError:
|
|
1177
|
+
from mcp.server.fastmcp import FastMCP
|
|
1178
|
+
|
|
1179
|
+
mcp = FastMCP(name="test")
|
|
1180
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
1181
|
+
|
|
1182
|
+
tool = await mcp.get_tool("kind_ingress_setup_tool")
|
|
1183
|
+
result = tool.fn()
|
|
1184
|
+
result_dict = json.loads(result)
|
|
1185
|
+
assert result_dict["success"] is False
|
|
1186
|
+
assert "non-destructive" in result_dict["error"].lower()
|
|
1187
|
+
|
|
1188
|
+
@pytest.mark.unit
|
|
1189
|
+
@pytest.mark.asyncio
|
|
1190
|
+
async def test_read_operations_allowed_in_non_destructive(self, mock_all_kubernetes_apis):
|
|
1191
|
+
"""Test that new read operations work in non-destructive mode."""
|
|
1192
|
+
from kubectl_mcp_tool.tools.kind import register_kind_tools
|
|
1193
|
+
|
|
1194
|
+
try:
|
|
1195
|
+
from fastmcp import FastMCP
|
|
1196
|
+
except ImportError:
|
|
1197
|
+
from mcp.server.fastmcp import FastMCP
|
|
1198
|
+
|
|
1199
|
+
mcp = FastMCP(name="test")
|
|
1200
|
+
register_kind_tools(mcp, non_destructive=True)
|
|
1201
|
+
|
|
1202
|
+
tool = await mcp.get_tool("kind_available_images_tool")
|
|
1203
|
+
result = tool.fn()
|
|
1204
|
+
result_dict = json.loads(result)
|
|
1205
|
+
assert result_dict["success"] is True
|
|
1206
|
+
assert "images" in result_dict
|