kubectl-mcp-server 1.14.0__py3-none-any.whl → 1.16.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.16.0.dist-info/METADATA +1047 -0
- kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +304 -63
- kubectl_mcp_tool/mcp_server.py +27 -0
- kubectl_mcp_tool/tools/__init__.py +20 -0
- kubectl_mcp_tool/tools/backup.py +881 -0
- kubectl_mcp_tool/tools/capi.py +727 -0
- kubectl_mcp_tool/tools/certs.py +709 -0
- kubectl_mcp_tool/tools/cilium.py +582 -0
- kubectl_mcp_tool/tools/cluster.py +395 -121
- kubectl_mcp_tool/tools/core.py +157 -60
- kubectl_mcp_tool/tools/cost.py +97 -41
- kubectl_mcp_tool/tools/deployments.py +173 -56
- kubectl_mcp_tool/tools/diagnostics.py +40 -13
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/helm.py +133 -46
- kubectl_mcp_tool/tools/keda.py +464 -0
- kubectl_mcp_tool/tools/kiali.py +652 -0
- kubectl_mcp_tool/tools/kubevirt.py +803 -0
- kubectl_mcp_tool/tools/networking.py +106 -32
- kubectl_mcp_tool/tools/operations.py +176 -50
- kubectl_mcp_tool/tools/pods.py +162 -50
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- kubectl_mcp_tool/tools/security.py +89 -36
- kubectl_mcp_tool/tools/storage.py +35 -16
- tests/test_browser.py +2 -2
- tests/test_ecosystem.py +331 -0
- tests/test_tools.py +73 -10
- kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
- kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"""Kiali/Istio service mesh observability toolset for kubectl-mcp-server.
|
|
2
|
+
|
|
3
|
+
Provides tools for service mesh visualization and Istio configuration inspection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
from fastmcp.tools import ToolAnnotations
|
|
14
|
+
except ImportError:
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
from mcp.types import ToolAnnotations
|
|
17
|
+
|
|
18
|
+
from ..k8s_config import _get_kubectl_context_args
|
|
19
|
+
from ..crd_detector import crd_exists
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Istio CRDs
|
|
23
|
+
VIRTUALSERVICE_CRD = "virtualservices.networking.istio.io"
|
|
24
|
+
DESTINATIONRULE_CRD = "destinationrules.networking.istio.io"
|
|
25
|
+
GATEWAY_CRD = "gateways.networking.istio.io"
|
|
26
|
+
SERVICEENTRY_CRD = "serviceentries.networking.istio.io"
|
|
27
|
+
SIDECAR_CRD = "sidecars.networking.istio.io"
|
|
28
|
+
PEERAUTHENTICATION_CRD = "peerauthentications.security.istio.io"
|
|
29
|
+
AUTHORIZATIONPOLICY_CRD = "authorizationpolicies.security.istio.io"
|
|
30
|
+
REQUESTAUTHENTICATION_CRD = "requestauthentications.security.istio.io"
|
|
31
|
+
|
|
32
|
+
|
|
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
|
+
def _istioctl_available() -> bool:
|
|
68
|
+
"""Check if istioctl CLI is available."""
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(["istioctl", "version", "--remote=false"],
|
|
71
|
+
capture_output=True, timeout=5)
|
|
72
|
+
return result.returncode == 0
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_kiali_config() -> Dict[str, str]:
|
|
78
|
+
"""Get Kiali connection configuration from environment."""
|
|
79
|
+
return {
|
|
80
|
+
"url": os.environ.get("KIALI_URL", ""),
|
|
81
|
+
"token": os.environ.get("KIALI_TOKEN", ""),
|
|
82
|
+
"username": os.environ.get("KIALI_USERNAME", ""),
|
|
83
|
+
"password": os.environ.get("KIALI_PASSWORD", ""),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ============== Istio Resource Functions ==============
|
|
88
|
+
|
|
89
|
+
def istio_virtualservices_list(
|
|
90
|
+
namespace: str = "",
|
|
91
|
+
context: str = "",
|
|
92
|
+
label_selector: str = ""
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""List Istio VirtualServices.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
98
|
+
context: Kubernetes context to use (optional)
|
|
99
|
+
label_selector: Label selector to filter
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of VirtualServices with their configuration
|
|
103
|
+
"""
|
|
104
|
+
if not crd_exists(VIRTUALSERVICE_CRD, context):
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": "Istio is not installed (virtualservices.networking.istio.io CRD not found)"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
virtualservices = []
|
|
111
|
+
for item in _get_resources("virtualservices.networking.istio.io", namespace, context, label_selector):
|
|
112
|
+
spec = item.get("spec", {})
|
|
113
|
+
hosts = spec.get("hosts", [])
|
|
114
|
+
gateways = spec.get("gateways", [])
|
|
115
|
+
http_routes = spec.get("http", [])
|
|
116
|
+
tcp_routes = spec.get("tcp", [])
|
|
117
|
+
tls_routes = spec.get("tls", [])
|
|
118
|
+
|
|
119
|
+
virtualservices.append({
|
|
120
|
+
"name": item["metadata"]["name"],
|
|
121
|
+
"namespace": item["metadata"]["namespace"],
|
|
122
|
+
"hosts": hosts,
|
|
123
|
+
"gateways": gateways,
|
|
124
|
+
"http_routes_count": len(http_routes),
|
|
125
|
+
"tcp_routes_count": len(tcp_routes),
|
|
126
|
+
"tls_routes_count": len(tls_routes),
|
|
127
|
+
"total_routes": len(http_routes) + len(tcp_routes) + len(tls_routes),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"context": context or "current",
|
|
132
|
+
"total": len(virtualservices),
|
|
133
|
+
"virtualservices": virtualservices,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def istio_virtualservice_get(
|
|
138
|
+
name: str,
|
|
139
|
+
namespace: str,
|
|
140
|
+
context: str = ""
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""Get detailed information about a VirtualService.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
name: Name of the VirtualService
|
|
146
|
+
namespace: Namespace of the VirtualService
|
|
147
|
+
context: Kubernetes context to use (optional)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Detailed VirtualService information
|
|
151
|
+
"""
|
|
152
|
+
if not crd_exists(VIRTUALSERVICE_CRD, context):
|
|
153
|
+
return {"success": False, "error": "Istio is not installed"}
|
|
154
|
+
|
|
155
|
+
args = ["get", "virtualservices.networking.istio.io", name, "-n", namespace, "-o", "json"]
|
|
156
|
+
result = _run_kubectl(args, context)
|
|
157
|
+
|
|
158
|
+
if result["success"]:
|
|
159
|
+
try:
|
|
160
|
+
data = json.loads(result["output"])
|
|
161
|
+
return {
|
|
162
|
+
"success": True,
|
|
163
|
+
"context": context or "current",
|
|
164
|
+
"virtualservice": data,
|
|
165
|
+
}
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
168
|
+
|
|
169
|
+
return {"success": False, "error": result.get("error", "Unknown error")}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def istio_destinationrules_list(
|
|
173
|
+
namespace: str = "",
|
|
174
|
+
context: str = "",
|
|
175
|
+
label_selector: str = ""
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""List Istio DestinationRules.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
181
|
+
context: Kubernetes context to use (optional)
|
|
182
|
+
label_selector: Label selector to filter
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of DestinationRules
|
|
186
|
+
"""
|
|
187
|
+
if not crd_exists(DESTINATIONRULE_CRD, context):
|
|
188
|
+
return {
|
|
189
|
+
"success": False,
|
|
190
|
+
"error": "Istio DestinationRules CRD not found"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
rules = []
|
|
194
|
+
for item in _get_resources("destinationrules.networking.istio.io", namespace, context, label_selector):
|
|
195
|
+
spec = item.get("spec", {})
|
|
196
|
+
traffic_policy = spec.get("trafficPolicy", {})
|
|
197
|
+
subsets = spec.get("subsets", [])
|
|
198
|
+
|
|
199
|
+
rules.append({
|
|
200
|
+
"name": item["metadata"]["name"],
|
|
201
|
+
"namespace": item["metadata"]["namespace"],
|
|
202
|
+
"host": spec.get("host", ""),
|
|
203
|
+
"subsets_count": len(subsets),
|
|
204
|
+
"subsets": [s.get("name") for s in subsets],
|
|
205
|
+
"has_traffic_policy": bool(traffic_policy),
|
|
206
|
+
"load_balancer": traffic_policy.get("loadBalancer", {}).get("simple"),
|
|
207
|
+
"connection_pool": bool(traffic_policy.get("connectionPool")),
|
|
208
|
+
"outlier_detection": bool(traffic_policy.get("outlierDetection")),
|
|
209
|
+
"tls_mode": traffic_policy.get("tls", {}).get("mode"),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"context": context or "current",
|
|
214
|
+
"total": len(rules),
|
|
215
|
+
"destinationrules": rules,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def istio_gateways_list(
|
|
220
|
+
namespace: str = "",
|
|
221
|
+
context: str = "",
|
|
222
|
+
label_selector: str = ""
|
|
223
|
+
) -> Dict[str, Any]:
|
|
224
|
+
"""List Istio Gateways.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
228
|
+
context: Kubernetes context to use (optional)
|
|
229
|
+
label_selector: Label selector to filter
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of Gateways
|
|
233
|
+
"""
|
|
234
|
+
if not crd_exists(GATEWAY_CRD, context):
|
|
235
|
+
return {
|
|
236
|
+
"success": False,
|
|
237
|
+
"error": "Istio Gateways CRD not found"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
gateways = []
|
|
241
|
+
for item in _get_resources("gateways.networking.istio.io", namespace, context, label_selector):
|
|
242
|
+
spec = item.get("spec", {})
|
|
243
|
+
selector = spec.get("selector", {})
|
|
244
|
+
servers = spec.get("servers", [])
|
|
245
|
+
|
|
246
|
+
# Extract hosts and ports from servers
|
|
247
|
+
all_hosts = []
|
|
248
|
+
all_ports = []
|
|
249
|
+
for server in servers:
|
|
250
|
+
all_hosts.extend(server.get("hosts", []))
|
|
251
|
+
port = server.get("port", {})
|
|
252
|
+
if port:
|
|
253
|
+
all_ports.append({
|
|
254
|
+
"number": port.get("number"),
|
|
255
|
+
"name": port.get("name"),
|
|
256
|
+
"protocol": port.get("protocol"),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
gateways.append({
|
|
260
|
+
"name": item["metadata"]["name"],
|
|
261
|
+
"namespace": item["metadata"]["namespace"],
|
|
262
|
+
"selector": selector,
|
|
263
|
+
"servers_count": len(servers),
|
|
264
|
+
"hosts": list(set(all_hosts)),
|
|
265
|
+
"ports": all_ports,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"context": context or "current",
|
|
270
|
+
"total": len(gateways),
|
|
271
|
+
"gateways": gateways,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def istio_peerauthentications_list(
|
|
276
|
+
namespace: str = "",
|
|
277
|
+
context: str = "",
|
|
278
|
+
label_selector: str = ""
|
|
279
|
+
) -> Dict[str, Any]:
|
|
280
|
+
"""List Istio PeerAuthentication policies.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
284
|
+
context: Kubernetes context to use (optional)
|
|
285
|
+
label_selector: Label selector to filter
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
List of PeerAuthentication policies
|
|
289
|
+
"""
|
|
290
|
+
if not crd_exists(PEERAUTHENTICATION_CRD, context):
|
|
291
|
+
return {
|
|
292
|
+
"success": False,
|
|
293
|
+
"error": "Istio PeerAuthentication CRD not found"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
policies = []
|
|
297
|
+
for item in _get_resources("peerauthentications.security.istio.io", namespace, context, label_selector):
|
|
298
|
+
spec = item.get("spec", {})
|
|
299
|
+
selector = spec.get("selector", {})
|
|
300
|
+
mtls = spec.get("mtls", {})
|
|
301
|
+
port_level_mtls = spec.get("portLevelMtls", {})
|
|
302
|
+
|
|
303
|
+
policies.append({
|
|
304
|
+
"name": item["metadata"]["name"],
|
|
305
|
+
"namespace": item["metadata"]["namespace"],
|
|
306
|
+
"selector": selector.get("matchLabels", {}),
|
|
307
|
+
"mtls_mode": mtls.get("mode", "UNSET"),
|
|
308
|
+
"port_level_mtls_count": len(port_level_mtls),
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"context": context or "current",
|
|
313
|
+
"total": len(policies),
|
|
314
|
+
"peerauthentications": policies,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def istio_authorizationpolicies_list(
|
|
319
|
+
namespace: str = "",
|
|
320
|
+
context: str = "",
|
|
321
|
+
label_selector: str = ""
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
"""List Istio AuthorizationPolicies.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
327
|
+
context: Kubernetes context to use (optional)
|
|
328
|
+
label_selector: Label selector to filter
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of AuthorizationPolicies
|
|
332
|
+
"""
|
|
333
|
+
if not crd_exists(AUTHORIZATIONPOLICY_CRD, context):
|
|
334
|
+
return {
|
|
335
|
+
"success": False,
|
|
336
|
+
"error": "Istio AuthorizationPolicy CRD not found"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
policies = []
|
|
340
|
+
for item in _get_resources("authorizationpolicies.security.istio.io", namespace, context, label_selector):
|
|
341
|
+
spec = item.get("spec", {})
|
|
342
|
+
selector = spec.get("selector", {})
|
|
343
|
+
rules = spec.get("rules", [])
|
|
344
|
+
action = spec.get("action", "ALLOW")
|
|
345
|
+
|
|
346
|
+
policies.append({
|
|
347
|
+
"name": item["metadata"]["name"],
|
|
348
|
+
"namespace": item["metadata"]["namespace"],
|
|
349
|
+
"selector": selector.get("matchLabels", {}),
|
|
350
|
+
"action": action,
|
|
351
|
+
"rules_count": len(rules),
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"context": context or "current",
|
|
356
|
+
"total": len(policies),
|
|
357
|
+
"authorizationpolicies": policies,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def istio_proxy_status(context: str = "") -> Dict[str, Any]:
|
|
362
|
+
"""Get Istio proxy (Envoy) synchronization status.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
context: Kubernetes context to use (optional)
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Proxy sync status for all workloads
|
|
369
|
+
"""
|
|
370
|
+
if not _istioctl_available():
|
|
371
|
+
return {
|
|
372
|
+
"success": False,
|
|
373
|
+
"error": "istioctl CLI not available"
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
cmd = ["istioctl", "proxy-status", "-o", "json"]
|
|
377
|
+
if context:
|
|
378
|
+
cmd.extend(["--context", context])
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
382
|
+
if result.returncode == 0:
|
|
383
|
+
try:
|
|
384
|
+
data = json.loads(result.stdout)
|
|
385
|
+
proxies = []
|
|
386
|
+
for proxy in data:
|
|
387
|
+
proxies.append({
|
|
388
|
+
"name": proxy.get("proxy", ""),
|
|
389
|
+
"cluster_id": proxy.get("cluster_id", ""),
|
|
390
|
+
"istiod": proxy.get("istiod", ""),
|
|
391
|
+
"cds": proxy.get("cluster_status", ""),
|
|
392
|
+
"lds": proxy.get("listener_status", ""),
|
|
393
|
+
"eds": proxy.get("endpoint_status", ""),
|
|
394
|
+
"rds": proxy.get("route_status", ""),
|
|
395
|
+
"ecds": proxy.get("extension_config_status", ""),
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
synced = sum(1 for p in proxies if all(
|
|
399
|
+
p.get(s) == "SYNCED" for s in ["cds", "lds", "eds", "rds"]
|
|
400
|
+
))
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
"success": True,
|
|
404
|
+
"context": context or "current",
|
|
405
|
+
"total": len(proxies),
|
|
406
|
+
"synced": synced,
|
|
407
|
+
"proxies": proxies,
|
|
408
|
+
}
|
|
409
|
+
except json.JSONDecodeError:
|
|
410
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
411
|
+
|
|
412
|
+
return {"success": False, "error": result.stderr}
|
|
413
|
+
except Exception as e:
|
|
414
|
+
return {"success": False, "error": str(e)}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def istio_analyze(
|
|
418
|
+
namespace: str = "",
|
|
419
|
+
context: str = ""
|
|
420
|
+
) -> Dict[str, Any]:
|
|
421
|
+
"""Analyze Istio configuration for potential issues.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
namespace: Namespace to analyze (empty for all)
|
|
425
|
+
context: Kubernetes context to use (optional)
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Analysis results with warnings and errors
|
|
429
|
+
"""
|
|
430
|
+
if not _istioctl_available():
|
|
431
|
+
return {
|
|
432
|
+
"success": False,
|
|
433
|
+
"error": "istioctl CLI not available"
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
cmd = ["istioctl", "analyze", "-o", "json"]
|
|
437
|
+
if namespace:
|
|
438
|
+
cmd.extend(["-n", namespace])
|
|
439
|
+
else:
|
|
440
|
+
cmd.append("-A")
|
|
441
|
+
if context:
|
|
442
|
+
cmd.extend(["--context", context])
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
446
|
+
# istioctl analyze returns non-zero if issues found, but still outputs valid JSON
|
|
447
|
+
try:
|
|
448
|
+
data = json.loads(result.stdout) if result.stdout else []
|
|
449
|
+
|
|
450
|
+
messages = []
|
|
451
|
+
for msg in data:
|
|
452
|
+
messages.append({
|
|
453
|
+
"code": msg.get("code", ""),
|
|
454
|
+
"level": msg.get("level", ""),
|
|
455
|
+
"message": msg.get("message", ""),
|
|
456
|
+
"origin": msg.get("origin", ""),
|
|
457
|
+
"documentation_url": msg.get("documentationUrl", ""),
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
errors = sum(1 for m in messages if m["level"] == "Error")
|
|
461
|
+
warnings = sum(1 for m in messages if m["level"] == "Warning")
|
|
462
|
+
info = sum(1 for m in messages if m["level"] == "Info")
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
"success": True,
|
|
466
|
+
"context": context or "current",
|
|
467
|
+
"namespace": namespace or "all",
|
|
468
|
+
"total_issues": len(messages),
|
|
469
|
+
"errors": errors,
|
|
470
|
+
"warnings": warnings,
|
|
471
|
+
"info": info,
|
|
472
|
+
"messages": messages,
|
|
473
|
+
}
|
|
474
|
+
except json.JSONDecodeError:
|
|
475
|
+
# If not JSON, return the text output
|
|
476
|
+
return {
|
|
477
|
+
"success": True,
|
|
478
|
+
"context": context or "current",
|
|
479
|
+
"namespace": namespace or "all",
|
|
480
|
+
"output": result.stdout,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
return {"success": False, "error": str(e)}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def istio_sidecar_status(
|
|
488
|
+
namespace: str = "",
|
|
489
|
+
context: str = ""
|
|
490
|
+
) -> Dict[str, Any]:
|
|
491
|
+
"""Get sidecar injection status for pods.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
namespace: Namespace to check (empty for all)
|
|
495
|
+
context: Kubernetes context to use (optional)
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Pods with their sidecar injection status
|
|
499
|
+
"""
|
|
500
|
+
args = ["get", "pods", "-o", "json"]
|
|
501
|
+
if namespace:
|
|
502
|
+
args.extend(["-n", namespace])
|
|
503
|
+
else:
|
|
504
|
+
args.append("-A")
|
|
505
|
+
|
|
506
|
+
result = _run_kubectl(args, context)
|
|
507
|
+
if not result["success"]:
|
|
508
|
+
return {"success": False, "error": result.get("error", "Failed to list pods")}
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
data = json.loads(result["output"])
|
|
512
|
+
pods = data.get("items", [])
|
|
513
|
+
except json.JSONDecodeError:
|
|
514
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
515
|
+
|
|
516
|
+
pod_status = []
|
|
517
|
+
for pod in pods:
|
|
518
|
+
containers = pod.get("spec", {}).get("containers", [])
|
|
519
|
+
container_names = [c.get("name") for c in containers]
|
|
520
|
+
|
|
521
|
+
has_sidecar = "istio-proxy" in container_names
|
|
522
|
+
annotations = pod.get("metadata", {}).get("annotations", {})
|
|
523
|
+
inject_status = annotations.get("sidecar.istio.io/status")
|
|
524
|
+
|
|
525
|
+
pod_status.append({
|
|
526
|
+
"name": pod["metadata"]["name"],
|
|
527
|
+
"namespace": pod["metadata"]["namespace"],
|
|
528
|
+
"has_sidecar": has_sidecar,
|
|
529
|
+
"inject_annotation": annotations.get("sidecar.istio.io/inject"),
|
|
530
|
+
"sidecar_status": "injected" if has_sidecar else "not_injected",
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
injected = sum(1 for p in pod_status if p["has_sidecar"])
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
"context": context or "current",
|
|
537
|
+
"total": len(pod_status),
|
|
538
|
+
"injected": injected,
|
|
539
|
+
"not_injected": len(pod_status) - injected,
|
|
540
|
+
"pods": pod_status,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def istio_detect(context: str = "") -> Dict[str, Any]:
|
|
545
|
+
"""Detect if Istio is installed and its components.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
context: Kubernetes context to use (optional)
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Detection results for Istio
|
|
552
|
+
"""
|
|
553
|
+
return {
|
|
554
|
+
"context": context or "current",
|
|
555
|
+
"installed": crd_exists(VIRTUALSERVICE_CRD, context),
|
|
556
|
+
"cli_available": _istioctl_available(),
|
|
557
|
+
"kiali_configured": bool(_get_kiali_config().get("url")),
|
|
558
|
+
"crds": {
|
|
559
|
+
"virtualservices": crd_exists(VIRTUALSERVICE_CRD, context),
|
|
560
|
+
"destinationrules": crd_exists(DESTINATIONRULE_CRD, context),
|
|
561
|
+
"gateways": crd_exists(GATEWAY_CRD, context),
|
|
562
|
+
"serviceentries": crd_exists(SERVICEENTRY_CRD, context),
|
|
563
|
+
"sidecars": crd_exists(SIDECAR_CRD, context),
|
|
564
|
+
"peerauthentications": crd_exists(PEERAUTHENTICATION_CRD, context),
|
|
565
|
+
"authorizationpolicies": crd_exists(AUTHORIZATIONPOLICY_CRD, context),
|
|
566
|
+
"requestauthentications": crd_exists(REQUESTAUTHENTICATION_CRD, context),
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def register_istio_tools(mcp: FastMCP, non_destructive: bool = False):
|
|
572
|
+
"""Register Istio/Kiali tools with the MCP server."""
|
|
573
|
+
|
|
574
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
575
|
+
def istio_virtualservices_list_tool(
|
|
576
|
+
namespace: str = "",
|
|
577
|
+
context: str = "",
|
|
578
|
+
label_selector: str = ""
|
|
579
|
+
) -> str:
|
|
580
|
+
"""List Istio VirtualServices."""
|
|
581
|
+
return json.dumps(istio_virtualservices_list(namespace, context, label_selector), indent=2)
|
|
582
|
+
|
|
583
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
584
|
+
def istio_virtualservice_get_tool(
|
|
585
|
+
name: str,
|
|
586
|
+
namespace: str,
|
|
587
|
+
context: str = ""
|
|
588
|
+
) -> str:
|
|
589
|
+
"""Get detailed information about a VirtualService."""
|
|
590
|
+
return json.dumps(istio_virtualservice_get(name, namespace, context), indent=2)
|
|
591
|
+
|
|
592
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
593
|
+
def istio_destinationrules_list_tool(
|
|
594
|
+
namespace: str = "",
|
|
595
|
+
context: str = "",
|
|
596
|
+
label_selector: str = ""
|
|
597
|
+
) -> str:
|
|
598
|
+
"""List Istio DestinationRules."""
|
|
599
|
+
return json.dumps(istio_destinationrules_list(namespace, context, label_selector), indent=2)
|
|
600
|
+
|
|
601
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
602
|
+
def istio_gateways_list_tool(
|
|
603
|
+
namespace: str = "",
|
|
604
|
+
context: str = "",
|
|
605
|
+
label_selector: str = ""
|
|
606
|
+
) -> str:
|
|
607
|
+
"""List Istio Gateways."""
|
|
608
|
+
return json.dumps(istio_gateways_list(namespace, context, label_selector), indent=2)
|
|
609
|
+
|
|
610
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
611
|
+
def istio_peerauthentications_list_tool(
|
|
612
|
+
namespace: str = "",
|
|
613
|
+
context: str = "",
|
|
614
|
+
label_selector: str = ""
|
|
615
|
+
) -> str:
|
|
616
|
+
"""List Istio PeerAuthentication policies."""
|
|
617
|
+
return json.dumps(istio_peerauthentications_list(namespace, context, label_selector), indent=2)
|
|
618
|
+
|
|
619
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
620
|
+
def istio_authorizationpolicies_list_tool(
|
|
621
|
+
namespace: str = "",
|
|
622
|
+
context: str = "",
|
|
623
|
+
label_selector: str = ""
|
|
624
|
+
) -> str:
|
|
625
|
+
"""List Istio AuthorizationPolicies."""
|
|
626
|
+
return json.dumps(istio_authorizationpolicies_list(namespace, context, label_selector), indent=2)
|
|
627
|
+
|
|
628
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
629
|
+
def istio_proxy_status_tool(context: str = "") -> str:
|
|
630
|
+
"""Get Istio proxy synchronization status."""
|
|
631
|
+
return json.dumps(istio_proxy_status(context), indent=2)
|
|
632
|
+
|
|
633
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
634
|
+
def istio_analyze_tool(
|
|
635
|
+
namespace: str = "",
|
|
636
|
+
context: str = ""
|
|
637
|
+
) -> str:
|
|
638
|
+
"""Analyze Istio configuration for potential issues."""
|
|
639
|
+
return json.dumps(istio_analyze(namespace, context), indent=2)
|
|
640
|
+
|
|
641
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
642
|
+
def istio_sidecar_status_tool(
|
|
643
|
+
namespace: str = "",
|
|
644
|
+
context: str = ""
|
|
645
|
+
) -> str:
|
|
646
|
+
"""Get sidecar injection status for pods."""
|
|
647
|
+
return json.dumps(istio_sidecar_status(namespace, context), indent=2)
|
|
648
|
+
|
|
649
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
650
|
+
def istio_detect_tool(context: str = "") -> str:
|
|
651
|
+
"""Detect if Istio is installed and its components."""
|
|
652
|
+
return json.dumps(istio_detect(context), indent=2)
|