feat: implement institutional governance and recovery architecture
This commit is contained in:
@@ -28,6 +28,12 @@ LOCK_FILENAME = ".deployment.lock"
|
||||
RUNTIME_ENV_KEYS = (
|
||||
"APP_ENV",
|
||||
"GOVOPLAN_INSTALL_PROFILE",
|
||||
"GOVOPLAN_INSTALLATION_ID",
|
||||
"GOVOPLAN_STATE_PROFILE",
|
||||
"GOVOPLAN_RUNTIME_HEARTBEAT_SECONDS",
|
||||
"GOVOPLAN_RUNTIME_STALE_AFTER_SECONDS",
|
||||
"GOVOPLAN_EXPECTED_API_REPLICAS",
|
||||
"GOVOPLAN_EXPECTED_WORKER_REPLICAS",
|
||||
"MASTER_KEY_B64",
|
||||
"DATABASE_URL",
|
||||
"GOVOPLAN_DATABASE_URL_PGTOOLS",
|
||||
@@ -55,6 +61,7 @@ RUNTIME_ENV_KEYS = (
|
||||
"FILE_STORAGE_S3_SECRET_ACCESS_KEY",
|
||||
"FILE_STORAGE_S3_BUCKET",
|
||||
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED",
|
||||
"FILE_STORAGE_S3_ENDPOINT_TRUSTED",
|
||||
)
|
||||
|
||||
|
||||
@@ -169,6 +176,18 @@ def reconcile_runtime_environment(
|
||||
{
|
||||
"APP_ENV": "production" if spec.profile == "self-hosted" else "staging",
|
||||
"GOVOPLAN_INSTALL_PROFILE": spec.profile,
|
||||
"GOVOPLAN_INSTALLATION_ID": spec.installation_id,
|
||||
# Generated Compose replicas share one Docker host. A deliberately
|
||||
# Redis-free, single-API evaluation remains process-local; the
|
||||
# Kubernetes exporter overrides this to ``shared`` for independent
|
||||
# hosts.
|
||||
"GOVOPLAN_STATE_PROFILE": (
|
||||
"local" if redis.mode == "disabled" else "host-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),
|
||||
"ENABLED_MODULES": ",".join(spec.enabled_modules),
|
||||
"CELERY_ENABLED": "true" if redis.mode != "disabled" else "false",
|
||||
"CELERY_QUEUES": (
|
||||
@@ -198,6 +217,7 @@ def reconcile_runtime_environment(
|
||||
if storage.mode == "local":
|
||||
values["FILE_STORAGE_BACKEND"] = "local"
|
||||
values["FILE_STORAGE_S3_DEPLOYMENT_MANAGED"] = "false"
|
||||
values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"] = "false"
|
||||
values["FILE_STORAGE_LOCAL_ROOT"] = "/var/lib/govoplan/files"
|
||||
elif storage.mode == "garage":
|
||||
access_key = values.setdefault(
|
||||
@@ -221,11 +241,13 @@ def reconcile_runtime_environment(
|
||||
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": secret_key,
|
||||
"FILE_STORAGE_S3_BUCKET": bucket,
|
||||
"FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "true",
|
||||
"FILE_STORAGE_S3_ENDPOINT_TRUSTED": "false",
|
||||
}
|
||||
)
|
||||
else:
|
||||
values["FILE_STORAGE_BACKEND"] = "s3"
|
||||
values["FILE_STORAGE_S3_DEPLOYMENT_MANAGED"] = "false"
|
||||
values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"] = "true"
|
||||
required = (
|
||||
"FILE_STORAGE_S3_ENDPOINT_URL",
|
||||
"FILE_STORAGE_S3_REGION",
|
||||
@@ -243,12 +265,13 @@ def reconcile_runtime_environment(
|
||||
schemes={"http", "https"},
|
||||
label="S3 endpoint URL",
|
||||
)
|
||||
if spec.profile == "self-hosted" and endpoint.scheme != "https":
|
||||
raise ValueError("self-hosted S3 endpoint URL must use HTTPS")
|
||||
if endpoint.scheme != "https":
|
||||
raise ValueError("external S3 endpoint URL must use HTTPS")
|
||||
return dict(sorted(values.items()))
|
||||
|
||||
|
||||
def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
health_host = urlsplit(spec.public_url).hostname or "localhost"
|
||||
runtime_env = {
|
||||
"environment": _environment_references(RUNTIME_ENV_KEYS),
|
||||
}
|
||||
@@ -378,6 +401,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
|
||||
services["migrate"] = {
|
||||
**runtime_env,
|
||||
"environment": {
|
||||
**runtime_env["environment"],
|
||||
"GOVOPLAN_RUNTIME_ROLE": "migration",
|
||||
},
|
||||
"image": spec.release.api_image,
|
||||
"command": ["python", "-m", "govoplan_core.commands.init_db"],
|
||||
"restart": "no",
|
||||
@@ -387,6 +414,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
}
|
||||
services["api"] = {
|
||||
**common_runtime,
|
||||
"environment": {
|
||||
**runtime_env["environment"],
|
||||
"GOVOPLAN_RUNTIME_ROLE": "api",
|
||||
},
|
||||
"image": spec.release.api_image,
|
||||
"scale": spec.replicas.api,
|
||||
"command": [
|
||||
@@ -407,7 +438,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
"-c",
|
||||
(
|
||||
"import urllib.request;"
|
||||
"urllib.request.urlopen('http://127.0.0.1:8000/health',timeout=3)"
|
||||
"request=urllib.request.Request("
|
||||
"'http://127.0.0.1:8000/health/ready',"
|
||||
f"headers={{'Host': {health_host!r}}});"
|
||||
"urllib.request.urlopen(request,timeout=3)"
|
||||
),
|
||||
],
|
||||
"interval": "10s",
|
||||
@@ -454,6 +488,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
if spec.components.redis.mode != "disabled":
|
||||
services["worker"] = {
|
||||
**common_runtime,
|
||||
"environment": {
|
||||
**runtime_env["environment"],
|
||||
"GOVOPLAN_RUNTIME_ROLE": "worker",
|
||||
},
|
||||
"image": spec.release.api_image,
|
||||
"scale": spec.replicas.worker,
|
||||
"command": [
|
||||
@@ -474,8 +512,20 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
}
|
||||
services["scheduler"] = {
|
||||
**common_runtime,
|
||||
"environment": {
|
||||
**runtime_env["environment"],
|
||||
"GOVOPLAN_RUNTIME_ROLE": "scheduler",
|
||||
},
|
||||
"image": spec.release.api_image,
|
||||
"command": [
|
||||
"python",
|
||||
"-m",
|
||||
"govoplan_core.commands.fenced_run",
|
||||
"--resource",
|
||||
"scheduler:celery-beat",
|
||||
"--wait-seconds",
|
||||
"120",
|
||||
"--",
|
||||
"python",
|
||||
"-m",
|
||||
"celery",
|
||||
@@ -531,6 +581,7 @@ api_bind_addr = "[::]:3903"
|
||||
|
||||
|
||||
def render_load_balancer_config(spec: InstallationSpec) -> str:
|
||||
health_host = urlsplit(spec.public_url).hostname or "localhost"
|
||||
return f"""global
|
||||
log stdout format raw local0
|
||||
maxconn 4096
|
||||
@@ -572,7 +623,8 @@ frontend internal_api
|
||||
|
||||
backend api_replicas
|
||||
balance leastconn
|
||||
option httpchk GET /health
|
||||
option httpchk GET /health/ready
|
||||
http-check send hdr Host {health_host}
|
||||
http-check expect status 200
|
||||
server-template api- {spec.replicas.api} api:8000 check resolvers docker init-addr libc,none
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user