mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
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:
1 parent
e6e94dd23a
commit
587dcec12a
2 files changed
+75
-19
No files matched your search
+37
-19
@@ -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'])
|
||||
|
||||
@@ -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'
|
||||
Reference in new issue
Block a user