Files
ServiceMap-Public/backend/tests/test_v130_probe_reachability.py
T

323 lines
11 KiB
Python

from __future__ import annotations
from pathlib import Path
from app.discovery import DiscoveryManager
from app.discovery_store import DiscoveryStore
from app.docker_discovery import _kind_and_endpoint, _preflight_candidate
from app.models import (
DiscoveryCategory,
DiscoveryConfidence,
DiscoveryRunStatus,
DockerCandidate,
DockerPort,
DockerSuggestion,
HealthStatus,
MonitoringState,
ProbeEndpointCandidate,
ProbeEndpointSource,
ProbeReachability,
ProbeResult,
ServiceCreate,
ServiceKind,
)
from app.store import Store
def _manager(tmp_path: Path) -> DiscoveryManager:
store = Store(str(tmp_path / "servicemap.db"))
return DiscoveryManager(store, DiscoveryStore(store))
def _candidate(
name: str,
reference: str,
*,
project: str | None = None,
compose_service: str | None = None,
labels: dict[str, str] | None = None,
networks: list[str] | None = None,
) -> DockerCandidate:
return DockerCandidate(
container_id=reference[-12:],
name=name,
image="example/service:1",
state="running",
networks=networks or [],
ports=[],
labels=labels or {},
stable_source_ref=reference,
compose_project=project,
compose_service=compose_service,
category=DiscoveryCategory.APPLICATION,
)
def test_internal_docker_dns_is_evidence_not_an_active_probe() -> None:
kind, url, host, port, evidence = _kind_and_endpoint(
"postgres",
[DockerPort(container_port="5432/tcp")],
{},
)
assert kind == ServiceKind.TCP
assert url is None
assert host == "postgres"
assert port == 5432
assert "internal container endpoint candidate" in evidence
def test_published_port_requires_an_explicit_probe_host() -> None:
ports = [DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=1226)]
labels = {"servicemap.probe": "http"}
_, unsafe_url, unsafe_host, _, _ = _kind_and_endpoint("app", ports, labels)
kind, url, host, port, evidence = _kind_and_endpoint("app", ports, labels, "monitoring-host")
assert unsafe_url is None
assert unsafe_host == "app"
assert kind == ServiceKind.HTTP
assert url == "http://monitoring-host:1226"
assert host is None and port is None
assert "host-published endpoint" in evidence
def test_running_container_without_reachable_endpoint_is_runtime_observed(tmp_path: Path) -> None:
manager = _manager(tmp_path)
candidate = _candidate("worker", "docker:name:worker")
candidate.suggestions = [DockerSuggestion(name="worker", kind=ServiceKind.TCP, host="worker", port=9000)]
candidate.probe_candidates = [
ProbeEndpointCandidate(
source=ProbeEndpointSource.CONTAINER_INTERNAL,
kind=ServiceKind.TCP,
host="worker",
port=9000,
reachability=ProbeReachability.INTERNAL_ONLY,
)
]
payload = manager._payload_for_candidate(candidate)
assert payload.monitoring_enabled is False
assert payload.monitoring_state.value == "observed"
assert payload.probe_reachability == ProbeReachability.INTERNAL_ONLY
def test_docker_healthcheck_observation_does_not_enable_network_probe(tmp_path: Path) -> None:
manager = _manager(tmp_path)
candidate = _candidate("database", "docker:name:database")
candidate.healthcheck_present = True
candidate.probe_candidates = [
ProbeEndpointCandidate(
source=ProbeEndpointSource.DOCKER_HEALTHCHECK,
kind=ServiceKind.TCP,
reachability=ProbeReachability.VERIFIED,
confidence=DiscoveryConfidence.HIGH,
)
]
payload = manager._payload_for_candidate(candidate)
assert payload.monitoring_enabled is False
assert payload.monitoring_state.value == "docker_health"
assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK
def test_appops_application_health_is_provenance_not_component_health(tmp_path: Path) -> None:
manager = _manager(tmp_path)
candidate = _candidate("application", "docker:name:application")
candidate.healthcheck_present = True
candidate.appops_health = "healthy"
candidate.appops_app_id = "northstar"
candidate.appops_discovery_confidence = 0.9
payload = manager._payload_for_candidate(candidate)
assert payload.monitoring_enabled is False
assert payload.monitoring_state == MonitoringState.DOCKER_HEALTH
assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK
assert payload.runtime_health is None
service = manager.store.create_service(payload)
manager._record_candidate_evidence(candidate, service)
resolution = manager.store.health_resolution(service.id)
assert resolution.selected_evidence_id is None
assert resolution.evidence[0].scope.value == "application"
assert "not used as component" in resolution.explanation
def test_compose_reference_prefers_same_project_over_duplicate_stack_name(tmp_path: Path) -> None:
manager = _manager(tmp_path)
run = manager.discovery_store.start_run()
source = _candidate(
"api-a",
"docker:compose:stack-a:api",
project="stack-a",
compose_service="api",
labels={"com.docker.compose.depends_on": "postgres:service_healthy:false"},
)
postgres_a = _candidate("postgres-a", "docker:compose:stack-a:postgres", project="stack-a", compose_service="postgres")
postgres_b = _candidate("postgres-b", "docker:compose:stack-b:postgres", project="stack-b", compose_service="postgres")
suggestions = manager._relation_suggestions(run, [source, postgres_a, postgres_b], {})
assert len(suggestions) == 1
assert suggestions[0].target_ref == postgres_a.stable_source_ref
assert suggestions[0].confidence == DiscoveryConfidence.HIGH
def test_ambiguous_global_name_stays_review_only(tmp_path: Path) -> None:
manager = _manager(tmp_path)
run = manager.discovery_store.start_run()
source = _candidate(
"api",
"docker:name:api",
labels={"servicemap.depends_on": "redis"},
)
redis_a = _candidate("redis-a", "docker:compose:a:redis", project="a", compose_service="redis")
redis_b = _candidate("redis-b", "docker:compose:b:redis", project="b", compose_service="redis")
suggestions = manager._relation_suggestions(run, [source, redis_a, redis_b], {})
assert len(suggestions) == 1
assert suggestions[0].target_ref is None
assert suggestions[0].confidence == DiscoveryConfidence.LOW
assert "Manual review required" in " ".join(suggestions[0].evidence)
def test_reachability_repair_is_dry_run_first_and_preserves_probe_history(tmp_path: Path) -> None:
manager = _manager(tmp_path)
candidate = _candidate("legacy-db", "docker:name:legacy-db")
candidate.suggestions = [DockerSuggestion(name="legacy-db", kind=ServiceKind.TCP, host="legacy-db", port=5432)]
candidate.probe_candidates = [
ProbeEndpointCandidate(
source=ProbeEndpointSource.CONTAINER_INTERNAL,
kind=ServiceKind.TCP,
host="legacy-db",
port=5432,
reachability=ProbeReachability.INTERNAL_ONLY,
)
]
legacy = manager.store.create_service(
ServiceCreate(
name="legacy-db",
kind=ServiceKind.TCP,
host="legacy-db",
port=5432,
source_type="docker",
source_ref=candidate.stable_source_ref,
)
)
manager.store.save_probe(
ProbeResult(
id="legacy-probe",
service_id=legacy.id,
status=HealthStatus.DOWN,
latency_ms=None,
detail="legacy unreachable internal DNS target",
checked_at=legacy.created_at,
)
)
run = manager.discovery_store.start_run()
source = manager.discovery_store.save_source(
source_type="docker",
source_id=candidate.stable_source_ref or "",
source_name=candidate.name,
lifecycle=candidate.lifecycle,
metadata={"candidate": candidate.model_dump(mode="json")},
)
manager.discovery_store.save_observation(
run_id=run.id,
source=source,
source_name=candidate.name,
lifecycle=candidate.lifecycle,
change_type="changed",
metadata={"candidate": candidate.model_dump(mode="json")},
)
manager.discovery_store.finish_run(run.id, status=DiscoveryRunStatus.COMPLETED, source_counts={}, diff=run.diff)
preview = manager.repair_probe_reachability(run_id=run.id, dry_run=True)
assert preview.changed == 1
assert manager.store.get_service(legacy.id).monitoring_enabled is True # type: ignore[union-attr]
applied = manager.repair_probe_reachability(run_id=run.id, dry_run=False)
updated = manager.store.get_service(legacy.id)
assert applied.disabled_unsafe == 1
assert updated is not None and updated.monitoring_enabled is False
assert updated.monitoring_state.value == "observed"
assert len(manager.store.probe_history(legacy.id)) == 1
def test_container_http_port_is_not_misclassified_by_a_custom_host_port() -> None:
kind, url, host, port, evidence = _kind_and_endpoint(
"adminer",
[DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=8070)],
{},
"127.0.0.1",
)
assert kind == ServiceKind.HTTP
assert url == "http://127.0.0.1:8070"
assert host is None and port is None
assert "HTTP-like container port" in evidence
def test_published_preflight_requires_a_real_tcp_connection(monkeypatch) -> None:
candidate = ProbeEndpointCandidate(
source=ProbeEndpointSource.PUBLISHED_PORT,
kind=ServiceKind.TCP,
host="127.0.0.1",
port=3306,
)
class _Connection:
def __enter__(self):
return self
def __exit__(self, *_: object) -> None:
return None
monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection())
verified = _preflight_candidate(candidate)
assert verified.primary is True
assert verified.reachability == ProbeReachability.VERIFIED
assert verified.preflight_detail == "TCP connect succeeded"
def test_http_preflight_records_response_without_scanning_paths(monkeypatch) -> None:
candidate = ProbeEndpointCandidate(
source=ProbeEndpointSource.PUBLISHED_PORT,
kind=ServiceKind.HTTP,
url="http://127.0.0.1:8070",
)
class _Connection:
def __enter__(self):
return self
def __exit__(self, *_: object) -> None:
return None
class _Response:
status_code = 204
class _Client:
def __enter__(self):
return self
def __exit__(self, *_: object) -> None:
return None
def request(self, method: str, url: str) -> _Response:
assert method == "HEAD"
assert url == candidate.url
return _Response()
monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection())
monkeypatch.setattr("app.docker_discovery.httpx.Client", lambda **_kwargs: _Client())
verified = _preflight_candidate(candidate)
assert verified.primary is True
assert verified.preflight_detail == "HTTP 204 responded"