kubectl-mcp-server 1.12.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.12.0.dist-info/METADATA +711 -0
- kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
- kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
- kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
- kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
- kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
- kubectl_mcp_tool/__init__.py +21 -0
- kubectl_mcp_tool/__main__.py +46 -0
- kubectl_mcp_tool/auth/__init__.py +13 -0
- kubectl_mcp_tool/auth/config.py +71 -0
- kubectl_mcp_tool/auth/scopes.py +148 -0
- kubectl_mcp_tool/auth/verifier.py +82 -0
- kubectl_mcp_tool/cli/__init__.py +9 -0
- kubectl_mcp_tool/cli/__main__.py +10 -0
- kubectl_mcp_tool/cli/cli.py +111 -0
- kubectl_mcp_tool/diagnostics.py +355 -0
- kubectl_mcp_tool/k8s_config.py +289 -0
- kubectl_mcp_tool/mcp_server.py +530 -0
- kubectl_mcp_tool/prompts/__init__.py +5 -0
- kubectl_mcp_tool/prompts/prompts.py +823 -0
- kubectl_mcp_tool/resources/__init__.py +5 -0
- kubectl_mcp_tool/resources/resources.py +305 -0
- kubectl_mcp_tool/tools/__init__.py +28 -0
- kubectl_mcp_tool/tools/browser.py +371 -0
- kubectl_mcp_tool/tools/cluster.py +315 -0
- kubectl_mcp_tool/tools/core.py +421 -0
- kubectl_mcp_tool/tools/cost.py +680 -0
- kubectl_mcp_tool/tools/deployments.py +381 -0
- kubectl_mcp_tool/tools/diagnostics.py +174 -0
- kubectl_mcp_tool/tools/helm.py +1561 -0
- kubectl_mcp_tool/tools/networking.py +296 -0
- kubectl_mcp_tool/tools/operations.py +501 -0
- kubectl_mcp_tool/tools/pods.py +582 -0
- kubectl_mcp_tool/tools/security.py +333 -0
- kubectl_mcp_tool/tools/storage.py +133 -0
- kubectl_mcp_tool/utils/__init__.py +17 -0
- kubectl_mcp_tool/utils/helpers.py +80 -0
- tests/__init__.py +9 -0
- tests/conftest.py +379 -0
- tests/test_auth.py +256 -0
- tests/test_browser.py +349 -0
- tests/test_prompts.py +536 -0
- tests/test_resources.py +343 -0
- tests/test_server.py +384 -0
- tests/test_tools.py +659 -0
tests/test_resources.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for MCP Resources in kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
This module tests all FastMCP 3 resources including:
|
|
5
|
+
- kubeconfig:// resources
|
|
6
|
+
- namespace:// resources
|
|
7
|
+
- cluster:// resources
|
|
8
|
+
- manifest:// resources
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import json
|
|
13
|
+
from unittest.mock import patch, MagicMock
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestKubeconfigResources:
|
|
18
|
+
"""Tests for kubeconfig:// resources."""
|
|
19
|
+
|
|
20
|
+
@pytest.mark.unit
|
|
21
|
+
def test_get_kubeconfig_contexts(self, mock_kube_contexts):
|
|
22
|
+
"""Test listing all kubectl contexts."""
|
|
23
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
24
|
+
|
|
25
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
26
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
27
|
+
server = MCPServer(name="test")
|
|
28
|
+
|
|
29
|
+
# Verify the resource is registered
|
|
30
|
+
resources = server.server._resource_manager._resources if hasattr(server.server, '_resource_manager') else {}
|
|
31
|
+
assert server is not None
|
|
32
|
+
|
|
33
|
+
@pytest.mark.unit
|
|
34
|
+
def test_get_current_context(self, mock_kube_contexts):
|
|
35
|
+
"""Test getting the current active context."""
|
|
36
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
37
|
+
|
|
38
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
39
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
40
|
+
server = MCPServer(name="test")
|
|
41
|
+
|
|
42
|
+
assert server is not None
|
|
43
|
+
|
|
44
|
+
@pytest.mark.unit
|
|
45
|
+
def test_context_returns_json(self, mock_kube_contexts):
|
|
46
|
+
"""Test that context resource returns valid JSON."""
|
|
47
|
+
contexts = [
|
|
48
|
+
{"name": "minikube", "context": {"cluster": "minikube", "user": "minikube", "namespace": "default"}}
|
|
49
|
+
]
|
|
50
|
+
active = contexts[0]
|
|
51
|
+
|
|
52
|
+
with patch("kubernetes.config.list_kube_config_contexts", return_value=(contexts, active)):
|
|
53
|
+
result = {
|
|
54
|
+
"active_context": active.get("name"),
|
|
55
|
+
"contexts": contexts
|
|
56
|
+
}
|
|
57
|
+
json_str = json.dumps(result)
|
|
58
|
+
parsed = json.loads(json_str)
|
|
59
|
+
assert "active_context" in parsed
|
|
60
|
+
assert "contexts" in parsed
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestNamespaceResources:
|
|
64
|
+
"""Tests for namespace:// resources."""
|
|
65
|
+
|
|
66
|
+
@pytest.mark.unit
|
|
67
|
+
def test_get_current_namespace(self, mock_kube_contexts):
|
|
68
|
+
"""Test getting the current namespace."""
|
|
69
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
70
|
+
|
|
71
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
72
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
73
|
+
server = MCPServer(name="test")
|
|
74
|
+
|
|
75
|
+
assert server is not None
|
|
76
|
+
|
|
77
|
+
@pytest.mark.unit
|
|
78
|
+
def test_list_all_namespaces(self, mock_all_kubernetes_apis):
|
|
79
|
+
"""Test listing all namespaces."""
|
|
80
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
81
|
+
|
|
82
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
83
|
+
server = MCPServer(name="test")
|
|
84
|
+
|
|
85
|
+
assert server is not None
|
|
86
|
+
|
|
87
|
+
@pytest.mark.unit
|
|
88
|
+
def test_namespace_includes_metadata(self, mock_all_kubernetes_apis):
|
|
89
|
+
"""Test that namespace list includes metadata."""
|
|
90
|
+
mock_ns = MagicMock()
|
|
91
|
+
mock_ns.metadata.name = "test-namespace"
|
|
92
|
+
mock_ns.metadata.labels = {"env": "test"}
|
|
93
|
+
mock_ns.metadata.creation_timestamp = datetime.now()
|
|
94
|
+
mock_ns.status.phase = "Active"
|
|
95
|
+
|
|
96
|
+
result = {
|
|
97
|
+
"name": mock_ns.metadata.name,
|
|
98
|
+
"status": mock_ns.status.phase,
|
|
99
|
+
"labels": mock_ns.metadata.labels
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
assert result["name"] == "test-namespace"
|
|
103
|
+
assert result["status"] == "Active"
|
|
104
|
+
assert result["labels"]["env"] == "test"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestClusterResources:
|
|
108
|
+
"""Tests for cluster:// resources."""
|
|
109
|
+
|
|
110
|
+
@pytest.mark.unit
|
|
111
|
+
def test_get_cluster_info(self, mock_all_kubernetes_apis):
|
|
112
|
+
"""Test getting cluster info."""
|
|
113
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
114
|
+
|
|
115
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
116
|
+
server = MCPServer(name="test")
|
|
117
|
+
|
|
118
|
+
assert server is not None
|
|
119
|
+
|
|
120
|
+
@pytest.mark.unit
|
|
121
|
+
def test_get_cluster_nodes(self, mock_all_kubernetes_apis):
|
|
122
|
+
"""Test getting cluster nodes."""
|
|
123
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
124
|
+
|
|
125
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
126
|
+
server = MCPServer(name="test")
|
|
127
|
+
|
|
128
|
+
assert server is not None
|
|
129
|
+
|
|
130
|
+
@pytest.mark.unit
|
|
131
|
+
def test_get_cluster_version(self, mock_all_kubernetes_apis, mock_version_api):
|
|
132
|
+
"""Test getting cluster version."""
|
|
133
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
134
|
+
|
|
135
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
136
|
+
server = MCPServer(name="test")
|
|
137
|
+
|
|
138
|
+
version_info = mock_version_api.get_code()
|
|
139
|
+
assert version_info.git_version == "v1.28.0"
|
|
140
|
+
assert version_info.major == "1"
|
|
141
|
+
assert version_info.minor == "28"
|
|
142
|
+
|
|
143
|
+
@pytest.mark.unit
|
|
144
|
+
def test_get_api_resources(self, mock_kubectl_subprocess):
|
|
145
|
+
"""Test getting API resources."""
|
|
146
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
147
|
+
|
|
148
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
149
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
150
|
+
server = MCPServer(name="test")
|
|
151
|
+
|
|
152
|
+
assert server is not None
|
|
153
|
+
|
|
154
|
+
@pytest.mark.unit
|
|
155
|
+
def test_cluster_info_includes_node_count(self, mock_all_kubernetes_apis):
|
|
156
|
+
"""Test that cluster info includes node count."""
|
|
157
|
+
mock_nodes = [MagicMock(), MagicMock()]
|
|
158
|
+
for node in mock_nodes:
|
|
159
|
+
node.status.conditions = [MagicMock(type="Ready", status="True")]
|
|
160
|
+
|
|
161
|
+
result = {
|
|
162
|
+
"nodes": {
|
|
163
|
+
"count": len(mock_nodes),
|
|
164
|
+
"ready": sum(1 for n in mock_nodes if any(
|
|
165
|
+
c.type == "Ready" and c.status == "True"
|
|
166
|
+
for c in n.status.conditions
|
|
167
|
+
))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assert result["nodes"]["count"] == 2
|
|
172
|
+
assert result["nodes"]["ready"] == 2
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestManifestResources:
|
|
176
|
+
"""Tests for manifest:// resources."""
|
|
177
|
+
|
|
178
|
+
@pytest.mark.unit
|
|
179
|
+
def test_get_deployment_manifest(self, mock_all_kubernetes_apis):
|
|
180
|
+
"""Test getting deployment manifest."""
|
|
181
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
182
|
+
|
|
183
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
184
|
+
server = MCPServer(name="test")
|
|
185
|
+
|
|
186
|
+
assert server is not None
|
|
187
|
+
|
|
188
|
+
@pytest.mark.unit
|
|
189
|
+
def test_get_service_manifest(self, mock_all_kubernetes_apis):
|
|
190
|
+
"""Test getting service manifest."""
|
|
191
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
192
|
+
|
|
193
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
194
|
+
server = MCPServer(name="test")
|
|
195
|
+
|
|
196
|
+
assert server is not None
|
|
197
|
+
|
|
198
|
+
@pytest.mark.unit
|
|
199
|
+
def test_get_configmap_manifest(self, mock_all_kubernetes_apis):
|
|
200
|
+
"""Test getting ConfigMap manifest."""
|
|
201
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
202
|
+
|
|
203
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
204
|
+
server = MCPServer(name="test")
|
|
205
|
+
|
|
206
|
+
assert server is not None
|
|
207
|
+
|
|
208
|
+
@pytest.mark.unit
|
|
209
|
+
def test_get_pod_manifest(self, mock_all_kubernetes_apis):
|
|
210
|
+
"""Test getting pod manifest."""
|
|
211
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
212
|
+
|
|
213
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
214
|
+
server = MCPServer(name="test")
|
|
215
|
+
|
|
216
|
+
assert server is not None
|
|
217
|
+
|
|
218
|
+
@pytest.mark.unit
|
|
219
|
+
def test_get_secret_manifest_masks_data(self, mock_all_kubernetes_apis):
|
|
220
|
+
"""Test that secret manifest masks sensitive data."""
|
|
221
|
+
mock_manifest = {
|
|
222
|
+
"apiVersion": "v1",
|
|
223
|
+
"kind": "Secret",
|
|
224
|
+
"metadata": {"name": "test-secret", "namespace": "default"},
|
|
225
|
+
"data": {"password": "c2VjcmV0", "api-key": "YXBpa2V5"}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Simulate masking
|
|
229
|
+
if "data" in mock_manifest and mock_manifest["data"]:
|
|
230
|
+
mock_manifest["data"] = {k: "[MASKED]" for k in mock_manifest["data"].keys()}
|
|
231
|
+
|
|
232
|
+
assert mock_manifest["data"]["password"] == "[MASKED]"
|
|
233
|
+
assert mock_manifest["data"]["api-key"] == "[MASKED]"
|
|
234
|
+
|
|
235
|
+
@pytest.mark.unit
|
|
236
|
+
def test_get_ingress_manifest(self, mock_all_kubernetes_apis):
|
|
237
|
+
"""Test getting ingress manifest."""
|
|
238
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
239
|
+
|
|
240
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
241
|
+
server = MCPServer(name="test")
|
|
242
|
+
|
|
243
|
+
assert server is not None
|
|
244
|
+
|
|
245
|
+
@pytest.mark.unit
|
|
246
|
+
def test_manifest_returns_yaml(self):
|
|
247
|
+
"""Test that manifest resources return valid YAML."""
|
|
248
|
+
import yaml
|
|
249
|
+
|
|
250
|
+
manifest = {
|
|
251
|
+
"apiVersion": "apps/v1",
|
|
252
|
+
"kind": "Deployment",
|
|
253
|
+
"metadata": {"name": "test", "namespace": "default"},
|
|
254
|
+
"spec": {"replicas": 3}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
yaml_str = yaml.dump(manifest, default_flow_style=False)
|
|
258
|
+
parsed = yaml.safe_load(yaml_str)
|
|
259
|
+
|
|
260
|
+
assert parsed["apiVersion"] == "apps/v1"
|
|
261
|
+
assert parsed["kind"] == "Deployment"
|
|
262
|
+
assert parsed["spec"]["replicas"] == 3
|
|
263
|
+
|
|
264
|
+
@pytest.mark.unit
|
|
265
|
+
def test_manifest_handles_not_found(self):
|
|
266
|
+
"""Test that manifest resources handle not found errors."""
|
|
267
|
+
error_result = "# Error: Deployment 'not-found' not found in namespace 'default'"
|
|
268
|
+
assert "Error" in error_result
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestResourceErrorHandling:
|
|
272
|
+
"""Tests for error handling in resources."""
|
|
273
|
+
|
|
274
|
+
@pytest.mark.unit
|
|
275
|
+
def test_handles_kube_config_error(self):
|
|
276
|
+
"""Test handling of kubeconfig errors."""
|
|
277
|
+
with patch("kubernetes.config.list_kube_config_contexts") as mock_contexts:
|
|
278
|
+
mock_contexts.side_effect = Exception("Config not found")
|
|
279
|
+
result = json.dumps({"error": "Config not found"})
|
|
280
|
+
parsed = json.loads(result)
|
|
281
|
+
assert "error" in parsed
|
|
282
|
+
|
|
283
|
+
@pytest.mark.unit
|
|
284
|
+
def test_handles_api_connection_error(self):
|
|
285
|
+
"""Test handling of API connection errors."""
|
|
286
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
287
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
288
|
+
mock_api.return_value.list_namespace.side_effect = Exception("Connection refused")
|
|
289
|
+
result = json.dumps({"error": "Connection refused"})
|
|
290
|
+
parsed = json.loads(result)
|
|
291
|
+
assert "error" in parsed
|
|
292
|
+
|
|
293
|
+
@pytest.mark.unit
|
|
294
|
+
def test_handles_permission_error(self):
|
|
295
|
+
"""Test handling of permission errors."""
|
|
296
|
+
result = json.dumps({"error": "Forbidden: User does not have permission"})
|
|
297
|
+
parsed = json.loads(result)
|
|
298
|
+
assert "error" in parsed
|
|
299
|
+
assert "Forbidden" in parsed["error"]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestResourceRegistration:
|
|
303
|
+
"""Tests for resource registration."""
|
|
304
|
+
|
|
305
|
+
@pytest.mark.unit
|
|
306
|
+
def test_all_resources_registered(self):
|
|
307
|
+
"""Test that all expected resources are registered."""
|
|
308
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
309
|
+
|
|
310
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
311
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
312
|
+
server = MCPServer(name="test")
|
|
313
|
+
|
|
314
|
+
# Server should initialize with resources
|
|
315
|
+
assert server is not None
|
|
316
|
+
assert hasattr(server, 'server')
|
|
317
|
+
|
|
318
|
+
@pytest.mark.unit
|
|
319
|
+
def test_resource_uris_are_valid(self):
|
|
320
|
+
"""Test that resource URIs follow correct format."""
|
|
321
|
+
valid_uris = [
|
|
322
|
+
"kubeconfig://contexts",
|
|
323
|
+
"kubeconfig://current-context",
|
|
324
|
+
"namespace://current",
|
|
325
|
+
"namespace://list",
|
|
326
|
+
"cluster://info",
|
|
327
|
+
"cluster://nodes",
|
|
328
|
+
"cluster://version",
|
|
329
|
+
"cluster://api-resources",
|
|
330
|
+
"manifest://deployments/{namespace}/{name}",
|
|
331
|
+
"manifest://services/{namespace}/{name}",
|
|
332
|
+
"manifest://configmaps/{namespace}/{name}",
|
|
333
|
+
"manifest://pods/{namespace}/{name}",
|
|
334
|
+
"manifest://secrets/{namespace}/{name}",
|
|
335
|
+
"manifest://ingresses/{namespace}/{name}",
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
for uri in valid_uris:
|
|
339
|
+
# URI should have a scheme and path
|
|
340
|
+
assert "://" in uri
|
|
341
|
+
scheme, path = uri.split("://", 1)
|
|
342
|
+
assert scheme in ["kubeconfig", "namespace", "cluster", "manifest"]
|
|
343
|
+
assert len(path) > 0
|