feat(platform): tini as PID 1, Hypercorn, a log-level setting, the image on CI's Python, an airgapped boot gate before push (#5159)
- Dockerfile: python:3.14-slim, the Python CI tests on (was 3.13). tini from apt is PID 1 (ENTRYPOINT tini -- entrypoint.sh): it reaps the zombies ssh and ansible leave and forwards SIGTERM. - cli: Hypercorn's serve() with one worker (run, stream and scheduler state is in-process), a 10 s graceful timeout, and SIGTERM/SIGINT as the shutdown trigger. Access and error lines go through Steward's logging. --debug keeps Quart's reloading dev server. - Logging: steward/core/logging_setup.py installs one root handler before anything logs (until now a library's basicConfig on import was the only configuration). Its level is the `log.level` setting (DEBUG/INFO/WARNING/ ERROR, default INFO), applied at boot and on save in Settings → General. - CI publish: build, then "The image boots with no internet" (rule 164, snippet #3552), then push, so a broken image never reaches a tag (rule 177). On an --internal network with its own Postgres, the image must migrate and answer /health, run tini as PID 1, serve first-run setup with its assets from /static and no third-party host, and exit 0 on SIGTERM. Logs and clean-up run always. Tests: tests/core/test_logging_setup.py; the smoke step itself. Not in this commit: the dependency lockfile (it conflicts with rule 154 and is waiting on the operator). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from quart import Blueprint, current_app, render_template, request, redirect, ur
|
||||
|
||||
from steward.auth.middleware import require_role
|
||||
from steward.core.audit import audit_request, log_audit
|
||||
from steward.core.logging_setup import LEVELS as LOG_LEVELS, set_log_level
|
||||
from steward.models.users import UserRole
|
||||
from steward.core.settings import (
|
||||
get_all_settings, set_setting, delete_setting,
|
||||
@@ -93,7 +94,8 @@ async def index():
|
||||
async def general():
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
settings = await get_all_settings(db)
|
||||
return await render_template("settings/general.html", settings=settings)
|
||||
return await render_template("settings/general.html", settings=settings,
|
||||
log_levels=LOG_LEVELS)
|
||||
|
||||
|
||||
@settings_bp.post("/general/")
|
||||
@@ -117,6 +119,7 @@ async def save_general():
|
||||
int(form.get("data.retention_days", 90)))
|
||||
await set_setting(db, "monitors.poll_interval_seconds",
|
||||
int(form.get("monitors.poll_interval_seconds", 60)))
|
||||
await set_setting(db, "log.level", set_log_level(form.get("log.level")))
|
||||
await _reload_app_config()
|
||||
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
|
||||
"settings.saved", detail={"section": "general"})
|
||||
|
||||
Reference in New Issue
Block a user