Add catatonit/nftables/docker-compose packages, fix CSRF/streaming/storage bugs found by live testing
- Package #9-11: catatonit (pod infra init), nftables (netavark firewall backend), docker-compose (external compose provider for `podman compose`) — all vendored prebuilt binaries, versions.env pinned, propagated through build-packages.sh/release.sh/podman.plg/verify+update-packages.sh. - Fix WebUI: every POST action was silently failing (empty response body) because Unraid's own CSRF protection was never satisfied — app.js now sends the page's csrf_token as X-CSRF-Token. - Fix WebUI: PodmanClient::pullImage() assumed a single JSON response, but /images/pull actually streams newline-delimited JSON — every successful pull was throwing "Expected a JSON object/array response". - Fix WebUI: compose.php's up/down status detection had the same single-JSON-vs-NDJSON bug for `podman compose ps`, plus stderr was corrupting the parse. - Add cache-busting (?v=<mtime>) to Podman.page's script/style tags so a redeployed JS/CSS fix isn't served stale from browser cache. - Add a reusable modal dialog (app.js openFormModal) replacing prompt()/alert() for New Volume/Network/Pull Image. - Add host-path (bind-mount) support when creating a named volume. - Add Create Container (image, name, network mode incl. custom networks, ports, volumes, env, restart policy, privileged, start-after-create), auto-pulling the image on first use since /containers/create doesn't. All fixes verified live against a real podman system service and, where reachable, via the actual WebUI over the real socket — not just unit-level. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,23 @@ final class PodmanClient
|
||||
return $this->request('GET', '/containers/' . rawurlencode($id) . '/stats', ['stream' => 'false']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /containers/create — takes a libpod SpecGenerator body. Field
|
||||
* names/shapes below (image, name, command, env, portmappings,
|
||||
* netns, networks, mounts, volumes, restart_policy, privileged) were
|
||||
* verified live against a real podman system service, not assumed
|
||||
* from docs — see ajax/containers.php's create action, which builds
|
||||
* this array from the WebUI's Create Container form.
|
||||
*
|
||||
* @param array<string,mixed> $spec
|
||||
* @return string the new container's ID
|
||||
*/
|
||||
public function createContainer(array $spec): string
|
||||
{
|
||||
$result = $this->request('POST', '/containers/create', [], false, $spec);
|
||||
return (string) ($result['Id'] ?? '');
|
||||
}
|
||||
|
||||
public function startContainer(string $id): void
|
||||
{
|
||||
$this->request('POST', '/containers/' . rawurlencode($id) . '/start', [], true);
|
||||
@@ -220,10 +237,54 @@ final class PodmanClient
|
||||
return $this->request('GET', '/images/json');
|
||||
}
|
||||
|
||||
/** POST /images/pull — pulls (or updates) an image by reference, e.g. "docker.io/library/postgres:16". */
|
||||
/**
|
||||
* POST /images/pull — pulls (or updates) an image by reference, e.g.
|
||||
* "docker.io/library/postgres:16".
|
||||
*
|
||||
* Unlike virtually every other libpod endpoint, a successful pull's
|
||||
* response body is NOT one JSON document — it's newline-delimited
|
||||
* JSON, one progress object per line (verified live:
|
||||
* `{"status":"pulling","stream":"..."}` repeated, then a final
|
||||
* `{"status":"success","images":[...],"id":"..."}` line). Feeding
|
||||
* that whole blob through the normal single-document request() here
|
||||
* made json_decode() fail on every successful pull with "Expected a
|
||||
* JSON object/array response from /images/pull" — found by
|
||||
* live-testing a real pull through the WebUI's Images panel, not
|
||||
* from reading libpod's docs. An error that happens before any
|
||||
* image data is found (e.g. unknown reference) is unaffected: libpod
|
||||
* sends that as a normal single-JSON-object 4xx response, which
|
||||
* requestRaw()/request()'s existing status>=400 handling already
|
||||
* covers correctly.
|
||||
*/
|
||||
public function pullImage(string $reference): array
|
||||
{
|
||||
return $this->request('POST', '/images/pull', ['reference' => $reference]);
|
||||
$raw = $this->requestRaw('POST', '/images/pull', ['reference' => $reference]);
|
||||
|
||||
$last = null;
|
||||
foreach (explode("\n", trim($raw)) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$decoded = json_decode($line, true);
|
||||
if (!is_array($decoded)) {
|
||||
continue;
|
||||
}
|
||||
// A mid-stream error (pull started, then failed — e.g. the
|
||||
// connection dropped partway through a layer) is reported as
|
||||
// an {"error": "..."} line rather than an HTTP error status,
|
||||
// since headers/status are already committed by the time
|
||||
// libpod knows the pull failed.
|
||||
if (isset($decoded['error'])) {
|
||||
throw new PodmanApiException((string) $decoded['error'], 502);
|
||||
}
|
||||
$last = $decoded;
|
||||
}
|
||||
|
||||
if ($last === null) {
|
||||
throw new PodmanApiException('Expected a JSON object/array response from /images/pull');
|
||||
}
|
||||
return $last;
|
||||
}
|
||||
|
||||
public function removeImage(string $id, bool $force = false): void
|
||||
@@ -240,9 +301,21 @@ final class PodmanClient
|
||||
return $this->request('GET', '/volumes/json');
|
||||
}
|
||||
|
||||
public function createVolume(string $name, string $driver = 'local'): array
|
||||
/**
|
||||
* $hostPath, if given, binds the volume directly to an existing host
|
||||
* directory instead of a podman-managed one — the local driver's
|
||||
* `type=none,o=bind,device=<path>` option trio (same mechanism
|
||||
* `podman volume create --opt type=none --opt o=bind --opt device=...`
|
||||
* uses on the CLI). Verified live: a container mounting such a volume
|
||||
* reads/writes the host path directly, not an internal copy.
|
||||
*/
|
||||
public function createVolume(string $name, string $driver = 'local', ?string $hostPath = null): array
|
||||
{
|
||||
return $this->request('POST', '/volumes/create', [], false, ['Name' => $name, 'Driver' => $driver]);
|
||||
$body = ['Name' => $name, 'Driver' => $driver];
|
||||
if ($hostPath !== null && $hostPath !== '') {
|
||||
$body['Options'] = ['type' => 'none', 'device' => $hostPath, 'o' => 'bind'];
|
||||
}
|
||||
return $this->request('POST', '/volumes/create', [], false, $body);
|
||||
}
|
||||
|
||||
public function removeVolume(string $name, bool $force = false): void
|
||||
|
||||
Reference in New Issue
Block a user