Files
Juan Denis 55d8fb1e44 Read route bodies through json_object()
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.

Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
2026-10-10 20:53:43 -04:00

98 lines
4.0 KiB
Python

# Bucket: PER-APP. Reads gate on can_access_app; changes gate on
# can_edit_app.
"""A/B slot deploys. Mounted under ``/api/v1/apps``.
GET /<app_id>/slots — slots, active slot, eligibility + reasons
PUT /<app_id>/slots — {enabled}: opt in (adopts the current
container as slot a, no restart) or out
POST /<app_id>/slots/switch-back — make the standby live again (a rollback
to the release it holds: seconds while
it is warm)
GET /<app_id>/slots/compose-split — preview moving stateful compose services
into the shared <name>-data project
POST /<app_id>/slots/compose-split — {confirm: true}: do it (one announced
outage; the data stays on its volumes)
POST /<app_id>/slots/restore-db — {deployment_id}: restore the database
snapshot taken before that deployment's
release ran. Destroys later writes; an
explicit second step after a rollback.
"""
from flask import Blueprint, jsonify
from app.exceptions import ValidationError
from app.middleware.rbac import developer_required, get_current_user, viewer_required
from app.services.slot_deploy_service import SlotDeployService
from app.api._access import require_app
from app.api._request import json_object
app_slots_bp = Blueprint('app_slots', __name__)
@app_slots_bp.route('/<int:app_id>/slots', methods=['GET'])
@viewer_required
def get_slots(app_id):
app = require_app(app_id, conceal=True)
return jsonify(SlotDeployService.status(app))
@app_slots_bp.route('/<int:app_id>/slots', methods=['PUT'])
@developer_required
def set_slots(app_id):
app = require_app(app_id, write=True, conceal=True)
data = json_object(optional=True)
enabled = data.get('enabled')
if not isinstance(enabled, bool):
raise ValidationError("'enabled' must be true or false")
result = SlotDeployService.set_enabled(app, enabled)
if not result.get('success'):
return jsonify(result), 409
return jsonify(result)
@app_slots_bp.route('/<int:app_id>/slots/switch-back', methods=['POST'])
@developer_required
def switch_back(app_id):
app = require_app(app_id, write=True, conceal=True)
result = SlotDeployService.switch_back(app, user_id=get_current_user().id)
if not result.get('success'):
return jsonify(result), 409
return jsonify(result)
@app_slots_bp.route('/<int:app_id>/slots/restore-db', methods=['POST'])
@developer_required
def restore_db(app_id):
app = require_app(app_id, write=True, conceal=True)
data = json_object(optional=True)
deployment_id = data.get('deployment_id')
if not isinstance(deployment_id, int) or isinstance(deployment_id, bool):
raise ValidationError("'deployment_id' is required")
result = SlotDeployService.restore_databases(app, deployment_id)
if not result.get('success'):
return jsonify(result), 409
return jsonify(result)
@app_slots_bp.route('/<int:app_id>/slots/compose-split', methods=['GET'])
@viewer_required
def compose_split_preview(app_id):
app = require_app(app_id, conceal=True)
result = SlotDeployService.compose_split_preview(app)
if not result.get('success'):
return jsonify(result), 409
return jsonify(result)
@app_slots_bp.route('/<int:app_id>/slots/compose-split', methods=['POST'])
@developer_required
def compose_split_apply(app_id):
app = require_app(app_id, write=True, conceal=True)
data = json_object(optional=True)
if data.get('confirm') is not True:
raise ValidationError("Moving the stateful services stops the service once; send "
"{'confirm': true} after reading the preview")
result = SlotDeployService.compose_split_apply(app)
if not result.get('success'):
return jsonify(result), 409
return jsonify(result)