mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 21:43:05 +09:00
Plan 75 §G1, the remaining 33 files. Every raw subprocess call site in
backend/app that can go through a door now does. What is left is what
should be:
8 app/utils/system.py the doors themselves
8 database_service mysqldump|gzip, gunzip|mysql, pg_dump|gzip,
gunzip|psql — a pipe is two processes
7 build/docker/db_sync/ftp streaming output, gzip pipes
1 log_service tail -f
2 serverkit-analytics sibling repo; see below
Extensions were migrated in BOTH trees. builtin-extensions/ is the source
and backend/app/plugins/ the live copy, compared by normalised hash;
test_builtin_extension_drift stays green. serverkit-email has both sides
tracked, serverkit-ftp and serverkit-cloud-provision only the source.
serverkit-analytics (2 calls) is deliberately untouched: it has no source
in builtin-extensions/ at all, and backend/app/plugins/serverkit-analytics
is a gitignored materialised copy. Editing it would be a change that
cannot be committed and is overwritten on the next sync — that fix belongs
in the serverkit-analytics repo.
Exit-code semantics are preserved wherever they carry meaning, with a
comment at each site, because they are the thing a mechanical migration
destroys: clamscan 1 = INFECTED, grype 1 = vulnerabilities FOUND, yara
1 = matches, cron_runner 127 = never launched. Those read
result['returncode'], never result['success'].
run_checked gained two things real call sites needed: it yields when the
caller directs the streams itself (writing straight to a log file), rather
than adding capture_output on top and raising ValueError; and text/errors
pass through, so site_importers/ssh keeps decoding with errors='replace'
instead of gaining a UnicodeDecodeError on a bad byte.
A regression this migration introduced, caught by the suite and fixed:
install_profile_service._docker_usable() is deliberately TRI-STATE —
None means the probe could not run, because inside LXC the docker client
is usually present while the daemon never started, and callers "must not
treat a broken probe as proof of absence". Writing it as
run_checked(...)['success'] turned an exec failure into False, i.e. "docker
is not usable" — a fact nobody established. That is the §A violation this
plan exists to remove, reintroduced by the migration meant to prevent it.
test_probe_honesty_doctor caught it. Every other run_checked(...)['success']
caller was then audited; the rest are genuinely two-state, and their
originals also returned False on a missing binary.
Seven more slips of the same shape were caught before running by an AST
check for `x = run_checked(...)` followed by `x.stdout/.stderr/.returncode`
— a dict read as an object, which raises only on the failure path, so
happy-path tests stay green.
api_controller_boundary_baseline.json: pure deletion. Routing api/docker.py
through DockerService removed the last 6 subprocess crossings, so the
`subprocess` category is gone from the API layer entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
2.3 KiB
Python
53 lines
2.3 KiB
Python
"""One-shot appliance first-boot bootstrap (plan 35).
|
|
|
|
Some appliances generate a config/certificate tree exactly once, on first run.
|
|
Abusing a compose init container leaves it behind; abusing an entrypoint reruns
|
|
it every deploy. Instead this runs the declared ``bootstrap.command`` ONCE via
|
|
``docker compose run --rm <service> <command>`` — after the volumes exist and
|
|
before the first ``up`` — and flips ``Application.bootstrap_done`` on success so
|
|
it never re-runs. Failure is a visible, retryable apply-step failure.
|
|
|
|
The runner is injectable (``set_runner``) so the apply engine can be proven
|
|
without Docker.
|
|
"""
|
|
|
|
import shlex
|
|
from typing import Any, Callable, Dict, Optional
|
|
|
|
|
|
class BootstrapService:
|
|
|
|
_runner: Optional[Callable] = None # test seam
|
|
|
|
@classmethod
|
|
def set_runner(cls, fn: Optional[Callable]) -> None:
|
|
"""Install/clear an injected runner ``fn(app, service, command, timeout)``
|
|
returning ``{'success': bool, 'output'|'error': str}``."""
|
|
cls._runner = fn
|
|
|
|
@classmethod
|
|
def run_once(cls, app, command: str, timeout_seconds: Optional[int] = None,
|
|
service: Optional[str] = None) -> Dict[str, Any]:
|
|
service = service or app.name
|
|
if cls._runner is not None:
|
|
return cls._runner(app, service, command, timeout_seconds)
|
|
return cls._docker_run(app, service, command, timeout_seconds)
|
|
|
|
@staticmethod
|
|
def _docker_run(app, service: str, command: str,
|
|
timeout_seconds: Optional[int]) -> Dict[str, Any]:
|
|
from app.utils.system import run_checked
|
|
root = getattr(app, 'root_path', None)
|
|
if not root:
|
|
return {'success': False,
|
|
'error': 'app has no deployed compose project to run bootstrap in'}
|
|
cmd = ['docker', 'compose', 'run', '--rm', service] + shlex.split(command)
|
|
proc = run_checked(cmd, cwd=root, timeout=timeout_seconds or 300)
|
|
if proc['returncode'] is None:
|
|
# timed out, or docker is absent
|
|
return {'success': False, 'error': proc['error']}
|
|
if not proc['success']:
|
|
return {'success': False,
|
|
'error': (proc['error'] or proc['output'] or 'bootstrap failed')[:500]}
|
|
return {'success': True, 'output': proc['output'].strip()[:500]}
|