Add GPU/macvlan passthrough, container edit/update, image prune/tag
Create Container form: - GPU passthrough dropdown (AMD/Intel via /dev/dri detection, NVIDIA excluded since it needs a different runtime) - device paths strictly validated server-side against the host's own detected list. - Macvlan network support: selecting a macvlan network reveals a static IP field and hides port mappings (meaningless once the container has its own LAN address), matching Unraid Docker Manager's "Custom: br0" behavior. Networks panel gained a matching macvlan network-creation flow, with the parent-interface dropdown read from Unraid's own network.cfg so it lists exactly what Docker Manager itself offers. Containers panel: - Edit: reopens the create form pre-filled from the container's current config (image/ports/volumes/env/network/restart policy/GPU/static IP); saving stops+removes the old container and recreates it under the same settings, since podman/Docker have no in-place "modify" API for most of this. - Update: same stop/remove/recreate flow, but pulls the current image first. "Check for Updates" compares each in-use image's local digest against its origin registry (Docker Hub/GHCR/self-hosted registries all verified live) with no podman-side feature backing it - implemented via the registry's own HTTP API. A small log-modal shows progress for both actions instead of a silent wait. - Fixed a real bug hit live: PodmanClient's flat 15s HTTP timeout aborted real image pulls/container creates mid-request; bumped to 600s (nginx already allows up to 640s for this plugin's requests). Images panel: - "Prune unused" (removes every image with zero containers referencing it, not just dangling ones - confirmation copy says so explicitly since this is more aggressive than it sounds) and per-image "Tag". Also several real UI bugs found via live screenshots: unused-image prune having no visible effect until reloaded, table action-button columns drifting row to row (a bare "display:flex" on a <td> was fighting the table layout algorithm), Templates category badges dumping raw multi-tag strings from real Unraid templates, and low-contrast search/filter controls that were nearly invisible against the card background. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,15 @@ final class PodmanClient
|
||||
*/
|
||||
public function createContainer(array $spec): string
|
||||
{
|
||||
$result = $this->request('POST', '/containers/create', [], false, $spec);
|
||||
// Longer than this client's normal 15s operation timeout as cheap
|
||||
// insurance: creating a container involves setting up its mounts
|
||||
// (often onto Unraid array/spinning-disk shares, not the cache
|
||||
// pool) and network namespace, which can occasionally run past 15s
|
||||
// even with the image already pulled — found live via a real
|
||||
// "Operation timed out after 15001 milliseconds" error creating a
|
||||
// container. Same 600s ceiling as pullImage(), safely under
|
||||
// nginx's 640s fastcgi_read_timeout.
|
||||
$result = $this->request('POST', '/containers/create', [], false, $spec, 600);
|
||||
return (string) ($result['Id'] ?? '');
|
||||
}
|
||||
|
||||
@@ -300,7 +308,14 @@ final class PodmanClient
|
||||
*/
|
||||
public function pullImage(string $reference): array
|
||||
{
|
||||
$raw = $this->requestRaw('POST', '/images/pull', ['reference' => $reference]);
|
||||
// A real image (e.g. a Plex/media-server image, easily several
|
||||
// hundred MB) routinely takes far longer than this client's normal
|
||||
// 15s operation timeout to download — found live: a pull aborted
|
||||
// mid-stream with "Operation timed out after 15001 milliseconds"
|
||||
// after only ~1.4KB of progress data. nginx's own fastcgi_read_timeout
|
||||
// (640s, see /etc/nginx/nginx.conf) already anticipates long-running
|
||||
// plugin requests, so 600s here stays safely under that.
|
||||
$raw = $this->requestRaw('POST', '/images/pull', ['reference' => $reference], null, 600);
|
||||
|
||||
$last = null;
|
||||
foreach (explode("\n", trim($raw)) as $line) {
|
||||
@@ -334,6 +349,28 @@ final class PodmanClient
|
||||
$this->request('DELETE', '/images/' . rawurlencode($id), ['force' => $force ? 'true' : 'false'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /images/prune?all=true — removes every image with zero containers
|
||||
* (running or stopped) referencing it, matching this app's own "Used By"
|
||||
* column — not just dangling/untagged images. Verified live: a tagged
|
||||
* but unused image IS removed with all=true (found the hard way: it
|
||||
* also removed every image on a host with no containers at all, which
|
||||
* is correct behavior, just aggressive — see ajax/images.php's prune
|
||||
* action for the confirmation-copy this justifies).
|
||||
*
|
||||
* @return array<int,array{Id:string,Size:int}> one entry per removed image
|
||||
*/
|
||||
public function pruneImages(): array
|
||||
{
|
||||
return $this->request('POST', '/images/prune', ['all' => 'true']);
|
||||
}
|
||||
|
||||
/** POST /images/{id}/tag?repo=...&tag=... — adds a new repo:tag pointing at an existing image. */
|
||||
public function tagImage(string $id, string $repo, string $tag): void
|
||||
{
|
||||
$this->request('POST', '/images/' . rawurlencode($id) . '/tag', ['repo' => $repo, 'tag' => $tag], true);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Volumes
|
||||
// -------------------------------------------------------------------
|
||||
@@ -374,12 +411,25 @@ final class PodmanClient
|
||||
return $this->request('GET', '/networks/json');
|
||||
}
|
||||
|
||||
public function createNetwork(string $name, string $driver, ?string $subnet = null, ?string $gateway = null): array
|
||||
/**
|
||||
* $parentInterface (only meaningful for driver="macvlan") attaches the
|
||||
* network directly to an existing host bridge/VLAN interface (e.g.
|
||||
* Unraid's own "br0" or a VLAN sub-interface like "br0.3") via
|
||||
* libpod's "network_interface" field — verified live: containers on
|
||||
* such a network get a real address on that LAN/VLAN's own subnet,
|
||||
* not a NATed one, matching Unraid Docker Manager's "Custom: br0"
|
||||
* network type. See ajax/networks.php's macvlan_parent_interfaces()
|
||||
* for where the interface list itself comes from.
|
||||
*/
|
||||
public function createNetwork(string $name, string $driver, ?string $subnet = null, ?string $gateway = null, ?string $parentInterface = null): array
|
||||
{
|
||||
$body = ['name' => $name, 'driver' => $driver];
|
||||
if ($subnet !== null) {
|
||||
$body['subnets'] = [array_filter(['subnet' => $subnet, 'gateway' => $gateway])];
|
||||
}
|
||||
if ($parentInterface !== null && $parentInterface !== '') {
|
||||
$body['network_interface'] = $parentInterface;
|
||||
}
|
||||
return $this->request('POST', '/networks/create', [], false, $body);
|
||||
}
|
||||
|
||||
@@ -400,9 +450,9 @@ final class PodmanClient
|
||||
* @param array<mixed>|null $jsonBody request body to send as JSON, for POST/PUT endpoints that take one
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function request(string $method, string $path, array $query = [], bool $expectEmptyBody = false, ?array $jsonBody = null): array
|
||||
private function request(string $method, string $path, array $query = [], bool $expectEmptyBody = false, ?array $jsonBody = null, ?int $timeoutSeconds = null): array
|
||||
{
|
||||
$raw = $this->requestRaw($method, $path, $query, $jsonBody);
|
||||
$raw = $this->requestRaw($method, $path, $query, $jsonBody, $timeoutSeconds);
|
||||
if ($expectEmptyBody || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
@@ -421,7 +471,7 @@ final class PodmanClient
|
||||
* @param array<string,string> $query
|
||||
* @param array<mixed>|null $jsonBody
|
||||
*/
|
||||
private function requestRaw(string $method, string $path, array $query = [], ?array $jsonBody = null): string
|
||||
private function requestRaw(string $method, string $path, array $query = [], ?array $jsonBody = null, ?int $timeoutSeconds = null): string
|
||||
{
|
||||
$url = 'http://d/' . self::API_VERSION . '/libpod' . $path;
|
||||
if (!empty($query)) {
|
||||
@@ -434,7 +484,7 @@ final class PodmanClient
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||
CURLOPT_TIMEOUT => $timeoutSeconds ?? $this->timeoutSeconds,
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user