fix: repair logs, inspect, and rename tools
- logs: add server/services params for container/stack log endpoints - inspect: add service param for deployment_container/stack_container types - rename: remove incorrect tag special-casing (RenameTag expects id field) - scripts: add comprehensive test_all_tools.py covering all 13 tools
This commit is contained in:
Executable
+958
@@ -0,0 +1,958 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Komodo MCP Server — Comprehensive Tool Test
|
||||
Tests ALL 13 MCP tools: komodo_list, komodo_get, komodo_create, komodo_update,
|
||||
komodo_delete, komodo_execute, komodo_logs, komodo_summary, komodo_inspect,
|
||||
komodo_search_logs, komodo_list_detail, komodo_copy, komodo_rename.
|
||||
|
||||
Phases:
|
||||
1. Health + Session init
|
||||
2. Read operations (safe, no side effects)
|
||||
3. Write lifecycle (create -> get -> update -> copy -> rename -> delete)
|
||||
4. Execute operations (safe ones only)
|
||||
5. Summary report
|
||||
|
||||
Usage:
|
||||
KOMODO_MCP_URL=http://10.10.2.114:9800 python3 scripts/test_all_tools.py
|
||||
|
||||
No external deps — stdlib only.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
MCP_URL = os.environ.get("KOMODO_MCP_URL", "http://10.10.2.114:9800")
|
||||
MCP_ENDPOINT = f"{MCP_URL}/mcp"
|
||||
HEALTH_ENDPOINT = f"{MCP_URL}/health"
|
||||
|
||||
STAMP = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M")
|
||||
PREFIX = f"mcp-at-{STAMP}"
|
||||
|
||||
# Track created resources for cleanup.
|
||||
CREATED_IDS: dict[str, list[str]] = {}
|
||||
|
||||
# Per-test results: test_key -> status (PASS / FAIL / PERM / SKIP)
|
||||
RESULTS: dict[str, str] = {}
|
||||
|
||||
# Detailed per-step messages.
|
||||
DETAILS: dict[str, list[str]] = {}
|
||||
|
||||
# The 13 MCP tools we must cover.
|
||||
EXPECTED_TOOLS = {
|
||||
"komodo_list", "komodo_get", "komodo_create", "komodo_update",
|
||||
"komodo_delete", "komodo_execute", "komodo_logs", "komodo_summary",
|
||||
"komodo_inspect", "komodo_search_logs", "komodo_list_detail",
|
||||
"komodo_copy", "komodo_rename",
|
||||
}
|
||||
|
||||
# Track which MCP tool names were actually invoked.
|
||||
TOOLS_INVOKED: set[str] = set()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP JSON-RPC client (copied from test_write_lifecycle.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _post(body: dict, session_id: str | None = None) -> tuple[dict, str | None]:
|
||||
"""POST JSON-RPC to the MCP server. Returns (response_body, new_session_id).
|
||||
|
||||
Handles both direct JSON and SSE (text/event-stream) responses from
|
||||
StreamableHTTPServerTransport.
|
||||
"""
|
||||
data = json.dumps(body).encode()
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/event-stream",
|
||||
}
|
||||
if session_id:
|
||||
headers["mcp-session-id"] = session_id
|
||||
req = urllib.request.Request(MCP_ENDPOINT, data=data, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
new_sid = resp.headers.get("mcp-session-id") or session_id
|
||||
ct = resp.headers.get("Content-Type", "")
|
||||
raw = resp.read().decode()
|
||||
if not raw:
|
||||
return {}, new_sid
|
||||
# SSE response: parse "data: ..." lines
|
||||
if "text/event-stream" in ct:
|
||||
for line in raw.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("data:"):
|
||||
payload = line[len("data:"):].strip()
|
||||
if payload:
|
||||
try:
|
||||
return json.loads(payload), new_sid
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
# Fallback: try parsing the whole thing as JSON
|
||||
try:
|
||||
return json.loads(raw), new_sid
|
||||
except json.JSONDecodeError:
|
||||
return {}, new_sid
|
||||
# Direct JSON response
|
||||
return json.loads(raw), new_sid
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode() if e.fp else ""
|
||||
raise RuntimeError(f"HTTP {e.code}: {body_text}") from e
|
||||
|
||||
|
||||
class McpClient:
|
||||
def __init__(self) -> None:
|
||||
self.session_id: str | None = None
|
||||
self._req_id = 0
|
||||
|
||||
def _next_id(self) -> int:
|
||||
self._req_id += 1
|
||||
return self._req_id
|
||||
|
||||
def initialize(self) -> None:
|
||||
resp, sid = _post({
|
||||
"jsonrpc": "2.0",
|
||||
"id": self._next_id(),
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "all-tools-test", "version": "1.0.0"},
|
||||
},
|
||||
}, None)
|
||||
self.session_id = sid
|
||||
_post({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/initialized",
|
||||
}, self.session_id)
|
||||
|
||||
def call_tool(self, name: str, arguments: dict):
|
||||
TOOLS_INVOKED.add(name)
|
||||
resp, self.session_id = _post({
|
||||
"jsonrpc": "2.0",
|
||||
"id": self._next_id(),
|
||||
"method": "tools/call",
|
||||
"params": {"name": name, "arguments": arguments},
|
||||
}, self.session_id)
|
||||
if "error" in resp:
|
||||
raise RuntimeError(f"MCP error: {resp['error']}")
|
||||
result = resp.get("result", {})
|
||||
if result.get("isError"):
|
||||
contents = result.get("content", [])
|
||||
err_text = contents[0].get("text", "unknown error") if contents else "unknown error"
|
||||
raise RuntimeError(err_text)
|
||||
contents = result.get("content", [])
|
||||
if not contents:
|
||||
return None
|
||||
text = contents[0].get("text", "")
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return json.loads(text)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _is_perm_error(exc: Exception) -> bool:
|
||||
"""Return True if the error is a permission / authorization error."""
|
||||
s = str(exc).lower()
|
||||
return any(kw in s for kw in [
|
||||
"permission", "forbidden", "unauthorized", "not allowed",
|
||||
"insufficient", "not permitted",
|
||||
])
|
||||
|
||||
|
||||
def _is_not_found(exc: Exception) -> bool:
|
||||
s = str(exc).lower()
|
||||
return any(kw in s for kw in [
|
||||
"404", "not found", "does not exist", "no such",
|
||||
])
|
||||
|
||||
|
||||
def log(tool: str, msg: str) -> None:
|
||||
DETAILS.setdefault(tool, []).append(msg)
|
||||
print(f" {msg}")
|
||||
|
||||
|
||||
def result(tool: str, status: str) -> None:
|
||||
RESULTS[tool] = status
|
||||
|
||||
|
||||
def _first_item_id(client: McpClient, resource_type: str) -> str | None:
|
||||
"""List a resource type and return the id/name of the first item, or None."""
|
||||
try:
|
||||
items = client.call_tool("komodo_list", {"resource_type": resource_type})
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(items, list) or len(items) == 0:
|
||||
return None
|
||||
first = items[0]
|
||||
if isinstance(first, dict):
|
||||
return first.get("id") or first.get("name") or str(first)
|
||||
return str(first)
|
||||
|
||||
|
||||
def _first_server_name(client: McpClient) -> str | None:
|
||||
return _first_item_id(client, "server")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: Health + Session init
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def phase1_health_and_session() -> tuple[bool, McpClient]:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Phase 1: Health + Session init")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
print(f"\nHealth check: {HEALTH_ENDPOINT}")
|
||||
try:
|
||||
req = urllib.request.Request(HEALTH_ENDPOINT, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
print(f" \u2713 MCP server is up (HTTP {resp.status})")
|
||||
except Exception as e:
|
||||
print(f" \u2717 MCP server unreachable: {e}")
|
||||
return False, McpClient()
|
||||
|
||||
print(f"\nInitializing MCP session...")
|
||||
client = McpClient()
|
||||
try:
|
||||
client.initialize()
|
||||
print(f" \u2713 Session established (id={client.session_id})")
|
||||
except Exception as e:
|
||||
print(f" \u2717 Failed to initialize: {e}")
|
||||
return False, client
|
||||
|
||||
return True, client
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Read operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def phase2_read_operations(client: McpClient) -> None:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Phase 2: Read operations")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
_test_list_all(client)
|
||||
_test_get_known(client)
|
||||
_test_summary_all(client)
|
||||
_test_list_detail(client)
|
||||
_test_inspect(client)
|
||||
_test_logs(client)
|
||||
_test_search_logs(client)
|
||||
|
||||
|
||||
# ---- komodo_list ----
|
||||
|
||||
def _test_list_all(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_list (all 17 resource types) ---")
|
||||
all_types = [
|
||||
"stack", "build", "server", "procedure", "deployment", "alerter",
|
||||
"image_registry_account", "sync_resource", "user", "tag", "action",
|
||||
"repo", "builder", "swarm", "variable", "user_group",
|
||||
"git_provider_account",
|
||||
]
|
||||
for rt in all_types:
|
||||
key = f"komodo_list({rt})"
|
||||
try:
|
||||
items = client.call_tool("komodo_list", {"resource_type": rt})
|
||||
count = len(items) if isinstance(items, list) else 0
|
||||
log(key, f"\u2713 komodo_list {rt} \u2192 {count} items")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 komodo_list {rt} \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 komodo_list {rt} \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 komodo_list {rt} \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_get ----
|
||||
|
||||
def _test_get_known(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_get (known resources) ---")
|
||||
targets = [
|
||||
("stack", "first stack"),
|
||||
("build", "first build"),
|
||||
("server", "first server"),
|
||||
("tag", "first tag"),
|
||||
("procedure", "first procedure"),
|
||||
]
|
||||
for rt, label in targets:
|
||||
key = f"komodo_get({rt})"
|
||||
rid = _first_item_id(client, rt)
|
||||
if rid is None:
|
||||
log(key, f"\u2298 komodo_get {rt} \u2192 SKIP: no {label} found")
|
||||
result(key, "SKIP")
|
||||
continue
|
||||
try:
|
||||
data = client.call_tool("komodo_get", {"resource_type": rt, "id": rid})
|
||||
log(key, f"\u2713 komodo_get {rt}/{rid} returned data")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_summary ----
|
||||
|
||||
def _test_summary_all(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_summary (11 resource types) ---")
|
||||
summary_types = [
|
||||
"build", "deployment", "server", "stack", "alerter",
|
||||
"procedure", "sync_resource", "repo", "builder", "swarm", "action",
|
||||
]
|
||||
for rt in summary_types:
|
||||
key = f"komodo_summary({rt})"
|
||||
try:
|
||||
data = client.call_tool("komodo_summary", {"resource_type": rt})
|
||||
log(key, f"\u2713 komodo_summary {rt} returned data")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_list_detail ----
|
||||
|
||||
def _test_list_detail(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_list_detail ---")
|
||||
|
||||
# Simple list_detail calls (no extra params)
|
||||
simple_types = ["full_stacks", "full_builds", "full_servers", "schedules", "api_keys"]
|
||||
for lt in simple_types:
|
||||
key = f"komodo_list_detail({lt})"
|
||||
try:
|
||||
data = client.call_tool("komodo_list_detail", {"list_type": lt})
|
||||
count = len(data) if isinstance(data, list) else 0
|
||||
log(key, f"\u2713 komodo_list_detail {lt} \u2192 {count} items")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# Containers (requires server param)
|
||||
key = "komodo_list_detail(containers)"
|
||||
try:
|
||||
server_name = _first_server_name(client)
|
||||
if server_name is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no servers found")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_list_detail", {
|
||||
"list_type": "containers", "server": server_name,
|
||||
})
|
||||
count = len(data) if isinstance(data, list) else 0
|
||||
log(key, f"\u2713 komodo_list_detail containers ({server_name}) \u2192 {count} items")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_inspect ----
|
||||
|
||||
def _test_inspect(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_inspect ---")
|
||||
|
||||
# container (server-scoped)
|
||||
key = "komodo_inspect(container)"
|
||||
try:
|
||||
server_name = _first_server_name(client)
|
||||
if server_name is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no servers")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
containers = client.call_tool("komodo_list_detail", {
|
||||
"list_type": "containers", "server": server_name,
|
||||
})
|
||||
if not isinstance(containers, list) or len(containers) == 0:
|
||||
log(key, f"\u2298 \u2192 SKIP: no containers on {server_name}")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
first_c = containers[0]
|
||||
cid = first_c.get("Id", first_c.get("id", "")) if isinstance(first_c, dict) else str(first_c)
|
||||
if not cid:
|
||||
log(key, f"\u2298 \u2192 SKIP: could not determine container id")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_inspect", {
|
||||
"inspect_type": "container", "id": cid, "server": server_name,
|
||||
})
|
||||
log(key, f"\u2713 komodo_inspect container/{cid[:16]} on {server_name}")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# deployment_container (needs service param from Komodo API)
|
||||
key = "komodo_inspect(deployment_container)"
|
||||
try:
|
||||
deployments = client.call_tool("komodo_list", {"resource_type": "deployment"})
|
||||
if not isinstance(deployments, list) or len(deployments) == 0:
|
||||
log(key, f"\u2298 \u2192 SKIP: no deployments")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
first = deployments[0]
|
||||
dep_name = first.get("name", first.get("id", str(first))) if isinstance(first, dict) else str(first)
|
||||
data = client.call_tool("komodo_inspect", {
|
||||
"inspect_type": "deployment_container", "id": dep_name,
|
||||
})
|
||||
log(key, f"\u2713 komodo_inspect deployment_container/{dep_name}")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if "missing field" in str(e).lower():
|
||||
log(key, f"\u2298 \u2192 API_LIMIT: {e}")
|
||||
result(key, "FAIL")
|
||||
elif _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# stack_container (needs service param from Komodo API)
|
||||
key = "komodo_inspect(stack_container)"
|
||||
try:
|
||||
stacks = client.call_tool("komodo_list", {"resource_type": "stack"})
|
||||
if not isinstance(stacks, list) or len(stacks) == 0:
|
||||
log(key, f"\u2298 \u2192 SKIP: no stacks")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
first = stacks[0]
|
||||
stack_name = first.get("name", first.get("id", str(first))) if isinstance(first, dict) else str(first)
|
||||
data = client.call_tool("komodo_inspect", {
|
||||
"inspect_type": "stack_container", "id": stack_name,
|
||||
})
|
||||
log(key, f"\u2713 komodo_inspect stack_container/{stack_name}")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if "missing field" in str(e).lower():
|
||||
log(key, f"\u2298 \u2192 API_LIMIT: {e}")
|
||||
result(key, "FAIL")
|
||||
elif _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_logs ----
|
||||
|
||||
def _test_logs(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_logs ---")
|
||||
|
||||
# deployment logs
|
||||
key = "komodo_logs(deployment)"
|
||||
try:
|
||||
dep_id = _first_item_id(client, "deployment")
|
||||
if dep_id is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no deployments")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_logs", {
|
||||
"resource_type": "deployment", "id": dep_id, "tail": 5,
|
||||
})
|
||||
log(key, f"\u2713 komodo_logs deployment/{dep_id} (tail=5)")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# stack logs (needs services param — Komodo API requirement)
|
||||
key = "komodo_logs(stack)"
|
||||
try:
|
||||
stack_id = _first_item_id(client, "stack")
|
||||
if stack_id is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no stacks")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_logs", {
|
||||
"resource_type": "stack", "id": stack_id, "tail": 5,
|
||||
})
|
||||
log(key, f"\u2713 komodo_logs stack/{stack_id} (tail=5)")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if "missing field" in str(e).lower():
|
||||
log(key, f"\u2298 \u2192 API_LIMIT: {e}")
|
||||
result(key, "FAIL")
|
||||
elif _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# container logs (needs container param — Komodo API requirement)
|
||||
key = "komodo_logs(container)"
|
||||
try:
|
||||
server_name = _first_server_name(client)
|
||||
if server_name is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no servers")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
containers = client.call_tool("komodo_list_detail", {
|
||||
"list_type": "containers", "server": server_name,
|
||||
})
|
||||
if not isinstance(containers, list) or len(containers) == 0:
|
||||
log(key, f"\u2298 \u2192 SKIP: no containers")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
first_c = containers[0]
|
||||
cid = first_c.get("Id", first_c.get("id", "")) if isinstance(first_c, dict) else str(first_c)
|
||||
if not cid:
|
||||
log(key, f"\u2298 \u2192 SKIP: no container id")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_logs", {
|
||||
"resource_type": "container", "id": cid,
|
||||
"server": server_name, "tail": 5,
|
||||
})
|
||||
log(key, f"\u2713 komodo_logs container/{cid[:16]} on {server_name}")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if "missing field" in str(e).lower():
|
||||
log(key, f"\u2298 \u2192 API_LIMIT: {e}")
|
||||
result(key, "FAIL")
|
||||
elif _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---- komodo_search_logs ----
|
||||
|
||||
def _test_search_logs(client: McpClient) -> None:
|
||||
print(f"\n--- komodo_search_logs ---")
|
||||
key = "komodo_search_logs(deployment)"
|
||||
try:
|
||||
dep_id = _first_item_id(client, "deployment")
|
||||
if dep_id is None:
|
||||
log(key, f"\u2298 \u2192 SKIP: no deployments")
|
||||
result(key, "SKIP")
|
||||
return
|
||||
data = client.call_tool("komodo_search_logs", {
|
||||
"resource_type": "deployment", "id": dep_id,
|
||||
"query": "error", "tail": 5,
|
||||
})
|
||||
log(key, f"\u2713 komodo_search_logs deployment/{dep_id} query='error'")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: Write lifecycle (create -> get -> update -> copy -> rename -> delete)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def phase3_write_lifecycle(client: McpClient) -> None:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Phase 3: Write lifecycle")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
_test_tag_lifecycle(client)
|
||||
_test_procedure_lifecycle(client)
|
||||
|
||||
|
||||
def _test_tag_lifecycle(client: McpClient) -> None:
|
||||
"""Tag: create -> get -> update -> delete.
|
||||
Tags cannot be copied via komodo_copy (not in enum).
|
||||
Tag rename/delete via Komodo API requires ObjectId — the MCP tool
|
||||
passes the name which the API rejects, so we mark these as API_LIMIT.
|
||||
"""
|
||||
tool = "write(tag)"
|
||||
name = f"{PREFIX}-tag"
|
||||
rid = None
|
||||
try:
|
||||
# CREATE
|
||||
log(tool, f"CREATE tag name={name}")
|
||||
created = client.call_tool("komodo_create", {
|
||||
"resource_type": "tag",
|
||||
"params": {"name": name, "color": "Slate"},
|
||||
})
|
||||
rid = created.get("name") if isinstance(created, dict) else created
|
||||
rid = str(rid)
|
||||
CREATED_IDS.setdefault("tag", []).append(rid)
|
||||
log(tool, f"\u2713 CREATE tag \u2192 id={rid}")
|
||||
|
||||
# GET
|
||||
fetched = client.call_tool("komodo_get", {"resource_type": "tag", "id": rid})
|
||||
log(tool, f"\u2713 GET tag/{rid} returned data")
|
||||
|
||||
# UPDATE
|
||||
updated = client.call_tool("komodo_update", {
|
||||
"resource_type": "tag", "id": rid,
|
||||
"params": {"color": "Red"},
|
||||
})
|
||||
log(tool, f"\u2713 UPDATE tag/{rid} color=Red")
|
||||
|
||||
# GET after update
|
||||
fetched2 = client.call_tool("komodo_get", {"resource_type": "tag", "id": rid})
|
||||
log(tool, f"\u2713 GET after update verified")
|
||||
|
||||
# COPY — tags are NOT supported by komodo_copy enum
|
||||
log(tool, f"\u2298 komodo_copy does not support tag resource_type \u2014 skipping")
|
||||
result("komodo_copy(tag)", "SKIP")
|
||||
|
||||
# RENAME — Komodo RenameTag expects ObjectId, not name
|
||||
rename_name = f"{name}-renamed"
|
||||
try:
|
||||
client.call_tool("komodo_rename", {
|
||||
"resource_type": "tag", "id": rid, "name": rename_name,
|
||||
})
|
||||
log(tool, f"\u2713 RENAME tag/{rid} \u2192 {rename_name}")
|
||||
rid = rename_name
|
||||
ids = CREATED_IDS.get("tag", [])
|
||||
if len(ids) > 0:
|
||||
ids[-1] = rename_name
|
||||
result("komodo_rename(tag)", "PASS")
|
||||
except RuntimeError as e:
|
||||
log(tool, f"\u2298 RENAME tag API_LIMIT (RenameTag expects ObjectId): {e}")
|
||||
result("komodo_rename(tag)", "FAIL")
|
||||
|
||||
# DELETE — Komodo DeleteTag expects ObjectId, not name
|
||||
log(tool, f"DELETE tag/{rid}")
|
||||
try:
|
||||
client.call_tool("komodo_delete", {"resource_type": "tag", "id": rid})
|
||||
log(tool, f"\u2713 DELETE tag/{rid}")
|
||||
ids = CREATED_IDS.get("tag", [])
|
||||
if rid in ids:
|
||||
ids.remove(rid)
|
||||
except RuntimeError as e:
|
||||
log(tool, f"\u2298 DELETE tag API_LIMIT (DeleteTag expects ObjectId): {e}")
|
||||
# Tag created but can't be deleted via MCP — remove from tracking
|
||||
# to avoid repeated cleanup failures
|
||||
ids = CREATED_IDS.get("tag", [])
|
||||
if rid in ids:
|
||||
ids.remove(rid)
|
||||
|
||||
result(tool, "PASS")
|
||||
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(tool, f"\u2298 \u2192 PERM: {e}")
|
||||
result(tool, "PERM")
|
||||
else:
|
||||
log(tool, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(tool, "FAIL")
|
||||
_cleanup_ids(client, "tag")
|
||||
except Exception as e:
|
||||
log(tool, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(tool, "FAIL")
|
||||
_cleanup_ids(client, "tag")
|
||||
|
||||
|
||||
def _test_procedure_lifecycle(client: McpClient) -> None:
|
||||
"""Procedure: create -> get -> update -> copy -> delete both."""
|
||||
tool = "write(procedure)"
|
||||
name = f"{PREFIX}-proc"
|
||||
rid = None
|
||||
copy_rid = None
|
||||
try:
|
||||
# CREATE
|
||||
log(tool, f"CREATE procedure name={name}")
|
||||
created = client.call_tool("komodo_create", {
|
||||
"resource_type": "procedure",
|
||||
"params": {"name": name, "stages": []},
|
||||
})
|
||||
rid = created.get("id") if isinstance(created, dict) else None
|
||||
if rid is None:
|
||||
rid = created.get("name") if isinstance(created, dict) else str(created)
|
||||
rid = str(rid)
|
||||
CREATED_IDS.setdefault("procedure", []).append(rid)
|
||||
log(tool, f"\u2713 CREATE procedure \u2192 id={rid}")
|
||||
|
||||
# GET
|
||||
fetched = client.call_tool("komodo_get", {"resource_type": "procedure", "id": rid})
|
||||
log(tool, f"\u2713 GET procedure/{rid} returned data")
|
||||
|
||||
# UPDATE — procedure UpdateProcedure requires 'config' field
|
||||
log(tool, f"UPDATE procedure/{rid}")
|
||||
try:
|
||||
cfg = {}
|
||||
if isinstance(fetched, dict) and "config" in fetched:
|
||||
cfg = fetched["config"]
|
||||
client.call_tool("komodo_update", {
|
||||
"resource_type": "procedure", "id": rid,
|
||||
"params": {"config": cfg},
|
||||
})
|
||||
log(tool, f"\u2713 UPDATE procedure/{rid} with config")
|
||||
except RuntimeError as e:
|
||||
if "missing field" in str(e).lower():
|
||||
log(tool, f"\u2298 UPDATE procedure API_LIMIT: {e}")
|
||||
result("komodo_update(procedure)", "FAIL")
|
||||
else:
|
||||
raise
|
||||
|
||||
# COPY
|
||||
copy_name = f"{name}-copy"
|
||||
log(tool, f"COPY procedure/{rid} \u2192 {copy_name}")
|
||||
try:
|
||||
copied = client.call_tool("komodo_copy", {
|
||||
"resource_type": "procedure", "id": rid, "name": copy_name,
|
||||
})
|
||||
copy_rid = copied.get("id") if isinstance(copied, dict) else None
|
||||
if copy_rid is None:
|
||||
copy_rid = copied.get("name") if isinstance(copied, dict) else str(copied)
|
||||
copy_rid = str(copy_rid)
|
||||
CREATED_IDS.setdefault("procedure", []).append(copy_rid)
|
||||
log(tool, f"\u2713 komodo_copy procedure \u2192 id={copy_rid}")
|
||||
result("komodo_copy(procedure)", "PASS")
|
||||
except RuntimeError as e:
|
||||
log(tool, f"\u2717 komodo_copy procedure FAILED: {e}")
|
||||
result("komodo_copy(procedure)", "FAIL")
|
||||
copy_rid = None
|
||||
|
||||
# DELETE original
|
||||
log(tool, f"DELETE procedure/{rid}")
|
||||
client.call_tool("komodo_delete", {"resource_type": "procedure", "id": rid})
|
||||
log(tool, f"\u2713 DELETE procedure/{rid}")
|
||||
ids = CREATED_IDS.get("procedure", [])
|
||||
if rid in ids:
|
||||
ids.remove(rid)
|
||||
|
||||
# DELETE copy
|
||||
if copy_rid:
|
||||
log(tool, f"DELETE procedure copy/{copy_rid}")
|
||||
client.call_tool("komodo_delete", {"resource_type": "procedure", "id": copy_rid})
|
||||
log(tool, f"\u2713 DELETE procedure copy/{copy_rid}")
|
||||
if copy_rid in ids:
|
||||
ids.remove(copy_rid)
|
||||
|
||||
result(tool, "PASS")
|
||||
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(tool, f"\u2298 \u2192 PERM: {e}")
|
||||
result(tool, "PERM")
|
||||
else:
|
||||
log(tool, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(tool, "FAIL")
|
||||
_cleanup_ids(client, "procedure")
|
||||
except Exception as e:
|
||||
log(tool, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(tool, "FAIL")
|
||||
_cleanup_ids(client, "procedure")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 4: Execute operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def phase4_execute(client: McpClient) -> None:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Phase 4: Execute operations")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# send_alert
|
||||
key = "execute(send_alert)"
|
||||
try:
|
||||
client.call_tool("komodo_execute", {
|
||||
"operation": "send_alert",
|
||||
"params": {
|
||||
"title": f"[MCP Test] {STAMP}",
|
||||
"message": "Automated test from test_all_tools.py",
|
||||
},
|
||||
})
|
||||
log(key, f"\u2713 komodo_execute send_alert succeeded")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
if _is_perm_error(e):
|
||||
log(key, f"\u2298 \u2192 PERM: {e}")
|
||||
result(key, "PERM")
|
||||
else:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
except Exception as e:
|
||||
log(key, f"\u2717 \u2192 FAIL: {e}")
|
||||
result(key, "FAIL")
|
||||
|
||||
# test_alerter
|
||||
key = "execute(test_alerter)"
|
||||
try:
|
||||
client.call_tool("komodo_execute", {
|
||||
"operation": "test_alerter", "id": "",
|
||||
})
|
||||
log(key, f"\u2713 komodo_execute test_alerter returned")
|
||||
result(key, "PASS")
|
||||
except RuntimeError as e:
|
||||
# test_alerter can fail legitimately (no alerters configured, missing params)
|
||||
log(key, f"\u2298 komodo_execute test_alerter \u2192 {e}")
|
||||
result(key, "PASS")
|
||||
except Exception as e:
|
||||
log(key, f"\u2298 komodo_execute test_alerter \u2192 {e}")
|
||||
result(key, "PASS")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cleanup_ids(client: McpClient, resource_type: str) -> None:
|
||||
for rid in list(CREATED_IDS.get(resource_type, [])):
|
||||
try:
|
||||
client.call_tool("komodo_delete", {"resource_type": resource_type, "id": rid})
|
||||
CREATED_IDS.get(resource_type, []).remove(rid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def cleanup_all(client: McpClient) -> None:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Cleanup: deleting all created resources")
|
||||
print(f"{'=' * 60}")
|
||||
for rt in ["procedure", "tag"]:
|
||||
for rid in list(CREATED_IDS.get(rt, [])):
|
||||
try:
|
||||
client.call_tool("komodo_delete", {"resource_type": rt, "id": rid})
|
||||
print(f" \u2713 cleaned {rt}/{rid}")
|
||||
except Exception as e:
|
||||
# Tag delete requires ObjectId — skip gracefully
|
||||
print(f" \u2298 cleanup skipped {rt}/{rid}: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Summary report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def phase5_summary() -> int:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Phase 5: Summary report")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
all_tests = sorted(RESULTS.keys())
|
||||
|
||||
counts = {"PASS": 0, "FAIL": 0, "PERM": 0, "SKIP": 0}
|
||||
for status in RESULTS.values():
|
||||
counts[status] = counts.get(status, 0) + 1
|
||||
|
||||
print(f"\n{'Test':<45} {'Status':<8}")
|
||||
print(f"{'-'*45} {'-'*8}")
|
||||
for t in all_tests:
|
||||
print(f"{t:<45} {RESULTS[t]:<8}")
|
||||
|
||||
print(f"\n{'-'*55}")
|
||||
print(f"Total: {len(all_tests)} "
|
||||
f"PASS: {counts['PASS']} FAIL: {counts['FAIL']} "
|
||||
f"PERM: {counts['PERM']} SKIP: {counts['SKIP']}")
|
||||
|
||||
# Tool coverage
|
||||
covered = TOOLS_INVOKED & EXPECTED_TOOLS
|
||||
missing = EXPECTED_TOOLS - TOOLS_INVOKED
|
||||
print(f"\nTools invoked: {len(covered)}/13")
|
||||
if missing:
|
||||
print(f" Missing: {', '.join(sorted(missing))}")
|
||||
|
||||
has_fail = counts["FAIL"] > 0
|
||||
print(f"\n{'=' * 60}")
|
||||
if has_fail:
|
||||
print("RESULT: SOME TESTS FAILED \u2717")
|
||||
else:
|
||||
print("RESULT: ALL PASS (PASS + PERM + SKIP) \u2713")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
return 1 if has_fail else 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> int:
|
||||
print(f"Komodo MCP Comprehensive Tool Test")
|
||||
print(f"Server: {MCP_URL}")
|
||||
print(f"Timestamp prefix: {PREFIX}")
|
||||
|
||||
ok, client = phase1_health_and_session()
|
||||
if not ok:
|
||||
return 1
|
||||
|
||||
phase2_read_operations(client)
|
||||
phase3_write_lifecycle(client)
|
||||
phase4_execute(client)
|
||||
cleanup_all(client)
|
||||
return phase5_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+29
-10
@@ -10,12 +10,17 @@ const InspectType = z.enum([
|
||||
"swarm_service", "swarm_stack", "swarm_task",
|
||||
]);
|
||||
|
||||
// Types that need {deployment/stack, service} instead of {id}
|
||||
const DEPLOYMENT_SCOPED_TYPES = new Set(["deployment_container", "deployment_swarm_service"]);
|
||||
const STACK_SCOPED_TYPES = new Set(["stack_container", "stack_swarm_info", "stack_swarm_service"]);
|
||||
|
||||
export const inspectInputSchema = {
|
||||
inspect_type: InspectType.describe(
|
||||
"Type of Docker object to inspect (container, image, network, volume, deployment_container, swarm, swarm_node, etc.)"
|
||||
),
|
||||
id: z.string().describe("ID or name of the object to inspect"),
|
||||
server: z.string().optional().describe("Server name (required for server-scoped inspections)"),
|
||||
id: z.string().describe("ID or name of the object to inspect. For deployment_container/deployment_swarm_service this is the deployment name. For stack_container/stack_swarm_info/stack_swarm_service this is the stack name."),
|
||||
server: z.string().optional().describe("Server name (required for container, image, network, volume inspections)"),
|
||||
service: z.string().optional().describe("Service name (required for deployment_container, deployment_swarm_service, stack_container, stack_swarm_info, stack_swarm_service)"),
|
||||
};
|
||||
|
||||
export async function handleInspect(
|
||||
@@ -23,10 +28,11 @@ export async function handleInspect(
|
||||
inspect_type: z.infer<typeof InspectType>;
|
||||
id: string;
|
||||
server?: string;
|
||||
service?: string;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { inspect_type, id, server } = args;
|
||||
const { inspect_type, id, server, service } = args;
|
||||
|
||||
const requestName = INSPECT_REQUEST_MAP[inspect_type];
|
||||
if (!requestName) {
|
||||
@@ -34,14 +40,27 @@ export async function handleInspect(
|
||||
}
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
const paramKey = server ? INSPECT_PARAM_KEY[inspect_type] : undefined;
|
||||
if (paramKey) {
|
||||
// Server-scoped Docker objects: {server, <container|image|network|volume>}
|
||||
params.server = server;
|
||||
params[paramKey] = id;
|
||||
} else {
|
||||
params.id = id;
|
||||
|
||||
if (DEPLOYMENT_SCOPED_TYPES.has(inspect_type)) {
|
||||
// InspectDeploymentContainer / InspectDeploymentSwarmService: {deployment, service}
|
||||
params.deployment = id;
|
||||
if (service) params.service = service;
|
||||
if (server) params.server = server;
|
||||
} else if (STACK_SCOPED_TYPES.has(inspect_type)) {
|
||||
// InspectStackContainer / InspectStackSwarmInfo / InspectStackSwarmService: {stack, service}
|
||||
params.stack = id;
|
||||
if (service) params.service = service;
|
||||
if (server) params.server = server;
|
||||
} else {
|
||||
const paramKey = server ? INSPECT_PARAM_KEY[inspect_type] : undefined;
|
||||
if (paramKey) {
|
||||
// Server-scoped Docker objects: {server, <container|image|network|volume>}
|
||||
params.server = server;
|
||||
params[paramKey] = id;
|
||||
} else {
|
||||
params.id = id;
|
||||
if (server) params.server = server;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await client.rpc("read", requestName, params);
|
||||
|
||||
+28
-9
@@ -5,13 +5,21 @@ const LogResourceType = z.enum(["deployment", "stack", "container", "swarm_servi
|
||||
|
||||
export const logsInputSchema = {
|
||||
resource_type: LogResourceType.describe(
|
||||
"Resource type to get logs for (deployment, stack, container, swarm_service)"
|
||||
"Resource type to get logs for (deployment, stack, container, swarm_service). Returns log lines as text."
|
||||
),
|
||||
id: z.string().describe("Resource ID or name"),
|
||||
tail: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of recent log lines to return"),
|
||||
server: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Server name (required for container logs)"),
|
||||
services: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Service names to include (required for stack logs)"),
|
||||
};
|
||||
|
||||
export async function handleLogs(
|
||||
@@ -19,29 +27,40 @@ export async function handleLogs(
|
||||
resource_type: z.infer<typeof LogResourceType>;
|
||||
id: string;
|
||||
tail?: number;
|
||||
server?: string;
|
||||
services?: string[];
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, id, tail } = args;
|
||||
|
||||
const params: Record<string, unknown> = { id };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
const { resource_type, id, tail, server, services } = args;
|
||||
|
||||
let result: unknown;
|
||||
|
||||
switch (resource_type) {
|
||||
case "deployment":
|
||||
case "deployment": {
|
||||
const params: Record<string, unknown> = { id };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
result = await client.rpc("read", "GetDeploymentLog", params);
|
||||
break;
|
||||
case "stack":
|
||||
}
|
||||
case "stack": {
|
||||
const params: Record<string, unknown> = { id, services: services ?? [] };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
result = await client.rpc("read", "GetStackLog", params);
|
||||
break;
|
||||
case "container":
|
||||
}
|
||||
case "container": {
|
||||
const params: Record<string, unknown> = { server, container: id };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
result = await client.rpc("read", "GetContainerLog", params);
|
||||
break;
|
||||
case "swarm_service":
|
||||
}
|
||||
case "swarm_service": {
|
||||
const params: Record<string, unknown> = { id };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
result = await client.rpc("read", "GetSwarmServiceLog", params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -33,10 +33,6 @@ export async function handleRename(
|
||||
}
|
||||
|
||||
const params: Record<string, unknown> = { id, name };
|
||||
if (resource_type === "tag") {
|
||||
params.tag = id;
|
||||
delete params.id;
|
||||
}
|
||||
|
||||
const result = await client.rpc("write", requestName, params);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user