kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.17.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.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
- kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +19 -0
- kubectl_mcp_tool/mcp_server.py +246 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/__init__.py +20 -0
- kubectl_mcp_tool/tools/backup.py +881 -0
- kubectl_mcp_tool/tools/capi.py +727 -0
- kubectl_mcp_tool/tools/certs.py +709 -0
- kubectl_mcp_tool/tools/cilium.py +582 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/keda.py +464 -0
- kubectl_mcp_tool/tools/kiali.py +652 -0
- kubectl_mcp_tool/tools/kubevirt.py +803 -0
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- tests/test_browser.py +2 -2
- tests/test_config.py +386 -0
- tests/test_ecosystem.py +331 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- tests/test_tools.py +70 -8
- kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
tests/test_ecosystem.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for ecosystem tools (GitOps, Cert-Manager, Policy, Backup).
|
|
3
|
+
|
|
4
|
+
This module tests the CRD detector and all ecosystem toolsets.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import patch, MagicMock
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCRDDetector:
|
|
13
|
+
"""Tests for the CRD auto-discovery framework."""
|
|
14
|
+
|
|
15
|
+
@pytest.mark.unit
|
|
16
|
+
def test_crd_detector_imports(self):
|
|
17
|
+
"""Test that CRD detector module can be imported."""
|
|
18
|
+
from kubectl_mcp_tool.crd_detector import (
|
|
19
|
+
CRD_GROUPS,
|
|
20
|
+
detect_crds,
|
|
21
|
+
crd_exists,
|
|
22
|
+
get_enabled_toolsets,
|
|
23
|
+
get_crd_status_summary,
|
|
24
|
+
FeatureNotInstalledError,
|
|
25
|
+
require_crd,
|
|
26
|
+
require_any_crd,
|
|
27
|
+
)
|
|
28
|
+
assert CRD_GROUPS is not None
|
|
29
|
+
assert callable(detect_crds)
|
|
30
|
+
assert callable(crd_exists)
|
|
31
|
+
assert callable(get_enabled_toolsets)
|
|
32
|
+
assert callable(get_crd_status_summary)
|
|
33
|
+
assert issubclass(FeatureNotInstalledError, Exception)
|
|
34
|
+
assert callable(require_crd)
|
|
35
|
+
assert callable(require_any_crd)
|
|
36
|
+
|
|
37
|
+
@pytest.mark.unit
|
|
38
|
+
def test_crd_groups_structure(self):
|
|
39
|
+
"""Test that CRD_GROUPS has expected structure."""
|
|
40
|
+
from kubectl_mcp_tool.crd_detector import CRD_GROUPS
|
|
41
|
+
|
|
42
|
+
expected_groups = ["flux", "argocd", "certmanager", "kyverno", "gatekeeper", "velero"]
|
|
43
|
+
for group in expected_groups:
|
|
44
|
+
assert group in CRD_GROUPS, f"Expected group '{group}' in CRD_GROUPS"
|
|
45
|
+
assert isinstance(CRD_GROUPS[group], list), f"CRD_GROUPS['{group}'] should be a list"
|
|
46
|
+
assert len(CRD_GROUPS[group]) > 0, f"CRD_GROUPS['{group}'] should not be empty"
|
|
47
|
+
|
|
48
|
+
@pytest.mark.unit
|
|
49
|
+
def test_detect_crds_with_mocked_kubectl(self):
|
|
50
|
+
"""Test CRD detection with mocked kubectl."""
|
|
51
|
+
from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
|
|
52
|
+
|
|
53
|
+
# Clear cache before test
|
|
54
|
+
_crd_cache.clear()
|
|
55
|
+
|
|
56
|
+
mock_output = """NAME CREATED AT
|
|
57
|
+
applications.argoproj.io 2024-01-01T00:00:00Z
|
|
58
|
+
certificates.cert-manager.io 2024-01-01T00:00:00Z
|
|
59
|
+
"""
|
|
60
|
+
with patch("subprocess.run") as mock_run:
|
|
61
|
+
mock_run.return_value = MagicMock(
|
|
62
|
+
returncode=0,
|
|
63
|
+
stdout=mock_output
|
|
64
|
+
)
|
|
65
|
+
result = detect_crds(force_refresh=True)
|
|
66
|
+
|
|
67
|
+
assert isinstance(result, dict)
|
|
68
|
+
assert "argocd" in result
|
|
69
|
+
assert "certmanager" in result
|
|
70
|
+
|
|
71
|
+
@pytest.mark.unit
|
|
72
|
+
def test_detect_crds_handles_kubectl_failure(self):
|
|
73
|
+
"""Test CRD detection handles kubectl failure gracefully."""
|
|
74
|
+
from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
|
|
75
|
+
|
|
76
|
+
_crd_cache.clear()
|
|
77
|
+
|
|
78
|
+
with patch("subprocess.run") as mock_run:
|
|
79
|
+
mock_run.side_effect = subprocess.SubprocessError("Command failed")
|
|
80
|
+
result = detect_crds(force_refresh=True)
|
|
81
|
+
|
|
82
|
+
assert isinstance(result, dict)
|
|
83
|
+
# All groups should be False when kubectl fails
|
|
84
|
+
for value in result.values():
|
|
85
|
+
assert value is False
|
|
86
|
+
|
|
87
|
+
@pytest.mark.unit
|
|
88
|
+
def test_feature_not_installed_error(self):
|
|
89
|
+
"""Test FeatureNotInstalledError exception."""
|
|
90
|
+
from kubectl_mcp_tool.crd_detector import FeatureNotInstalledError
|
|
91
|
+
|
|
92
|
+
error = FeatureNotInstalledError("velero", ["backups.velero.io"])
|
|
93
|
+
assert "velero" in str(error)
|
|
94
|
+
assert "backups.velero.io" in str(error)
|
|
95
|
+
assert error.toolset == "velero"
|
|
96
|
+
assert "backups.velero.io" in error.required_crds
|
|
97
|
+
|
|
98
|
+
@pytest.mark.unit
|
|
99
|
+
def test_get_crd_status_summary(self):
|
|
100
|
+
"""Test CRD status summary generation."""
|
|
101
|
+
from kubectl_mcp_tool.crd_detector import get_crd_status_summary, _crd_cache
|
|
102
|
+
|
|
103
|
+
_crd_cache.clear()
|
|
104
|
+
|
|
105
|
+
with patch("subprocess.run") as mock_run:
|
|
106
|
+
mock_run.return_value = MagicMock(
|
|
107
|
+
returncode=0,
|
|
108
|
+
stdout="applications.argoproj.io 2024-01-01T00:00:00Z\n"
|
|
109
|
+
)
|
|
110
|
+
summary = get_crd_status_summary()
|
|
111
|
+
|
|
112
|
+
# Summary returns a dict with crd_groups and enabled_toolsets
|
|
113
|
+
assert isinstance(summary, dict)
|
|
114
|
+
assert "crd_groups" in summary
|
|
115
|
+
assert "enabled_toolsets" in summary
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestGitOpsTools:
|
|
119
|
+
"""Tests for GitOps toolset (Flux and ArgoCD)."""
|
|
120
|
+
|
|
121
|
+
@pytest.mark.unit
|
|
122
|
+
def test_gitops_tools_import(self):
|
|
123
|
+
"""Test that GitOps tools can be imported."""
|
|
124
|
+
from kubectl_mcp_tool.tools.gitops import register_gitops_tools
|
|
125
|
+
assert callable(register_gitops_tools)
|
|
126
|
+
|
|
127
|
+
@pytest.mark.unit
|
|
128
|
+
def test_gitops_tools_register(self, mock_all_kubernetes_apis):
|
|
129
|
+
"""Test that GitOps tools register correctly."""
|
|
130
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
131
|
+
import asyncio
|
|
132
|
+
|
|
133
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
134
|
+
server = MCPServer(name="test")
|
|
135
|
+
|
|
136
|
+
async def get_tools():
|
|
137
|
+
return await server.server.list_tools()
|
|
138
|
+
|
|
139
|
+
tools = asyncio.run(get_tools())
|
|
140
|
+
tool_names = {t.name for t in tools}
|
|
141
|
+
|
|
142
|
+
gitops_tools = [
|
|
143
|
+
"gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
|
|
144
|
+
"gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
|
|
145
|
+
"gitops_detect_engine_tool"
|
|
146
|
+
]
|
|
147
|
+
for tool in gitops_tools:
|
|
148
|
+
assert tool in tool_names, f"GitOps tool '{tool}' not registered"
|
|
149
|
+
|
|
150
|
+
@pytest.mark.unit
|
|
151
|
+
def test_gitops_non_destructive_mode(self, mock_all_kubernetes_apis):
|
|
152
|
+
"""Test that GitOps sync is blocked in non-destructive mode."""
|
|
153
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
154
|
+
|
|
155
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
156
|
+
server = MCPServer(name="test", non_destructive=True)
|
|
157
|
+
|
|
158
|
+
# Server should initialize with non_destructive=True
|
|
159
|
+
assert server.non_destructive is True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestCertManagerTools:
|
|
163
|
+
"""Tests for Cert-Manager toolset."""
|
|
164
|
+
|
|
165
|
+
@pytest.mark.unit
|
|
166
|
+
def test_certs_tools_import(self):
|
|
167
|
+
"""Test that Cert-Manager tools can be imported."""
|
|
168
|
+
from kubectl_mcp_tool.tools.certs import register_certs_tools
|
|
169
|
+
assert callable(register_certs_tools)
|
|
170
|
+
|
|
171
|
+
@pytest.mark.unit
|
|
172
|
+
def test_certs_tools_register(self, mock_all_kubernetes_apis):
|
|
173
|
+
"""Test that Cert-Manager tools register correctly."""
|
|
174
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
178
|
+
server = MCPServer(name="test")
|
|
179
|
+
|
|
180
|
+
async def get_tools():
|
|
181
|
+
return await server.server.list_tools()
|
|
182
|
+
|
|
183
|
+
tools = asyncio.run(get_tools())
|
|
184
|
+
tool_names = {t.name for t in tools}
|
|
185
|
+
|
|
186
|
+
certs_tools = [
|
|
187
|
+
"certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
|
|
188
|
+
"certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
|
|
189
|
+
"certs_requests_list_tool", "certs_detect_tool"
|
|
190
|
+
]
|
|
191
|
+
for tool in certs_tools:
|
|
192
|
+
assert tool in tool_names, f"Cert-Manager tool '{tool}' not registered"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestPolicyTools:
|
|
196
|
+
"""Tests for Policy toolset (Kyverno and Gatekeeper)."""
|
|
197
|
+
|
|
198
|
+
@pytest.mark.unit
|
|
199
|
+
def test_policy_tools_import(self):
|
|
200
|
+
"""Test that Policy tools can be imported."""
|
|
201
|
+
from kubectl_mcp_tool.tools.policy import register_policy_tools
|
|
202
|
+
assert callable(register_policy_tools)
|
|
203
|
+
|
|
204
|
+
@pytest.mark.unit
|
|
205
|
+
def test_policy_tools_register(self, mock_all_kubernetes_apis):
|
|
206
|
+
"""Test that Policy tools register correctly."""
|
|
207
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
208
|
+
import asyncio
|
|
209
|
+
|
|
210
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
211
|
+
server = MCPServer(name="test")
|
|
212
|
+
|
|
213
|
+
async def get_tools():
|
|
214
|
+
return await server.server.list_tools()
|
|
215
|
+
|
|
216
|
+
tools = asyncio.run(get_tools())
|
|
217
|
+
tool_names = {t.name for t in tools}
|
|
218
|
+
|
|
219
|
+
policy_tools = [
|
|
220
|
+
"policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
|
|
221
|
+
"policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool"
|
|
222
|
+
]
|
|
223
|
+
for tool in policy_tools:
|
|
224
|
+
assert tool in tool_names, f"Policy tool '{tool}' not registered"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestBackupTools:
|
|
228
|
+
"""Tests for Backup toolset (Velero)."""
|
|
229
|
+
|
|
230
|
+
@pytest.mark.unit
|
|
231
|
+
def test_backup_tools_import(self):
|
|
232
|
+
"""Test that Backup tools can be imported."""
|
|
233
|
+
from kubectl_mcp_tool.tools.backup import register_backup_tools
|
|
234
|
+
assert callable(register_backup_tools)
|
|
235
|
+
|
|
236
|
+
@pytest.mark.unit
|
|
237
|
+
def test_backup_tools_register(self, mock_all_kubernetes_apis):
|
|
238
|
+
"""Test that Backup tools register correctly."""
|
|
239
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
240
|
+
import asyncio
|
|
241
|
+
|
|
242
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
243
|
+
server = MCPServer(name="test")
|
|
244
|
+
|
|
245
|
+
async def get_tools():
|
|
246
|
+
return await server.server.list_tools()
|
|
247
|
+
|
|
248
|
+
tools = asyncio.run(get_tools())
|
|
249
|
+
tool_names = {t.name for t in tools}
|
|
250
|
+
|
|
251
|
+
backup_tools = [
|
|
252
|
+
"backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
|
|
253
|
+
"restore_list_tool", "restore_create_tool", "restore_get_tool",
|
|
254
|
+
"backup_locations_list_tool", "backup_schedules_list_tool",
|
|
255
|
+
"backup_schedule_create_tool", "backup_detect_tool"
|
|
256
|
+
]
|
|
257
|
+
for tool in backup_tools:
|
|
258
|
+
assert tool in tool_names, f"Backup tool '{tool}' not registered"
|
|
259
|
+
|
|
260
|
+
@pytest.mark.unit
|
|
261
|
+
def test_backup_non_destructive_mode(self, mock_all_kubernetes_apis):
|
|
262
|
+
"""Test that backup operations are blocked in non-destructive mode."""
|
|
263
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
264
|
+
|
|
265
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
266
|
+
server = MCPServer(name="test", non_destructive=True)
|
|
267
|
+
|
|
268
|
+
# Server should initialize with non_destructive=True
|
|
269
|
+
assert server.non_destructive is True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TestEcosystemToolsIntegration:
|
|
273
|
+
"""Integration tests for ecosystem tools."""
|
|
274
|
+
|
|
275
|
+
@pytest.mark.unit
|
|
276
|
+
def test_all_ecosystem_tools_have_descriptions(self, mock_all_kubernetes_apis):
|
|
277
|
+
"""Test that all ecosystem tools have descriptions."""
|
|
278
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
279
|
+
import asyncio
|
|
280
|
+
|
|
281
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
282
|
+
server = MCPServer(name="test")
|
|
283
|
+
|
|
284
|
+
async def get_tools():
|
|
285
|
+
return await server.server.list_tools()
|
|
286
|
+
|
|
287
|
+
tools = asyncio.run(get_tools())
|
|
288
|
+
|
|
289
|
+
ecosystem_prefixes = ["gitops_", "certs_", "policy_", "backup_", "restore_"]
|
|
290
|
+
ecosystem_tools = [t for t in tools if any(t.name.startswith(p) for p in ecosystem_prefixes)]
|
|
291
|
+
|
|
292
|
+
tools_without_description = [
|
|
293
|
+
t.name for t in ecosystem_tools
|
|
294
|
+
if not t.description or len(t.description.strip()) == 0
|
|
295
|
+
]
|
|
296
|
+
assert not tools_without_description, f"Ecosystem tools without descriptions: {tools_without_description}"
|
|
297
|
+
|
|
298
|
+
@pytest.mark.unit
|
|
299
|
+
def test_ecosystem_tool_count(self, mock_all_kubernetes_apis):
|
|
300
|
+
"""Test that correct number of ecosystem tools are registered."""
|
|
301
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
302
|
+
import asyncio
|
|
303
|
+
|
|
304
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
305
|
+
server = MCPServer(name="test")
|
|
306
|
+
|
|
307
|
+
async def get_tools():
|
|
308
|
+
return await server.server.list_tools()
|
|
309
|
+
|
|
310
|
+
tools = asyncio.run(get_tools())
|
|
311
|
+
|
|
312
|
+
# Filter ecosystem tools, but exclude backup_resource which is in operations.py
|
|
313
|
+
ecosystem_tool_names = [
|
|
314
|
+
"gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
|
|
315
|
+
"gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
|
|
316
|
+
"gitops_detect_engine_tool",
|
|
317
|
+
"certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
|
|
318
|
+
"certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
|
|
319
|
+
"certs_requests_list_tool", "certs_detect_tool",
|
|
320
|
+
"policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
|
|
321
|
+
"policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool",
|
|
322
|
+
"backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
|
|
323
|
+
"restore_list_tool", "restore_create_tool", "restore_get_tool",
|
|
324
|
+
"backup_locations_list_tool", "backup_schedules_list_tool",
|
|
325
|
+
"backup_schedule_create_tool", "backup_detect_tool"
|
|
326
|
+
]
|
|
327
|
+
tool_names = {t.name for t in tools}
|
|
328
|
+
ecosystem_tools = [name for name in ecosystem_tool_names if name in tool_names]
|
|
329
|
+
|
|
330
|
+
# 7 GitOps + 9 Certs + 6 Policy + 11 Backup = 33 ecosystem tools
|
|
331
|
+
assert len(ecosystem_tools) == 33, f"Expected 33 ecosystem tools, got {len(ecosystem_tools)}"
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Integration tests for MCP server with safety, observability, and config modules."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMCPServerIntegration:
|
|
10
|
+
"""Test MCP server integration with safety, observability, and config modules."""
|
|
11
|
+
|
|
12
|
+
def test_import_mcp_server(self):
|
|
13
|
+
"""Test that MCP server can be imported without errors."""
|
|
14
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
15
|
+
assert MCPServer is not None
|
|
16
|
+
|
|
17
|
+
def test_mcp_server_init_default(self):
|
|
18
|
+
"""Test MCP server initialization with defaults."""
|
|
19
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
20
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
21
|
+
|
|
22
|
+
# Reset to normal mode
|
|
23
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
24
|
+
|
|
25
|
+
server = MCPServer("test-server")
|
|
26
|
+
assert server.name == "test-server"
|
|
27
|
+
assert get_safety_mode() == SafetyMode.NORMAL
|
|
28
|
+
|
|
29
|
+
def test_mcp_server_init_read_only(self):
|
|
30
|
+
"""Test MCP server initialization with read-only mode."""
|
|
31
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
32
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
33
|
+
|
|
34
|
+
# Reset to normal mode first
|
|
35
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
36
|
+
|
|
37
|
+
server = MCPServer("test-server", read_only=True)
|
|
38
|
+
assert server.name == "test-server"
|
|
39
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
40
|
+
assert server.non_destructive is True
|
|
41
|
+
|
|
42
|
+
def test_mcp_server_init_disable_destructive(self):
|
|
43
|
+
"""Test MCP server initialization with disable-destructive mode."""
|
|
44
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
45
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
46
|
+
|
|
47
|
+
# Reset to normal mode first
|
|
48
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
49
|
+
|
|
50
|
+
server = MCPServer("test-server", disable_destructive=True)
|
|
51
|
+
assert server.name == "test-server"
|
|
52
|
+
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
53
|
+
assert server.non_destructive is True
|
|
54
|
+
|
|
55
|
+
def test_mcp_server_init_with_config_file(self):
|
|
56
|
+
"""Test MCP server initialization with config file."""
|
|
57
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
58
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode, get_safety_mode
|
|
59
|
+
|
|
60
|
+
# Reset to normal mode first
|
|
61
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
62
|
+
|
|
63
|
+
# Create a temporary config file with valid transport
|
|
64
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
|
|
65
|
+
f.write("""
|
|
66
|
+
[server]
|
|
67
|
+
transport = "stdio"
|
|
68
|
+
port = 9000
|
|
69
|
+
|
|
70
|
+
[safety]
|
|
71
|
+
mode = "read-only"
|
|
72
|
+
""")
|
|
73
|
+
config_file = f.name
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
server = MCPServer("test-server", config_file=config_file)
|
|
77
|
+
assert server.name == "test-server"
|
|
78
|
+
assert server.config is not None
|
|
79
|
+
# Config file sets read-only mode
|
|
80
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
81
|
+
finally:
|
|
82
|
+
os.unlink(config_file)
|
|
83
|
+
|
|
84
|
+
def test_mcp_server_has_stats_collector(self):
|
|
85
|
+
"""Test MCP server has stats collector initialized."""
|
|
86
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
87
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
88
|
+
|
|
89
|
+
# Reset to normal mode first
|
|
90
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
91
|
+
|
|
92
|
+
server = MCPServer("test-server")
|
|
93
|
+
assert server._stats is not None
|
|
94
|
+
# Can get stats
|
|
95
|
+
stats = server._stats.get_stats()
|
|
96
|
+
assert "uptime_seconds" in stats
|
|
97
|
+
assert "tool_calls_total" in stats
|
|
98
|
+
|
|
99
|
+
def test_mcp_server_reload_callback(self):
|
|
100
|
+
"""Test MCP server registers reload callback."""
|
|
101
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
102
|
+
from kubectl_mcp_tool.config import reload_config
|
|
103
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
104
|
+
|
|
105
|
+
# Reset to normal mode first
|
|
106
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
107
|
+
|
|
108
|
+
server = MCPServer("test-server")
|
|
109
|
+
|
|
110
|
+
# Reload config should not raise
|
|
111
|
+
# The callback is registered and will be called
|
|
112
|
+
try:
|
|
113
|
+
reload_config()
|
|
114
|
+
except Exception:
|
|
115
|
+
# Config files may not exist, which is fine
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def test_cli_parameters_read_only(self):
|
|
119
|
+
"""Test CLI parameters for read-only mode."""
|
|
120
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
121
|
+
|
|
122
|
+
# Reset to normal mode first
|
|
123
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
124
|
+
|
|
125
|
+
# Verify the set_safety_mode works as expected
|
|
126
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
127
|
+
from kubectl_mcp_tool.safety import get_safety_mode
|
|
128
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
129
|
+
|
|
130
|
+
def test_cli_parameters_disable_destructive(self):
|
|
131
|
+
"""Test CLI parameters for disable-destructive mode."""
|
|
132
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode, get_safety_mode
|
|
133
|
+
|
|
134
|
+
# Reset to normal mode first
|
|
135
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
136
|
+
|
|
137
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
138
|
+
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestMCPServerObservability:
|
|
142
|
+
"""Test observability integration in MCP server."""
|
|
143
|
+
|
|
144
|
+
def test_stats_collector_integration(self):
|
|
145
|
+
"""Test stats collector is available in MCP server."""
|
|
146
|
+
from kubectl_mcp_tool.observability import get_stats_collector
|
|
147
|
+
|
|
148
|
+
stats = get_stats_collector()
|
|
149
|
+
assert stats is not None
|
|
150
|
+
|
|
151
|
+
# Record some calls
|
|
152
|
+
stats.record_tool_call("integration_test_tool", success=True, duration=0.1)
|
|
153
|
+
tool_stats = stats.get_tool_stats("integration_test_tool")
|
|
154
|
+
assert tool_stats is not None
|
|
155
|
+
assert tool_stats["calls"] >= 1
|
|
156
|
+
assert tool_stats["errors"] == 0
|
|
157
|
+
|
|
158
|
+
def test_metrics_availability(self):
|
|
159
|
+
"""Test Prometheus metrics availability check."""
|
|
160
|
+
from kubectl_mcp_tool.observability import is_prometheus_available, get_metrics
|
|
161
|
+
|
|
162
|
+
# Check availability (may or may not be installed)
|
|
163
|
+
available = is_prometheus_available()
|
|
164
|
+
|
|
165
|
+
if available:
|
|
166
|
+
metrics = get_metrics()
|
|
167
|
+
assert isinstance(metrics, str)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestMCPServerConfig:
|
|
171
|
+
"""Test config integration in MCP server."""
|
|
172
|
+
|
|
173
|
+
def test_load_config(self):
|
|
174
|
+
"""Test config loading."""
|
|
175
|
+
from kubectl_mcp_tool.config import load_config
|
|
176
|
+
|
|
177
|
+
config = load_config()
|
|
178
|
+
assert config is not None
|
|
179
|
+
assert hasattr(config, 'server')
|
|
180
|
+
assert hasattr(config, 'safety')
|
|
181
|
+
assert hasattr(config, 'browser')
|
|
182
|
+
|
|
183
|
+
def test_config_reload_callbacks(self):
|
|
184
|
+
"""Test config reload callback registration."""
|
|
185
|
+
from kubectl_mcp_tool.config import (
|
|
186
|
+
register_reload_callback,
|
|
187
|
+
unregister_reload_callback,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
callback_called = []
|
|
191
|
+
|
|
192
|
+
def test_callback(config):
|
|
193
|
+
callback_called.append(config)
|
|
194
|
+
|
|
195
|
+
register_reload_callback(test_callback)
|
|
196
|
+
|
|
197
|
+
# Unregister to clean up
|
|
198
|
+
unregister_reload_callback(test_callback)
|
|
199
|
+
|
|
200
|
+
# Verify unregister worked
|
|
201
|
+
assert len(callback_called) == 0 # Not called since we unregistered
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestMCPServerSafety:
|
|
205
|
+
"""Test safety mode integration in MCP server."""
|
|
206
|
+
|
|
207
|
+
def test_safety_mode_info(self):
|
|
208
|
+
"""Test safety mode info retrieval."""
|
|
209
|
+
from kubectl_mcp_tool.safety import (
|
|
210
|
+
SafetyMode,
|
|
211
|
+
set_safety_mode,
|
|
212
|
+
get_mode_info,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
216
|
+
info = get_mode_info()
|
|
217
|
+
assert info["mode"] == "normal"
|
|
218
|
+
assert "description" in info
|
|
219
|
+
assert info["blocked_operations"] == []
|
|
220
|
+
|
|
221
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
222
|
+
info = get_mode_info()
|
|
223
|
+
assert info["mode"] == "read_only"
|
|
224
|
+
assert len(info["blocked_operations"]) > 0
|
|
225
|
+
|
|
226
|
+
def test_operation_allowed_check(self):
|
|
227
|
+
"""Test operation allowed check."""
|
|
228
|
+
from kubectl_mcp_tool.safety import (
|
|
229
|
+
SafetyMode,
|
|
230
|
+
set_safety_mode,
|
|
231
|
+
is_operation_allowed,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
235
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
236
|
+
assert allowed is True
|
|
237
|
+
assert reason == ""
|
|
238
|
+
|
|
239
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
240
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
241
|
+
assert allowed is False
|
|
242
|
+
assert "blocked" in reason.lower()
|
|
243
|
+
|
|
244
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
245
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
246
|
+
assert allowed is False
|
|
247
|
+
assert "blocked" in reason.lower()
|
|
248
|
+
|
|
249
|
+
# Non-destructive write should be allowed
|
|
250
|
+
allowed, reason = is_operation_allowed("create_deployment")
|
|
251
|
+
assert allowed is True
|