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
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
import shlex
|
|
4
|
+
import tempfile
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from mcp.types import ToolAnnotations
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("mcp-server")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_operations_tools(server, non_destructive: bool):
|
|
14
|
+
"""Register kubectl operations tools (apply, describe, patch, etc.)."""
|
|
15
|
+
|
|
16
|
+
def check_destructive():
|
|
17
|
+
"""Check if operation is blocked in non-destructive mode."""
|
|
18
|
+
if non_destructive:
|
|
19
|
+
return {"success": False, "error": "Operation blocked: non-destructive mode enabled"}
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
@server.tool(
|
|
23
|
+
annotations=ToolAnnotations(
|
|
24
|
+
title="Kubectl Apply",
|
|
25
|
+
destructiveHint=True,
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
def kubectl_apply(manifest: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
29
|
+
"""Apply a YAML manifest to the cluster."""
|
|
30
|
+
blocked = check_destructive()
|
|
31
|
+
if blocked:
|
|
32
|
+
return blocked
|
|
33
|
+
try:
|
|
34
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
35
|
+
f.write(manifest)
|
|
36
|
+
temp_path = f.name
|
|
37
|
+
|
|
38
|
+
cmd = ["kubectl", "apply", "-f", temp_path, "-n", namespace]
|
|
39
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
40
|
+
|
|
41
|
+
os.unlink(temp_path)
|
|
42
|
+
|
|
43
|
+
if result.returncode == 0:
|
|
44
|
+
return {"success": True, "output": result.stdout.strip()}
|
|
45
|
+
else:
|
|
46
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error applying manifest: {e}")
|
|
49
|
+
return {"success": False, "error": str(e)}
|
|
50
|
+
|
|
51
|
+
@server.tool(
|
|
52
|
+
annotations=ToolAnnotations(
|
|
53
|
+
title="Kubectl Describe",
|
|
54
|
+
readOnlyHint=True,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
def kubectl_describe(resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
58
|
+
"""Describe a Kubernetes resource in detail."""
|
|
59
|
+
try:
|
|
60
|
+
cmd = ["kubectl", "describe", resource_type, name, "-n", namespace]
|
|
61
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
62
|
+
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
return {"success": True, "description": result.stdout}
|
|
65
|
+
else:
|
|
66
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Error describing resource: {e}")
|
|
69
|
+
return {"success": False, "error": str(e)}
|
|
70
|
+
|
|
71
|
+
@server.tool(
|
|
72
|
+
annotations=ToolAnnotations(
|
|
73
|
+
title="Kubectl Generic",
|
|
74
|
+
readOnlyHint=True,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
def kubectl_generic(command: str) -> Dict[str, Any]:
|
|
78
|
+
"""Execute any kubectl command. Use with caution."""
|
|
79
|
+
try:
|
|
80
|
+
# Security: validate command starts with allowed operations
|
|
81
|
+
allowed_prefixes = [
|
|
82
|
+
"get", "describe", "logs", "top", "explain", "api-resources",
|
|
83
|
+
"config", "version", "cluster-info", "auth"
|
|
84
|
+
]
|
|
85
|
+
cmd_parts = shlex.split(command)
|
|
86
|
+
if not cmd_parts:
|
|
87
|
+
return {"success": False, "error": "Empty command"}
|
|
88
|
+
|
|
89
|
+
# Remove 'kubectl' prefix if present
|
|
90
|
+
if cmd_parts[0] == "kubectl":
|
|
91
|
+
cmd_parts = cmd_parts[1:]
|
|
92
|
+
|
|
93
|
+
if not cmd_parts or cmd_parts[0] not in allowed_prefixes:
|
|
94
|
+
return {
|
|
95
|
+
"success": False,
|
|
96
|
+
"error": f"Command not allowed. Allowed: {', '.join(allowed_prefixes)}"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
full_cmd = ["kubectl"] + cmd_parts
|
|
100
|
+
result = subprocess.run(full_cmd, capture_output=True, text=True, timeout=60)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"success": result.returncode == 0,
|
|
104
|
+
"output": result.stdout,
|
|
105
|
+
"error": result.stderr if result.returncode != 0 else None
|
|
106
|
+
}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error running kubectl command: {e}")
|
|
109
|
+
return {"success": False, "error": str(e)}
|
|
110
|
+
|
|
111
|
+
@server.tool(
|
|
112
|
+
annotations=ToolAnnotations(
|
|
113
|
+
title="Kubectl Patch",
|
|
114
|
+
destructiveHint=True,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
def kubectl_patch(resource_type: str, name: str, patch: str, patch_type: str = "strategic", namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
118
|
+
"""Patch a Kubernetes resource."""
|
|
119
|
+
blocked = check_destructive()
|
|
120
|
+
if blocked:
|
|
121
|
+
return blocked
|
|
122
|
+
try:
|
|
123
|
+
type_flag = {
|
|
124
|
+
"strategic": "strategic",
|
|
125
|
+
"merge": "merge",
|
|
126
|
+
"json": "json"
|
|
127
|
+
}.get(patch_type, "strategic")
|
|
128
|
+
|
|
129
|
+
cmd = [
|
|
130
|
+
"kubectl", "patch", resource_type, name,
|
|
131
|
+
"-n", namespace,
|
|
132
|
+
"--type", type_flag,
|
|
133
|
+
"-p", patch
|
|
134
|
+
]
|
|
135
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
136
|
+
|
|
137
|
+
if result.returncode == 0:
|
|
138
|
+
return {"success": True, "output": result.stdout.strip()}
|
|
139
|
+
else:
|
|
140
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Error patching resource: {e}")
|
|
143
|
+
return {"success": False, "error": str(e)}
|
|
144
|
+
|
|
145
|
+
@server.tool(
|
|
146
|
+
annotations=ToolAnnotations(
|
|
147
|
+
title="Kubectl Rollout",
|
|
148
|
+
destructiveHint=True,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
def kubectl_rollout(action: str, resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
152
|
+
"""Manage rollouts (restart, status, history, undo, pause, resume)."""
|
|
153
|
+
try:
|
|
154
|
+
allowed_actions = ["status", "history", "restart", "undo", "pause", "resume"]
|
|
155
|
+
if action not in allowed_actions:
|
|
156
|
+
return {"success": False, "error": f"Invalid action. Allowed: {', '.join(allowed_actions)}"}
|
|
157
|
+
|
|
158
|
+
# Destructive actions need check
|
|
159
|
+
if action in ["restart", "undo", "pause", "resume"]:
|
|
160
|
+
blocked = check_destructive()
|
|
161
|
+
if blocked:
|
|
162
|
+
return blocked
|
|
163
|
+
|
|
164
|
+
cmd = ["kubectl", "rollout", action, f"{resource_type}/{name}", "-n", namespace]
|
|
165
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
166
|
+
|
|
167
|
+
if result.returncode == 0:
|
|
168
|
+
return {"success": True, "output": result.stdout.strip()}
|
|
169
|
+
else:
|
|
170
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Error managing rollout: {e}")
|
|
173
|
+
return {"success": False, "error": str(e)}
|
|
174
|
+
|
|
175
|
+
@server.tool(
|
|
176
|
+
annotations=ToolAnnotations(
|
|
177
|
+
title="Kubectl Create",
|
|
178
|
+
destructiveHint=True,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
def kubectl_create(resource_type: str, name: str, namespace: Optional[str] = "default", image: Optional[str] = None) -> Dict[str, Any]:
|
|
182
|
+
"""Create a Kubernetes resource."""
|
|
183
|
+
blocked = check_destructive()
|
|
184
|
+
if blocked:
|
|
185
|
+
return blocked
|
|
186
|
+
try:
|
|
187
|
+
cmd = ["kubectl", "create", resource_type, name, "-n", namespace]
|
|
188
|
+
if image and resource_type in ["deployment", "pod"]:
|
|
189
|
+
cmd.extend(["--image", image])
|
|
190
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
191
|
+
if result.returncode == 0:
|
|
192
|
+
return {"success": True, "output": result.stdout.strip()}
|
|
193
|
+
else:
|
|
194
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Error creating resource: {e}")
|
|
197
|
+
return {"success": False, "error": str(e)}
|
|
198
|
+
|
|
199
|
+
@server.tool(
|
|
200
|
+
annotations=ToolAnnotations(
|
|
201
|
+
title="Delete Resource",
|
|
202
|
+
destructiveHint=True,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
def delete_resource(resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
206
|
+
"""Delete a Kubernetes resource."""
|
|
207
|
+
blocked = check_destructive()
|
|
208
|
+
if blocked:
|
|
209
|
+
return blocked
|
|
210
|
+
try:
|
|
211
|
+
cmd = ["kubectl", "delete", resource_type, name, "-n", namespace]
|
|
212
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
213
|
+
if result.returncode == 0:
|
|
214
|
+
return {"success": True, "message": f"Deleted {resource_type}/{name}"}
|
|
215
|
+
else:
|
|
216
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error deleting resource: {e}")
|
|
219
|
+
return {"success": False, "error": str(e)}
|
|
220
|
+
|
|
221
|
+
@server.tool(
|
|
222
|
+
annotations=ToolAnnotations(
|
|
223
|
+
title="Kubectl Copy",
|
|
224
|
+
destructiveHint=True,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
def kubectl_cp(source: str, destination: str, namespace: str = "default", container: Optional[str] = None) -> Dict[str, Any]:
|
|
228
|
+
"""Copy files between local filesystem and pods.
|
|
229
|
+
|
|
230
|
+
Use pod:path format for pod paths, e.g.:
|
|
231
|
+
- Local to pod: kubectl_cp("/tmp/file.txt", "mypod:/tmp/file.txt")
|
|
232
|
+
- Pod to local: kubectl_cp("mypod:/tmp/file.txt", "/tmp/file.txt")
|
|
233
|
+
"""
|
|
234
|
+
blocked = check_destructive()
|
|
235
|
+
if blocked:
|
|
236
|
+
return blocked
|
|
237
|
+
try:
|
|
238
|
+
cmd = ["kubectl", "cp", source, destination, "-n", namespace]
|
|
239
|
+
if container:
|
|
240
|
+
cmd.extend(["-c", container])
|
|
241
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
242
|
+
if result.returncode == 0:
|
|
243
|
+
return {"success": True, "message": f"Copied {source} to {destination}"}
|
|
244
|
+
else:
|
|
245
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Error copying files: {e}")
|
|
248
|
+
return {"success": False, "error": str(e)}
|
|
249
|
+
|
|
250
|
+
@server.tool(
|
|
251
|
+
annotations=ToolAnnotations(
|
|
252
|
+
title="Backup Resource as YAML",
|
|
253
|
+
readOnlyHint=True,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
def backup_resource(resource_type: str, name: str, namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
257
|
+
"""Export a resource as YAML for backup or migration."""
|
|
258
|
+
try:
|
|
259
|
+
cmd = ["kubectl", "get", resource_type, name, "-o", "yaml"]
|
|
260
|
+
if namespace:
|
|
261
|
+
cmd.extend(["-n", namespace])
|
|
262
|
+
|
|
263
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
264
|
+
|
|
265
|
+
if result.returncode != 0:
|
|
266
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"success": True,
|
|
270
|
+
"resource": {
|
|
271
|
+
"type": resource_type,
|
|
272
|
+
"name": name,
|
|
273
|
+
"namespace": namespace
|
|
274
|
+
},
|
|
275
|
+
"yaml": result.stdout,
|
|
276
|
+
"hint": "Save this YAML to a file and use 'kubectl apply -f' to restore"
|
|
277
|
+
}
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Error backing up resource: {e}")
|
|
280
|
+
return {"success": False, "error": str(e)}
|
|
281
|
+
|
|
282
|
+
@server.tool(
|
|
283
|
+
annotations=ToolAnnotations(
|
|
284
|
+
title="Label Resource",
|
|
285
|
+
destructiveHint=True,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
def label_resource(
|
|
289
|
+
resource_type: str,
|
|
290
|
+
name: str,
|
|
291
|
+
labels: Dict[str, str],
|
|
292
|
+
namespace: Optional[str] = None,
|
|
293
|
+
overwrite: bool = False
|
|
294
|
+
) -> Dict[str, Any]:
|
|
295
|
+
"""Add or update labels on a resource."""
|
|
296
|
+
blocked = check_destructive()
|
|
297
|
+
if blocked:
|
|
298
|
+
return blocked
|
|
299
|
+
try:
|
|
300
|
+
cmd = ["kubectl", "label", resource_type, name]
|
|
301
|
+
if namespace:
|
|
302
|
+
cmd.extend(["-n", namespace])
|
|
303
|
+
|
|
304
|
+
for key, value in labels.items():
|
|
305
|
+
if value is None:
|
|
306
|
+
cmd.append(f"{key}-") # Remove label
|
|
307
|
+
else:
|
|
308
|
+
cmd.append(f"{key}={value}")
|
|
309
|
+
|
|
310
|
+
if overwrite:
|
|
311
|
+
cmd.append("--overwrite")
|
|
312
|
+
|
|
313
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
314
|
+
|
|
315
|
+
if result.returncode != 0:
|
|
316
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"success": True,
|
|
320
|
+
"message": result.stdout.strip(),
|
|
321
|
+
"resource": {"type": resource_type, "name": name, "namespace": namespace},
|
|
322
|
+
"appliedLabels": labels
|
|
323
|
+
}
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Error labeling resource: {e}")
|
|
326
|
+
return {"success": False, "error": str(e)}
|
|
327
|
+
|
|
328
|
+
@server.tool(
|
|
329
|
+
annotations=ToolAnnotations(
|
|
330
|
+
title="Annotate Resource",
|
|
331
|
+
destructiveHint=True,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
def annotate_resource(
|
|
335
|
+
resource_type: str,
|
|
336
|
+
name: str,
|
|
337
|
+
annotations: Dict[str, str],
|
|
338
|
+
namespace: Optional[str] = None,
|
|
339
|
+
overwrite: bool = False
|
|
340
|
+
) -> Dict[str, Any]:
|
|
341
|
+
"""Add or update annotations on a resource."""
|
|
342
|
+
blocked = check_destructive()
|
|
343
|
+
if blocked:
|
|
344
|
+
return blocked
|
|
345
|
+
try:
|
|
346
|
+
cmd = ["kubectl", "annotate", resource_type, name]
|
|
347
|
+
if namespace:
|
|
348
|
+
cmd.extend(["-n", namespace])
|
|
349
|
+
|
|
350
|
+
for key, value in annotations.items():
|
|
351
|
+
if value is None:
|
|
352
|
+
cmd.append(f"{key}-") # Remove annotation
|
|
353
|
+
else:
|
|
354
|
+
cmd.append(f"{key}={value}")
|
|
355
|
+
|
|
356
|
+
if overwrite:
|
|
357
|
+
cmd.append("--overwrite")
|
|
358
|
+
|
|
359
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
360
|
+
|
|
361
|
+
if result.returncode != 0:
|
|
362
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"success": True,
|
|
366
|
+
"message": result.stdout.strip(),
|
|
367
|
+
"resource": {"type": resource_type, "name": name, "namespace": namespace},
|
|
368
|
+
"appliedAnnotations": annotations
|
|
369
|
+
}
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Error annotating resource: {e}")
|
|
372
|
+
return {"success": False, "error": str(e)}
|
|
373
|
+
|
|
374
|
+
@server.tool(
|
|
375
|
+
annotations=ToolAnnotations(
|
|
376
|
+
title="Taint Node",
|
|
377
|
+
destructiveHint=True,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
def taint_node(
|
|
381
|
+
node_name: str,
|
|
382
|
+
key: str,
|
|
383
|
+
value: Optional[str] = None,
|
|
384
|
+
effect: str = "NoSchedule",
|
|
385
|
+
remove: bool = False
|
|
386
|
+
) -> Dict[str, Any]:
|
|
387
|
+
"""Add or remove taints on a node."""
|
|
388
|
+
blocked = check_destructive()
|
|
389
|
+
if blocked:
|
|
390
|
+
return blocked
|
|
391
|
+
try:
|
|
392
|
+
if effect not in ["NoSchedule", "PreferNoSchedule", "NoExecute"]:
|
|
393
|
+
return {"success": False, "error": f"Invalid effect: {effect}. Must be NoSchedule, PreferNoSchedule, or NoExecute"}
|
|
394
|
+
|
|
395
|
+
cmd = ["kubectl", "taint", "nodes", node_name]
|
|
396
|
+
|
|
397
|
+
if remove:
|
|
398
|
+
taint_str = f"{key}:{effect}-"
|
|
399
|
+
else:
|
|
400
|
+
if value:
|
|
401
|
+
taint_str = f"{key}={value}:{effect}"
|
|
402
|
+
else:
|
|
403
|
+
taint_str = f"{key}:{effect}"
|
|
404
|
+
|
|
405
|
+
cmd.append(taint_str)
|
|
406
|
+
|
|
407
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
408
|
+
|
|
409
|
+
if result.returncode != 0:
|
|
410
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"success": True,
|
|
414
|
+
"message": result.stdout.strip(),
|
|
415
|
+
"node": node_name,
|
|
416
|
+
"action": "removed" if remove else "added",
|
|
417
|
+
"taint": {"key": key, "value": value, "effect": effect}
|
|
418
|
+
}
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error tainting node: {e}")
|
|
421
|
+
return {"success": False, "error": str(e)}
|
|
422
|
+
|
|
423
|
+
@server.tool(
|
|
424
|
+
annotations=ToolAnnotations(
|
|
425
|
+
title="Wait for Condition",
|
|
426
|
+
readOnlyHint=True,
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
def wait_for_condition(
|
|
430
|
+
resource_type: str,
|
|
431
|
+
name: str,
|
|
432
|
+
condition: str,
|
|
433
|
+
namespace: Optional[str] = None,
|
|
434
|
+
timeout: int = 60
|
|
435
|
+
) -> Dict[str, Any]:
|
|
436
|
+
"""Wait for a resource to reach a specific condition."""
|
|
437
|
+
try:
|
|
438
|
+
cmd = ["kubectl", "wait", f"{resource_type}/{name}", f"--for={condition}", f"--timeout={timeout}s"]
|
|
439
|
+
if namespace:
|
|
440
|
+
cmd.extend(["-n", namespace])
|
|
441
|
+
|
|
442
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 10)
|
|
443
|
+
|
|
444
|
+
if result.returncode != 0:
|
|
445
|
+
return {
|
|
446
|
+
"success": False,
|
|
447
|
+
"conditionMet": False,
|
|
448
|
+
"error": result.stderr.strip(),
|
|
449
|
+
"resource": {"type": resource_type, "name": name, "namespace": namespace},
|
|
450
|
+
"condition": condition
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
"success": True,
|
|
455
|
+
"conditionMet": True,
|
|
456
|
+
"message": result.stdout.strip(),
|
|
457
|
+
"resource": {"type": resource_type, "name": name, "namespace": namespace},
|
|
458
|
+
"condition": condition
|
|
459
|
+
}
|
|
460
|
+
except subprocess.TimeoutExpired:
|
|
461
|
+
return {
|
|
462
|
+
"success": False,
|
|
463
|
+
"conditionMet": False,
|
|
464
|
+
"error": f"Timeout waiting for condition '{condition}' after {timeout}s",
|
|
465
|
+
"resource": {"type": resource_type, "name": name, "namespace": namespace}
|
|
466
|
+
}
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Error waiting for condition: {e}")
|
|
469
|
+
return {"success": False, "error": str(e)}
|
|
470
|
+
|
|
471
|
+
@server.tool(
|
|
472
|
+
annotations=ToolAnnotations(
|
|
473
|
+
title="Node Management",
|
|
474
|
+
destructiveHint=True,
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
def node_management(action: str, node_name: str, force: bool = False) -> Dict[str, Any]:
|
|
478
|
+
"""Manage nodes: cordon, uncordon, or drain."""
|
|
479
|
+
blocked = check_destructive()
|
|
480
|
+
if blocked:
|
|
481
|
+
return blocked
|
|
482
|
+
try:
|
|
483
|
+
allowed_actions = ["cordon", "uncordon", "drain"]
|
|
484
|
+
if action not in allowed_actions:
|
|
485
|
+
return {"success": False, "error": f"Invalid action. Allowed: {', '.join(allowed_actions)}"}
|
|
486
|
+
|
|
487
|
+
cmd = ["kubectl", action, node_name]
|
|
488
|
+
if action == "drain":
|
|
489
|
+
cmd.extend(["--ignore-daemonsets", "--delete-emptydir-data"])
|
|
490
|
+
if force:
|
|
491
|
+
cmd.append("--force")
|
|
492
|
+
|
|
493
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
494
|
+
|
|
495
|
+
if result.returncode == 0:
|
|
496
|
+
return {"success": True, "output": result.stdout.strip()}
|
|
497
|
+
else:
|
|
498
|
+
return {"success": False, "error": result.stderr.strip()}
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Error managing node: {e}")
|
|
501
|
+
return {"success": False, "error": str(e)}
|