Initial public ModelForge release

This commit is contained in:
Jens
2026-09-01 21:30:16 +02:00
commit 7082ab955a
490 changed files with 104252 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
"""Generate `.env.example` and `docs/CONFIGURATION.md` from the typed settings.
Hand-maintained configuration documentation drifts, and the operator discovers the drift when a
production deployment does something the manual said it would not. Both files are generated from
`Settings` joined with `SETTING_DOCS`, and a test fails when the committed files no longer match —
so adding a setting without documenting it breaks the build rather than shipping quietly.
python scripts/generate_configuration_docs.py # write both files
python scripts/generate_configuration_docs.py --check # fail if they are stale
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path, PurePath
from types import UnionType
from typing import Any, Literal, Union, get_args, get_origin
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "backend" / "src"))
from modelforge_api.domain.configuration_reference import ( # noqa: E402
DEPLOYMENT_DOCS,
SETTING_DOCS,
Sensitivity,
)
from modelforge_api.domain.release import ( # noqa: E402
MINIMUM_POSTGRES_MAJOR,
PRODUCT_NAME,
PRODUCT_VERSION,
)
from modelforge_api.settings import Settings # noqa: E402
ENV_PREFIX = "MODELFORGE_"
#: Values a generated example must never contain. The example is committed to the repository, so a
#: real secret placed here would be published with it.
EXAMPLE_SECRET_PLACEHOLDER = ""
#: Settings a container sets for itself. Listing them in the example invites an operator to override
#: a path that only makes sense inside the image.
CONTAINER_MANAGED = frozenset(
{
"config_root",
"alembic_directory",
"build_commit",
"build_timestamp",
"build_image_digest",
"agent_protocol_version",
}
)
def _default(name: str) -> str:
field = Settings.model_fields[name]
default: Any = field.default
if default is None:
return ""
if isinstance(default, bool):
return "true" if default else "false"
if isinstance(default, PurePath):
# These are paths *inside a Linux container*, so they must render with forward slashes
# whatever platform generates the file. `str(Path("/data/backups"))` gives `\dataackups`
# on Windows, and the committed .env.example shipped exactly that — telling operators to
# point a Linux container at a Windows path. It also made the generated files differ by
# platform, so the freshness check passed on one and failed on the other.
return default.as_posix()
if callable(default): # pragma: no cover - default_factory fields
return ""
return str(default)
def _render_annotation(annotation: Any) -> str:
"""A stable, human-readable name for a setting's type.
Deliberately not `str(annotation)`: Python 3.13 moved Path into `pathlib._local`, so the same
field rendered as `pathlib.Path` on 3.12 and `pathlib._local.Path` on 3.13. The generated
reference then differed by interpreter version and the freshness check failed on whichever
machine had not produced it.
"""
simple = {
"str": "string",
"int": "integer",
"float": "number",
"bool": "boolean",
"Path": "path",
"PosixPath": "path",
"WindowsPath": "path",
"SecretStr": "secret",
"NoneType": "none",
}
origin = get_origin(annotation)
if origin is Literal:
# Comma-separated, not pipe-separated: these land in a markdown table cell, and a pipe
# there silently splits the row into extra columns.
return ", ".join(f"`{value}`" for value in get_args(annotation))
if origin in (Union, UnionType):
parts = [
_render_annotation(argument)
for argument in get_args(annotation)
if argument is not type(None)
]
rendered = " or ".join(dict.fromkeys(parts))
return f"{rendered}, optional"
if isinstance(annotation, type):
return simple.get(annotation.__name__, annotation.__name__)
return simple.get(str(annotation), str(annotation))
def _type_name(name: str) -> str:
return _render_annotation(Settings.model_fields[name].annotation)
def render_env_example() -> str:
lines = [
f"# {PRODUCT_NAME} {PRODUCT_VERSION} — configuration example",
"#",
"# Generated by scripts/generate_configuration_docs.py. Do not edit by hand.",
"# Copy to .env and fill in the values marked REQUIRED. See docs/CONFIGURATION.md.",
"#",
"# Secrets are intentionally empty here. This file is committed to the repository, so a",
"# real value placed in it would be published with the release.",
"",
]
for name, doc in SETTING_DOCS.items():
if name in CONTAINER_MANAGED:
continue
marker = " (REQUIRED in production)" if doc.required_in_production else ""
secret = doc.sensitivity is Sensitivity.SECRET
lines.append(f"# {doc.description}{marker}")
value = EXAMPLE_SECRET_PLACEHOLDER if secret else _default(name)
lines.append(f"{ENV_PREFIX}{name.upper()}={value}")
lines.append("")
lines += [
"# --------------------------------------------------------------------------",
"# Deployment variables. Read by Compose, the Node Agent and the Runtime Worker",
"# rather than by the control-plane process - an operator still has to set them.",
"# --------------------------------------------------------------------------",
"",
]
for name, doc in DEPLOYMENT_DOCS.items():
marker = " (REQUIRED in production)" if doc.required_in_production else ""
lines.append(f"# {doc.description}{marker}")
lines.append(f"{name}=")
lines.append("")
return "\n".join(lines).rstrip("\n") + "\n"
def render_configuration_doc() -> str:
required = [name for name, doc in SETTING_DOCS.items() if doc.required_in_production]
secrets = [
name for name, doc in SETTING_DOCS.items() if doc.sensitivity is Sensitivity.SECRET
]
lines = [
"# Configuration reference",
"",
f"Generated from the typed settings by `scripts/generate_configuration_docs.py` for "
f"{PRODUCT_NAME} {PRODUCT_VERSION}. Every setting the control plane reads appears here; a "
"setting added without documentation fails the build.",
"",
"All settings are environment variables with the `MODELFORGE_` prefix, read from the "
"process environment or from `.env`.",
"",
"## Required in production",
"",
"`MODELFORGE_ENV=production` turns on fail-closed startup validation. With it set, the "
"control plane refuses to start unless each of these is present and sound:",
"",
]
for name in required:
lines.append(f"- `{ENV_PREFIX}{name.upper()}` — {SETTING_DOCS[name].description}")
lines += [
"",
"Production additionally refuses: a well-known development database password, a wildcard "
"CORS origin, remote model code execution, an unwritable storage root, an unsupported "
f"schema revision, a PostgreSQL major below {MINIMUM_POSTGRES_MAJOR}, and three policy "
"combinations that cannot all hold at once.",
"",
"## Generating secrets",
"",
"ModelForge never mints its own credentials — a platform that generates its own admin "
"secret has no way to tell you it did. Generate them yourself and store them outside the "
"deployment:",
"",
"```bash",
"# Operator API key (at least 32 characters)",
"python -c \"import secrets; print(secrets.token_urlsafe(48))\"",
"",
"# Backup encryption key (base64 AES-256)",
"python -c \"import base64, os; print(base64.b64encode(os.urandom(32)).decode())\"",
"",
"# Database password",
"python -c \"import secrets; print(secrets.token_urlsafe(32))\"",
"```",
"",
"Losing the backup encryption key makes every existing backup unrecoverable. It is the one "
"value that must be stored somewhere the deployment cannot take down with it.",
"",
"## Sensitivity",
"",
f"{len(secrets)} settings are credentials. They are never logged, never written to a "
"release artefact and never echoed in an error response:",
"",
]
for name in secrets:
lines.append(f"- `{ENV_PREFIX}{name.upper()}`")
lines += ["", "## Every setting", "", "| Variable | Type | Default | Required | Sensitivity | Description |", "| --- | --- | --- | --- | --- | --- |"]
for name, doc in SETTING_DOCS.items():
default = _default(name)
rendered = f"`{default}`" if default else ""
if doc.sensitivity is Sensitivity.SECRET:
rendered = ""
lines.append(
f"| `{ENV_PREFIX}{name.upper()}` | {_type_name(name)} | {rendered} | "
f"{'yes' if doc.required_in_production else 'no'} | {doc.sensitivity} | "
f"{doc.description} |"
)
lines += [
"",
"## Deployment variables",
"",
"Read by Compose, the Node Agent and the Runtime Worker rather than by the control-plane "
"process. An operator still has to set them, so they are documented here too.",
"",
"| Variable | Required | Sensitivity | Description |",
"| --- | --- | --- | --- |",
]
for name, doc in DEPLOYMENT_DOCS.items():
lines.append(
f"| `{name}` | {'yes' if doc.required_in_production else 'no'} | "
f"{doc.sensitivity} | {doc.description} |"
)
lines += [
"",
"## Restarts",
"",
"Every setting is read at process start. Changing any of them requires restarting the "
"control plane; none is re-read from the environment while the process is running.",
"",
]
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args(argv)
targets = {
ROOT / ".env.example": render_env_example(),
ROOT / "docs" / "CONFIGURATION.md": render_configuration_doc(),
}
stale: list[str] = []
for path, content in targets.items():
current = path.read_text("utf-8") if path.is_file() else None
if current == content:
print(f" current {path.relative_to(ROOT)}")
continue
stale.append(str(path.relative_to(ROOT)))
if not args.check:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
print(f" written {path.relative_to(ROOT)}")
if args.check and stale:
print(f"stale: {', '.join(stale)} — run scripts/generate_configuration_docs.py")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())