feat: implement institutional governance and recovery architecture
Dependency Audit / dependency-audit (push) Failing after 10s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 9s

This commit is contained in:
2026-08-01 17:46:53 +02:00
parent 3c658fa32d
commit d78b13f9d3
45 changed files with 4388 additions and 262 deletions
@@ -0,0 +1,666 @@
"""Deterministic Kubernetes export for the stateless multi-host profile."""
from __future__ import annotations
from hashlib import sha256
from pathlib import Path
import re
from typing import Any, Mapping
from urllib.parse import urlsplit
from .model import InstallationSpec, image_is_digest_pinned
_DNS_LABEL = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$")
_SECRET_KEYS = (
"MASTER_KEY_B64",
"DATABASE_URL",
"GOVOPLAN_DATABASE_URL_PGTOOLS",
"REDIS_URL",
"FILE_STORAGE_S3_ACCESS_KEY_ID",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY",
)
_CONFIG_KEYS = (
"APP_ENV",
"GOVOPLAN_INSTALL_PROFILE",
"ENABLED_MODULES",
"CELERY_ENABLED",
"CELERY_QUEUES",
"CORS_ORIGINS",
"GOVOPLAN_TRUSTED_HOSTS",
"FORWARDED_ALLOW_IPS",
"AUTH_COOKIE_SECURE",
"AUTH_COOKIE_SAMESITE",
"GOVOPLAN_HTTP_HSTS_SECONDS",
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS",
"GOVOPLAN_MIGRATION_TRACK",
"DEV_AUTO_MIGRATE_ENABLED",
"DEV_BOOTSTRAP_ENABLED",
"FILE_STORAGE_BACKEND",
"FILE_STORAGE_S3_ENDPOINT_URL",
"FILE_STORAGE_S3_REGION",
"FILE_STORAGE_S3_BUCKET",
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED",
"FILE_STORAGE_S3_ENDPOINT_TRUSTED",
)
def render_kubernetes(
spec: InstallationSpec,
environment: Mapping[str, str],
*,
namespace: str = "govoplan",
secret_name: str = "govoplan-runtime",
tls_secret_name: str = "govoplan-tls",
ingress_class_name: str | None = None,
) -> dict[str, Any]:
"""Render runtime roles only; shared state services stay externally managed."""
_validate_cluster_profile(spec, environment, namespace, secret_name)
name = _resource_name(spec.installation_id)
public_host = urlsplit(spec.public_url).hostname or "localhost"
labels = {"app.kubernetes.io/name": "govoplan", "app.kubernetes.io/instance": name}
config_name = f"{name}-runtime"
service_account = f"{name}-runtime"
config = {
key: str(environment[key])
for key in _CONFIG_KEYS
if environment.get(key) is not None
}
config.update(
{
"GOVOPLAN_INSTALLATION_ID": spec.installation_id,
"GOVOPLAN_STATE_PROFILE": "shared",
"GOVOPLAN_RUNTIME_HEARTBEAT_SECONDS": "15",
"GOVOPLAN_RUNTIME_STALE_AFTER_SECONDS": "60",
"GOVOPLAN_EXPECTED_API_REPLICAS": str(spec.replicas.api),
"GOVOPLAN_EXPECTED_WORKER_REPLICAS": str(spec.replicas.worker),
"FILE_STORAGE_BACKEND": "s3",
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "false",
"FILE_STORAGE_S3_ENDPOINT_TRUSTED": "true",
# Trust no cross-pod proxy headers by default. Operators can bind
# an exact ingress-controller CIDR through their ConfigMap overlay.
"FORWARDED_ALLOW_IPS": "127.0.0.1",
}
)
items: list[dict[str, Any]] = [
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {"name": namespace, "labels": labels},
},
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": service_account,
"namespace": namespace,
"labels": labels,
},
"automountServiceAccountToken": False,
},
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {"name": config_name, "namespace": namespace, "labels": labels},
"data": dict(sorted(config.items())),
},
_deployment(
name=f"{name}-api",
namespace=namespace,
labels=labels,
role="api",
replicas=spec.replicas.api,
image=spec.release.api_image,
command=(
"python",
"-m",
"uvicorn",
"govoplan_core.server.app:app",
"--host",
"0.0.0.0",
"--port",
"8000",
"--proxy-headers",
),
config_name=config_name,
secret_name=secret_name,
service_account=service_account,
container_port=8000,
readiness_path="/health/ready",
liveness_path="/health",
probe_host=public_host,
),
_service(
name=f"{name}-api",
namespace=namespace,
labels=labels,
role="api",
port=8000,
),
_deployment(
name=f"{name}-web",
namespace=namespace,
labels=labels,
role="web",
replicas=spec.replicas.web,
image=spec.release.web_image,
command=(),
config_name=None,
secret_name=None,
service_account=service_account,
container_port=8080,
extra_environment={"GOVOPLAN_API_UPSTREAM": f"http://{name}-api:8000"},
),
_service(
name=f"{name}-web",
namespace=namespace,
labels=labels,
role="web",
port=8080,
),
_migration_job(
name=name,
release_key=_release_key(spec),
namespace=namespace,
labels=labels,
image=spec.release.api_image,
config_name=config_name,
secret_name=secret_name,
service_account=service_account,
),
]
if spec.replicas.worker:
items.append(
_deployment(
name=f"{name}-worker",
namespace=namespace,
labels=labels,
role="worker",
replicas=spec.replicas.worker,
image=spec.release.api_image,
command=(
"python",
"-m",
"celery",
"-A",
"govoplan_core.celery_app:celery",
"worker",
"--queues",
str(environment["CELERY_QUEUES"]),
"--loglevel",
"INFO",
),
config_name=config_name,
secret_name=secret_name,
service_account=service_account,
)
)
items.append(
_deployment(
name=f"{name}-scheduler",
namespace=namespace,
labels=labels,
role="scheduler",
replicas=1,
image=spec.release.api_image,
command=(
"python",
"-m",
"govoplan_core.commands.fenced_run",
"--resource",
"scheduler:celery-beat",
"--wait-seconds",
"120",
"--",
"python",
"-m",
"celery",
"-A",
"govoplan_core.celery_app:celery",
"beat",
"--loglevel",
"INFO",
),
config_name=config_name,
secret_name=secret_name,
service_account=service_account,
)
)
if spec.replicas.api > 1:
items.append(_pod_disruption_budget(f"{name}-api", namespace, labels, "api"))
if spec.replicas.web > 1:
items.append(_pod_disruption_budget(f"{name}-web", namespace, labels, "web"))
items.append(
_ingress(
spec,
name=name,
namespace=namespace,
labels=labels,
tls_secret_name=tls_secret_name,
ingress_class_name=ingress_class_name,
)
)
return {
"apiVersion": "v1",
"kind": "List",
"metadata": {
"annotations": {
"govoplan.add-ideas.de/profile": "stateless-shared-state",
"govoplan.add-ideas.de/secret-contract": ",".join(_SECRET_KEYS),
}
},
"items": items,
}
def kubernetes_secret_contract() -> tuple[str, ...]:
return _SECRET_KEYS
def _validate_cluster_profile(
spec: InstallationSpec,
environment: Mapping[str, str],
namespace: str,
secret_name: str,
) -> None:
if not _DNS_LABEL.fullmatch(namespace) or not _DNS_LABEL.fullmatch(secret_name):
raise ValueError("Kubernetes namespace and secret names must be DNS labels")
if spec.installation_id == "govoplan-local":
raise ValueError(
"Kubernetes export requires a non-default stable installation id"
)
if spec.components.postgres.mode != "external":
raise ValueError("Kubernetes export requires external shared PostgreSQL")
if spec.components.redis.mode != "external":
raise ValueError("Kubernetes export requires external shared Redis")
if spec.components.storage.mode != "s3":
raise ValueError("Kubernetes export requires external shared S3 storage")
unpinned_images = [
label
for label, image in (
("release.api_image", spec.release.api_image),
("release.web_image", spec.release.web_image),
)
if not image_is_digest_pinned(image)
]
if unpinned_images:
raise ValueError(
"Kubernetes export requires immutable digest-pinned release images: "
+ ", ".join(unpinned_images)
)
missing = [
key for key in (*_SECRET_KEYS, "CELERY_QUEUES") if not environment.get(key)
]
if missing:
raise ValueError(
"Kubernetes runtime contract is missing: " + ", ".join(missing)
)
def _deployment(
*,
name: str,
namespace: str,
labels: dict[str, str],
role: str,
replicas: int,
image: str,
command: tuple[str, ...],
config_name: str | None,
secret_name: str | None,
service_account: str,
container_port: int | None = None,
readiness_path: str | None = None,
liveness_path: str | None = None,
probe_host: str | None = None,
extra_environment: Mapping[str, str] | None = None,
) -> dict[str, Any]:
role_labels = {**labels, "app.kubernetes.io/component": role}
environment: list[dict[str, Any]] = [
{"name": "TMPDIR", "value": "/tmp"},
{"name": "GOVOPLAN_RUNTIME_ROLE", "value": role},
{
"name": "GOVOPLAN_NODE_ID",
"valueFrom": {"fieldRef": {"fieldPath": "metadata.name"}},
},
]
environment.extend(
{"name": key, "value": value}
for key, value in sorted((extra_environment or {}).items())
)
if secret_name:
environment.extend(_secret_environment(secret_name))
container: dict[str, Any] = {
"name": role,
"image": image,
"imagePullPolicy": "IfNotPresent",
"env": environment,
"securityContext": {
"allowPrivilegeEscalation": False,
"capabilities": {"drop": ["ALL"]},
"readOnlyRootFilesystem": True,
},
"resources": {
"requests": {"cpu": "100m", "memory": "256Mi"},
"limits": {"memory": "1Gi"},
},
}
if command:
container["command"] = list(command)
if container_port is not None:
container["ports"] = [{"name": "http", "containerPort": container_port}]
if readiness_path:
container["readinessProbe"] = _http_probe(
readiness_path,
container_port or 8000,
host=probe_host,
)
if liveness_path:
container["livenessProbe"] = _http_probe(
liveness_path,
container_port or 8000,
host=probe_host,
)
pod_spec: dict[str, Any] = {
"serviceAccountName": service_account,
"automountServiceAccountToken": False,
"securityContext": {
"runAsNonRoot": True,
"seccompProfile": {"type": "RuntimeDefault"},
},
"containers": [container],
"volumes": [{"name": "tmp", "emptyDir": {}}],
"topologySpreadConstraints": [
{
"maxSkew": 1,
"topologyKey": "kubernetes.io/hostname",
"whenUnsatisfiable": "ScheduleAnyway",
"labelSelector": {"matchLabels": role_labels},
}
],
}
container["volumeMounts"] = [{"name": "tmp", "mountPath": "/tmp"}]
if config_name:
pod_spec["containers"][0]["envFrom"] = [{"configMapRef": {"name": config_name}}]
if config_name and secret_name:
pod_spec["initContainers"] = [
{
"name": "wait-for-database",
"image": image,
"imagePullPolicy": "IfNotPresent",
"command": [
"python",
"-m",
"govoplan_core.commands.wait_for_database",
"--timeout-seconds",
"900",
],
"envFrom": [{"configMapRef": {"name": config_name}}],
"env": [
{"name": "TMPDIR", "value": "/tmp"},
{"name": "GOVOPLAN_RUNTIME_ROLE", "value": "migration-wait"},
*_secret_environment(secret_name),
],
"securityContext": {
"allowPrivilegeEscalation": False,
"capabilities": {"drop": ["ALL"]},
"readOnlyRootFilesystem": True,
},
"volumeMounts": [{"name": "tmp", "mountPath": "/tmp"}],
}
]
return {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {"name": name, "namespace": namespace, "labels": role_labels},
"spec": {
"replicas": replicas,
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {"maxUnavailable": 0, "maxSurge": 1},
},
"selector": {"matchLabels": role_labels},
"template": {
"metadata": {"labels": role_labels},
"spec": pod_spec,
},
},
}
def _migration_job(
*,
name: str,
release_key: str,
namespace: str,
labels: dict[str, str],
image: str,
config_name: str,
secret_name: str,
service_account: str,
) -> dict[str, Any]:
job_labels = {**labels, "app.kubernetes.io/component": "migration"}
job_name = _name_with_suffix(name, f"migrate-{release_key}")
return {
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": job_name,
"namespace": namespace,
"labels": job_labels,
"annotations": {
"govoplan.add-ideas.de/recovery-mode": "forward-recovery",
"govoplan.add-ideas.de/backup-required": "true",
"argocd.argoproj.io/sync-wave": "-1",
},
},
"spec": {
"backoffLimit": 1,
"ttlSecondsAfterFinished": 86400,
"template": {
"metadata": {"labels": job_labels},
"spec": {
"restartPolicy": "Never",
"serviceAccountName": service_account,
"automountServiceAccountToken": False,
"securityContext": {
"runAsNonRoot": True,
"seccompProfile": {"type": "RuntimeDefault"},
},
"containers": [
{
"name": "migration",
"image": image,
"command": [
"python",
"-m",
"govoplan_core.commands.init_db",
],
"envFrom": [{"configMapRef": {"name": config_name}}],
"env": [
{"name": "TMPDIR", "value": "/tmp"},
{"name": "GOVOPLAN_RUNTIME_ROLE", "value": "migration"},
{
"name": "GOVOPLAN_NODE_ID",
"valueFrom": {
"fieldRef": {"fieldPath": "metadata.name"}
},
},
*_secret_environment(secret_name),
],
"securityContext": {
"allowPrivilegeEscalation": False,
"capabilities": {"drop": ["ALL"]},
"readOnlyRootFilesystem": True,
},
"volumeMounts": [{"name": "tmp", "mountPath": "/tmp"}],
}
],
"volumes": [{"name": "tmp", "emptyDir": {}}],
},
},
},
}
def _secret_environment(secret_name: str) -> list[dict[str, Any]]:
return [
{
"name": key,
"valueFrom": {
"secretKeyRef": {"name": secret_name, "key": key, "optional": False}
},
}
for key in _SECRET_KEYS
]
def _service(
*,
name: str,
namespace: str,
labels: dict[str, str],
role: str,
port: int,
) -> dict[str, Any]:
role_labels = {**labels, "app.kubernetes.io/component": role}
return {
"apiVersion": "v1",
"kind": "Service",
"metadata": {"name": name, "namespace": namespace, "labels": role_labels},
"spec": {
"selector": role_labels,
"ports": [{"name": "http", "port": port, "targetPort": "http"}],
},
}
def _pod_disruption_budget(
name: str,
namespace: str,
labels: dict[str, str],
role: str,
) -> dict[str, Any]:
role_labels = {**labels, "app.kubernetes.io/component": role}
return {
"apiVersion": "policy/v1",
"kind": "PodDisruptionBudget",
"metadata": {"name": name, "namespace": namespace, "labels": role_labels},
"spec": {"minAvailable": 1, "selector": {"matchLabels": role_labels}},
}
def _ingress(
spec: InstallationSpec,
*,
name: str,
namespace: str,
labels: dict[str, str],
tls_secret_name: str,
ingress_class_name: str | None,
) -> dict[str, Any]:
public = urlsplit(spec.public_url)
ingress_spec: dict[str, Any] = {
"rules": [
{
"host": public.hostname,
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": f"{name}-web",
"port": {"name": "http"},
}
},
}
]
},
}
]
}
if public.scheme == "https":
if not tls_secret_name:
raise ValueError("HTTPS Kubernetes export requires a TLS secret name")
ingress_spec["tls"] = [
{"hosts": [public.hostname], "secretName": tls_secret_name}
]
if ingress_class_name:
ingress_spec["ingressClassName"] = ingress_class_name
return {
"apiVersion": "networking.k8s.io/v1",
"kind": "Ingress",
"metadata": {"name": name, "namespace": namespace, "labels": labels},
"spec": ingress_spec,
}
def _http_probe(
path: str,
port: int,
*,
host: str | None = None,
) -> dict[str, Any]:
request: dict[str, Any] = {"path": path, "port": port}
if host:
request["httpHeaders"] = [{"name": "Host", "value": host}]
return {
"httpGet": request,
"initialDelaySeconds": 10,
"periodSeconds": 10,
"timeoutSeconds": 3,
"failureThreshold": 6,
}
def _resource_name(installation_id: str) -> str:
value = installation_id[:48].rstrip("-")
if not _DNS_LABEL.fullmatch(value):
raise ValueError("Installation id cannot be represented as a Kubernetes name")
return value
def _release_key(spec: InstallationSpec) -> str:
manifest_hash = str(spec.release.manifest_sha256 or "").strip().lower()
if re.fullmatch(r"[0-9a-f]{8,}", manifest_hash):
return manifest_hash[:8]
payload = "\0".join(
(
spec.release.version,
spec.release.api_image,
spec.release.web_image,
)
).encode("utf-8")
return sha256(payload).hexdigest()[:8]
def _name_with_suffix(name: str, suffix: str) -> str:
available = 63 - len(suffix) - 1
prefix = name[:available].rstrip("-")
candidate = f"{prefix}-{suffix}"
if not _DNS_LABEL.fullmatch(candidate):
raise ValueError("Kubernetes resource name cannot include the release suffix")
return candidate
def write_secret_creation_hint(
environment_path: Path,
*,
namespace: str,
secret_name: str,
) -> str:
keys = " ".join(f"--from-env-file={environment_path}" for _ in (0,))
return (
f"kubectl -n {namespace} create secret generic {secret_name} {keys} "
"--dry-run=client -o yaml | kubectl apply -f -"
)
__all__ = [
"kubernetes_secret_contract",
"render_kubernetes",
"write_secret_creation_hint",
]