Files
ITWorx_MCP_Hub-Public/services/docker_read_broker/server.py
T

107 lines
3.7 KiB
Python

"""Docker broker process entrypoint."""
from __future__ import annotations
import json
import os
from collections.abc import Mapping
from pathlib import Path
from packages.hub_runtime import SecretFileError, read_secret_file
from .broker import (
BrokerApp,
BrokerError,
DockerBackend,
DockerEngineBackend,
Registration,
RegistrationStore,
ServiceTokenVerifier,
make_handler,
)
def _read_secret() -> str:
path = Path(os.getenv("BROKER_TOKEN_FILE", "/run/secrets/docker_broker_service_token"))
try:
return read_secret_file(
path,
expected_uid=(
int(os.environ["SECRET_EXPECTED_UID"]) if os.getenv("SECRET_EXPECTED_UID") else None
),
expected_gid=(
int(os.environ["SECRET_EXPECTED_GID"]) if os.getenv("SECRET_EXPECTED_GID") else None
),
)
except (SecretFileError, ValueError):
raise SystemExit("required broker secret validation failed") from None
def _labels(container: Mapping[str, object]) -> Mapping[str, object]:
labels = container.get("Labels")
if isinstance(labels, Mapping):
return {str(key): value for key, value in labels.items()}
config = container.get("Config")
nested = config.get("Labels") if isinstance(config, Mapping) else None
if isinstance(nested, Mapping):
return {str(key): value for key, value in nested.items()}
return {}
def _resolve_container_id(backend: DockerBackend, compose_project: str, service: str) -> str:
matches = []
for container in backend.containers():
labels = _labels(container)
if (
labels.get("com.docker.compose.project") == compose_project
and labels.get("com.docker.compose.service") == service
):
matches.append(str(container.get("Id", "")))
if len(matches) != 1 or not matches[0]:
raise ValueError("registration target must resolve to exactly one container")
return matches[0]
def _load_registrations(backend: DockerBackend) -> RegistrationStore:
path = Path(os.getenv("BROKER_REGISTRATIONS_FILE", "/etc/itworx/registered-targets.json"))
if not path.is_file():
return RegistrationStore(())
try:
document = json.loads(path.read_text(encoding="utf-8"))
if document.get("defaultDecision") != "deny":
raise ValueError("broker registration default must deny")
registrations = []
for item in document.get("containers", []):
match = item["match"]
compose_project = str(match["composeProject"])
service = str(match["service"])
registrations.append(
Registration(
project=str(item["project"]),
environment=str(item["environment"]),
registered_id=str(item["registeredId"]),
container_id=_resolve_container_id(backend, compose_project, service),
compose_project=compose_project,
service=service,
allowed_reads=frozenset(str(value) for value in item.get("allowedReads", [])),
)
)
return RegistrationStore(registrations)
except (BrokerError, json.JSONDecodeError, KeyError, OSError, TypeError, ValueError):
raise SystemExit("broker registration validation failed") from None
if __name__ == "__main__":
from http.server import ThreadingHTTPServer
backend = DockerEngineBackend()
application = BrokerApp(
backend,
_load_registrations(backend),
ServiceTokenVerifier(_read_secret()),
)
ThreadingHTTPServer(
("0.0.0.0", int(os.getenv("BROKER_PORT", "8080"))), # noqa: S104
make_handler(application), # noqa: S104
).serve_forever()