normalized array of containers for the table view * inspect GET (&id=...) -> raw inspect JSON, for a detail dialog * start POST {"id": "..."} * stop POST {"id": "...", "timeout": 10} * restart POST {"id": "...", "timeout": 10} * remove POST {"id": "...", "force": false} * pause POST {"id": "..."} * unpause POST {"id": "..."} * kill POST {"id": "...", "signal": "SIGKILL"} * rename POST {"id": "...", "name": "..."} * logs GET (&id=...&tail=200) -> plain text * create POST {"image": "...", "name": "...", "networkMode": "bridge"|"host"|"none"|"", * "ports": [{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}], * "volumes": [{"kind": "named"|"path", "source": "myvol"|"/mnt/...", "containerPath": "/data"}], * "env": [{"key": "...", "value": "..."}], "restartPolicy": "no", * "privileged": false, "startAfterCreate": true} */ declare(strict_types=1); require __DIR__ . '/../include/bootstrap.php'; $action = $_GET['action'] ?? ''; switch ($action) { case 'list': podman_json_response(containers_list($client)); break; case 'inspect': $id = (string) ($_GET['id'] ?? ''); if ($id === '') { podman_json_error('Missing id', 400); } podman_json_response($client->inspectContainer($id)); break; case 'logs': $id = (string) ($_GET['id'] ?? ''); if ($id === '') { podman_json_error('Missing id', 400); } $tail = (int) ($_GET['tail'] ?? 200); podman_json_response(['text' => $client->containerLogs($id, $tail)]); break; case 'start': $body = podman_read_json_body(); $client->startContainer(require_id($body)); podman_json_response(['status' => 'started']); break; case 'stop': $body = podman_read_json_body(); $client->stopContainer(require_id($body), (int) ($body['timeout'] ?? $podmanConfig->stopTimeoutSeconds)); podman_json_response(['status' => 'stopped']); break; case 'restart': $body = podman_read_json_body(); $client->restartContainer(require_id($body), (int) ($body['timeout'] ?? $podmanConfig->stopTimeoutSeconds)); podman_json_response(['status' => 'restarted']); break; case 'remove': $body = podman_read_json_body(); $client->removeContainer(require_id($body), (bool) ($body['force'] ?? false)); podman_json_response(['status' => 'removed']); break; case 'pause': $body = podman_read_json_body(); $client->pauseContainer(require_id($body)); podman_json_response(['status' => 'paused']); break; case 'unpause': $body = podman_read_json_body(); $client->unpauseContainer(require_id($body)); podman_json_response(['status' => 'unpaused']); break; case 'kill': $body = podman_read_json_body(); $client->killContainer(require_id($body), (string) ($body['signal'] ?? 'SIGKILL')); podman_json_response(['status' => 'killed']); break; case 'rename': $body = podman_read_json_body(); $newName = trim((string) ($body['name'] ?? '')); if ($newName === '') { podman_json_error('Missing name in request body', 400); } $client->renameContainer(require_id($body), $newName); podman_json_response(['status' => 'renamed']); break; case 'create': $body = podman_read_json_body(); $image = trim((string) ($body['image'] ?? '')); if ($image === '') { podman_json_error('Missing image in request body', 400); } $spec = build_container_spec($image, $body); // Unlike `podman run`, /containers/create does NOT auto-pull a // missing image — it fails outright with a 404 "no such image" // (found by live-testing the Create Container form against a // freshly-typed image reference that wasn't pulled yet). Retry // once after an explicit pull rather than always pulling // up-front, so re-creating with an image the user already has // stays fast and offline-friendly. try { $id = $client->createContainer($spec); } catch (PodmanApiException $e) { if ($e->httpStatus !== 404) { throw $e; } $client->pullImage($image); $id = $client->createContainer($spec); } if ($body['startAfterCreate'] ?? true) { $client->startContainer($id); } podman_json_response(['id' => $id, 'status' => ($body['startAfterCreate'] ?? true) ? 'started' : 'created']); break; default: podman_json_error("Unknown action '{$action}'", 400); } /** * Builds a libpod SpecGenerator body (POST /containers/create) from the * WebUI's Create Container form fields. Field names/shapes here * (portmappings, netns, networks, mounts, volumes, restart_policy) were * verified live against a real podman system service — see * PodmanClient::createContainer()'s header comment. * * @param array $body * @return array */ function build_container_spec(string $image, array $body): array { $spec = ['image' => $image]; $name = trim((string) ($body['name'] ?? '')); if ($name !== '') { $spec['name'] = $name; } $env = []; foreach (($body['env'] ?? []) as $row) { $key = trim((string) ($row['key'] ?? '')); if ($key !== '') { $env[$key] = (string) ($row['value'] ?? ''); } } if ($env !== []) { $spec['env'] = $env; } $ports = []; foreach (($body['ports'] ?? []) as $row) { $hostPort = (int) ($row['hostPort'] ?? 0); $containerPort = (int) ($row['containerPort'] ?? 0); if ($hostPort > 0 && $containerPort > 0) { $ports[] = [ 'host_ip' => '', 'host_port' => $hostPort, 'container_port' => $containerPort, 'protocol' => (string) ($row['protocol'] ?? 'tcp'), ]; } } if ($ports !== []) { $spec['portmappings'] = $ports; } $mounts = []; $volumes = []; foreach (($body['volumes'] ?? []) as $row) { $source = trim((string) ($row['source'] ?? '')); $containerPath = trim((string) ($row['containerPath'] ?? '')); if ($source === '' || $containerPath === '') { continue; } if (($row['kind'] ?? 'named') === 'path') { $mounts[] = ['destination' => $containerPath, 'type' => 'bind', 'source' => $source, 'options' => ['rbind']]; } else { $volumes[] = ['name' => $source, 'dest' => $containerPath]; } } if ($mounts !== []) { $spec['mounts'] = $mounts; } if ($volumes !== []) { $spec['volumes'] = $volumes; } // "bridge"/"host"/"none" are podman's own reserved netns modes; any // other value is an existing custom podman network's name, attached // via the "networks" field instead (verified live: passing a // network name through "networks" attaches it without needing an // explicit netns mode at all). $networkMode = (string) ($body['networkMode'] ?? 'bridge'); if (in_array($networkMode, ['bridge', 'host', 'none'], true)) { $spec['netns'] = ['nsmode' => $networkMode]; } elseif ($networkMode !== '') { $spec['networks'] = [$networkMode => new \stdClass()]; } if (isset($body['restartPolicy']) && $body['restartPolicy'] !== '') { $spec['restart_policy'] = (string) $body['restartPolicy']; } if ($body['privileged'] ?? false) { $spec['privileged'] = true; } return $spec; } /** @param array $body */ function require_id(array $body): string { $id = (string) ($body['id'] ?? ''); if ($id === '') { podman_json_error('Missing id in request body', 400); } return $id; } /** * Normalizes libpod's /containers/json entries into exactly what the * Containers table (javascript/containers.js) renders — keeping this * shaping logic server-side means the frontend never has to know libpod's * raw field names/quirks (e.g. Names is an array, State vs Status, etc). * * @return array> */ function containers_list(PodmanClient $client): array { $raw = $client->listContainers(true); $out = []; foreach ($raw as $c) { $names = $c['Names'] ?? []; $name = is_array($names) && count($names) > 0 ? ltrim((string) $names[0], '/') : podman_short_id((string) ($c['Id'] ?? '')); $ports = []; foreach (($c['Ports'] ?? []) as $p) { if (isset($p['host_port'], $p['container_port'])) { $ports[] = "{$p['host_port']}:{$p['container_port']}/" . ($p['protocol'] ?? 'tcp'); } elseif (isset($p['container_port'])) { $ports[] = "{$p['container_port']}/" . ($p['protocol'] ?? 'tcp'); } } $startedAt = podman_parse_time($c['StartedAt'] ?? null); $state = strtolower((string) ($c['State'] ?? 'unknown')); $out[] = [ 'id' => (string) ($c['Id'] ?? ''), 'shortId' => podman_short_id((string) ($c['Id'] ?? '')), 'name' => $name, 'image' => (string) ($c['Image'] ?? ''), 'state' => $state, 'status' => (string) ($c['Status'] ?? ''), 'health' => $c['Health']['Status'] ?? null, 'ports' => $ports, 'pod' => $c['Pod'] ?? null, 'podName' => $c['PodName'] ?? null, 'uptimeSeconds' => ($state === 'running' && $startedAt !== null) ? (time() - $startedAt) : null, 'createdAt' => podman_parse_time($c['Created'] ?? null), ]; } usort($out, static fn($a, $b) => strcmp($a['name'], $b['name'])); return $out; }