kubectl-mcp-server 1.18.0__py3-none-any.whl → 1.19.1__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.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/METADATA +41 -18
- {kubectl_mcp_server-1.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/RECORD +24 -23
- {kubectl_mcp_server-1.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/WHEEL +1 -1
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/k8s_config.py +233 -340
- kubectl_mcp_tool/mcp_server.py +30 -0
- kubectl_mcp_tool/tools/__init__.py +2 -1
- kubectl_mcp_tool/tools/backup.py +10 -47
- kubectl_mcp_tool/tools/capi.py +12 -56
- kubectl_mcp_tool/tools/certs.py +11 -29
- kubectl_mcp_tool/tools/cilium.py +10 -47
- kubectl_mcp_tool/tools/cluster.py +489 -9
- kubectl_mcp_tool/tools/gitops.py +12 -51
- kubectl_mcp_tool/tools/keda.py +9 -47
- kubectl_mcp_tool/tools/kiali.py +10 -50
- kubectl_mcp_tool/tools/kubevirt.py +11 -49
- kubectl_mcp_tool/tools/pods.py +93 -0
- kubectl_mcp_tool/tools/policy.py +11 -49
- kubectl_mcp_tool/tools/rollouts.py +11 -65
- kubectl_mcp_tool/tools/utils.py +41 -0
- tests/test_tools.py +44 -11
- {kubectl_mcp_server-1.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.18.0.dist-info → kubectl_mcp_server-1.19.1.dist-info}/top_level.txt +0 -0
kubectl_mcp_tool/tools/gitops.py
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
"""GitOps toolset for kubectl-mcp-server.
|
|
1
|
+
"""GitOps toolset for kubectl-mcp-server (Flux and Argo CD)."""
|
|
2
2
|
|
|
3
|
-
Provides tools for managing Flux and Argo CD applications.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import subprocess
|
|
7
3
|
import json
|
|
8
4
|
from typing import Dict, Any, List
|
|
9
|
-
from datetime import datetime
|
|
10
5
|
|
|
11
6
|
try:
|
|
12
7
|
from fastmcp import FastMCP
|
|
@@ -15,8 +10,8 @@ except ImportError:
|
|
|
15
10
|
from mcp.server.fastmcp import FastMCP
|
|
16
11
|
from mcp.types import ToolAnnotations
|
|
17
12
|
|
|
18
|
-
from ..k8s_config import _get_kubectl_context_args
|
|
19
13
|
from ..crd_detector import crd_exists, require_any_crd
|
|
14
|
+
from .utils import run_kubectl, get_resources
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
FLUX_KUSTOMIZATION_CRD = "kustomizations.kustomize.toolkit.fluxcd.io"
|
|
@@ -27,40 +22,6 @@ ARGOCD_APP_CRD = "applications.argoproj.io"
|
|
|
27
22
|
ARGOCD_APPSET_CRD = "applicationsets.argoproj.io"
|
|
28
23
|
|
|
29
24
|
|
|
30
|
-
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
31
|
-
"""Run kubectl command and return result."""
|
|
32
|
-
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
33
|
-
try:
|
|
34
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
35
|
-
if result.returncode == 0:
|
|
36
|
-
return {"success": True, "output": result.stdout}
|
|
37
|
-
return {"success": False, "error": result.stderr}
|
|
38
|
-
except subprocess.TimeoutExpired:
|
|
39
|
-
return {"success": False, "error": "Command timed out"}
|
|
40
|
-
except Exception as e:
|
|
41
|
-
return {"success": False, "error": str(e)}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
45
|
-
"""Get Kubernetes resources of a specific kind."""
|
|
46
|
-
args = ["get", kind, "-o", "json"]
|
|
47
|
-
if namespace:
|
|
48
|
-
args.extend(["-n", namespace])
|
|
49
|
-
else:
|
|
50
|
-
args.append("-A")
|
|
51
|
-
if label_selector:
|
|
52
|
-
args.extend(["-l", label_selector])
|
|
53
|
-
|
|
54
|
-
result = _run_kubectl(args, context)
|
|
55
|
-
if result["success"]:
|
|
56
|
-
try:
|
|
57
|
-
data = json.loads(result["output"])
|
|
58
|
-
return data.get("items", [])
|
|
59
|
-
except json.JSONDecodeError:
|
|
60
|
-
return []
|
|
61
|
-
return []
|
|
62
|
-
|
|
63
|
-
|
|
64
25
|
def gitops_apps_list(
|
|
65
26
|
namespace: str = "",
|
|
66
27
|
context: str = "",
|
|
@@ -82,7 +43,7 @@ def gitops_apps_list(
|
|
|
82
43
|
|
|
83
44
|
if not kind or kind.lower() == "kustomization":
|
|
84
45
|
if crd_exists(FLUX_KUSTOMIZATION_CRD, context):
|
|
85
|
-
for item in
|
|
46
|
+
for item in get_resources("kustomizations.kustomize.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
86
47
|
status = item.get("status", {})
|
|
87
48
|
conditions = status.get("conditions", [])
|
|
88
49
|
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
@@ -101,7 +62,7 @@ def gitops_apps_list(
|
|
|
101
62
|
|
|
102
63
|
if not kind or kind.lower() == "helmrelease":
|
|
103
64
|
if crd_exists(FLUX_HELMRELEASE_CRD, context):
|
|
104
|
-
for item in
|
|
65
|
+
for item in get_resources("helmreleases.helm.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
105
66
|
status = item.get("status", {})
|
|
106
67
|
conditions = status.get("conditions", [])
|
|
107
68
|
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
@@ -119,7 +80,7 @@ def gitops_apps_list(
|
|
|
119
80
|
|
|
120
81
|
if not kind or kind.lower() == "application":
|
|
121
82
|
if crd_exists(ARGOCD_APP_CRD, context):
|
|
122
|
-
for item in
|
|
83
|
+
for item in get_resources("applications.argoproj.io", namespace, context, label_selector):
|
|
123
84
|
status = item.get("status", {})
|
|
124
85
|
health = status.get("health", {})
|
|
125
86
|
sync = status.get("sync", {})
|
|
@@ -171,7 +132,7 @@ def gitops_app_get(
|
|
|
171
132
|
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
172
133
|
|
|
173
134
|
args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
|
|
174
|
-
result =
|
|
135
|
+
result = run_kubectl(args, context)
|
|
175
136
|
|
|
176
137
|
if result["success"]:
|
|
177
138
|
try:
|
|
@@ -218,7 +179,7 @@ def gitops_app_sync(
|
|
|
218
179
|
name, "-n", namespace,
|
|
219
180
|
f"{annotation}={timestamp}", "--overwrite"
|
|
220
181
|
]
|
|
221
|
-
result =
|
|
182
|
+
result = run_kubectl(args, context)
|
|
222
183
|
|
|
223
184
|
if result["success"]:
|
|
224
185
|
return {
|
|
@@ -241,7 +202,7 @@ def gitops_app_sync(
|
|
|
241
202
|
name, "-n", namespace,
|
|
242
203
|
f"{annotation}={timestamp}", "--overwrite"
|
|
243
204
|
]
|
|
244
|
-
result =
|
|
205
|
+
result = run_kubectl(args, context)
|
|
245
206
|
|
|
246
207
|
if result["success"]:
|
|
247
208
|
return {
|
|
@@ -262,7 +223,7 @@ def gitops_app_sync(
|
|
|
262
223
|
name, "-n", namespace,
|
|
263
224
|
f"{annotation}=hard", "--overwrite"
|
|
264
225
|
]
|
|
265
|
-
result =
|
|
226
|
+
result = run_kubectl(args, context)
|
|
266
227
|
|
|
267
228
|
if result["success"]:
|
|
268
229
|
return {
|
|
@@ -363,7 +324,7 @@ def gitops_sources_list(
|
|
|
363
324
|
|
|
364
325
|
if not kind or kind.lower() == "gitrepository":
|
|
365
326
|
if crd_exists(FLUX_GITREPO_CRD, context):
|
|
366
|
-
for item in
|
|
327
|
+
for item in get_resources("gitrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
367
328
|
status = item.get("status", {})
|
|
368
329
|
conditions = status.get("conditions", [])
|
|
369
330
|
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
@@ -382,7 +343,7 @@ def gitops_sources_list(
|
|
|
382
343
|
|
|
383
344
|
if not kind or kind.lower() == "helmrepository":
|
|
384
345
|
if crd_exists(FLUX_HELMREPO_CRD, context):
|
|
385
|
-
for item in
|
|
346
|
+
for item in get_resources("helmrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
386
347
|
status = item.get("status", {})
|
|
387
348
|
conditions = status.get("conditions", [])
|
|
388
349
|
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
@@ -432,7 +393,7 @@ def gitops_source_get(
|
|
|
432
393
|
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
433
394
|
|
|
434
395
|
args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
|
|
435
|
-
result =
|
|
396
|
+
result = run_kubectl(args, context)
|
|
436
397
|
|
|
437
398
|
if result["success"]:
|
|
438
399
|
try:
|
kubectl_mcp_tool/tools/keda.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
"""KEDA autoscaling toolset for kubectl-mcp-server.
|
|
1
|
+
"""KEDA autoscaling toolset for kubectl-mcp-server."""
|
|
2
2
|
|
|
3
|
-
Provides tools for managing KEDA ScaledObjects, ScaledJobs, and TriggerAuthentications.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import subprocess
|
|
7
3
|
import json
|
|
8
4
|
from typing import Dict, Any, List
|
|
9
5
|
|
|
@@ -14,8 +10,8 @@ except ImportError:
|
|
|
14
10
|
from mcp.server.fastmcp import FastMCP
|
|
15
11
|
from mcp.types import ToolAnnotations
|
|
16
12
|
|
|
17
|
-
from ..k8s_config import _get_kubectl_context_args
|
|
18
13
|
from ..crd_detector import crd_exists
|
|
14
|
+
from .utils import run_kubectl, get_resources
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
SCALEDOBJECT_CRD = "scaledobjects.keda.sh"
|
|
@@ -24,40 +20,6 @@ TRIGGERAUTH_CRD = "triggerauthentications.keda.sh"
|
|
|
24
20
|
CLUSTERTRIGGERAUTH_CRD = "clustertriggerauthentications.keda.sh"
|
|
25
21
|
|
|
26
22
|
|
|
27
|
-
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
28
|
-
"""Run kubectl command and return result."""
|
|
29
|
-
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
30
|
-
try:
|
|
31
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
32
|
-
if result.returncode == 0:
|
|
33
|
-
return {"success": True, "output": result.stdout}
|
|
34
|
-
return {"success": False, "error": result.stderr}
|
|
35
|
-
except subprocess.TimeoutExpired:
|
|
36
|
-
return {"success": False, "error": "Command timed out"}
|
|
37
|
-
except Exception as e:
|
|
38
|
-
return {"success": False, "error": str(e)}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
42
|
-
"""Get Kubernetes resources of a specific kind."""
|
|
43
|
-
args = ["get", kind, "-o", "json"]
|
|
44
|
-
if namespace:
|
|
45
|
-
args.extend(["-n", namespace])
|
|
46
|
-
else:
|
|
47
|
-
args.append("-A")
|
|
48
|
-
if label_selector:
|
|
49
|
-
args.extend(["-l", label_selector])
|
|
50
|
-
|
|
51
|
-
result = _run_kubectl(args, context)
|
|
52
|
-
if result["success"]:
|
|
53
|
-
try:
|
|
54
|
-
data = json.loads(result["output"])
|
|
55
|
-
return data.get("items", [])
|
|
56
|
-
except json.JSONDecodeError:
|
|
57
|
-
return []
|
|
58
|
-
return []
|
|
59
|
-
|
|
60
|
-
|
|
61
23
|
def keda_scaledobjects_list(
|
|
62
24
|
namespace: str = "",
|
|
63
25
|
context: str = "",
|
|
@@ -80,7 +42,7 @@ def keda_scaledobjects_list(
|
|
|
80
42
|
}
|
|
81
43
|
|
|
82
44
|
objects = []
|
|
83
|
-
for item in
|
|
45
|
+
for item in get_resources("scaledobjects.keda.sh", namespace, context, label_selector):
|
|
84
46
|
status = item.get("status", {})
|
|
85
47
|
spec = item.get("spec", {})
|
|
86
48
|
conditions = status.get("conditions", [])
|
|
@@ -138,7 +100,7 @@ def keda_scaledobject_get(
|
|
|
138
100
|
return {"success": False, "error": "KEDA is not installed"}
|
|
139
101
|
|
|
140
102
|
args = ["get", "scaledobjects.keda.sh", name, "-n", namespace, "-o", "json"]
|
|
141
|
-
result =
|
|
103
|
+
result = run_kubectl(args, context)
|
|
142
104
|
|
|
143
105
|
if result["success"]:
|
|
144
106
|
try:
|
|
@@ -176,7 +138,7 @@ def keda_scaledjobs_list(
|
|
|
176
138
|
}
|
|
177
139
|
|
|
178
140
|
jobs = []
|
|
179
|
-
for item in
|
|
141
|
+
for item in get_resources("scaledjobs.keda.sh", namespace, context, label_selector):
|
|
180
142
|
status = item.get("status", {})
|
|
181
143
|
spec = item.get("spec", {})
|
|
182
144
|
conditions = status.get("conditions", [])
|
|
@@ -227,7 +189,7 @@ def keda_triggerauths_list(
|
|
|
227
189
|
auths = []
|
|
228
190
|
|
|
229
191
|
if crd_exists(TRIGGERAUTH_CRD, context):
|
|
230
|
-
for item in
|
|
192
|
+
for item in get_resources("triggerauthentications.keda.sh", namespace, context):
|
|
231
193
|
spec = item.get("spec", {})
|
|
232
194
|
secret_refs = spec.get("secretTargetRef", [])
|
|
233
195
|
env_refs = spec.get("env", [])
|
|
@@ -244,7 +206,7 @@ def keda_triggerauths_list(
|
|
|
244
206
|
})
|
|
245
207
|
|
|
246
208
|
if include_cluster and crd_exists(CLUSTERTRIGGERAUTH_CRD, context):
|
|
247
|
-
for item in
|
|
209
|
+
for item in get_resources("clustertriggerauthentications.keda.sh", "", context):
|
|
248
210
|
spec = item.get("spec", {})
|
|
249
211
|
secret_refs = spec.get("secretTargetRef", [])
|
|
250
212
|
env_refs = spec.get("env", [])
|
|
@@ -294,7 +256,7 @@ def keda_triggerauth_get(
|
|
|
294
256
|
if not crd_exists(crd, context):
|
|
295
257
|
return {"success": False, "error": f"{crd} not found"}
|
|
296
258
|
|
|
297
|
-
result =
|
|
259
|
+
result = run_kubectl(args, context)
|
|
298
260
|
|
|
299
261
|
if result["success"]:
|
|
300
262
|
try:
|
|
@@ -337,7 +299,7 @@ def keda_hpa_list(
|
|
|
337
299
|
if selector:
|
|
338
300
|
args.extend(["-l", selector])
|
|
339
301
|
|
|
340
|
-
result =
|
|
302
|
+
result = run_kubectl(args, context)
|
|
341
303
|
if not result["success"]:
|
|
342
304
|
return {"success": False, "error": result.get("error", "Failed to list HPAs")}
|
|
343
305
|
|
kubectl_mcp_tool/tools/kiali.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
"""Kiali/Istio service mesh observability toolset for kubectl-mcp-server.
|
|
2
|
-
|
|
3
|
-
Provides tools for service mesh visualization and Istio configuration inspection.
|
|
4
|
-
"""
|
|
1
|
+
"""Kiali/Istio service mesh observability toolset for kubectl-mcp-server."""
|
|
5
2
|
|
|
6
3
|
import subprocess
|
|
7
4
|
import json
|
|
8
5
|
import os
|
|
9
|
-
from typing import Dict, Any, List
|
|
6
|
+
from typing import Dict, Any, List
|
|
10
7
|
|
|
11
8
|
try:
|
|
12
9
|
from fastmcp import FastMCP
|
|
@@ -15,11 +12,10 @@ except ImportError:
|
|
|
15
12
|
from mcp.server.fastmcp import FastMCP
|
|
16
13
|
from mcp.types import ToolAnnotations
|
|
17
14
|
|
|
18
|
-
from ..k8s_config import _get_kubectl_context_args
|
|
19
15
|
from ..crd_detector import crd_exists
|
|
16
|
+
from .utils import run_kubectl, get_resources
|
|
20
17
|
|
|
21
18
|
|
|
22
|
-
# Istio CRDs
|
|
23
19
|
VIRTUALSERVICE_CRD = "virtualservices.networking.istio.io"
|
|
24
20
|
DESTINATIONRULE_CRD = "destinationrules.networking.istio.io"
|
|
25
21
|
GATEWAY_CRD = "gateways.networking.istio.io"
|
|
@@ -30,40 +26,6 @@ AUTHORIZATIONPOLICY_CRD = "authorizationpolicies.security.istio.io"
|
|
|
30
26
|
REQUESTAUTHENTICATION_CRD = "requestauthentications.security.istio.io"
|
|
31
27
|
|
|
32
28
|
|
|
33
|
-
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
34
|
-
"""Run kubectl command and return result."""
|
|
35
|
-
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
36
|
-
try:
|
|
37
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
38
|
-
if result.returncode == 0:
|
|
39
|
-
return {"success": True, "output": result.stdout}
|
|
40
|
-
return {"success": False, "error": result.stderr}
|
|
41
|
-
except subprocess.TimeoutExpired:
|
|
42
|
-
return {"success": False, "error": "Command timed out"}
|
|
43
|
-
except Exception as e:
|
|
44
|
-
return {"success": False, "error": str(e)}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
48
|
-
"""Get Kubernetes resources of a specific kind."""
|
|
49
|
-
args = ["get", kind, "-o", "json"]
|
|
50
|
-
if namespace:
|
|
51
|
-
args.extend(["-n", namespace])
|
|
52
|
-
else:
|
|
53
|
-
args.append("-A")
|
|
54
|
-
if label_selector:
|
|
55
|
-
args.extend(["-l", label_selector])
|
|
56
|
-
|
|
57
|
-
result = _run_kubectl(args, context)
|
|
58
|
-
if result["success"]:
|
|
59
|
-
try:
|
|
60
|
-
data = json.loads(result["output"])
|
|
61
|
-
return data.get("items", [])
|
|
62
|
-
except json.JSONDecodeError:
|
|
63
|
-
return []
|
|
64
|
-
return []
|
|
65
|
-
|
|
66
|
-
|
|
67
29
|
def _istioctl_available() -> bool:
|
|
68
30
|
"""Check if istioctl CLI is available."""
|
|
69
31
|
try:
|
|
@@ -84,8 +46,6 @@ def _get_kiali_config() -> Dict[str, str]:
|
|
|
84
46
|
}
|
|
85
47
|
|
|
86
48
|
|
|
87
|
-
# ============== Istio Resource Functions ==============
|
|
88
|
-
|
|
89
49
|
def istio_virtualservices_list(
|
|
90
50
|
namespace: str = "",
|
|
91
51
|
context: str = "",
|
|
@@ -108,7 +68,7 @@ def istio_virtualservices_list(
|
|
|
108
68
|
}
|
|
109
69
|
|
|
110
70
|
virtualservices = []
|
|
111
|
-
for item in
|
|
71
|
+
for item in get_resources("virtualservices.networking.istio.io", namespace, context, label_selector):
|
|
112
72
|
spec = item.get("spec", {})
|
|
113
73
|
hosts = spec.get("hosts", [])
|
|
114
74
|
gateways = spec.get("gateways", [])
|
|
@@ -153,7 +113,7 @@ def istio_virtualservice_get(
|
|
|
153
113
|
return {"success": False, "error": "Istio is not installed"}
|
|
154
114
|
|
|
155
115
|
args = ["get", "virtualservices.networking.istio.io", name, "-n", namespace, "-o", "json"]
|
|
156
|
-
result =
|
|
116
|
+
result = run_kubectl(args, context)
|
|
157
117
|
|
|
158
118
|
if result["success"]:
|
|
159
119
|
try:
|
|
@@ -191,7 +151,7 @@ def istio_destinationrules_list(
|
|
|
191
151
|
}
|
|
192
152
|
|
|
193
153
|
rules = []
|
|
194
|
-
for item in
|
|
154
|
+
for item in get_resources("destinationrules.networking.istio.io", namespace, context, label_selector):
|
|
195
155
|
spec = item.get("spec", {})
|
|
196
156
|
traffic_policy = spec.get("trafficPolicy", {})
|
|
197
157
|
subsets = spec.get("subsets", [])
|
|
@@ -238,7 +198,7 @@ def istio_gateways_list(
|
|
|
238
198
|
}
|
|
239
199
|
|
|
240
200
|
gateways = []
|
|
241
|
-
for item in
|
|
201
|
+
for item in get_resources("gateways.networking.istio.io", namespace, context, label_selector):
|
|
242
202
|
spec = item.get("spec", {})
|
|
243
203
|
selector = spec.get("selector", {})
|
|
244
204
|
servers = spec.get("servers", [])
|
|
@@ -294,7 +254,7 @@ def istio_peerauthentications_list(
|
|
|
294
254
|
}
|
|
295
255
|
|
|
296
256
|
policies = []
|
|
297
|
-
for item in
|
|
257
|
+
for item in get_resources("peerauthentications.security.istio.io", namespace, context, label_selector):
|
|
298
258
|
spec = item.get("spec", {})
|
|
299
259
|
selector = spec.get("selector", {})
|
|
300
260
|
mtls = spec.get("mtls", {})
|
|
@@ -337,7 +297,7 @@ def istio_authorizationpolicies_list(
|
|
|
337
297
|
}
|
|
338
298
|
|
|
339
299
|
policies = []
|
|
340
|
-
for item in
|
|
300
|
+
for item in get_resources("authorizationpolicies.security.istio.io", namespace, context, label_selector):
|
|
341
301
|
spec = item.get("spec", {})
|
|
342
302
|
selector = spec.get("selector", {})
|
|
343
303
|
rules = spec.get("rules", [])
|
|
@@ -503,7 +463,7 @@ def istio_sidecar_status(
|
|
|
503
463
|
else:
|
|
504
464
|
args.append("-A")
|
|
505
465
|
|
|
506
|
-
result =
|
|
466
|
+
result = run_kubectl(args, context)
|
|
507
467
|
if not result["success"]:
|
|
508
468
|
return {"success": False, "error": result.get("error", "Failed to list pods")}
|
|
509
469
|
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
"""KubeVirt VM lifecycle toolset for kubectl-mcp-server.
|
|
2
|
-
|
|
3
|
-
Provides tools for managing virtual machines on Kubernetes via KubeVirt.
|
|
4
|
-
"""
|
|
1
|
+
"""KubeVirt VM lifecycle toolset for kubectl-mcp-server."""
|
|
5
2
|
|
|
6
3
|
import subprocess
|
|
7
4
|
import json
|
|
@@ -14,11 +11,10 @@ except ImportError:
|
|
|
14
11
|
from mcp.server.fastmcp import FastMCP
|
|
15
12
|
from mcp.types import ToolAnnotations
|
|
16
13
|
|
|
17
|
-
from ..k8s_config import _get_kubectl_context_args
|
|
18
14
|
from ..crd_detector import crd_exists
|
|
15
|
+
from .utils import run_kubectl, get_resources
|
|
19
16
|
|
|
20
17
|
|
|
21
|
-
# KubeVirt CRDs
|
|
22
18
|
VM_CRD = "virtualmachines.kubevirt.io"
|
|
23
19
|
VMI_CRD = "virtualmachineinstances.kubevirt.io"
|
|
24
20
|
VMIPRESET_CRD = "virtualmachineinstancepresets.kubevirt.io"
|
|
@@ -32,40 +28,6 @@ CLUSTERINSTANCETYPE_CRD = "virtualmachineclusterinstancetypes.instancetype.kubev
|
|
|
32
28
|
PREFERENCE_CRD = "virtualmachinepreferences.instancetype.kubevirt.io"
|
|
33
29
|
|
|
34
30
|
|
|
35
|
-
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
36
|
-
"""Run kubectl command and return result."""
|
|
37
|
-
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
38
|
-
try:
|
|
39
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
40
|
-
if result.returncode == 0:
|
|
41
|
-
return {"success": True, "output": result.stdout}
|
|
42
|
-
return {"success": False, "error": result.stderr}
|
|
43
|
-
except subprocess.TimeoutExpired:
|
|
44
|
-
return {"success": False, "error": "Command timed out"}
|
|
45
|
-
except Exception as e:
|
|
46
|
-
return {"success": False, "error": str(e)}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
50
|
-
"""Get Kubernetes resources of a specific kind."""
|
|
51
|
-
args = ["get", kind, "-o", "json"]
|
|
52
|
-
if namespace:
|
|
53
|
-
args.extend(["-n", namespace])
|
|
54
|
-
else:
|
|
55
|
-
args.append("-A")
|
|
56
|
-
if label_selector:
|
|
57
|
-
args.extend(["-l", label_selector])
|
|
58
|
-
|
|
59
|
-
result = _run_kubectl(args, context)
|
|
60
|
-
if result["success"]:
|
|
61
|
-
try:
|
|
62
|
-
data = json.loads(result["output"])
|
|
63
|
-
return data.get("items", [])
|
|
64
|
-
except json.JSONDecodeError:
|
|
65
|
-
return []
|
|
66
|
-
return []
|
|
67
|
-
|
|
68
|
-
|
|
69
31
|
def _virtctl_available() -> bool:
|
|
70
32
|
"""Check if virtctl CLI is available."""
|
|
71
33
|
try:
|
|
@@ -118,7 +80,7 @@ def kubevirt_vms_list(
|
|
|
118
80
|
}
|
|
119
81
|
|
|
120
82
|
vms = []
|
|
121
|
-
for item in
|
|
83
|
+
for item in get_resources("virtualmachines.kubevirt.io", namespace, context, label_selector):
|
|
122
84
|
status = item.get("status", {})
|
|
123
85
|
spec = item.get("spec", {})
|
|
124
86
|
conditions = status.get("conditions", [])
|
|
@@ -182,7 +144,7 @@ def kubevirt_vm_get(
|
|
|
182
144
|
return {"success": False, "error": "KubeVirt is not installed"}
|
|
183
145
|
|
|
184
146
|
args = ["get", "virtualmachines.kubevirt.io", name, "-n", namespace, "-o", "json"]
|
|
185
|
-
result =
|
|
147
|
+
result = run_kubectl(args, context)
|
|
186
148
|
|
|
187
149
|
if result["success"]:
|
|
188
150
|
try:
|
|
@@ -220,7 +182,7 @@ def kubevirt_vmis_list(
|
|
|
220
182
|
}
|
|
221
183
|
|
|
222
184
|
vmis = []
|
|
223
|
-
for item in
|
|
185
|
+
for item in get_resources("virtualmachineinstances.kubevirt.io", namespace, context, label_selector):
|
|
224
186
|
status = item.get("status", {})
|
|
225
187
|
spec = item.get("spec", {})
|
|
226
188
|
conditions = status.get("conditions", [])
|
|
@@ -300,7 +262,7 @@ def kubevirt_vm_start(
|
|
|
300
262
|
"--type=merge",
|
|
301
263
|
"-p", json.dumps(patch)
|
|
302
264
|
]
|
|
303
|
-
result =
|
|
265
|
+
result = run_kubectl(args, context)
|
|
304
266
|
|
|
305
267
|
if result["success"]:
|
|
306
268
|
return {
|
|
@@ -354,7 +316,7 @@ def kubevirt_vm_stop(
|
|
|
354
316
|
"--type=merge",
|
|
355
317
|
"-p", json.dumps(patch)
|
|
356
318
|
]
|
|
357
|
-
result =
|
|
319
|
+
result = run_kubectl(args, context)
|
|
358
320
|
|
|
359
321
|
if result["success"]:
|
|
360
322
|
return {
|
|
@@ -516,7 +478,7 @@ def kubevirt_datasources_list(
|
|
|
516
478
|
}
|
|
517
479
|
|
|
518
480
|
datasources = []
|
|
519
|
-
for item in
|
|
481
|
+
for item in get_resources("datasources.cdi.kubevirt.io", namespace, context, label_selector):
|
|
520
482
|
spec = item.get("spec", {})
|
|
521
483
|
status = item.get("status", {})
|
|
522
484
|
conditions = status.get("conditions", [])
|
|
@@ -557,7 +519,7 @@ def kubevirt_instancetypes_list(
|
|
|
557
519
|
instancetypes = []
|
|
558
520
|
|
|
559
521
|
if crd_exists(INSTANCETYPE_CRD, context):
|
|
560
|
-
for item in
|
|
522
|
+
for item in get_resources("virtualmachineinstancetypes.instancetype.kubevirt.io", namespace, context):
|
|
561
523
|
spec = item.get("spec", {})
|
|
562
524
|
cpu = spec.get("cpu", {})
|
|
563
525
|
memory = spec.get("memory", {})
|
|
@@ -573,7 +535,7 @@ def kubevirt_instancetypes_list(
|
|
|
573
535
|
})
|
|
574
536
|
|
|
575
537
|
if include_cluster and crd_exists(CLUSTERINSTANCETYPE_CRD, context):
|
|
576
|
-
for item in
|
|
538
|
+
for item in get_resources("virtualmachineclusterinstancetypes.instancetype.kubevirt.io", "", context):
|
|
577
539
|
spec = item.get("spec", {})
|
|
578
540
|
cpu = spec.get("cpu", {})
|
|
579
541
|
memory = spec.get("memory", {})
|
|
@@ -617,7 +579,7 @@ def kubevirt_datavolumes_list(
|
|
|
617
579
|
}
|
|
618
580
|
|
|
619
581
|
datavolumes = []
|
|
620
|
-
for item in
|
|
582
|
+
for item in get_resources("datavolumes.cdi.kubevirt.io", namespace, context, label_selector):
|
|
621
583
|
spec = item.get("spec", {})
|
|
622
584
|
status = item.get("status", {})
|
|
623
585
|
conditions = status.get("conditions", [])
|
kubectl_mcp_tool/tools/pods.py
CHANGED
|
@@ -287,6 +287,99 @@ def register_pod_tools(
|
|
|
287
287
|
logger.error(f"Error cleaning up pods: {e}")
|
|
288
288
|
return {"success": False, "error": str(e)}
|
|
289
289
|
|
|
290
|
+
@server.tool(
|
|
291
|
+
annotations=ToolAnnotations(
|
|
292
|
+
title="Run Pod",
|
|
293
|
+
destructiveHint=True,
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
def run_pod(
|
|
297
|
+
image: str,
|
|
298
|
+
name: Optional[str] = None,
|
|
299
|
+
namespace: str = "default",
|
|
300
|
+
command: Optional[List[str]] = None,
|
|
301
|
+
args: Optional[List[str]] = None,
|
|
302
|
+
env: Optional[Dict[str, str]] = None,
|
|
303
|
+
labels: Optional[Dict[str, str]] = None,
|
|
304
|
+
restart_policy: str = "Never",
|
|
305
|
+
context: str = ""
|
|
306
|
+
) -> Dict[str, Any]:
|
|
307
|
+
"""Run a container image as a pod (kubectl run equivalent).
|
|
308
|
+
|
|
309
|
+
Creates a pod with the specified container image. This is useful for
|
|
310
|
+
quick debugging, running one-off tasks, or testing container images.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
image: Container image to run (e.g., 'nginx:latest', 'busybox')
|
|
314
|
+
name: Name for the pod (auto-generated if not specified)
|
|
315
|
+
namespace: Namespace to create the pod in
|
|
316
|
+
command: Override the container's entrypoint (list of strings)
|
|
317
|
+
args: Arguments to pass to the container command (list of strings)
|
|
318
|
+
env: Environment variables as key-value pairs
|
|
319
|
+
labels: Labels to apply to the pod
|
|
320
|
+
restart_policy: Pod restart policy (Never, OnFailure, Always)
|
|
321
|
+
context: Kubernetes context to use (uses current context if not specified)
|
|
322
|
+
|
|
323
|
+
Examples:
|
|
324
|
+
- Run nginx: run_pod(image="nginx:latest")
|
|
325
|
+
- Run busybox with command: run_pod(image="busybox", command=["sh", "-c"], args=["echo hello"])
|
|
326
|
+
- Run with env vars: run_pod(image="alpine", env={"MY_VAR": "value"})
|
|
327
|
+
"""
|
|
328
|
+
if non_destructive:
|
|
329
|
+
return {"success": False, "error": "Blocked: non-destructive mode"}
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
from kubernetes import client as k8s_client
|
|
333
|
+
import uuid
|
|
334
|
+
|
|
335
|
+
if not name:
|
|
336
|
+
short_id = str(uuid.uuid4())[:8]
|
|
337
|
+
image_base = image.split("/")[-1].split(":")[0].replace(".", "-")
|
|
338
|
+
name = f"{image_base}-{short_id}"
|
|
339
|
+
|
|
340
|
+
container = k8s_client.V1Container(name=name, image=image, command=command, args=args)
|
|
341
|
+
if env:
|
|
342
|
+
container.env = [k8s_client.V1EnvVar(name=k, value=v) for k, v in env.items()]
|
|
343
|
+
|
|
344
|
+
pod_labels = {"app": name, "run": name}
|
|
345
|
+
if labels:
|
|
346
|
+
pod_labels.update(labels)
|
|
347
|
+
|
|
348
|
+
valid_policies = ["Never", "OnFailure", "Always"]
|
|
349
|
+
if restart_policy not in valid_policies:
|
|
350
|
+
return {"success": False, "error": f"Invalid restart_policy '{restart_policy}'. Must be one of: {valid_policies}"}
|
|
351
|
+
|
|
352
|
+
pod = k8s_client.V1Pod(
|
|
353
|
+
api_version="v1",
|
|
354
|
+
kind="Pod",
|
|
355
|
+
metadata=k8s_client.V1ObjectMeta(name=name, namespace=namespace, labels=pod_labels),
|
|
356
|
+
spec=k8s_client.V1PodSpec(containers=[container], restart_policy=restart_policy)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
v1 = get_k8s_client(context)
|
|
360
|
+
created = v1.create_namespaced_pod(namespace=namespace, body=pod)
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"context": context or "current",
|
|
365
|
+
"pod": {
|
|
366
|
+
"name": created.metadata.name,
|
|
367
|
+
"namespace": created.metadata.namespace,
|
|
368
|
+
"image": image,
|
|
369
|
+
"status": created.status.phase,
|
|
370
|
+
"uid": created.metadata.uid
|
|
371
|
+
},
|
|
372
|
+
"message": f"Pod '{name}' created successfully in namespace '{namespace}'",
|
|
373
|
+
"commands": {
|
|
374
|
+
"logs": f"kubectl logs {name} -n {namespace}",
|
|
375
|
+
"exec": f"kubectl exec -it {name} -n {namespace} -- /bin/sh",
|
|
376
|
+
"delete": f"kubectl delete pod {name} -n {namespace}"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error(f"Error running pod: {e}")
|
|
381
|
+
return {"success": False, "error": str(e)}
|
|
382
|
+
|
|
290
383
|
@server.tool(
|
|
291
384
|
annotations=ToolAnnotations(
|
|
292
385
|
title="Get Pod Conditions Detailed",
|