Coverage for mcp/resources/k8s.py: 98%
48 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
1"""Kubernetes manifest and live-state resources (k8s:// + gco://k8s/...) for
2the GCO MCP server.
4The static ``k8s://gco/manifests/...`` paths surface the cluster-bootstrap
5manifests that ship under ``lambda/kubectl-applier-simple/manifests``. The
6live ``gco://k8s/{namespace}/{kind}/{name}`` template wraps ``kubectl get``
7so an LLM can pin any cluster resource for inspection across turns.
8"""
10from __future__ import annotations
12import json
13import re
14from pathlib import Path
15from typing import Any
17import cli_runner
18from server import mcp
20PROJECT_ROOT = Path(__file__).parent.parent.parent
21MANIFESTS_DIR = PROJECT_ROOT / "lambda" / "kubectl-applier-simple" / "manifests"
23# Permissive RFC 1123 label rule for namespace and resource names.
24# Bounded length plus the alphanumeric+hyphen alphabet rules out
25# command-injection vectors when the value is forwarded to ``kubectl``.
26_K8S_NAME_RE = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,251}[a-z0-9])?$")
28# Kubernetes resource kinds — alphanumeric only, including dotted CRD
29# group forms like ``deployments.apps`` and ``ingresses.networking.k8s.io``.
30# Kept deliberately tight so ``kubectl get <kind>`` cannot be coerced
31# into a flag.
32_K8S_KIND_RE = re.compile(r"^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9-]+)*$")
34_KUBECTL_TIMEOUT_SECONDS = 30
37@mcp.resource("k8s://gco/manifests/index")
38def k8s_manifests_index() -> str:
39 """List all Kubernetes manifests deployed to the EKS cluster."""
40 lines = ["# Kubernetes Cluster Manifests\n"]
41 lines.append("Applied in order during `gco stacks deploy`:\n")
42 for f in sorted(MANIFESTS_DIR.glob("*.yaml")):
43 lines.append(f"- `k8s://gco/manifests/{f.name}` — {f.stem}")
44 readme = MANIFESTS_DIR / "README.md"
45 if readme.is_file(): 45 ↛ 47line 45 didn't jump to line 47 because the condition on line 45 was always true
46 lines.append("\n- `k8s://gco/manifests/README.md` — manifest documentation")
47 return "\n".join(lines)
50@mcp.resource("k8s://gco/manifests/{filename}")
51def k8s_manifest_resource(filename: str) -> str:
52 """Read a Kubernetes manifest that gets applied to the EKS cluster."""
53 path = MANIFESTS_DIR / filename
54 if not path.is_file():
55 available = sorted(f.name for f in MANIFESTS_DIR.glob("*") if f.is_file())
56 return f"Manifest '{filename}' not found. Available:\n" + "\n".join(available)
57 return path.read_text()
60def _k8s_live_resource(namespace: str, kind: str, name: str) -> str:
61 """Return the live YAML for ``<kind>/<name>`` in ``<namespace>``."""
62 if not _K8S_NAME_RE.match(namespace):
63 return json.dumps({"error": "invalid namespace", "value": namespace})
64 if not _K8S_KIND_RE.match(kind):
65 return json.dumps({"error": "invalid kind", "value": kind})
66 if not _K8S_NAME_RE.match(name):
67 return json.dumps({"error": "invalid name", "value": name})
68 try:
69 result = cli_runner.subprocess.run( # type: ignore[attr-defined] # nosemgrep: dangerous-subprocess-use-audit - shell=False; every argv element is regex-validated above
70 ["kubectl", "get", kind, name, "-n", namespace, "-o", "yaml"],
71 capture_output=True,
72 text=True,
73 timeout=_KUBECTL_TIMEOUT_SECONDS,
74 )
75 except FileNotFoundError:
76 return json.dumps({"error": "kubectl not found"})
77 except cli_runner.subprocess.TimeoutExpired: # type: ignore[attr-defined]
78 return json.dumps({"error": f"kubectl timed out after {_KUBECTL_TIMEOUT_SECONDS}s"})
79 if result.returncode != 0:
80 err = (result.stderr or result.stdout or "").strip()
81 return json.dumps(
82 {"error": err or "kubectl command failed", "exit_code": result.returncode}
83 )
84 return str(result.stdout)
87def register(mcp_instance: Any) -> None:
88 """Register the live Kubernetes resource template against ``mcp_instance``.
90 The static ``k8s://gco/manifests/...`` resources are decorated at
91 import time and don't need re-registration; this function exists
92 so ``register_all_resources()`` can wire the live template in
93 alongside the rest of the live-state modules.
94 """
95 mcp_instance.resource("gco://k8s/{namespace}/{kind}/{name}")(_k8s_live_resource)