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
590 lines
23 KiB
Python
590 lines
23 KiB
Python
"""Cloud-OpsBench cache-backed Kubernetes tools.
|
|
|
|
Replays Cloud-OpsBench (Wang et al., arXiv:2603.00468) actions against the
|
|
per-case ``tool_cache.json`` instead of talking to a real EKS cluster.
|
|
The tools are gated on the presence of a backend at
|
|
``sources["eks"]["_bench_backend"]`` — a slot the bench adapter sets and
|
|
production code never populates. The real EKS tools take over in any
|
|
non-bench context.
|
|
|
|
Each ``@tool`` declaration sets ``injected_params=("cloudops_backend",)``
|
|
so the replay backend is hidden from the LLM's tool-call schema and
|
|
supplied at call time by ``extract_params``. Without that, the LLM would
|
|
treat ``cloudops_backend`` as a free-text param and dispatch's
|
|
``{**injected, **tc.input}`` merge would let the LLM string override the
|
|
real backend, crashing every call with
|
|
``'str' object has no attribute '<Action>'``.
|
|
|
|
The ``extract_params`` callbacks pre-fill positional args from the case's
|
|
recorded ``process`` steps. After the injected-params fix landed these
|
|
prefills are mostly dead-code — the LLM owns the real values via
|
|
``tc.input`` — but they still serve as a sane-default safety net when the
|
|
LLM omits a required param.
|
|
|
|
CloudOpsBench dataset conventions encoded here:
|
|
- ``case.process`` is split into ``path1`` (alert trigger sequence) and
|
|
``path2`` (recovery / diagnostic actions).
|
|
- Each process step is encoded as ``"Action::param1::param2::..."``.
|
|
- ``case.result.fault_object`` is encoded as ``"app/<service_name>"``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any, Protocol, cast
|
|
|
|
from core.tool_framework.tool_decorator import tool
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Dataset conventions — change only when the upstream dataset format changes. #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# Process-step actions that encode service_name in position [1].
|
|
# The dataset guarantees this contract for these four action names.
|
|
_ACTIONS_WITH_SERVICE_NAME: frozenset[str] = frozenset(
|
|
{
|
|
"GetErrorLogs",
|
|
"GetRecentLogs",
|
|
"GetServiceDependencies",
|
|
"GetAppYAML",
|
|
}
|
|
)
|
|
|
|
# Prefix used in ``case.result.fault_object``: ``"app/<service_name>"``.
|
|
_FAULT_OBJECT_APP_PREFIX = "app/"
|
|
|
|
# Search order over ``case.process``. The asymmetry is intentional:
|
|
# - alert-first: when we want the affected service, path1 names it
|
|
# - recovery-first: when we want action parameters, path2 has the calls
|
|
_PATHS_ALERT_FIRST: tuple[str, ...] = ("path1", "path2")
|
|
_PATHS_RECOVERY_FIRST: tuple[str, ...] = ("path2", "path1")
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Fallback defaults — dead-code on the happy path. #
|
|
# #
|
|
# After the injected-params fix the LLM is the source of truth for every #
|
|
# non-injected tool arg via ``tc.input``. These constants only fire when #
|
|
# BOTH the case process is missing the relevant step AND the LLM omits the #
|
|
# required param — a combination that should not happen for required fields. #
|
|
# Kept as a safety net, not as primary behavior. #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
_DEFAULT_SERVICE = "frontend" # most-frequent service in the dataset
|
|
_DEFAULT_NAMESPACE = "default" # Kubernetes' standard namespace
|
|
_DEFAULT_RESOURCE_TYPE = "pods" # most-listed K8s resource type
|
|
_DEFAULT_DESCRIBE_RESOURCE_TYPE = "services"
|
|
_DEFAULT_HTTP_PORT = 80
|
|
_DEFAULT_CONTROL_PLANE_NODE = "master" # legacy K8s naming used by the dataset
|
|
_DEFAULT_CONTROL_PLANE_SERVICE = "kube-scheduler"
|
|
|
|
|
|
class _CloudOpsBenchBackend(Protocol):
|
|
"""Duck-typed contract for the Cloud-OpsBench replay backend.
|
|
|
|
The concrete implementation lives at
|
|
``tests/benchmarks/cloudopsbench/replay_backend.py``. Capturing the
|
|
contract here instead of importing the class keeps ``config/`` runtime
|
|
code free of a dependency on ``tests/``.
|
|
|
|
Identification is by **dedicated source slot**, not a marker attribute:
|
|
the bench adapter sets ``sources["eks"]["_bench_backend"]`` (distinct
|
|
from the synthetic-test ``_backend`` slot), so production tool
|
|
availability checks stay completely unaware of bench backend types.
|
|
No ``is_cloudopsbench_backend`` flag needed.
|
|
"""
|
|
|
|
# The Cloud-OpsBench dataset case being replayed. Typed ``Any`` because
|
|
# the Case schema lives outside ``config/`` (see
|
|
# ``tests/benchmarks/cloudopsbench/case_loader.py``). Attributes consumed
|
|
# here: ``case.process`` (dict of path1/path2 step lists) and
|
|
# ``case.result.fault_object``.
|
|
case: Any
|
|
|
|
# Default K8s namespace recorded on the case. Last-resort fallback in
|
|
# ``_default_namespace`` when neither alert sources nor the case
|
|
# override it.
|
|
default_namespace: str
|
|
|
|
|
|
def _cloudops_backend(sources: dict[str, dict]) -> Any:
|
|
"""Look up the CloudOpsBench replay backend in its dedicated slot.
|
|
|
|
The bench adapter sets ``sources["eks"]["_bench_backend"]`` (not
|
|
``_backend``) deliberately: ``_backend`` is the slot for synthetic-test
|
|
fixture backends that share the EKS tool API, and the replay backend
|
|
speaks a different (paper-protocol) API. Using a separate slot means
|
|
production tools that read ``_backend`` (``_eks_available``,
|
|
``eks_available_or_backend``) stay completely unaware of bench
|
|
backends — no provider-specific branching needed in their availability
|
|
checks.
|
|
"""
|
|
return (sources.get("eks") or {}).get("_bench_backend")
|
|
|
|
|
|
def _cloudops_available(sources: dict[str, dict]) -> bool:
|
|
return _cloudops_backend(sources) is not None
|
|
|
|
|
|
def _service_from_process(backend: Any) -> str:
|
|
case = getattr(backend, "case", None)
|
|
process = getattr(case, "process", {}) or {}
|
|
for path_name in _PATHS_ALERT_FIRST:
|
|
for step in process.get(path_name, []):
|
|
if not isinstance(step, str):
|
|
continue
|
|
parts = step.split("::")
|
|
if len(parts) >= 2 and parts[0] in _ACTIONS_WITH_SERVICE_NAME:
|
|
return parts[1]
|
|
|
|
result = getattr(case, "result", None)
|
|
fault_object = getattr(result, "fault_object", "")
|
|
if isinstance(fault_object, str) and fault_object.startswith(_FAULT_OBJECT_APP_PREFIX):
|
|
return fault_object.split("/", 1)[1]
|
|
return _DEFAULT_SERVICE
|
|
|
|
|
|
def _process_parts_for_action(backend: Any, action_name: str) -> list[str]:
|
|
case = getattr(backend, "case", None)
|
|
process = getattr(case, "process", {}) or {}
|
|
for path_name in _PATHS_RECOVERY_FIRST:
|
|
for step in process.get(path_name, []):
|
|
if not isinstance(step, str):
|
|
continue
|
|
parts = step.split("::")
|
|
if parts and parts[0] == action_name:
|
|
return parts
|
|
return []
|
|
|
|
|
|
def _resource_type_from_process(backend: Any) -> str:
|
|
parts = _process_parts_for_action(backend, "GetResources")
|
|
if len(parts) >= 2:
|
|
return parts[1]
|
|
return _DEFAULT_RESOURCE_TYPE
|
|
|
|
|
|
def _default_namespace(backend: Any, sources: dict[str, dict]) -> str:
|
|
eks = sources.get("eks") or {}
|
|
namespace = eks.get("namespace") or getattr(backend, "default_namespace", "")
|
|
return str(namespace or _DEFAULT_NAMESPACE)
|
|
|
|
|
|
def _extract_backend(sources: dict[str, dict]) -> dict[str, Any]:
|
|
return {"cloudops_backend": _cloudops_backend(sources)}
|
|
|
|
|
|
def _extract_get_resources(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"resource_type": _resource_type_from_process(backend),
|
|
"namespace": _default_namespace(backend, sources),
|
|
}
|
|
|
|
|
|
def _extract_describe_resource(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "DescribeResource")
|
|
resource_type = parts[1] if len(parts) >= 2 else _DEFAULT_DESCRIBE_RESOURCE_TYPE
|
|
name = parts[2] if len(parts) >= 3 else _service_from_process(backend)
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"resource_type": resource_type,
|
|
"name": name,
|
|
"namespace": _default_namespace(backend, sources),
|
|
}
|
|
|
|
|
|
def _extract_error_logs(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "GetErrorLogs")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"namespace": _default_namespace(backend, sources),
|
|
"service_name": parts[1] if len(parts) >= 2 else _service_from_process(backend),
|
|
}
|
|
|
|
|
|
def _extract_recent_logs(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "GetRecentLogs")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"namespace": _default_namespace(backend, sources),
|
|
"service_name": parts[1] if len(parts) >= 2 else _service_from_process(backend),
|
|
}
|
|
|
|
|
|
def _extract_app_yaml(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "GetAppYAML")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"app_name": parts[1] if len(parts) >= 2 else _service_from_process(backend),
|
|
}
|
|
|
|
|
|
def _extract_service_dependencies(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "GetServiceDependencies")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"service_name": parts[1] if len(parts) >= 2 else _service_from_process(backend),
|
|
}
|
|
|
|
|
|
def _extract_connectivity(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "CheckServiceConnectivity")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"service_name": parts[1] if len(parts) >= 2 else _service_from_process(backend),
|
|
"port": int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else _DEFAULT_HTTP_PORT,
|
|
"namespace": _default_namespace(backend, sources),
|
|
}
|
|
|
|
|
|
def _extract_node_status(sources: dict[str, dict]) -> dict[str, Any]:
|
|
backend = _cloudops_backend(sources)
|
|
parts = _process_parts_for_action(backend, "CheckNodeServiceStatus")
|
|
return {
|
|
"cloudops_backend": backend,
|
|
"node_name": parts[1] if len(parts) >= 2 else _DEFAULT_CONTROL_PLANE_NODE,
|
|
"service_name": parts[2] if len(parts) >= 3 else _DEFAULT_CONTROL_PLANE_SERVICE,
|
|
}
|
|
|
|
|
|
def _run_backend(cloudops_backend: Any, method_name: str, **kwargs: Any) -> dict[str, Any]:
|
|
if cloudops_backend is None:
|
|
return {
|
|
"source": "cloudopsbench",
|
|
"available": False,
|
|
"error": "CloudOpsBench replay backend is not available.",
|
|
}
|
|
method = cast(Callable[..., dict[str, Any]], getattr(cloudops_backend, method_name))
|
|
return method(**kwargs)
|
|
|
|
|
|
@tool(
|
|
name="GetResources",
|
|
source="eks",
|
|
description=(
|
|
"List Kubernetes resources in the cluster — pods, deployments, "
|
|
"services, events, nodes, replicasets. Use this FIRST in most "
|
|
"investigations to identify which workloads are failing, see "
|
|
"pod status (CrashLoopBackOff, ImagePullBackOff, Pending, "
|
|
"ContainerCreating), and find recent events that indicate why."
|
|
),
|
|
use_cases=[
|
|
"Identify which pods are unhealthy: resource_type='pods' shows STATUS column",
|
|
"Find broken deployments: resource_type='deployments' shows READY vs DESIRED replicas",
|
|
"Discover failure signals: resource_type='events' shows scheduling errors, image pull failures, OOM kills, secret-binding errors",
|
|
"Enumerate services and their selectors: resource_type='services'",
|
|
"Check node health: resource_type='nodes' shows Ready / NotReady / SchedulingDisabled",
|
|
],
|
|
requires=["cluster_name"],
|
|
input_schema={"type": "object", "properties": {"resource_type": {"type": "string"}}},
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_get_resources,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_resources(
|
|
cloudops_backend: Any,
|
|
resource_type: str,
|
|
namespace: str = "",
|
|
name: str | None = None,
|
|
show_labels: bool = False,
|
|
output_wide: bool = False,
|
|
label_selector: str | None = None,
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"GetResources",
|
|
resource_type=resource_type,
|
|
namespace=namespace,
|
|
name=name,
|
|
show_labels=show_labels,
|
|
output_wide=output_wide,
|
|
label_selector=label_selector,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="DescribeResource",
|
|
source="eks",
|
|
description=(
|
|
"Get detailed configuration for a specific named Kubernetes resource "
|
|
"(pod, deployment, service, statefulset). Use AFTER GetResources "
|
|
"to investigate WHY a specific workload is failing — shows env "
|
|
"vars, secret references, volume mounts, container ports, "
|
|
"image tags, and the full status with event log."
|
|
),
|
|
use_cases=[
|
|
"Inspect a failing pod's env vars and secret references: resource_type='pod', name='<pod-name>'",
|
|
"Check a deployment's image, replica count, and selectors: resource_type='deployment', name='<deployment>'",
|
|
"Verify a service's port mappings, selectors, and endpoints: resource_type='service', name='<service>'",
|
|
"Examine a StatefulSet's volume claims and pod template: resource_type='statefulset', name='<sts-name>'",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_describe_resource,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def describe_resource(
|
|
cloudops_backend: Any,
|
|
resource_type: str,
|
|
name: str,
|
|
namespace: str = "",
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"DescribeResource",
|
|
resource_type=resource_type,
|
|
name=name,
|
|
namespace=namespace,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="GetClusterConfiguration",
|
|
source="eks",
|
|
description=(
|
|
"Get cluster-level state: node health, control-plane component "
|
|
"status (kubelet, kube-scheduler, kube-proxy, containerd), and "
|
|
"system-level conditions. Use when issues appear cluster-wide "
|
|
"rather than workload-specific — e.g. multiple unrelated services "
|
|
"failing simultaneously, or scheduling failures across namespaces."
|
|
),
|
|
use_cases=[
|
|
"Detect node-level problems: which nodes are NotReady, cordoned, or out of resources",
|
|
"Diagnose control-plane issues: kubelet down, scheduler offline, containerd crashed, kube-proxy unavailable",
|
|
"Establish baseline cluster health before narrowing to a specific workload",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_backend,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_cluster_configuration(cloudops_backend: Any) -> dict[str, Any]:
|
|
return _run_backend(cloudops_backend, "GetClusterConfiguration")
|
|
|
|
|
|
@tool(
|
|
name="GetAlerts",
|
|
source="eks",
|
|
description=(
|
|
"Get the active alerts that triggered this investigation. Call "
|
|
"this FIRST in every case — the alert message identifies the "
|
|
"affected service/namespace, severity, error rate, and timestamp. "
|
|
"Don't reason from the alert headline alone; always pull the "
|
|
"structured alert data."
|
|
),
|
|
use_cases=[
|
|
"Identify the affected service: alert tags name the failing component",
|
|
"Establish when the issue started: alert firstSeen timestamp",
|
|
"See the error pattern: alert message often contains HTTP 5xx, OOM, connection refused, etc.",
|
|
"Determine severity: critical vs warning helps prioritize sub-investigations",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_backend,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_alerts(cloudops_backend: Any) -> dict[str, Any]:
|
|
return _run_backend(cloudops_backend, "GetAlerts")
|
|
|
|
|
|
@tool(
|
|
name="GetErrorLogs",
|
|
source="eks",
|
|
description=(
|
|
"Get aggregated error-log signals for a specific service: counts "
|
|
"and example messages grouped by error type. Use AFTER finding a "
|
|
"failing service from GetResources/events — error logs typically "
|
|
"pinpoint the actual root cause (MySQL 'access denied', DNS "
|
|
"'no such host', 'OOMKilled', 'image pull backoff', etc.)."
|
|
),
|
|
use_cases=[
|
|
"Confirm a suspected MySQL credential issue: look for 'Access denied for user' or '1045' MySQL error codes",
|
|
"Confirm a DNS resolution failure: look for 'no such host' or 'DNS resolution failed'",
|
|
"Identify image pull issues: 'ErrImagePull' or 'manifest unknown'",
|
|
"Find HTTP 5xx patterns: 500/502/503/504 grouped by endpoint",
|
|
"Detect connection-refused / port-mismatch errors against downstream services",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_error_logs,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_error_logs(
|
|
cloudops_backend: Any,
|
|
namespace: str,
|
|
service_name: str,
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"GetErrorLogs",
|
|
namespace=namespace,
|
|
service_name=service_name,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="GetRecentLogs",
|
|
source="eks",
|
|
description=(
|
|
"Get the most recent log lines from a service — chronologically "
|
|
"ordered, unfiltered. Use when GetErrorLogs aggregation isn't "
|
|
"enough: recent logs show the SEQUENCE of events leading to "
|
|
"failure, often revealing race conditions, startup ordering "
|
|
"issues, or transient errors that don't surface in summaries."
|
|
),
|
|
use_cases=[
|
|
"See the moment of failure: tail logs around the alert timestamp",
|
|
"Detect startup-sequence problems: container init order, secret/volume mount timing",
|
|
"Find intermittent errors that aggregate-by-type misses",
|
|
"Confirm a fix's effect by checking the latest log lines",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_recent_logs,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_recent_logs(
|
|
cloudops_backend: Any,
|
|
namespace: str,
|
|
service_name: str,
|
|
lines: int = 50,
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"GetRecentLogs",
|
|
namespace=namespace,
|
|
service_name=service_name,
|
|
lines=lines,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="GetServiceDependencies",
|
|
source="eks",
|
|
description=(
|
|
"Map a service's upstream and downstream dependencies. Use this "
|
|
"to trace cascading failures: if service A is failing, what "
|
|
"calls A (impact blast-radius), and what does A call (potential "
|
|
"root cause upstream)? Critical for distinguishing 'A is broken' "
|
|
"from 'A is broken because B is broken'."
|
|
),
|
|
use_cases=[
|
|
"Trace the cause: which downstream services does the failing service depend on? Check those for errors too.",
|
|
"Trace the impact: which upstream services call the failing one? Useful for confirming user-visible impact.",
|
|
"Identify shared infrastructure: multiple failing services calling the same database/cache often points to that shared dep as the cause.",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_service_dependencies,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_service_dependencies(cloudops_backend: Any, service_name: str) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"GetServiceDependencies",
|
|
service_name=service_name,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="GetAppYAML",
|
|
source="eks",
|
|
description=(
|
|
"Get the full deployment YAML for an application — shows every "
|
|
"secret reference, env var, volume mount, image tag, and resource "
|
|
"limit. Use when DescribeResource doesn't show enough: the raw "
|
|
"YAML often reveals misconfigured secret bindings, mismatched "
|
|
"env-var names, wrong image tags, or sidecar/init-container "
|
|
"issues that aren't obvious from the high-level describe."
|
|
),
|
|
use_cases=[
|
|
"Diagnose a missing secret binding: check 'envFrom' and 'volumes' sections for secret references",
|
|
"Find image-tag mistakes: compare the spec's image vs the registered tag",
|
|
"Detect resource-limit misconfigurations: CPU/memory requests and limits",
|
|
"Check init-container ordering and sidecar configurations",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_app_yaml,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def get_app_yaml(cloudops_backend: Any, app_name: str) -> dict[str, Any]:
|
|
return _run_backend(cloudops_backend, "GetAppYAML", app_name=app_name)
|
|
|
|
|
|
@tool(
|
|
name="CheckServiceConnectivity",
|
|
source="eks",
|
|
description=(
|
|
"Test reachability of a Kubernetes service from inside the "
|
|
"cluster. Use to confirm suspected service-routing failures: "
|
|
"DNS resolution problems, port mismatches between Service and "
|
|
"Pod, sidecar (Istio) port conflicts, or selector mismatches "
|
|
"that leave a Service with zero endpoints."
|
|
),
|
|
use_cases=[
|
|
"Confirm DNS resolution failure: connectivity fails with 'no such host'",
|
|
"Confirm port-mapping mismatch: connection refused on the Service port but pod listens on a different port",
|
|
"Confirm zero-endpoint failures: 'no endpoints available'",
|
|
"Validate that a fix resolved the connectivity issue",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_connectivity,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def check_service_connectivity(
|
|
cloudops_backend: Any,
|
|
service_name: str,
|
|
port: int,
|
|
namespace: str,
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"CheckServiceConnectivity",
|
|
service_name=service_name,
|
|
port=port,
|
|
namespace=namespace,
|
|
)
|
|
|
|
|
|
@tool(
|
|
name="CheckNodeServiceStatus",
|
|
source="eks",
|
|
description=(
|
|
"Check the health of a specific Kubernetes control-plane "
|
|
"component (kubelet, kube-scheduler, kube-proxy, containerd) "
|
|
"on a named node. Use when GetClusterConfiguration reveals "
|
|
"node-level issues OR when GetResources shows scheduling "
|
|
"failures, Pending pods, or NotReady nodes."
|
|
),
|
|
use_cases=[
|
|
"Diagnose scheduling failures: check kube-scheduler on master nodes",
|
|
"Diagnose pod-startup failures: check kubelet on the worker node",
|
|
"Diagnose container-runtime issues: check containerd on the affected node",
|
|
"Diagnose service-routing failures: check kube-proxy on relevant nodes",
|
|
],
|
|
requires=["cluster_name"],
|
|
is_available=_cloudops_available,
|
|
extract_params=_extract_node_status,
|
|
injected_params=("cloudops_backend",),
|
|
)
|
|
def check_node_service_status(
|
|
cloudops_backend: Any,
|
|
node_name: str,
|
|
service_name: str,
|
|
) -> dict[str, Any]:
|
|
return _run_backend(
|
|
cloudops_backend,
|
|
"CheckNodeServiceStatus",
|
|
node_name=node_name,
|
|
service_name=service_name,
|
|
)
|