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_tools.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for all MCP tools in kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
This module contains comprehensive tests for all 121 Kubernetes tools
|
|
5
|
+
provided by the MCP server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import json
|
|
10
|
+
import asyncio
|
|
11
|
+
from unittest.mock import patch, MagicMock
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Complete list of all 121 tools that must be registered
|
|
16
|
+
EXPECTED_TOOLS = [
|
|
17
|
+
# Pods (pods.py)
|
|
18
|
+
"get_pods", "get_logs", "get_pod_events", "check_pod_health", "exec_in_pod",
|
|
19
|
+
"cleanup_pods", "get_pod_conditions", "get_previous_logs", "diagnose_pod_crash",
|
|
20
|
+
"detect_pending_pods", "get_evicted_pods",
|
|
21
|
+
# Deployments (deployments.py)
|
|
22
|
+
"get_deployments", "create_deployment", "scale_deployment", "restart_deployment",
|
|
23
|
+
"get_statefulsets", "get_daemonsets", "get_replicasets", "get_jobs",
|
|
24
|
+
# Core (core.py)
|
|
25
|
+
"get_namespaces", "get_configmaps", "get_secrets", "get_events",
|
|
26
|
+
"get_resource_quotas", "get_limit_ranges",
|
|
27
|
+
# Cluster (cluster.py)
|
|
28
|
+
"get_current_context", "switch_context", "list_contexts", "list_kubeconfig_contexts",
|
|
29
|
+
"get_context_details", "set_namespace_for_context", "get_cluster_info",
|
|
30
|
+
"get_cluster_version", "get_nodes", "get_api_resources", "health_check",
|
|
31
|
+
# Networking (networking.py)
|
|
32
|
+
"get_services", "get_endpoints", "get_ingress", "port_forward",
|
|
33
|
+
"diagnose_network_connectivity", "check_dns_resolution", "trace_service_chain",
|
|
34
|
+
"analyze_network_policies",
|
|
35
|
+
# Storage (storage.py)
|
|
36
|
+
"get_pvcs", "get_persistent_volumes", "get_storage_classes",
|
|
37
|
+
# Security (security.py)
|
|
38
|
+
"get_rbac_roles", "get_cluster_roles", "get_service_accounts",
|
|
39
|
+
"audit_rbac_permissions", "check_secrets_security", "get_pod_security_info",
|
|
40
|
+
"get_admission_webhooks", "get_crds", "get_priority_classes", "analyze_pod_security",
|
|
41
|
+
# Helm (helm.py) - 37 tools
|
|
42
|
+
"helm_list", "helm_status", "helm_history", "helm_get_values", "helm_get_manifest",
|
|
43
|
+
"helm_get_notes", "helm_get_hooks", "helm_get_all", "helm_show_chart",
|
|
44
|
+
"helm_show_values", "helm_show_readme", "helm_show_crds", "helm_show_all",
|
|
45
|
+
"helm_search_repo", "helm_search_hub", "helm_repo_list", "helm_repo_add",
|
|
46
|
+
"helm_repo_remove", "helm_repo_update", "install_helm_chart", "upgrade_helm_chart",
|
|
47
|
+
"uninstall_helm_chart", "helm_rollback", "helm_test", "helm_template",
|
|
48
|
+
"helm_template_apply", "helm_create", "helm_lint", "helm_package", "helm_pull",
|
|
49
|
+
"helm_dependency_list", "helm_dependency_update", "helm_dependency_build",
|
|
50
|
+
"helm_version", "helm_env",
|
|
51
|
+
# Operations (operations.py)
|
|
52
|
+
"kubectl_apply", "kubectl_describe", "kubectl_patch", "kubectl_rollout",
|
|
53
|
+
"kubectl_create", "delete_resource", "kubectl_cp", "backup_resource",
|
|
54
|
+
"label_resource", "annotate_resource", "taint_node", "wait_for_condition",
|
|
55
|
+
"node_management", "kubectl_generic", "kubectl_explain",
|
|
56
|
+
# Diagnostics (diagnostics.py)
|
|
57
|
+
"compare_namespaces", "get_pod_metrics", "get_node_metrics",
|
|
58
|
+
# Cost (cost.py)
|
|
59
|
+
"get_resource_recommendations", "get_idle_resources", "get_resource_quotas_usage",
|
|
60
|
+
"get_cost_analysis", "get_overprovisioned_resources", "get_resource_trends",
|
|
61
|
+
"get_namespace_cost_allocation", "optimize_resource_requests", "get_resource_usage",
|
|
62
|
+
# Autoscaling (deployments.py)
|
|
63
|
+
"get_hpa", "get_pdb",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestAllToolsRegistered:
|
|
68
|
+
"""Comprehensive tests to verify all 121 core tools are registered."""
|
|
69
|
+
|
|
70
|
+
@pytest.mark.unit
|
|
71
|
+
def test_all_121_tools_registered(self):
|
|
72
|
+
"""Verify all 121 expected core tools are registered (excluding optional browser tools)."""
|
|
73
|
+
import os
|
|
74
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
75
|
+
|
|
76
|
+
# Disable browser tools for this test
|
|
77
|
+
with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "false"}):
|
|
78
|
+
# Reload browser module to pick up env change
|
|
79
|
+
import importlib
|
|
80
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
81
|
+
importlib.reload(browser_module)
|
|
82
|
+
|
|
83
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
84
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
85
|
+
server = MCPServer(name="test")
|
|
86
|
+
|
|
87
|
+
async def get_tools():
|
|
88
|
+
return await server.server.list_tools()
|
|
89
|
+
|
|
90
|
+
tools = asyncio.run(get_tools())
|
|
91
|
+
tool_names = {t.name for t in tools}
|
|
92
|
+
|
|
93
|
+
# Verify count (121 core tools, browser tools disabled)
|
|
94
|
+
assert len(tools) == 121, f"Expected 121 tools, got {len(tools)}"
|
|
95
|
+
|
|
96
|
+
# Check for missing tools
|
|
97
|
+
missing_tools = set(EXPECTED_TOOLS) - tool_names
|
|
98
|
+
assert not missing_tools, f"Missing tools: {missing_tools}"
|
|
99
|
+
|
|
100
|
+
# Check for unexpected tools (tools not in expected list)
|
|
101
|
+
unexpected_tools = tool_names - set(EXPECTED_TOOLS)
|
|
102
|
+
assert not unexpected_tools, f"Unexpected tools: {unexpected_tools}"
|
|
103
|
+
|
|
104
|
+
@pytest.mark.unit
|
|
105
|
+
def test_tool_modules_import_correctly(self):
|
|
106
|
+
"""Test that all tool modules can be imported."""
|
|
107
|
+
from kubectl_mcp_tool.tools import (
|
|
108
|
+
register_helm_tools,
|
|
109
|
+
register_pod_tools,
|
|
110
|
+
register_core_tools,
|
|
111
|
+
register_cluster_tools,
|
|
112
|
+
register_deployment_tools,
|
|
113
|
+
register_security_tools,
|
|
114
|
+
register_networking_tools,
|
|
115
|
+
register_storage_tools,
|
|
116
|
+
register_operations_tools,
|
|
117
|
+
register_diagnostics_tools,
|
|
118
|
+
register_cost_tools,
|
|
119
|
+
)
|
|
120
|
+
# All imports should succeed
|
|
121
|
+
assert callable(register_helm_tools)
|
|
122
|
+
assert callable(register_pod_tools)
|
|
123
|
+
assert callable(register_core_tools)
|
|
124
|
+
assert callable(register_cluster_tools)
|
|
125
|
+
assert callable(register_deployment_tools)
|
|
126
|
+
assert callable(register_security_tools)
|
|
127
|
+
assert callable(register_networking_tools)
|
|
128
|
+
assert callable(register_storage_tools)
|
|
129
|
+
assert callable(register_operations_tools)
|
|
130
|
+
assert callable(register_diagnostics_tools)
|
|
131
|
+
assert callable(register_cost_tools)
|
|
132
|
+
|
|
133
|
+
@pytest.mark.unit
|
|
134
|
+
def test_all_tools_have_descriptions(self):
|
|
135
|
+
"""Verify all tools have non-empty descriptions."""
|
|
136
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
137
|
+
|
|
138
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
139
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
140
|
+
server = MCPServer(name="test")
|
|
141
|
+
|
|
142
|
+
async def get_tools():
|
|
143
|
+
return await server.server.list_tools()
|
|
144
|
+
|
|
145
|
+
tools = asyncio.run(get_tools())
|
|
146
|
+
|
|
147
|
+
tools_without_description = [t.name for t in tools if not t.description or len(t.description.strip()) == 0]
|
|
148
|
+
assert not tools_without_description, f"Tools without descriptions: {tools_without_description}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestPodTools:
|
|
152
|
+
"""Tests for pod-related tools."""
|
|
153
|
+
|
|
154
|
+
@pytest.mark.unit
|
|
155
|
+
def test_get_pods_all_namespaces(self, mock_all_kubernetes_apis):
|
|
156
|
+
"""Test getting pods from all namespaces."""
|
|
157
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
158
|
+
|
|
159
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
160
|
+
server = MCPServer(name="test")
|
|
161
|
+
|
|
162
|
+
# Verify server initialized successfully with tools
|
|
163
|
+
assert server is not None
|
|
164
|
+
assert hasattr(server, 'server')
|
|
165
|
+
|
|
166
|
+
@pytest.mark.unit
|
|
167
|
+
def test_get_pods_specific_namespace(self, mock_all_kubernetes_apis):
|
|
168
|
+
"""Test getting pods from a specific namespace."""
|
|
169
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
170
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
171
|
+
mock_pod = MagicMock()
|
|
172
|
+
mock_pod.metadata.name = "test-pod"
|
|
173
|
+
mock_pod.metadata.namespace = "default"
|
|
174
|
+
mock_pod.status.phase = "Running"
|
|
175
|
+
mock_pod.status.pod_ip = "10.0.0.1"
|
|
176
|
+
mock_api.return_value.list_namespaced_pod.return_value.items = [mock_pod]
|
|
177
|
+
|
|
178
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
179
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
180
|
+
server = MCPServer(name="test")
|
|
181
|
+
|
|
182
|
+
@pytest.mark.unit
|
|
183
|
+
def test_get_logs(self, mock_all_kubernetes_apis):
|
|
184
|
+
"""Test getting pod logs."""
|
|
185
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
186
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
187
|
+
mock_api.return_value.read_namespaced_pod_log.return_value = "Test log line 1\nTest log line 2"
|
|
188
|
+
|
|
189
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
190
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
191
|
+
server = MCPServer(name="test")
|
|
192
|
+
|
|
193
|
+
@pytest.mark.unit
|
|
194
|
+
def test_get_pod_events(self, mock_all_kubernetes_apis):
|
|
195
|
+
"""Test getting pod events."""
|
|
196
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
197
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
198
|
+
mock_event = MagicMock()
|
|
199
|
+
mock_event.metadata.name = "test-event"
|
|
200
|
+
mock_event.type = "Normal"
|
|
201
|
+
mock_event.reason = "Scheduled"
|
|
202
|
+
mock_event.message = "Successfully scheduled"
|
|
203
|
+
mock_api.return_value.list_namespaced_event.return_value.items = [mock_event]
|
|
204
|
+
|
|
205
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
206
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
207
|
+
server = MCPServer(name="test")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestDeploymentTools:
|
|
211
|
+
"""Tests for deployment-related tools."""
|
|
212
|
+
|
|
213
|
+
@pytest.mark.unit
|
|
214
|
+
def test_get_deployments(self, mock_all_kubernetes_apis):
|
|
215
|
+
"""Test getting deployments."""
|
|
216
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
217
|
+
with patch("kubernetes.client.AppsV1Api") as mock_api:
|
|
218
|
+
mock_deployment = MagicMock()
|
|
219
|
+
mock_deployment.metadata.name = "test-deployment"
|
|
220
|
+
mock_deployment.metadata.namespace = "default"
|
|
221
|
+
mock_deployment.spec.replicas = 3
|
|
222
|
+
mock_deployment.status.ready_replicas = 3
|
|
223
|
+
mock_api.return_value.list_namespaced_deployment.return_value.items = [mock_deployment]
|
|
224
|
+
|
|
225
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
226
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
227
|
+
server = MCPServer(name="test")
|
|
228
|
+
|
|
229
|
+
@pytest.mark.unit
|
|
230
|
+
def test_scale_deployment(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
231
|
+
"""Test scaling a deployment."""
|
|
232
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
233
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
234
|
+
server = MCPServer(name="test")
|
|
235
|
+
|
|
236
|
+
@pytest.mark.unit
|
|
237
|
+
def test_rollout_status(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
238
|
+
"""Test checking rollout status."""
|
|
239
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
240
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
241
|
+
server = MCPServer(name="test")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestServiceTools:
|
|
245
|
+
"""Tests for service-related tools."""
|
|
246
|
+
|
|
247
|
+
@pytest.mark.unit
|
|
248
|
+
def test_get_services(self, mock_all_kubernetes_apis):
|
|
249
|
+
"""Test getting services."""
|
|
250
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
251
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
252
|
+
mock_service = MagicMock()
|
|
253
|
+
mock_service.metadata.name = "test-service"
|
|
254
|
+
mock_service.metadata.namespace = "default"
|
|
255
|
+
mock_service.spec.type = "ClusterIP"
|
|
256
|
+
mock_service.spec.cluster_ip = "10.96.0.1"
|
|
257
|
+
mock_api.return_value.list_namespaced_service.return_value.items = [mock_service]
|
|
258
|
+
|
|
259
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
260
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
261
|
+
server = MCPServer(name="test")
|
|
262
|
+
|
|
263
|
+
@pytest.mark.unit
|
|
264
|
+
def test_get_endpoints(self, mock_all_kubernetes_apis):
|
|
265
|
+
"""Test getting service endpoints."""
|
|
266
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
267
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
268
|
+
mock_endpoints = MagicMock()
|
|
269
|
+
mock_endpoints.metadata.name = "test-service"
|
|
270
|
+
mock_api.return_value.read_namespaced_endpoints.return_value = mock_endpoints
|
|
271
|
+
|
|
272
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
273
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
274
|
+
server = MCPServer(name="test")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestNamespaceTools:
|
|
278
|
+
"""Tests for namespace-related tools."""
|
|
279
|
+
|
|
280
|
+
@pytest.mark.unit
|
|
281
|
+
def test_get_namespaces(self, mock_all_kubernetes_apis):
|
|
282
|
+
"""Test getting namespaces."""
|
|
283
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
284
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
285
|
+
mock_ns = MagicMock()
|
|
286
|
+
mock_ns.metadata.name = "default"
|
|
287
|
+
mock_ns.status.phase = "Active"
|
|
288
|
+
mock_api.return_value.list_namespace.return_value.items = [mock_ns]
|
|
289
|
+
|
|
290
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
291
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
292
|
+
server = MCPServer(name="test")
|
|
293
|
+
|
|
294
|
+
@pytest.mark.unit
|
|
295
|
+
def test_create_namespace(self, mock_all_kubernetes_apis):
|
|
296
|
+
"""Test creating a namespace."""
|
|
297
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
298
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
299
|
+
mock_ns = MagicMock()
|
|
300
|
+
mock_ns.metadata.name = "new-namespace"
|
|
301
|
+
mock_api.return_value.create_namespace.return_value = mock_ns
|
|
302
|
+
|
|
303
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
304
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
305
|
+
server = MCPServer(name="test")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestConfigMapAndSecretTools:
|
|
309
|
+
"""Tests for ConfigMap and Secret tools."""
|
|
310
|
+
|
|
311
|
+
@pytest.mark.unit
|
|
312
|
+
def test_get_configmaps(self, mock_all_kubernetes_apis):
|
|
313
|
+
"""Test getting ConfigMaps."""
|
|
314
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
315
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
316
|
+
mock_cm = MagicMock()
|
|
317
|
+
mock_cm.metadata.name = "test-configmap"
|
|
318
|
+
mock_cm.data = {"key": "value"}
|
|
319
|
+
mock_api.return_value.list_namespaced_config_map.return_value.items = [mock_cm]
|
|
320
|
+
|
|
321
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
322
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
323
|
+
server = MCPServer(name="test")
|
|
324
|
+
|
|
325
|
+
@pytest.mark.unit
|
|
326
|
+
def test_get_secrets(self, mock_all_kubernetes_apis):
|
|
327
|
+
"""Test getting secrets."""
|
|
328
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
329
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
330
|
+
mock_secret = MagicMock()
|
|
331
|
+
mock_secret.metadata.name = "test-secret"
|
|
332
|
+
mock_secret.type = "Opaque"
|
|
333
|
+
mock_api.return_value.list_namespaced_secret.return_value.items = [mock_secret]
|
|
334
|
+
|
|
335
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
336
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
337
|
+
server = MCPServer(name="test")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class TestNodeTools:
|
|
341
|
+
"""Tests for node-related tools."""
|
|
342
|
+
|
|
343
|
+
@pytest.mark.unit
|
|
344
|
+
def test_get_nodes(self, mock_all_kubernetes_apis):
|
|
345
|
+
"""Test getting nodes."""
|
|
346
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
347
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
348
|
+
mock_node = MagicMock()
|
|
349
|
+
mock_node.metadata.name = "test-node"
|
|
350
|
+
mock_node.status.conditions = [MagicMock(type="Ready", status="True")]
|
|
351
|
+
mock_api.return_value.list_node.return_value.items = [mock_node]
|
|
352
|
+
|
|
353
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
354
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
355
|
+
server = MCPServer(name="test")
|
|
356
|
+
|
|
357
|
+
@pytest.mark.unit
|
|
358
|
+
def test_cordon_node(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
359
|
+
"""Test cordoning a node."""
|
|
360
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
361
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
362
|
+
server = MCPServer(name="test")
|
|
363
|
+
|
|
364
|
+
@pytest.mark.unit
|
|
365
|
+
def test_drain_node(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
366
|
+
"""Test draining a node."""
|
|
367
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
368
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
369
|
+
server = MCPServer(name="test")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class TestHelmTools:
|
|
373
|
+
"""Tests for Helm-related tools."""
|
|
374
|
+
|
|
375
|
+
@pytest.mark.unit
|
|
376
|
+
def test_helm_list(self, mock_helm_subprocess):
|
|
377
|
+
"""Test listing Helm releases."""
|
|
378
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
379
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
380
|
+
server = MCPServer(name="test")
|
|
381
|
+
|
|
382
|
+
@pytest.mark.unit
|
|
383
|
+
def test_helm_status(self, mock_helm_subprocess):
|
|
384
|
+
"""Test getting Helm release status."""
|
|
385
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
386
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
387
|
+
server = MCPServer(name="test")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestClusterTools:
|
|
391
|
+
"""Tests for cluster-wide tools."""
|
|
392
|
+
|
|
393
|
+
@pytest.mark.unit
|
|
394
|
+
def test_cluster_info(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
395
|
+
"""Test getting cluster info."""
|
|
396
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
397
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
398
|
+
server = MCPServer(name="test")
|
|
399
|
+
|
|
400
|
+
@pytest.mark.unit
|
|
401
|
+
def test_get_contexts(self, mock_kube_contexts):
|
|
402
|
+
"""Test getting kubectl contexts."""
|
|
403
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
404
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
405
|
+
server = MCPServer(name="test")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class TestSecurityTools:
|
|
409
|
+
"""Tests for security-related tools."""
|
|
410
|
+
|
|
411
|
+
@pytest.mark.unit
|
|
412
|
+
def test_get_security_contexts(self, mock_all_kubernetes_apis):
|
|
413
|
+
"""Test getting security contexts."""
|
|
414
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
415
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
416
|
+
server = MCPServer(name="test")
|
|
417
|
+
|
|
418
|
+
@pytest.mark.unit
|
|
419
|
+
def test_get_rbac_rules(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
420
|
+
"""Test getting RBAC rules."""
|
|
421
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
422
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
423
|
+
server = MCPServer(name="test")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class TestNetworkTools:
|
|
427
|
+
"""Tests for network-related tools."""
|
|
428
|
+
|
|
429
|
+
@pytest.mark.unit
|
|
430
|
+
def test_get_network_policies(self, mock_all_kubernetes_apis):
|
|
431
|
+
"""Test getting network policies."""
|
|
432
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
433
|
+
with patch("kubernetes.client.NetworkingV1Api") as mock_api:
|
|
434
|
+
mock_policy = MagicMock()
|
|
435
|
+
mock_policy.metadata.name = "test-policy"
|
|
436
|
+
mock_api.return_value.list_namespaced_network_policy.return_value.items = [mock_policy]
|
|
437
|
+
|
|
438
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
439
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
440
|
+
server = MCPServer(name="test")
|
|
441
|
+
|
|
442
|
+
@pytest.mark.unit
|
|
443
|
+
def test_get_ingresses(self, mock_all_kubernetes_apis):
|
|
444
|
+
"""Test getting ingresses."""
|
|
445
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
446
|
+
with patch("kubernetes.client.NetworkingV1Api") as mock_api:
|
|
447
|
+
mock_ingress = MagicMock()
|
|
448
|
+
mock_ingress.metadata.name = "test-ingress"
|
|
449
|
+
mock_api.return_value.list_namespaced_ingress.return_value.items = [mock_ingress]
|
|
450
|
+
|
|
451
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
452
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
453
|
+
server = MCPServer(name="test")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class TestJobAndCronJobTools:
|
|
457
|
+
"""Tests for Job and CronJob tools."""
|
|
458
|
+
|
|
459
|
+
@pytest.mark.unit
|
|
460
|
+
def test_get_jobs(self, mock_all_kubernetes_apis):
|
|
461
|
+
"""Test getting jobs."""
|
|
462
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
463
|
+
with patch("kubernetes.client.BatchV1Api") as mock_api:
|
|
464
|
+
mock_job = MagicMock()
|
|
465
|
+
mock_job.metadata.name = "test-job"
|
|
466
|
+
mock_api.return_value.list_namespaced_job.return_value.items = [mock_job]
|
|
467
|
+
|
|
468
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
469
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
470
|
+
server = MCPServer(name="test")
|
|
471
|
+
|
|
472
|
+
@pytest.mark.unit
|
|
473
|
+
def test_get_cronjobs(self, mock_all_kubernetes_apis):
|
|
474
|
+
"""Test getting CronJobs."""
|
|
475
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
476
|
+
with patch("kubernetes.client.BatchV1Api") as mock_api:
|
|
477
|
+
mock_cronjob = MagicMock()
|
|
478
|
+
mock_cronjob.metadata.name = "test-cronjob"
|
|
479
|
+
mock_api.return_value.list_namespaced_cron_job.return_value.items = [mock_cronjob]
|
|
480
|
+
|
|
481
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
482
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
483
|
+
server = MCPServer(name="test")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class TestStorageTools:
|
|
487
|
+
"""Tests for storage-related tools."""
|
|
488
|
+
|
|
489
|
+
@pytest.mark.unit
|
|
490
|
+
def test_get_pvcs(self, mock_all_kubernetes_apis):
|
|
491
|
+
"""Test getting PersistentVolumeClaims."""
|
|
492
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
493
|
+
with patch("kubernetes.client.CoreV1Api") as mock_api:
|
|
494
|
+
mock_pvc = MagicMock()
|
|
495
|
+
mock_pvc.metadata.name = "test-pvc"
|
|
496
|
+
mock_pvc.status.phase = "Bound"
|
|
497
|
+
mock_api.return_value.list_namespaced_persistent_volume_claim.return_value.items = [mock_pvc]
|
|
498
|
+
|
|
499
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
500
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
501
|
+
server = MCPServer(name="test")
|
|
502
|
+
|
|
503
|
+
@pytest.mark.unit
|
|
504
|
+
def test_get_storage_classes(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
505
|
+
"""Test getting storage classes."""
|
|
506
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
507
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
508
|
+
server = MCPServer(name="test")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class TestCostOptimizationTools:
|
|
512
|
+
"""Tests for cost optimization tools (Phase 4)."""
|
|
513
|
+
|
|
514
|
+
@pytest.mark.unit
|
|
515
|
+
def test_get_resource_usage(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
516
|
+
"""Test getting resource usage."""
|
|
517
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
518
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
519
|
+
server = MCPServer(name="test")
|
|
520
|
+
|
|
521
|
+
@pytest.mark.unit
|
|
522
|
+
def test_get_idle_resources(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
523
|
+
"""Test finding idle resources."""
|
|
524
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
525
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
526
|
+
server = MCPServer(name="test")
|
|
527
|
+
|
|
528
|
+
@pytest.mark.unit
|
|
529
|
+
def test_get_cost_analysis(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
530
|
+
"""Test cost analysis."""
|
|
531
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
532
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
533
|
+
server = MCPServer(name="test")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class TestApplyAndDeleteTools:
|
|
537
|
+
"""Tests for apply and delete operations."""
|
|
538
|
+
|
|
539
|
+
@pytest.mark.unit
|
|
540
|
+
def test_kubectl_apply(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
541
|
+
"""Test applying YAML manifests."""
|
|
542
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
543
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
544
|
+
server = MCPServer(name="test")
|
|
545
|
+
|
|
546
|
+
@pytest.mark.unit
|
|
547
|
+
def test_kubectl_delete(self, mock_all_kubernetes_apis, mock_kubectl_subprocess):
|
|
548
|
+
"""Test deleting resources."""
|
|
549
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
550
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
551
|
+
server = MCPServer(name="test")
|
|
552
|
+
|
|
553
|
+
@pytest.mark.unit
|
|
554
|
+
def test_non_destructive_mode(self, mock_all_kubernetes_apis):
|
|
555
|
+
"""Test non-destructive mode blocks destructive operations."""
|
|
556
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
557
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
558
|
+
server = MCPServer(name="test", non_destructive=True)
|
|
559
|
+
result = server._check_destructive()
|
|
560
|
+
assert result is not None
|
|
561
|
+
assert result["success"] is False
|
|
562
|
+
assert "non-destructive mode" in result["error"]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class TestToolAnnotations:
|
|
566
|
+
"""Tests for tool annotations and metadata."""
|
|
567
|
+
|
|
568
|
+
@pytest.mark.unit
|
|
569
|
+
def test_read_only_tools_have_annotations(self):
|
|
570
|
+
"""Test that read-only tools have proper annotations."""
|
|
571
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
572
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
573
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
574
|
+
server = MCPServer(name="test")
|
|
575
|
+
# Server should initialize without errors
|
|
576
|
+
assert server is not None
|
|
577
|
+
|
|
578
|
+
@pytest.mark.unit
|
|
579
|
+
def test_all_tools_have_docstrings(self):
|
|
580
|
+
"""Test that all tools have documentation."""
|
|
581
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
582
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
583
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
584
|
+
server = MCPServer(name="test")
|
|
585
|
+
# Server should have tools registered
|
|
586
|
+
assert hasattr(server, 'server')
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class TestErrorHandling:
|
|
590
|
+
"""Tests for error handling in tools."""
|
|
591
|
+
|
|
592
|
+
@pytest.mark.unit
|
|
593
|
+
def test_handles_connection_error(self):
|
|
594
|
+
"""Test handling of connection errors."""
|
|
595
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
596
|
+
|
|
597
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
598
|
+
with patch("kubernetes.config.load_kube_config") as mock_config:
|
|
599
|
+
mock_config.side_effect = Exception("Connection refused")
|
|
600
|
+
# Server should still initialize
|
|
601
|
+
try:
|
|
602
|
+
server = MCPServer(name="test")
|
|
603
|
+
except:
|
|
604
|
+
pass # Expected behavior
|
|
605
|
+
|
|
606
|
+
@pytest.mark.unit
|
|
607
|
+
def test_handles_api_error(self):
|
|
608
|
+
"""Test handling of Kubernetes API errors."""
|
|
609
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
610
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
611
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
612
|
+
server = MCPServer(name="test")
|
|
613
|
+
assert server is not None
|
|
614
|
+
|
|
615
|
+
@pytest.mark.unit
|
|
616
|
+
def test_handles_timeout_error(self):
|
|
617
|
+
"""Test handling of timeout errors."""
|
|
618
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
619
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
620
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
621
|
+
server = MCPServer(name="test")
|
|
622
|
+
assert server is not None
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class TestMaskSecrets:
|
|
626
|
+
"""Tests for secret masking functionality."""
|
|
627
|
+
|
|
628
|
+
@pytest.mark.unit
|
|
629
|
+
def test_masks_base64_data(self):
|
|
630
|
+
"""Test that base64-encoded data is masked."""
|
|
631
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
632
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
633
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
634
|
+
server = MCPServer(name="test")
|
|
635
|
+
text = "data:\n password: c2VjcmV0UGFzc3dvcmQxMjM="
|
|
636
|
+
masked = server._mask_secrets(text)
|
|
637
|
+
assert "[MASKED]" in masked
|
|
638
|
+
|
|
639
|
+
@pytest.mark.unit
|
|
640
|
+
def test_masks_password_fields(self):
|
|
641
|
+
"""Test that password fields are masked."""
|
|
642
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
643
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
644
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
645
|
+
server = MCPServer(name="test")
|
|
646
|
+
text = 'password: "mysecretpassword"'
|
|
647
|
+
masked = server._mask_secrets(text)
|
|
648
|
+
assert "[MASKED]" in masked
|
|
649
|
+
|
|
650
|
+
@pytest.mark.unit
|
|
651
|
+
def test_masks_token_fields(self):
|
|
652
|
+
"""Test that token fields are masked."""
|
|
653
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
654
|
+
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
655
|
+
with patch("kubernetes.config.load_kube_config"):
|
|
656
|
+
server = MCPServer(name="test")
|
|
657
|
+
text = 'token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"'
|
|
658
|
+
masked = server._mask_secrets(text)
|
|
659
|
+
assert "[MASKED]" in masked
|