199 lines
6.5 KiB
Python
199 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
from fastapi.responses import FileResponse, PlainTextResponse
|
|
from sqlmodel import Session
|
|
|
|
from ..core.security import require_operator_request
|
|
from ..db.session import get_session
|
|
from ..services.audit_logger import log_event
|
|
from ..services.deployment_target_registry import DeploymentTargetRegistry
|
|
from ..services.maintenance_scheduler import maintenance_status
|
|
from ..services.request_metrics import (
|
|
request_metrics_openmetrics,
|
|
request_metrics_snapshot,
|
|
)
|
|
from ..services.runtime_service import (
|
|
checkpoint_sqlite,
|
|
create_sqlite_backup,
|
|
deep_health,
|
|
export_audit_summary,
|
|
export_config,
|
|
export_inventory,
|
|
export_policies,
|
|
get_sqlite_backup_path,
|
|
list_sqlite_backups,
|
|
runtime_info,
|
|
sqlite_maintenance_status,
|
|
verify_sqlite_backup,
|
|
)
|
|
|
|
router = APIRouter(prefix='/api')
|
|
|
|
|
|
class DeploymentTargetRegistrationRequest(BaseModel):
|
|
repository: str
|
|
revision: str
|
|
|
|
|
|
@router.get('/runtime/info')
|
|
def get_runtime_info(session: Session = Depends(get_session)):
|
|
return runtime_info(session)
|
|
|
|
|
|
@router.post('/admin/deployment-targets/single-container')
|
|
def admin_register_single_container_deployment_target(
|
|
payload: DeploymentTargetRegistrationRequest,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
):
|
|
"""Register one bounded legacy Actions deployment target.
|
|
|
|
This is deliberately not a generic Docker exec or host-file API. AppOps
|
|
derives the exact-source path, Compose filename/project and container name
|
|
from a canonical Jens repository identifier. The controller-scoped bridge
|
|
validates the immutable revision and Compose file directly against the bare
|
|
Gitea repository before it updates the deployment registry. Webhook secrets
|
|
and modern runnerless webhook onboarding are explicitly out of scope here.
|
|
"""
|
|
require_operator_request(request)
|
|
result = DeploymentTargetRegistry().register_single_container(
|
|
payload.repository,
|
|
payload.revision,
|
|
)
|
|
status = str(result.get('status') or 'error')
|
|
succeeded = status == 'registered'
|
|
audit = log_event(
|
|
session,
|
|
source='operator',
|
|
identity='operator',
|
|
action_id='deployment.target.register',
|
|
target=str(payload.repository).strip().lower(),
|
|
target_type='deployment-target',
|
|
risk='high',
|
|
policy_decision='allowed',
|
|
reason='Operator requested bounded single-container deployment target onboarding.',
|
|
result='success' if succeeded else 'failed',
|
|
execution_plan={
|
|
'steps': [
|
|
'validate canonical Jens repository and immutable revision',
|
|
'derive controller-owned exact-source and Compose topology',
|
|
'verify the exact revision is on main and contains docker-compose.yml',
|
|
'atomically update deployment controller projects.json',
|
|
'leave the registry unchanged on any source or validation failure',
|
|
]
|
|
},
|
|
after_state={
|
|
'registered': succeeded,
|
|
'repository': str(result.get('repository') or str(payload.repository).strip().lower()),
|
|
'revision': str(result.get('revision') or payload.revision),
|
|
'slug': str(result.get('slug') or ''),
|
|
},
|
|
redacted_metadata={
|
|
'scope': 'legacy-actions-single-container',
|
|
'webhookSecretsCreated': False,
|
|
},
|
|
)
|
|
if not succeeded:
|
|
code = 400 if status == 'invalid' else 409 if status == 'conflict' else 502
|
|
raise HTTPException(
|
|
status_code=code,
|
|
detail={
|
|
'status': status,
|
|
'reason': str(result.get('reason') or 'deployment target registration failed'),
|
|
'auditId': audit.id,
|
|
},
|
|
)
|
|
return {
|
|
'success': True,
|
|
'status': status,
|
|
'repository': result.get('repository'),
|
|
'revision': result.get('revision'),
|
|
'slug': result.get('slug'),
|
|
'composeProject': result.get('compose_project'),
|
|
'container': result.get('container'),
|
|
'auditId': audit.id,
|
|
}
|
|
|
|
|
|
@router.get('/health/deep')
|
|
def get_deep_health(session: Session = Depends(get_session)):
|
|
return deep_health(session)
|
|
|
|
|
|
@router.get('/admin/observability/requests')
|
|
def admin_request_metrics():
|
|
return request_metrics_snapshot()
|
|
|
|
|
|
@router.get('/admin/observability/metrics', response_class=PlainTextResponse)
|
|
def admin_openmetrics():
|
|
return PlainTextResponse(request_metrics_openmetrics(), media_type='text/plain; version=0.0.4')
|
|
|
|
|
|
@router.get('/admin/maintenance/status')
|
|
def admin_maintenance_status():
|
|
return maintenance_status()
|
|
|
|
|
|
@router.get('/admin/export/config')
|
|
def admin_export_config(session: Session = Depends(get_session)):
|
|
return export_config(session)
|
|
|
|
|
|
@router.get('/admin/export/policies')
|
|
def admin_export_policies(session: Session = Depends(get_session)):
|
|
return export_policies(session)
|
|
|
|
|
|
@router.get('/admin/export/inventory')
|
|
def admin_export_inventory(session: Session = Depends(get_session)):
|
|
return export_inventory(session)
|
|
|
|
|
|
@router.get('/admin/export/audit-summary')
|
|
def admin_export_audit_summary(session: Session = Depends(get_session)):
|
|
return export_audit_summary(session)
|
|
|
|
|
|
@router.get('/admin/backups/sqlite')
|
|
def admin_list_sqlite_backups():
|
|
return list_sqlite_backups()
|
|
|
|
|
|
@router.get('/admin/database/status')
|
|
def admin_database_status():
|
|
return sqlite_maintenance_status()
|
|
|
|
|
|
@router.post('/admin/database/checkpoint')
|
|
def admin_database_checkpoint(session: Session = Depends(get_session)):
|
|
return checkpoint_sqlite(session)
|
|
|
|
|
|
@router.post('/admin/backups/sqlite')
|
|
def admin_create_sqlite_backup(session: Session = Depends(get_session)):
|
|
return create_sqlite_backup(session)
|
|
|
|
|
|
@router.post('/admin/backups/sqlite/{name}/verify')
|
|
def admin_verify_sqlite_backup(name: str, session: Session = Depends(get_session)):
|
|
result = verify_sqlite_backup(session, name)
|
|
if result.get('status') == 'not_found':
|
|
raise HTTPException(status_code=404, detail='SQLite backup not found.')
|
|
return result
|
|
|
|
|
|
@router.get('/admin/backups/sqlite/{name}/download')
|
|
def admin_download_sqlite_backup(name: str):
|
|
path = get_sqlite_backup_path(name)
|
|
if not path:
|
|
raise HTTPException(status_code=404, detail='SQLite backup not found.')
|
|
return FileResponse(
|
|
path=path,
|
|
filename=path.name,
|
|
media_type='application/vnd.sqlite3',
|
|
headers={'Cache-Control': 'no-store'},
|
|
)
|