GRM-17: fix docker inspect Jinja2 conflict and add per-iteration exception handling
- docker inspect -f "{{.State.Status}}" used Go template braces that
conflicted with Ansible Jinja2 templating in the shell module.
Ansible tried to parse {{.State.Status}} as a Jinja2 variable (which
starts with a dot, making it invalid), causing a local template error.
The outer except Exception caught this immediately, so the fallback
loop never reached the legacy container name or systemctl checks.
- Replaced with: docker inspect <name> | python3 -c JSON parsing,
which avoids any brace syntax and uses python3 (already required by
Ansible on managed nodes).
- Added per-iteration try/except inside the fallback loop so a failure
on one container name continues to the next fallback instead of
aborting the entire check.
- Added tests for fallback behavior and binary mode exception path.
128 tests, 100% coverage, ruff + pyright clean
This commit is contained in:
@@ -295,27 +295,36 @@ class RunnerManager:
|
|||||||
lines = [ln for ln in stdout.splitlines() if ln.strip() and not any(p in ln for p in ansible_noise)]
|
lines = [ln for ln in stdout.splitlines() if ln.strip() and not any(p in ln for p in ansible_noise)]
|
||||||
return lines[-1].strip() if lines else "unknown"
|
return lines[-1].strip() if lines else "unknown"
|
||||||
|
|
||||||
try:
|
service_status = "unknown"
|
||||||
if mode == "docker":
|
if mode == "docker":
|
||||||
# Try expected container name first, then host-based fallback for legacy installs
|
# Try expected container name first, then host-based fallback for legacy installs.
|
||||||
for container_name in (f"gitea-runner-{name}", f"gitea-runner-{host}"):
|
# Use python3 JSON parsing to avoid Jinja2 brace conflicts in Ansible shell args.
|
||||||
|
for container_name in (f"gitea-runner-{name}", f"gitea-runner-{host}"):
|
||||||
|
try:
|
||||||
stdout = self._executor.run_ad_hoc(
|
stdout = self._executor.run_ad_hoc(
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
key,
|
key,
|
||||||
"shell",
|
"shell",
|
||||||
f"docker inspect -f '{{{{.State.Status}}}}' {container_name} 2>/dev/null",
|
(
|
||||||
|
f"docker inspect {container_name} 2>/dev/null | "
|
||||||
|
f"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" "
|
||||||
|
f"2>/dev/null || echo unknown"
|
||||||
|
),
|
||||||
become=True,
|
become=True,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
service_status = _parse_status(stdout)
|
except Exception:
|
||||||
if service_status != "unknown":
|
continue
|
||||||
docker_map = {"running": "active", "exited": "inactive", "dead": "failed"}
|
status = _parse_status(stdout)
|
||||||
service_status = docker_map.get(service_status, "unknown")
|
if status != "unknown":
|
||||||
break
|
docker_map = {"running": "active", "exited": "inactive", "dead": "failed"}
|
||||||
else:
|
service_status = docker_map.get(status, "unknown")
|
||||||
# Final fallback: check systemd service (for installs using the fixed template)
|
break
|
||||||
|
else:
|
||||||
|
# Final fallback: check systemd service (for installs using the fixed template)
|
||||||
|
try:
|
||||||
stdout = self._executor.run_ad_hoc(
|
stdout = self._executor.run_ad_hoc(
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
@@ -327,7 +336,10 @@ class RunnerManager:
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
service_status = _parse_status(stdout)
|
service_status = _parse_status(stdout)
|
||||||
else:
|
except Exception:
|
||||||
|
service_status = "unknown"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
stdout = self._executor.run_ad_hoc(
|
stdout = self._executor.run_ad_hoc(
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
@@ -339,8 +351,8 @@ class RunnerManager:
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
service_status = _parse_status(stdout)
|
service_status = _parse_status(stdout)
|
||||||
except Exception:
|
except Exception:
|
||||||
service_status = "unknown"
|
service_status = "unknown"
|
||||||
# Translate known status values
|
# Translate known status values
|
||||||
translated_status = _(
|
translated_status = _(
|
||||||
service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown"
|
service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown"
|
||||||
|
|||||||
@@ -412,7 +412,11 @@ class TestRunnerManager:
|
|||||||
"ubuntu",
|
"ubuntu",
|
||||||
"/key",
|
"/key",
|
||||||
"shell",
|
"shell",
|
||||||
"docker inspect -f '{{.State.Status}}' gitea-runner-r1 2>/dev/null",
|
(
|
||||||
|
"docker inspect gitea-runner-r1 2>/dev/null | "
|
||||||
|
"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" "
|
||||||
|
"2>/dev/null || echo unknown"
|
||||||
|
),
|
||||||
become=True,
|
become=True,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
check=False,
|
check=False,
|
||||||
@@ -442,6 +446,19 @@ class TestRunnerManager:
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_list_runners_binary_exception(self) -> None:
|
||||||
|
mock_registry = MagicMock()
|
||||||
|
mock_registry.list.return_value = {
|
||||||
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "binary"},
|
||||||
|
}
|
||||||
|
manager = RunnerManager(registry=mock_registry)
|
||||||
|
mock_executor = MagicMock()
|
||||||
|
mock_executor.run_ad_hoc.side_effect = Exception("ssh fail")
|
||||||
|
manager._executor = mock_executor
|
||||||
|
|
||||||
|
runners = manager.list_runners()
|
||||||
|
assert runners[0]["status"] == "unknown"
|
||||||
|
|
||||||
def test_list_runners_empty(self) -> None:
|
def test_list_runners_empty(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {}
|
mock_registry.list.return_value = {}
|
||||||
@@ -461,6 +478,21 @@ class TestRunnerManager:
|
|||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
assert runners[0]["status"] == "inactive"
|
assert runners[0]["status"] == "inactive"
|
||||||
|
|
||||||
|
def test_list_runners_docker_fallback_to_host_container(self) -> None:
|
||||||
|
mock_registry = MagicMock()
|
||||||
|
mock_registry.list.return_value = {
|
||||||
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||||
|
}
|
||||||
|
manager = RunnerManager(registry=mock_registry)
|
||||||
|
mock_executor = MagicMock()
|
||||||
|
# First call (name-based) returns unknown, second (host-based) returns running
|
||||||
|
mock_executor.run_ad_hoc.side_effect = ["unknown", "running"]
|
||||||
|
manager._executor = mock_executor
|
||||||
|
|
||||||
|
runners = manager.list_runners()
|
||||||
|
assert runners[0]["status"] == "active"
|
||||||
|
assert mock_executor.run_ad_hoc.call_count == 2
|
||||||
|
|
||||||
def test_list_runners_unknown_status(self) -> None:
|
def test_list_runners_unknown_status(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
@@ -474,6 +506,21 @@ class TestRunnerManager:
|
|||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
assert runners[0]["status"] == "unknown"
|
assert runners[0]["status"] == "unknown"
|
||||||
|
|
||||||
|
def test_list_runners_docker_first_command_falls_back(self) -> None:
|
||||||
|
"""When the first docker inspect raises, the loop continues to the next fallback."""
|
||||||
|
mock_registry = MagicMock()
|
||||||
|
mock_registry.list.return_value = {
|
||||||
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
||||||
|
}
|
||||||
|
manager = RunnerManager(registry=mock_registry)
|
||||||
|
mock_executor = MagicMock()
|
||||||
|
mock_executor.run_ad_hoc.side_effect = [Exception("ssh fail"), "running"]
|
||||||
|
manager._executor = mock_executor
|
||||||
|
|
||||||
|
runners = manager.list_runners()
|
||||||
|
assert runners[0]["status"] == "active"
|
||||||
|
assert mock_executor.run_ad_hoc.call_count == 2
|
||||||
|
|
||||||
def test_list_runners_ad_hoc_error(self) -> None:
|
def test_list_runners_ad_hoc_error(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
|
|||||||
Reference in New Issue
Block a user