4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
204 lines
7.8 KiB
Python
204 lines
7.8 KiB
Python
"""EKSBackend Protocol and FixtureEKSBackend for synthetic K8s testing.
|
|
|
|
The Protocol defines the minimal surface the Kubernetes investigation agent
|
|
uses to query EKS workload state. FixtureEKSBackend satisfies it by serving
|
|
scenario fixture data in the exact shape the EKS tools under ``tools/EKS*/``
|
|
return — no HTTP calls, no AWS credentials required.
|
|
|
|
Usage
|
|
-----
|
|
resolved_integrations = {
|
|
"eks": {
|
|
"cluster_name": "",
|
|
"role_arn": "",
|
|
"_backend": FixtureEKSBackend(fixture),
|
|
}
|
|
}
|
|
|
|
Each tool's production resolver checks the ``_backend`` key first and delegates
|
|
to it when present, falling back to real EKS API calls when absent.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
|
|
if TYPE_CHECKING:
|
|
from tests.synthetic.eks.scenario_loader import K8sScenarioFixture
|
|
|
|
|
|
@runtime_checkable
|
|
class EKSBackend(Protocol):
|
|
"""Minimal EKS interface used by the Kubernetes investigation agent.
|
|
|
|
One method per evidence source under ``tools/EKS*/``:
|
|
list_pods → EKSListPodsTool response shape
|
|
get_events → EKSEventsTool response shape
|
|
list_deployments → EKSListDeploymentsTool response shape
|
|
get_node_health → EKSNodeHealthTool response shape
|
|
get_pod_logs → EKSPodLogsTool response shape
|
|
"""
|
|
|
|
def list_pods(
|
|
self, cluster_name: str = "", namespace: str = "", **kwargs: Any
|
|
) -> dict[str, Any]:
|
|
"""Return a response matching ``list_eks_pods``."""
|
|
|
|
def get_events(
|
|
self, cluster_name: str = "", namespace: str = "", **kwargs: Any
|
|
) -> dict[str, Any]:
|
|
"""Return a response matching ``get_eks_events``."""
|
|
|
|
def list_deployments(
|
|
self, cluster_name: str = "", namespace: str = "", **kwargs: Any
|
|
) -> dict[str, Any]:
|
|
"""Return a response matching ``list_eks_deployments``."""
|
|
|
|
def get_node_health(self, cluster_name: str = "", **kwargs: Any) -> dict[str, Any]:
|
|
"""Return a response matching ``get_eks_node_health``."""
|
|
|
|
def get_pod_logs(
|
|
self, cluster_name: str = "", namespace: str = "", pod_name: str = "", **kwargs: Any
|
|
) -> dict[str, Any]:
|
|
"""Return a response matching ``get_eks_pod_logs``."""
|
|
|
|
|
|
class FixtureEKSBackend:
|
|
"""EKSBackend implementation backed by a K8sScenarioFixture.
|
|
|
|
Each method wraps the corresponding fixture file in the envelope that the
|
|
real tool function returns. Calling a method for an evidence source that
|
|
the scenario did not declare in ``available_evidence`` raises ValueError.
|
|
"""
|
|
|
|
def __init__(self, fixture: K8sScenarioFixture) -> None:
|
|
self._fixture = fixture
|
|
|
|
def _cluster_name(self, override: str) -> str:
|
|
return override or self._fixture.metadata.cluster_name
|
|
|
|
def _namespace(self, override: str) -> str:
|
|
return override or self._fixture.metadata.namespace
|
|
|
|
def list_pods(self, cluster_name: str = "", namespace: str = "", **_: Any) -> dict[str, Any]:
|
|
pods_fixture = self._fixture.evidence.eks_pods
|
|
if pods_fixture is None:
|
|
raise ValueError(
|
|
f"{self._fixture.scenario_id}: list_pods called but "
|
|
"'eks_pods' is not declared in available_evidence"
|
|
)
|
|
pods = list(pods_fixture.get("pods", []))
|
|
failing_pods = [p for p in pods if p.get("phase") not in ("Running", "Succeeded")]
|
|
high_restart_pods = [
|
|
p for p in pods if any(c.get("restart_count", 0) > 3 for c in p.get("containers", []))
|
|
]
|
|
return {
|
|
"source": "eks",
|
|
"available": True,
|
|
"cluster_name": self._cluster_name(cluster_name),
|
|
"namespace": self._namespace(namespace),
|
|
"total_pods": len(pods),
|
|
"pods": pods,
|
|
"failing_pods": failing_pods,
|
|
"high_restart_pods": high_restart_pods,
|
|
"error": None,
|
|
}
|
|
|
|
def get_events(self, cluster_name: str = "", namespace: str = "", **_: Any) -> dict[str, Any]:
|
|
events_fixture = self._fixture.evidence.eks_events
|
|
if events_fixture is None:
|
|
raise ValueError(
|
|
f"{self._fixture.scenario_id}: get_events called but "
|
|
"'eks_events' is not declared in available_evidence"
|
|
)
|
|
warning_events = list(events_fixture.get("warning_events", []))
|
|
return {
|
|
"source": "eks",
|
|
"available": True,
|
|
"cluster_name": self._cluster_name(cluster_name),
|
|
"namespace": self._namespace(namespace),
|
|
"warning_events": warning_events,
|
|
"total_warning_count": len(warning_events),
|
|
"error": None,
|
|
}
|
|
|
|
def list_deployments(
|
|
self, cluster_name: str = "", namespace: str = "", **_: Any
|
|
) -> dict[str, Any]:
|
|
deployments_fixture = self._fixture.evidence.eks_deployments
|
|
if deployments_fixture is None:
|
|
raise ValueError(
|
|
f"{self._fixture.scenario_id}: list_deployments called but "
|
|
"'eks_deployments' is not declared in available_evidence"
|
|
)
|
|
deployments: list[dict[str, Any]] = []
|
|
for raw in deployments_fixture.get("deployments", []):
|
|
desired = int(raw.get("desired", 0))
|
|
ready = int(raw.get("ready", 0))
|
|
available = int(raw.get("available", 0))
|
|
unavailable = int(raw.get("unavailable", 0))
|
|
deployments.append(
|
|
{
|
|
"name": raw.get("name", ""),
|
|
"namespace": raw.get("namespace", ""),
|
|
"desired": desired,
|
|
"ready": ready,
|
|
"available": available,
|
|
"unavailable": unavailable,
|
|
"degraded": unavailable > 0 or ready < desired,
|
|
}
|
|
)
|
|
degraded = [d for d in deployments if d["degraded"]]
|
|
return {
|
|
"source": "eks",
|
|
"available": True,
|
|
"cluster_name": self._cluster_name(cluster_name),
|
|
"namespace": self._namespace(namespace),
|
|
"total_deployments": len(deployments),
|
|
"deployments": deployments,
|
|
"degraded_deployments": degraded,
|
|
"error": None,
|
|
}
|
|
|
|
def get_node_health(self, cluster_name: str = "", **_: Any) -> dict[str, Any]:
|
|
nodes_fixture = self._fixture.evidence.eks_node_health
|
|
if nodes_fixture is None:
|
|
raise ValueError(
|
|
f"{self._fixture.scenario_id}: get_node_health called but "
|
|
"'eks_node_health' is not declared in available_evidence"
|
|
)
|
|
nodes = list(nodes_fixture.get("nodes", []))
|
|
not_ready_count = sum(1 for n in nodes if n.get("ready") != "True")
|
|
return {
|
|
"source": "eks",
|
|
"available": True,
|
|
"cluster_name": self._cluster_name(cluster_name),
|
|
"nodes": nodes,
|
|
"total_nodes": len(nodes),
|
|
"not_ready_count": not_ready_count,
|
|
"error": None,
|
|
}
|
|
|
|
def get_pod_logs(
|
|
self,
|
|
cluster_name: str = "",
|
|
namespace: str = "",
|
|
pod_name: str = "",
|
|
**_: Any,
|
|
) -> dict[str, Any]:
|
|
logs_fixture = self._fixture.evidence.eks_pod_logs
|
|
if logs_fixture is None:
|
|
raise ValueError(
|
|
f"{self._fixture.scenario_id}: get_pod_logs called but "
|
|
"'eks_pod_logs' is not declared in available_evidence"
|
|
)
|
|
return {
|
|
"source": "eks",
|
|
"available": True,
|
|
"cluster_name": self._cluster_name(cluster_name),
|
|
"namespace": self._namespace(namespace) or logs_fixture.get("namespace", ""),
|
|
"pod_name": pod_name or logs_fixture.get("pod_name", ""),
|
|
"logs": logs_fixture.get("logs", ""),
|
|
"error": None,
|
|
}
|