Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
This commit is contained in:
@@ -1,4 +1,91 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Container control. Start, stop, restart, tail logs, and pull-and-rebuild one container —
|
||||
// the action buttons on the docker tab.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Four of the five actions are synchronous and answer within the request. pull_rebuild is
|
||||
// not: a pull can take minutes, so it spawns docker_pull_worker.php detached, returns a job
|
||||
// id immediately, and the page polls job_status until the worker writes a terminal state.
|
||||
// The job file in /tmp is the only channel between the two.
|
||||
//
|
||||
// Deliberately separate from docker.php. That endpoint moves containers between folders —
|
||||
// metadata only. This one starts and stops them. Keeping the destructive verbs in their own
|
||||
// file is what lets the UI put a confirmation in front of exactly these and not the others.
|
||||
//
|
||||
// Loads no library at all. Every operation here is a docker CLI call, and pulling in the
|
||||
// config layer would add failure modes to the endpoint most likely to be used while
|
||||
// something else is broken.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Prefers Unraid's own rebuild helper over stop/start.
|
||||
// restart and pull_rebuild both try rebuild_container first, because recreating a
|
||||
// container correctly means reapplying its full template — ports, mounts, variables. The
|
||||
// stop/start fallback exists for when that helper is absent and is explicitly the lesser
|
||||
// option.
|
||||
//
|
||||
// The image is resolved from the container, never supplied.
|
||||
// pull_rebuild reads both the current image id and the configured image reference via
|
||||
// docker inspect. The request names a container; it cannot name what to pull into it.
|
||||
//
|
||||
// Job ids are random, not sequential.
|
||||
// bin2hex(random_bytes(8)) — a job's status is readable by anyone who can guess its id,
|
||||
// so the id is not guessable.
|
||||
//
|
||||
// Every action reports docker's own output.
|
||||
// Combined stdout and stderr are returned rather than a generic failure message. When a
|
||||
// container will not start, the reason is in that text and nowhere else.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// The container name is pattern-matched and then confirmed to exist.
|
||||
// ^[a-zA-Z0-9_.-]+$ excludes every shell metacharacter, and a `docker ps -a` lookup with
|
||||
// an anchored filter must return that exact name before any container-scoped action
|
||||
// runs. The existence check is what stops a valid-looking name from reaching an action
|
||||
// at all — and it is anchored (^name$) so a prefix cannot select a different container.
|
||||
//
|
||||
// Every value interpolated into a shell command is escaped.
|
||||
// Container name, image, job file, worker path and the rebuild helper all pass through
|
||||
// escapeshellarg(), on top of the pattern check rather than instead of it.
|
||||
//
|
||||
// job_status is reachable without a container name, and is validated separately.
|
||||
// It returns before the name check, because the container it refers to may have been
|
||||
// recreated by then. Its id must match ^[0-9a-f]+$, so it cannot escape the job
|
||||
// directory or name a file outside it.
|
||||
//
|
||||
// An unknown job id reports pending, not missing.
|
||||
// The worker writes its first state after the request returns, so a poll that arrives in
|
||||
// between must not be told the job does not exist.
|
||||
//
|
||||
// The job directory is private and created on demand.
|
||||
// mkdir 0700 under /tmp — a hardcoded literal, not a config value, so no conf edit can
|
||||
// redirect these writes. /tmp is tmpfs, so abandoned jobs clear on reboot.
|
||||
//
|
||||
// pull_rebuild refuses to start without an image.
|
||||
// An empty docker inspect result aborts before the worker is spawned, so a container
|
||||
// whose image cannot be determined is never stopped in pursuit of an update.
|
||||
//
|
||||
// Unknown actions fall through to an explicit error, so a typo cannot reach a container.
|
||||
//
|
||||
// REQUEST
|
||||
// POST action=start|stop|restart name=<container>
|
||||
// POST action=logs name=<container> last 200 lines, timestamped
|
||||
// POST action=pull_rebuild name=<container> spawns the worker, returns job_id
|
||||
// POST action=job_status job_id=<hex> poll a pull_rebuild job
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":bool,"output":"<docker output>"} start / stop / restart
|
||||
// {"ok":true,"logs":"…"} logs
|
||||
// {"ok":true,"status":"started","job_id":"…"} pull_rebuild
|
||||
// {"ok":true,"status":"pending"} or the worker's job file verbatim
|
||||
// {"ok":false,"error":"invalid name"|"invalid job_id"|"Container not found"
|
||||
// |"Could not determine image"|"Unknown action"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// api/docker_pull_worker.php detached worker for the pull_rebuild path
|
||||
// docker CLI ps, logs, start, stop, inspect
|
||||
// dynamix.docker.manager rebuild_container, when present
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
|
||||
define('VV_JOB_DIR', '/tmp/varaverk_dk_jobs');
|
||||
@@ -48,7 +135,7 @@ if ($action === 'start' || $action === 'stop') {
|
||||
if ($action === 'restart') {
|
||||
$rebuild = '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/rebuild_container';
|
||||
if (is_executable($rebuild)) {
|
||||
exec($rebuild . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
|
||||
exec(escapeshellarg($rebuild) . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
|
||||
} else {
|
||||
exec('docker stop ' . escapeshellarg($name) . ' 2>&1', $o1, $rc1);
|
||||
exec('docker start ' . escapeshellarg($name) . ' 2>&1', $o2, $rc2);
|
||||
|
||||
Reference in New Issue
Block a user