fix(dashboard): quiet docker polling errors
This commit is contained in:
@@ -371,7 +371,9 @@ def slug_from_compose_project(project: str) -> str:
|
|||||||
return project[len(COMPOSE_PROJECT_PREFIX):]
|
return project[len(COMPOSE_PROJECT_PREFIX):]
|
||||||
|
|
||||||
|
|
||||||
def list_compose_projects(*, include_stopped: bool = True) -> list[str]:
|
def list_compose_projects(
|
||||||
|
*, include_stopped: bool = True, warn_on_error: bool = True,
|
||||||
|
) -> list[str]:
|
||||||
"""All compose project names starting with `bot-bottle-`.
|
"""All compose project names starting with `bot-bottle-`.
|
||||||
`include_stopped=True` (default) runs `docker compose ls --all`
|
`include_stopped=True` (default) runs `docker compose ls --all`
|
||||||
so exited projects appear too; pass False to get only projects
|
so exited projects appear too; pass False to get only projects
|
||||||
@@ -379,7 +381,10 @@ def list_compose_projects(*, include_stopped: bool = True) -> list[str]:
|
|||||||
|
|
||||||
Returns [] on docker daemon errors or malformed output rather
|
Returns [] on docker daemon errors or malformed output rather
|
||||||
than raising — callers should treat the empty list as "no
|
than raising — callers should treat the empty list as "no
|
||||||
projects discoverable", not "no projects exist"."""
|
projects discoverable", not "no projects exist". `warn_on_error`
|
||||||
|
stays true for explicit operator commands like cleanup, but active
|
||||||
|
discovery paths set it false so dashboard refreshes don't spam
|
||||||
|
stderr while Docker Desktop is stopped."""
|
||||||
argv = ["docker", "compose", "ls", "--format", "json"]
|
argv = ["docker", "compose", "ls", "--format", "json"]
|
||||||
if include_stopped:
|
if include_stopped:
|
||||||
argv.insert(3, "--all")
|
argv.insert(3, "--all")
|
||||||
@@ -392,12 +397,14 @@ def list_compose_projects(*, include_stopped: bool = True) -> list[str]:
|
|||||||
# error from the caller's POV: no projects discoverable.
|
# error from the caller's POV: no projects discoverable.
|
||||||
return []
|
return []
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
warn(f"docker compose ls failed: {result.stderr.strip()}")
|
if warn_on_error:
|
||||||
|
warn(f"docker compose ls failed: {result.stderr.strip()}")
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
projects = json.loads(result.stdout or "[]")
|
projects = json.loads(result.stdout or "[]")
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
warn(f"docker compose ls returned malformed JSON: {e}")
|
if warn_on_error:
|
||||||
|
warn(f"docker compose ls returned malformed JSON: {e}")
|
||||||
return []
|
return []
|
||||||
names: list[str] = []
|
names: list[str] = []
|
||||||
for p in projects:
|
for p in projects:
|
||||||
@@ -409,14 +416,19 @@ def list_compose_projects(*, include_stopped: bool = True) -> list[str]:
|
|||||||
return sorted(set(names))
|
return sorted(set(names))
|
||||||
|
|
||||||
|
|
||||||
def list_active_slugs(*, include_stopped: bool = False) -> list[str]:
|
def list_active_slugs(
|
||||||
|
*, include_stopped: bool = False, warn_on_error: bool = True,
|
||||||
|
) -> list[str]:
|
||||||
"""Slugs (project name minus prefix) of currently-running
|
"""Slugs (project name minus prefix) of currently-running
|
||||||
bottles. Used by the dashboard's operator-edit verbs to choose
|
bottles. Used by the dashboard's operator-edit verbs to choose
|
||||||
a bottle to apply a config edit to."""
|
a bottle to apply a config edit to."""
|
||||||
return sorted(
|
return sorted(
|
||||||
slug for slug in (
|
slug for slug in (
|
||||||
slug_from_compose_project(p)
|
slug_from_compose_project(p)
|
||||||
for p in list_compose_projects(include_stopped=include_stopped)
|
for p in list_compose_projects(
|
||||||
|
include_stopped=include_stopped,
|
||||||
|
warn_on_error=warn_on_error,
|
||||||
|
)
|
||||||
) if slug
|
) if slug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def enumerate_active() -> list[ActiveAgent]:
|
|||||||
responsible for gating on `has_backend('docker')` if it
|
responsible for gating on `has_backend('docker')` if it
|
||||||
matters; if docker is missing the `docker ps` call below
|
matters; if docker is missing the `docker ps` call below
|
||||||
returns an empty list silently."""
|
returns an empty list silently."""
|
||||||
slugs = list_active_slugs(include_stopped=False)
|
slugs = list_active_slugs(include_stopped=False, warn_on_error=False)
|
||||||
if not slugs:
|
if not slugs:
|
||||||
return []
|
return []
|
||||||
services_by_project = _query_services_by_project()
|
services_by_project = _query_services_by_project()
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ supervise on/off).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from bot_bottle.backend import BottleSpec
|
from bot_bottle.backend import BottleSpec
|
||||||
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||||
@@ -18,6 +20,8 @@ from bot_bottle.backend.docker.compose import (
|
|||||||
COMPOSE_PROJECT_PREFIX,
|
COMPOSE_PROJECT_PREFIX,
|
||||||
bottle_plan_to_compose,
|
bottle_plan_to_compose,
|
||||||
compose_project_name,
|
compose_project_name,
|
||||||
|
list_active_slugs,
|
||||||
|
list_compose_projects,
|
||||||
slug_from_compose_project,
|
slug_from_compose_project,
|
||||||
)
|
)
|
||||||
from bot_bottle.egress import (
|
from bot_bottle.egress import (
|
||||||
@@ -455,5 +459,33 @@ class TestProjectNaming(unittest.TestCase):
|
|||||||
self.assertEqual("", slug_from_compose_project("other-project"))
|
self.assertEqual("", slug_from_compose_project("other-project"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestComposeProjectListing(unittest.TestCase):
|
||||||
|
def test_compose_ls_error_warns_by_default(self):
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"bot_bottle.backend.docker.compose.subprocess.run",
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=["docker"], returncode=1, stdout="", stderr="no daemon",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mock.patch("bot_bottle.backend.docker.compose.warn") as warn,
|
||||||
|
):
|
||||||
|
self.assertEqual([], list_compose_projects())
|
||||||
|
warn.assert_called_once_with("docker compose ls failed: no daemon")
|
||||||
|
|
||||||
|
def test_compose_ls_error_can_be_quiet_for_dashboard_polling(self):
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"bot_bottle.backend.docker.compose.subprocess.run",
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=["docker"], returncode=1, stdout="", stderr="no daemon",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mock.patch("bot_bottle.backend.docker.compose.warn") as warn,
|
||||||
|
):
|
||||||
|
self.assertEqual([], list_active_slugs(warn_on_error=False))
|
||||||
|
warn.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user