mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 00:19:14 +00:00
121 lines
5.0 KiB
Python
121 lines
5.0 KiB
Python
"""Regression test for the Docker Compose default published bind address.
|
|
|
|
``README.md`` documents DeerFlow as being deployed by default "in a local
|
|
trusted environment (accessible only via the 127.0.0.1 loopback interface)",
|
|
but the shipped compose files published the nginx entry as
|
|
``"${PORT:-2026}:2026"``, which Docker binds to ``0.0.0.0`` (and ``[::]``). The
|
|
shipped artifact therefore did not match its own documented default, and an
|
|
operator running it on a LAN or cloud host got a wider surface than the docs
|
|
implied without changing anything.
|
|
|
|
The Gateway itself binds ``0.0.0.0`` inside the container on purpose (nginx has
|
|
to reach it over the compose network) and its port is deliberately not
|
|
published, so the published nginx port is the whole external surface. This test
|
|
pins the loopback default there while keeping it overridable for operators who
|
|
intentionally expose the stack behind their own TLS/auth front door.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
COMPOSE_PATHS = {
|
|
"prod": REPO_ROOT / "docker" / "docker-compose.yaml",
|
|
"dev": REPO_ROOT / "docker" / "docker-compose-dev.yaml",
|
|
}
|
|
|
|
EXPECTED_NGINX_PORT_MAPPING = "${BIND_HOST:-127.0.0.1}:${PORT:-2026}:2026"
|
|
|
|
|
|
def _published_ports(compose_path: Path) -> dict[str, list[str]]:
|
|
"""Return {service_name: [port mapping, ...]} for every published port."""
|
|
compose = yaml.safe_load(compose_path.read_text(encoding="utf-8"))
|
|
published: dict[str, list[str]] = {}
|
|
for service_name, service in (compose.get("services") or {}).items():
|
|
ports = service.get("ports") if isinstance(service, dict) else None
|
|
if not ports:
|
|
continue
|
|
published[service_name] = [str(entry) for entry in ports]
|
|
return published
|
|
|
|
|
|
@pytest.mark.parametrize("variant", sorted(COMPOSE_PATHS))
|
|
def test_nginx_entry_defaults_to_loopback(variant: str):
|
|
"""With BIND_HOST unset, the entry port must bind 127.0.0.1, not 0.0.0.0."""
|
|
published = _published_ports(COMPOSE_PATHS[variant])
|
|
|
|
assert published.get("nginx") == [EXPECTED_NGINX_PORT_MAPPING], f"{variant} compose must publish nginx as {EXPECTED_NGINX_PORT_MAPPING!r}; got: {published.get('nginx')!r}"
|
|
|
|
|
|
@pytest.mark.parametrize("variant", sorted(COMPOSE_PATHS))
|
|
def test_no_service_publishes_on_all_interfaces(variant: str):
|
|
"""No compose service may publish a port without an explicit bind address.
|
|
|
|
A bare ``"HOST:CONTAINER"`` mapping binds every interface. Any port added
|
|
later must either stay internal to the compose network or opt in to the
|
|
same ``BIND_HOST`` default.
|
|
"""
|
|
offenders: list[str] = []
|
|
for service_name, mappings in _published_ports(COMPOSE_PATHS[variant]).items():
|
|
for mapping in mappings:
|
|
# A bind address is present only when the mapping has three
|
|
# colon-separated parts (``ADDR:HOST:CONTAINER``). Variable
|
|
# substitutions such as ``${PORT:-2026}`` also contain colons, so
|
|
# count separators outside ``${...}`` instead of splitting naively.
|
|
if _bind_address(mapping) is None:
|
|
offenders.append(f"{service_name}: {mapping}")
|
|
|
|
assert not offenders, f"{variant} compose publishes ports on all interfaces (add a bind address): {offenders}"
|
|
|
|
|
|
@pytest.mark.parametrize("variant", sorted(COMPOSE_PATHS))
|
|
def test_bind_address_remains_overridable(variant: str):
|
|
"""Operators fronting the stack themselves must be able to widen the bind."""
|
|
mapping = _published_ports(COMPOSE_PATHS[variant])["nginx"][0]
|
|
|
|
assert _bind_address(mapping) == "${BIND_HOST:-127.0.0.1}", f"{variant} compose must keep the bind address overridable via BIND_HOST; got: {mapping!r}"
|
|
|
|
|
|
def test_dev_frontend_allows_default_loopback_origins():
|
|
"""The Docker dev frontend must hydrate on its default published hosts."""
|
|
compose = yaml.safe_load(COMPOSE_PATHS["dev"].read_text(encoding="utf-8"))
|
|
environment = compose["services"]["frontend"]["environment"]
|
|
|
|
assert "DEER_FLOW_DEV_ALLOWED_ORIGINS=${DEER_FLOW_DEV_ALLOWED_ORIGINS:-127.0.0.1,::1}" in environment
|
|
|
|
|
|
def _bind_address(mapping: str) -> str | None:
|
|
"""Return the bind-address segment of a compose port mapping, if any.
|
|
|
|
Splits on ``:`` at nesting depth zero so ``${PORT:-2026}`` is treated as a
|
|
single segment rather than two.
|
|
"""
|
|
segments: list[str] = []
|
|
current: list[str] = []
|
|
depth = 0
|
|
index = 0
|
|
while index < len(mapping):
|
|
char = mapping[index]
|
|
if mapping.startswith("${", index):
|
|
depth += 1
|
|
current.append("${")
|
|
index += 2
|
|
continue
|
|
if char == "}" and depth > 0:
|
|
depth -= 1
|
|
elif char == ":" and depth == 0:
|
|
segments.append("".join(current))
|
|
current = []
|
|
index += 1
|
|
continue
|
|
current.append(char)
|
|
index += 1
|
|
segments.append("".join(current))
|
|
|
|
# ADDR:HOST:CONTAINER -> bound; HOST:CONTAINER or CONTAINER -> unbound.
|
|
return segments[0] if len(segments) >= 3 else None
|