107 lines
3.7 KiB
Python
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()
|