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,552 @@
|
|
|
1
|
+
"""GitOps toolset for kubectl-mcp-server.
|
|
2
|
+
|
|
3
|
+
Provides tools for managing Flux and Argo CD applications.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, Any, List
|
|
9
|
+
from datetime import datetime
|
|
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, require_any_crd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
FLUX_KUSTOMIZATION_CRD = "kustomizations.kustomize.toolkit.fluxcd.io"
|
|
23
|
+
FLUX_HELMRELEASE_CRD = "helmreleases.helm.toolkit.fluxcd.io"
|
|
24
|
+
FLUX_GITREPO_CRD = "gitrepositories.source.toolkit.fluxcd.io"
|
|
25
|
+
FLUX_HELMREPO_CRD = "helmrepositories.source.toolkit.fluxcd.io"
|
|
26
|
+
ARGOCD_APP_CRD = "applications.argoproj.io"
|
|
27
|
+
ARGOCD_APPSET_CRD = "applicationsets.argoproj.io"
|
|
28
|
+
|
|
29
|
+
|
|
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
|
+
def gitops_apps_list(
|
|
65
|
+
namespace: str = "",
|
|
66
|
+
context: str = "",
|
|
67
|
+
kind: str = "",
|
|
68
|
+
label_selector: str = ""
|
|
69
|
+
) -> Dict[str, Any]:
|
|
70
|
+
"""List GitOps applications from Flux or Argo CD.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
74
|
+
context: Kubernetes context to use (optional, uses current context if not specified)
|
|
75
|
+
kind: Filter by kind (Kustomization, HelmRelease, Application)
|
|
76
|
+
label_selector: Label selector to filter resources
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of GitOps applications with their status
|
|
80
|
+
"""
|
|
81
|
+
apps = []
|
|
82
|
+
|
|
83
|
+
if not kind or kind.lower() == "kustomization":
|
|
84
|
+
if crd_exists(FLUX_KUSTOMIZATION_CRD, context):
|
|
85
|
+
for item in _get_resources("kustomizations.kustomize.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
86
|
+
status = item.get("status", {})
|
|
87
|
+
conditions = status.get("conditions", [])
|
|
88
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
89
|
+
apps.append({
|
|
90
|
+
"name": item["metadata"]["name"],
|
|
91
|
+
"namespace": item["metadata"]["namespace"],
|
|
92
|
+
"kind": "Kustomization",
|
|
93
|
+
"engine": "flux",
|
|
94
|
+
"ready": ready_cond.get("status") == "True",
|
|
95
|
+
"status": ready_cond.get("reason", "Unknown"),
|
|
96
|
+
"message": ready_cond.get("message", ""),
|
|
97
|
+
"source": item.get("spec", {}).get("sourceRef", {}),
|
|
98
|
+
"path": item.get("spec", {}).get("path", ""),
|
|
99
|
+
"last_applied": status.get("lastAppliedRevision", ""),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if not kind or kind.lower() == "helmrelease":
|
|
103
|
+
if crd_exists(FLUX_HELMRELEASE_CRD, context):
|
|
104
|
+
for item in _get_resources("helmreleases.helm.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
105
|
+
status = item.get("status", {})
|
|
106
|
+
conditions = status.get("conditions", [])
|
|
107
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
108
|
+
apps.append({
|
|
109
|
+
"name": item["metadata"]["name"],
|
|
110
|
+
"namespace": item["metadata"]["namespace"],
|
|
111
|
+
"kind": "HelmRelease",
|
|
112
|
+
"engine": "flux",
|
|
113
|
+
"ready": ready_cond.get("status") == "True",
|
|
114
|
+
"status": ready_cond.get("reason", "Unknown"),
|
|
115
|
+
"message": ready_cond.get("message", ""),
|
|
116
|
+
"chart": item.get("spec", {}).get("chart", {}),
|
|
117
|
+
"version": status.get("lastAppliedRevision", ""),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if not kind or kind.lower() == "application":
|
|
121
|
+
if crd_exists(ARGOCD_APP_CRD, context):
|
|
122
|
+
for item in _get_resources("applications.argoproj.io", namespace, context, label_selector):
|
|
123
|
+
status = item.get("status", {})
|
|
124
|
+
health = status.get("health", {})
|
|
125
|
+
sync = status.get("sync", {})
|
|
126
|
+
apps.append({
|
|
127
|
+
"name": item["metadata"]["name"],
|
|
128
|
+
"namespace": item["metadata"]["namespace"],
|
|
129
|
+
"kind": "Application",
|
|
130
|
+
"engine": "argocd",
|
|
131
|
+
"ready": health.get("status") == "Healthy" and sync.get("status") == "Synced",
|
|
132
|
+
"health": health.get("status", "Unknown"),
|
|
133
|
+
"sync_status": sync.get("status", "Unknown"),
|
|
134
|
+
"message": health.get("message", ""),
|
|
135
|
+
"source": item.get("spec", {}).get("source", {}),
|
|
136
|
+
"destination": item.get("spec", {}).get("destination", {}),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"context": context or "current",
|
|
141
|
+
"total": len(apps),
|
|
142
|
+
"applications": apps,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def gitops_app_get(
|
|
147
|
+
name: str,
|
|
148
|
+
namespace: str,
|
|
149
|
+
kind: str,
|
|
150
|
+
context: str = ""
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Get details of a specific GitOps application.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: Name of the application
|
|
156
|
+
namespace: Namespace of the application
|
|
157
|
+
kind: Kind of application (Kustomization, HelmRelease, Application)
|
|
158
|
+
context: Kubernetes context to use (optional)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Detailed application information
|
|
162
|
+
"""
|
|
163
|
+
kind_map = {
|
|
164
|
+
"kustomization": "kustomizations.kustomize.toolkit.fluxcd.io",
|
|
165
|
+
"helmrelease": "helmreleases.helm.toolkit.fluxcd.io",
|
|
166
|
+
"application": "applications.argoproj.io",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
k8s_kind = kind_map.get(kind.lower())
|
|
170
|
+
if not k8s_kind:
|
|
171
|
+
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
172
|
+
|
|
173
|
+
args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
|
|
174
|
+
result = _run_kubectl(args, context)
|
|
175
|
+
|
|
176
|
+
if result["success"]:
|
|
177
|
+
try:
|
|
178
|
+
data = json.loads(result["output"])
|
|
179
|
+
return {
|
|
180
|
+
"success": True,
|
|
181
|
+
"context": context or "current",
|
|
182
|
+
"application": data,
|
|
183
|
+
}
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
186
|
+
|
|
187
|
+
return {"success": False, "error": result.get("error", "Unknown error")}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def gitops_app_sync(
|
|
191
|
+
name: str,
|
|
192
|
+
namespace: str,
|
|
193
|
+
kind: str,
|
|
194
|
+
context: str = ""
|
|
195
|
+
) -> Dict[str, Any]:
|
|
196
|
+
"""Trigger sync/reconciliation for a GitOps application.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
name: Name of the application
|
|
200
|
+
namespace: Namespace of the application
|
|
201
|
+
kind: Kind of application (Kustomization, HelmRelease for Flux)
|
|
202
|
+
context: Kubernetes context to use (optional)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Sync trigger result
|
|
206
|
+
"""
|
|
207
|
+
kind_lower = kind.lower()
|
|
208
|
+
|
|
209
|
+
if kind_lower == "kustomization":
|
|
210
|
+
if not crd_exists(FLUX_KUSTOMIZATION_CRD, context):
|
|
211
|
+
return {"success": False, "error": "Flux Kustomization CRD not installed"}
|
|
212
|
+
|
|
213
|
+
annotation = "reconcile.fluxcd.io/requestedAt"
|
|
214
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
215
|
+
|
|
216
|
+
args = [
|
|
217
|
+
"annotate", "kustomizations.kustomize.toolkit.fluxcd.io",
|
|
218
|
+
name, "-n", namespace,
|
|
219
|
+
f"{annotation}={timestamp}", "--overwrite"
|
|
220
|
+
]
|
|
221
|
+
result = _run_kubectl(args, context)
|
|
222
|
+
|
|
223
|
+
if result["success"]:
|
|
224
|
+
return {
|
|
225
|
+
"success": True,
|
|
226
|
+
"context": context or "current",
|
|
227
|
+
"message": f"Triggered reconciliation for Kustomization {name}",
|
|
228
|
+
"annotation": f"{annotation}={timestamp}",
|
|
229
|
+
}
|
|
230
|
+
return {"success": False, "error": result.get("error", "Failed to trigger sync")}
|
|
231
|
+
|
|
232
|
+
elif kind_lower == "helmrelease":
|
|
233
|
+
if not crd_exists(FLUX_HELMRELEASE_CRD, context):
|
|
234
|
+
return {"success": False, "error": "Flux HelmRelease CRD not installed"}
|
|
235
|
+
|
|
236
|
+
annotation = "reconcile.fluxcd.io/requestedAt"
|
|
237
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
238
|
+
|
|
239
|
+
args = [
|
|
240
|
+
"annotate", "helmreleases.helm.toolkit.fluxcd.io",
|
|
241
|
+
name, "-n", namespace,
|
|
242
|
+
f"{annotation}={timestamp}", "--overwrite"
|
|
243
|
+
]
|
|
244
|
+
result = _run_kubectl(args, context)
|
|
245
|
+
|
|
246
|
+
if result["success"]:
|
|
247
|
+
return {
|
|
248
|
+
"success": True,
|
|
249
|
+
"context": context or "current",
|
|
250
|
+
"message": f"Triggered reconciliation for HelmRelease {name}",
|
|
251
|
+
"annotation": f"{annotation}={timestamp}",
|
|
252
|
+
}
|
|
253
|
+
return {"success": False, "error": result.get("error", "Failed to trigger sync")}
|
|
254
|
+
|
|
255
|
+
elif kind_lower == "application":
|
|
256
|
+
if not crd_exists(ARGOCD_APP_CRD, context):
|
|
257
|
+
return {"success": False, "error": "ArgoCD Application CRD not installed"}
|
|
258
|
+
|
|
259
|
+
annotation = "argocd.argoproj.io/refresh"
|
|
260
|
+
args = [
|
|
261
|
+
"annotate", "applications.argoproj.io",
|
|
262
|
+
name, "-n", namespace,
|
|
263
|
+
f"{annotation}=hard", "--overwrite"
|
|
264
|
+
]
|
|
265
|
+
result = _run_kubectl(args, context)
|
|
266
|
+
|
|
267
|
+
if result["success"]:
|
|
268
|
+
return {
|
|
269
|
+
"success": True,
|
|
270
|
+
"context": context or "current",
|
|
271
|
+
"message": f"Triggered hard refresh for ArgoCD Application {name}",
|
|
272
|
+
}
|
|
273
|
+
return {"success": False, "error": result.get("error", "Failed to trigger sync")}
|
|
274
|
+
|
|
275
|
+
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def gitops_app_status(
|
|
279
|
+
name: str,
|
|
280
|
+
namespace: str,
|
|
281
|
+
kind: str,
|
|
282
|
+
context: str = ""
|
|
283
|
+
) -> Dict[str, Any]:
|
|
284
|
+
"""Get sync status of a GitOps application.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
name: Name of the application
|
|
288
|
+
namespace: Namespace of the application
|
|
289
|
+
kind: Kind of application (Kustomization, HelmRelease, Application)
|
|
290
|
+
context: Kubernetes context to use (optional)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Application status information
|
|
294
|
+
"""
|
|
295
|
+
result = gitops_app_get(name, namespace, kind, context)
|
|
296
|
+
if not result.get("success"):
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
app = result["application"]
|
|
300
|
+
status = app.get("status", {})
|
|
301
|
+
kind_lower = kind.lower()
|
|
302
|
+
|
|
303
|
+
if kind_lower in ("kustomization", "helmrelease"):
|
|
304
|
+
conditions = status.get("conditions", [])
|
|
305
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
306
|
+
reconciling_cond = next((c for c in conditions if c.get("type") == "Reconciling"), {})
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"success": True,
|
|
310
|
+
"context": context or "current",
|
|
311
|
+
"name": name,
|
|
312
|
+
"namespace": namespace,
|
|
313
|
+
"kind": kind,
|
|
314
|
+
"ready": ready_cond.get("status") == "True",
|
|
315
|
+
"reason": ready_cond.get("reason", "Unknown"),
|
|
316
|
+
"message": ready_cond.get("message", ""),
|
|
317
|
+
"reconciling": reconciling_cond.get("status") == "True",
|
|
318
|
+
"last_applied_revision": status.get("lastAppliedRevision", ""),
|
|
319
|
+
"last_attempted_revision": status.get("lastAttemptedRevision", ""),
|
|
320
|
+
"observed_generation": status.get("observedGeneration"),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
elif kind_lower == "application":
|
|
324
|
+
health = status.get("health", {})
|
|
325
|
+
sync = status.get("sync", {})
|
|
326
|
+
operation = status.get("operationState", {})
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
"success": True,
|
|
330
|
+
"context": context or "current",
|
|
331
|
+
"name": name,
|
|
332
|
+
"namespace": namespace,
|
|
333
|
+
"kind": kind,
|
|
334
|
+
"health_status": health.get("status", "Unknown"),
|
|
335
|
+
"health_message": health.get("message", ""),
|
|
336
|
+
"sync_status": sync.get("status", "Unknown"),
|
|
337
|
+
"sync_revision": sync.get("revision", ""),
|
|
338
|
+
"operation_phase": operation.get("phase", ""),
|
|
339
|
+
"operation_message": operation.get("message", ""),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def gitops_sources_list(
|
|
346
|
+
namespace: str = "",
|
|
347
|
+
context: str = "",
|
|
348
|
+
kind: str = "",
|
|
349
|
+
label_selector: str = ""
|
|
350
|
+
) -> Dict[str, Any]:
|
|
351
|
+
"""List Flux source resources (GitRepositories, HelmRepositories).
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
355
|
+
context: Kubernetes context to use (optional)
|
|
356
|
+
kind: Filter by kind (GitRepository, HelmRepository)
|
|
357
|
+
label_selector: Label selector to filter resources
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of source resources
|
|
361
|
+
"""
|
|
362
|
+
sources = []
|
|
363
|
+
|
|
364
|
+
if not kind or kind.lower() == "gitrepository":
|
|
365
|
+
if crd_exists(FLUX_GITREPO_CRD, context):
|
|
366
|
+
for item in _get_resources("gitrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
367
|
+
status = item.get("status", {})
|
|
368
|
+
conditions = status.get("conditions", [])
|
|
369
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
370
|
+
artifact = status.get("artifact", {})
|
|
371
|
+
sources.append({
|
|
372
|
+
"name": item["metadata"]["name"],
|
|
373
|
+
"namespace": item["metadata"]["namespace"],
|
|
374
|
+
"kind": "GitRepository",
|
|
375
|
+
"ready": ready_cond.get("status") == "True",
|
|
376
|
+
"status": ready_cond.get("reason", "Unknown"),
|
|
377
|
+
"url": item.get("spec", {}).get("url", ""),
|
|
378
|
+
"branch": item.get("spec", {}).get("ref", {}).get("branch", ""),
|
|
379
|
+
"revision": artifact.get("revision", ""),
|
|
380
|
+
"last_update": artifact.get("lastUpdateTime", ""),
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
if not kind or kind.lower() == "helmrepository":
|
|
384
|
+
if crd_exists(FLUX_HELMREPO_CRD, context):
|
|
385
|
+
for item in _get_resources("helmrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
|
|
386
|
+
status = item.get("status", {})
|
|
387
|
+
conditions = status.get("conditions", [])
|
|
388
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
389
|
+
artifact = status.get("artifact", {})
|
|
390
|
+
sources.append({
|
|
391
|
+
"name": item["metadata"]["name"],
|
|
392
|
+
"namespace": item["metadata"]["namespace"],
|
|
393
|
+
"kind": "HelmRepository",
|
|
394
|
+
"ready": ready_cond.get("status") == "True",
|
|
395
|
+
"status": ready_cond.get("reason", "Unknown"),
|
|
396
|
+
"url": item.get("spec", {}).get("url", ""),
|
|
397
|
+
"revision": artifact.get("revision", ""),
|
|
398
|
+
"last_update": artifact.get("lastUpdateTime", ""),
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
"context": context or "current",
|
|
403
|
+
"total": len(sources),
|
|
404
|
+
"sources": sources,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def gitops_source_get(
|
|
409
|
+
name: str,
|
|
410
|
+
namespace: str,
|
|
411
|
+
kind: str,
|
|
412
|
+
context: str = ""
|
|
413
|
+
) -> Dict[str, Any]:
|
|
414
|
+
"""Get details of a Flux source resource.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
name: Name of the source
|
|
418
|
+
namespace: Namespace of the source
|
|
419
|
+
kind: Kind of source (GitRepository, HelmRepository)
|
|
420
|
+
context: Kubernetes context to use (optional)
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Detailed source information
|
|
424
|
+
"""
|
|
425
|
+
kind_map = {
|
|
426
|
+
"gitrepository": "gitrepositories.source.toolkit.fluxcd.io",
|
|
427
|
+
"helmrepository": "helmrepositories.source.toolkit.fluxcd.io",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
k8s_kind = kind_map.get(kind.lower())
|
|
431
|
+
if not k8s_kind:
|
|
432
|
+
return {"success": False, "error": f"Unknown kind: {kind}"}
|
|
433
|
+
|
|
434
|
+
args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
|
|
435
|
+
result = _run_kubectl(args, context)
|
|
436
|
+
|
|
437
|
+
if result["success"]:
|
|
438
|
+
try:
|
|
439
|
+
data = json.loads(result["output"])
|
|
440
|
+
return {
|
|
441
|
+
"success": True,
|
|
442
|
+
"context": context or "current",
|
|
443
|
+
"source": data,
|
|
444
|
+
}
|
|
445
|
+
except json.JSONDecodeError:
|
|
446
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
447
|
+
|
|
448
|
+
return {"success": False, "error": result.get("error", "Unknown error")}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def gitops_detect_engine(context: str = "") -> Dict[str, Any]:
|
|
452
|
+
"""Detect which GitOps engines are installed in the cluster.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
context: Kubernetes context to use (optional)
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Detection results for Flux and ArgoCD
|
|
459
|
+
"""
|
|
460
|
+
flux_installed = any([
|
|
461
|
+
crd_exists(FLUX_KUSTOMIZATION_CRD, context),
|
|
462
|
+
crd_exists(FLUX_HELMRELEASE_CRD, context),
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
argocd_installed = crd_exists(ARGOCD_APP_CRD, context)
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
"context": context or "current",
|
|
469
|
+
"flux": {
|
|
470
|
+
"installed": flux_installed,
|
|
471
|
+
"kustomizations": crd_exists(FLUX_KUSTOMIZATION_CRD, context),
|
|
472
|
+
"helmreleases": crd_exists(FLUX_HELMRELEASE_CRD, context),
|
|
473
|
+
"gitrepositories": crd_exists(FLUX_GITREPO_CRD, context),
|
|
474
|
+
"helmrepositories": crd_exists(FLUX_HELMREPO_CRD, context),
|
|
475
|
+
},
|
|
476
|
+
"argocd": {
|
|
477
|
+
"installed": argocd_installed,
|
|
478
|
+
"applications": crd_exists(ARGOCD_APP_CRD, context),
|
|
479
|
+
"applicationsets": crd_exists(ARGOCD_APPSET_CRD, context),
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def register_gitops_tools(mcp: FastMCP, non_destructive: bool = False):
|
|
485
|
+
"""Register GitOps tools with the MCP server."""
|
|
486
|
+
|
|
487
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
488
|
+
def gitops_apps_list_tool(
|
|
489
|
+
namespace: str = "",
|
|
490
|
+
context: str = "",
|
|
491
|
+
kind: str = "",
|
|
492
|
+
label_selector: str = ""
|
|
493
|
+
) -> str:
|
|
494
|
+
"""List GitOps applications from Flux or Argo CD."""
|
|
495
|
+
return json.dumps(gitops_apps_list(namespace, context, kind, label_selector), indent=2)
|
|
496
|
+
|
|
497
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
498
|
+
def gitops_app_get_tool(
|
|
499
|
+
name: str,
|
|
500
|
+
namespace: str,
|
|
501
|
+
kind: str,
|
|
502
|
+
context: str = ""
|
|
503
|
+
) -> str:
|
|
504
|
+
"""Get details of a specific GitOps application."""
|
|
505
|
+
return json.dumps(gitops_app_get(name, namespace, kind, context), indent=2)
|
|
506
|
+
|
|
507
|
+
@mcp.tool()
|
|
508
|
+
def gitops_app_sync_tool(
|
|
509
|
+
name: str,
|
|
510
|
+
namespace: str,
|
|
511
|
+
kind: str,
|
|
512
|
+
context: str = ""
|
|
513
|
+
) -> str:
|
|
514
|
+
"""Trigger sync/reconciliation for a GitOps application."""
|
|
515
|
+
if non_destructive:
|
|
516
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
517
|
+
return json.dumps(gitops_app_sync(name, namespace, kind, context), indent=2)
|
|
518
|
+
|
|
519
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
520
|
+
def gitops_app_status_tool(
|
|
521
|
+
name: str,
|
|
522
|
+
namespace: str,
|
|
523
|
+
kind: str,
|
|
524
|
+
context: str = ""
|
|
525
|
+
) -> str:
|
|
526
|
+
"""Get sync status of a GitOps application."""
|
|
527
|
+
return json.dumps(gitops_app_status(name, namespace, kind, context), indent=2)
|
|
528
|
+
|
|
529
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
530
|
+
def gitops_sources_list_tool(
|
|
531
|
+
namespace: str = "",
|
|
532
|
+
context: str = "",
|
|
533
|
+
kind: str = "",
|
|
534
|
+
label_selector: str = ""
|
|
535
|
+
) -> str:
|
|
536
|
+
"""List Flux source resources (GitRepositories, HelmRepositories)."""
|
|
537
|
+
return json.dumps(gitops_sources_list(namespace, context, kind, label_selector), indent=2)
|
|
538
|
+
|
|
539
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
540
|
+
def gitops_source_get_tool(
|
|
541
|
+
name: str,
|
|
542
|
+
namespace: str,
|
|
543
|
+
kind: str,
|
|
544
|
+
context: str = ""
|
|
545
|
+
) -> str:
|
|
546
|
+
"""Get details of a Flux source resource."""
|
|
547
|
+
return json.dumps(gitops_source_get(name, namespace, kind, context), indent=2)
|
|
548
|
+
|
|
549
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
550
|
+
def gitops_detect_engine_tool(context: str = "") -> str:
|
|
551
|
+
"""Detect which GitOps engines are installed in the cluster."""
|
|
552
|
+
return json.dumps(gitops_detect_engine(context), indent=2)
|