Add Apps/Store tab, template WebUI/URL import, container RO volumes/device passthrough/run-as-user, and in-app confirm dialogs
Replaces every native confirm() with a shared P.confirm() modal (a hung native dialog was found live to block the whole tab, including auto-refresh, and once even double-confirmed an unrelated deletion). Also fixes Edit Container silently resetting to Bridge/blanking the Static IP for any container on a custom network, and a context menu losing its anchor to a mid-read auto-refresh. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,11 +28,19 @@
|
||||
* create POST {"image": "...", "name": "...", "networkMode": "bridge"|"host"|"none"|"<custom-network-name>",
|
||||
* "staticIp": "10.1.1.222" (only meaningful with a custom/macvlan networkMode),
|
||||
* "ports": [{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}],
|
||||
* "volumes": [{"kind": "named"|"path", "source": "myvol"|"/mnt/...", "containerPath": "/data"}],
|
||||
* "volumes": [{"kind": "named"|"path", "source": "myvol"|"/mnt/...", "containerPath": "/data",
|
||||
* "readOnly": false}],
|
||||
* "env": [{"key": "...", "value": "..."}], "restartPolicy": "no", "pod": "<existing-pod-name>",
|
||||
* "gpuDevices": ["/dev/dri/renderD128", "/dev/dri/card0"],
|
||||
* "devices": [{"path": "/dev/ttyACM0"}] (arbitrary host device passthrough, same path on both
|
||||
* sides — see build_container_spec()'s comment on why "same path both sides" is
|
||||
* the only shape supported here),
|
||||
* "privileged": false, "startAfterCreate": true, "icon": "https://..." (optional),
|
||||
* "webuiUrl": "http://10.1.1.1:8080/" (optional)}
|
||||
* "webuiUrl": "http://10.1.1.1:8080/" (optional),
|
||||
* "user": "99:100" (optional — overrides the image's own default user; a real container
|
||||
* migrated from Docker with an explicit --user needs this, since without it
|
||||
* podman falls back to whatever USER the image itself declares, which may not
|
||||
* own the bind-mounted appdata directory)}
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -342,10 +350,22 @@ function build_container_spec(string $image, array $body): array
|
||||
if ($source === '' || $containerPath === '') {
|
||||
continue;
|
||||
}
|
||||
// Verified live against a real bind mount (RW:false in the
|
||||
// resulting inspect) that appending "ro" to the mount's own
|
||||
// options is all read-only takes — no separate top-level flag.
|
||||
$readOnly = (bool) ($row['readOnly'] ?? false);
|
||||
if (($row['kind'] ?? 'named') === 'path') {
|
||||
$mounts[] = ['destination' => $containerPath, 'type' => 'bind', 'source' => $source, 'options' => ['rbind']];
|
||||
$options = ['rbind'];
|
||||
if ($readOnly) {
|
||||
$options[] = 'ro';
|
||||
}
|
||||
$mounts[] = ['destination' => $containerPath, 'type' => 'bind', 'source' => $source, 'options' => $options];
|
||||
} else {
|
||||
$volumes[] = ['name' => $source, 'dest' => $containerPath];
|
||||
$volume = ['name' => $source, 'dest' => $containerPath];
|
||||
if ($readOnly) {
|
||||
$volume['options'] = ['ro'];
|
||||
}
|
||||
$volumes[] = $volume;
|
||||
}
|
||||
}
|
||||
if ($mounts !== []) {
|
||||
@@ -388,6 +408,21 @@ function build_container_spec(string $image, array $body): array
|
||||
$spec['privileged'] = true;
|
||||
}
|
||||
|
||||
$user = trim((string) ($body['user'] ?? ''));
|
||||
if ($user !== '') {
|
||||
// "99:100" (Unraid's own nobody:users, the overwhelming majority of
|
||||
// real-world cases — a migrated container whose bind-mounted
|
||||
// appdata was written by that user needs this override, since
|
||||
// without it podman falls back to whatever USER the image itself
|
||||
// declares), a bare UID, or a username — never anything that could
|
||||
// be interpreted as a shell/path fragment, even though this goes
|
||||
// straight into a podman API JSON body, not a shell.
|
||||
if (preg_match('/^[a-zA-Z0-9_.-]+(:[a-zA-Z0-9_.-]+)?$/', $user) !== 1) {
|
||||
podman_json_error("\"Run as user\" (\"{$user}\") must look like \"99:100\", \"1000\", or a username.", 400);
|
||||
}
|
||||
$spec['user'] = $user;
|
||||
}
|
||||
|
||||
$devices = [];
|
||||
foreach (($body['gpuDevices'] ?? []) as $path) {
|
||||
// Only ever pass through paths matching the exact shape gpu_list()
|
||||
@@ -399,6 +434,26 @@ function build_container_spec(string $image, array $body): array
|
||||
$devices[] = ['path' => $path];
|
||||
}
|
||||
}
|
||||
// Generic device passthrough (e.g. a USB serial adapter like
|
||||
// /dev/ttyACM0) — unlike the curated GPU list above, this comes
|
||||
// straight from a free-text field, so it's restricted to a path
|
||||
// actually under /dev/ (verified live against podman's own API that
|
||||
// {"path": "/dev/x"} maps that host device at the SAME path inside
|
||||
// the container — there's no separate "container path" field to
|
||||
// remap it, matching how the overwhelming majority of real-world
|
||||
// USB/serial passthrough is done anyway, e.g. this plugin's own
|
||||
// migrated aoostar-rs template using `--device=/dev/ttyACM0:/dev/ttyACM0`,
|
||||
// identical on both sides).
|
||||
foreach (($body['devices'] ?? []) as $row) {
|
||||
$path = trim((string) ($row['path'] ?? ''));
|
||||
if ($path === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('#^/dev/[A-Za-z0-9_./-]+$#', $path) !== 1 || str_contains($path, '..')) {
|
||||
podman_json_error("Device path (\"{$path}\") must be an absolute path under /dev/.", 400);
|
||||
}
|
||||
$devices[] = ['path' => $path];
|
||||
}
|
||||
if ($devices !== []) {
|
||||
$spec['devices'] = $devices;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user