Make the health check probe the database

/system/health always said healthy, even with the database gone, so
container healthchecks and the installer trusted a panel that could not
serve; it now answers 503 when SELECT 1 fails. It also gave anonymous
callers the canonical domain and whether secrets encryption is set up;
those details now need a valid token (the Settings page sends one).
This commit is contained in:
Juan Denis committed 2026-10-10 18:23:24 -04:00
1 parent e6e94dd23a
commit 587dcec12a
2 files changed
+75 -19

No files matched your search

+37 -19
View File
@@ -270,28 +270,46 @@ def get_network_metrics():
@system_bp.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint - no auth required"""
from app.utils.crypto import is_encryption_configured
from app.services.settings_service import SettingsService
from app.utils.domain import canonical_origin
"""Liveness + readiness, no auth required.
canonical_domain = SettingsService.get('canonical_domain', '') or ''
https_enabled = bool(SettingsService.get('canonical_https_enabled', False))
Answers 503 when the database does not respond, so container healthchecks
and the installer stop treating a panel that cannot serve as healthy.
The domain and encryption details are for signed-in callers only: they
describe the deployment to anyone who asks.
"""
from sqlalchemy import text
from flask_jwt_extended import verify_jwt_in_request
from app import db
# Staging instances run with SERVERKIT_STAGING set. Echo
# it here so the verify step can prove WHICH instance answered and the UI
# can show its STAGING banner.
staging = env_bool('SERVERKIT_STAGING')
# Staging instances run with SERVERKIT_STAGING set. Echo it here so the
# verify step can prove WHICH instance answered and the UI can show its
# STAGING banner.
body = {'service': 'serverkit-api', 'staging': env_bool('SERVERKIT_STAGING')}
try:
db.session.execute(text('SELECT 1'))
except Exception:
db.session.rollback()
current_app.logger.exception('Health check: database unreachable')
return jsonify({**body, 'status': 'unhealthy', 'database': 'unreachable'}), 503
return jsonify({
'status': 'healthy',
'service': 'serverkit-api',
'encryption_configured': is_encryption_configured(),
'canonical_domain': canonical_domain,
'canonical_https_enabled': https_enabled,
'canonical_origin': canonical_origin(canonical_domain, https_enabled) if canonical_domain else None,
'staging': staging,
}), 200
try:
signed_in = verify_jwt_in_request(optional=True) is not None and get_jwt_identity() is not None
except Exception: # an expired or revoked token is just an anonymous probe
signed_in = False
if signed_in:
from app.utils.crypto import is_encryption_configured
from app.services.settings_service import SettingsService
from app.utils.domain import canonical_origin
canonical_domain = SettingsService.get('canonical_domain', '') or ''
https_enabled = bool(SettingsService.get('canonical_https_enabled', False))
body.update({
'encryption_configured': is_encryption_configured(),
'canonical_domain': canonical_domain,
'canonical_https_enabled': https_enabled,
'canonical_origin': (canonical_origin(canonical_domain, https_enabled)
if canonical_domain else None),
})
return jsonify({**body, 'status': 'healthy'}), 200
@system_bp.route('/notices', methods=['GET'])
+38
View File
@@ -0,0 +1,38 @@
"""/system/health tells the truth and keeps deployment details to signed-in callers.
It always answered "healthy" (even with the database gone) and handed any
anonymous caller the canonical domain and whether secrets encryption is
configured.
"""
from unittest.mock import patch
from factories import headers_for, make_user
DETAILS = ('encryption_configured', 'canonical_domain', 'canonical_https_enabled', 'canonical_origin')
def test_anonymous_probe_gets_status_only(client):
body = client.get('/api/v1/system/health').get_json()
assert body['status'] == 'healthy'
assert not any(k in body for k in DETAILS)
def test_signed_in_caller_gets_the_details(app, client):
from app import db
headers = headers_for(make_user(db, 'hr_user'))
body = client.get('/api/v1/system/health', headers=headers).get_json()
assert all(k in body for k in DETAILS)
def test_bad_token_is_treated_as_anonymous(client):
resp = client.get('/api/v1/system/health', headers={'Authorization': 'Bearer nope'})
assert resp.status_code == 200
assert 'canonical_domain' not in resp.get_json()
def test_unreachable_database_is_503(app, client):
from app import db
with patch.object(db.session, 'execute', side_effect=RuntimeError('db gone')):
resp = client.get('/api/v1/system/health')
assert resp.status_code == 503
assert resp.get_json()['status'] == 'unhealthy'