mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 08:36:55 +09:00
72 lines
3.5 KiB
Python
72 lines
3.5 KiB
Python
"""Generic preview transaction: validate, journal, build, gate, publish; compensate failures."""
|
|
import uuid
|
|
from app import db
|
|
from app.models import Domain
|
|
from app.models.application_preview import ApplicationPreview
|
|
from app.services.git_service import GitService
|
|
from app.services.nginx_service import NginxService
|
|
from . import runtime, source
|
|
from .plan import PreviewError, validate, identity, workspace
|
|
|
|
|
|
def check_domain(preview):
|
|
domain = preview.domain.lower()
|
|
if Domain.query_active().filter(db.func.lower(Domain.name) == domain).first():
|
|
raise PreviewError('Preview domain is already assigned to an application.')
|
|
if ApplicationPreview.query.filter(
|
|
ApplicationPreview.id != preview.id,
|
|
db.func.lower(ApplicationPreview.domain) == domain,
|
|
ApplicationPreview.status != 'destroyed',
|
|
).first():
|
|
raise PreviewError('Preview domain is already assigned to another preview.')
|
|
owned = preview.get_runtime_state().get('token')
|
|
own_name = identity(preview.id, owned) if owned else None
|
|
for site in NginxService.list_sites():
|
|
if site['name'] != own_name and domain in [d.lower() for d in site.get('domains', [])]:
|
|
raise PreviewError('Preview domain is already configured in nginx.')
|
|
|
|
|
|
def provision(app, preview, settings, log_callback=None):
|
|
config = GitService.get_app_config(app.id)
|
|
request_token = preview.request_token
|
|
try:
|
|
validate(app, preview, settings, config)
|
|
check_domain(preview)
|
|
# Stop/recreate semantics are deliberate: cleanup must finish before a
|
|
# new attempt. We never lose ownership of an abandoned generation.
|
|
runtime.cleanup(preview)
|
|
state = {'token': uuid.uuid4().hex}
|
|
runtime.save(preview, state)
|
|
target = workspace(runtime.ROOT, preview.id, state['token'])
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
name = identity(preview.id, state['token'])
|
|
state['source'] = True
|
|
runtime.save(preview, state)
|
|
if log_callback:
|
|
log_callback('Preparing an isolated checkout at the requested commit.')
|
|
source.prepare(app, preview, target, config['repo_url'])
|
|
state['image'] = True
|
|
runtime.save(preview, state)
|
|
source.build(app, preview, target, name + ':preview', log_callback=log_callback)
|
|
if log_callback:
|
|
log_callback('Starting preview and checking HTTP health before publishing.')
|
|
runtime.start(app, preview, state, inherit_environment=bool(settings.inherit_environment))
|
|
db.session.refresh(preview, ['request_token', 'requested_action'])
|
|
db.session.refresh(app, ['deleted_at'])
|
|
if app.deleted_at or preview.request_token != request_token or preview.requested_action == 'destroy':
|
|
raise PreviewError('Preview request was superseded or its application was deleted.')
|
|
runtime.publish(preview, state)
|
|
preview.error_message = None
|
|
return {'ok': True, 'container_ids': [state['container_id']]}
|
|
except Exception as exc:
|
|
# Only controlled diagnostics go to the browser; host errors may carry
|
|
# credentials or environment values. The journal survives cleanup errors.
|
|
error = str(exc) if isinstance(exc, PreviewError) else 'Preview provisioning failed.'
|
|
try:
|
|
runtime.cleanup(preview)
|
|
except Exception:
|
|
error += ' Cleanup is incomplete; retry deletion before deploying again.'
|
|
preview.error_message = error
|
|
db.session.commit()
|
|
return {'ok': False, 'container_ids': [], 'error': error}
|