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:
@@ -1,9 +1,10 @@
|
|||||||
name: Build Packages
|
name: Build Packages
|
||||||
|
|
||||||
# Builds the seven Slackware .txz packages defined under packages/
|
# Builds the eleven Slackware .txz packages defined under packages/
|
||||||
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs)
|
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs,
|
||||||
# inside a Slackware container, verifies + consolidates their checksums, and
|
# catatonit, nftables, docker-compose, unraid-podman) inside a Slackware
|
||||||
# uploads the result as a workflow artifact.
|
# container, verifies + consolidates their checksums, and uploads the
|
||||||
|
# result as a workflow artifact.
|
||||||
#
|
#
|
||||||
# Intentionally does NOT commit any built binary back to the repository —
|
# Intentionally does NOT commit any built binary back to the repository —
|
||||||
# packages/**, *.txz, dist/ are all git-ignored (see .gitignore). Artifacts
|
# packages/**, *.txz, dist/ are all git-ignored (see .gitignore). Artifacts
|
||||||
@@ -31,7 +32,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
packages:
|
packages:
|
||||||
description: >
|
description: >
|
||||||
Space-separated package names to build (default: all seven).
|
Space-separated package names to build (default: all eleven).
|
||||||
Example: "podman conmon"
|
Example: "podman conmon"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ wählen können — mit deutlicher GUI-Warnung bzgl. Performance und Spin-up-Ver
|
|||||||
| `fuse-overlayfs` | Fallback-Storage-Driver | Für Rootless-Phase 2 vorbereitet, in Phase 1 optional |
|
| `fuse-overlayfs` | Fallback-Storage-Driver | Für Rootless-Phase 2 vorbereitet, in Phase 1 optional |
|
||||||
| `passt`/`pasta` | Rootless-Networking | Nachfolger von slirp4netns, Phase 2, aber Paket schon mitbauen (geringe Kosten) |
|
| `passt`/`pasta` | Rootless-Networking | Nachfolger von slirp4netns, Phase 2, aber Paket schon mitbauen (geringe Kosten) |
|
||||||
| `catatonit` oder `tini` | Init-Prozess in Containern (optional, falls von Templates genutzt) | |
|
| `catatonit` oder `tini` | Init-Prozess in Containern (optional, falls von Templates genutzt) | |
|
||||||
|
| `nftables` | Firewall-Backend für `netavark` | Pflicht seit netavark 2.0 (iptables-Treiber entfernt); Unraid liefert kein `nft` mit — als offizielles Slackware-Paket vendored, nicht selbst gebaut |
|
||||||
|
| `docker-compose` (CLI-Plugin) | External-Compose-Provider für `podman compose` | `podman compose` hat keine eigene Compose-Implementierung, sondern sucht ein `docker-compose`-Binary in festen CLI-Plugin-Pfaden; ohne dieses Paket schlägt jede Compose-Panel-Aktion auf einem frischen Unraid-Install fehl |
|
||||||
|
|
||||||
### 5.2 Build-Strategie
|
### 5.2 Build-Strategie
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# packages/catatonit/
|
||||||
|
|
||||||
|
Pinned version: see `CATATONIT_VERSION` in [versions.env](../../versions.env).
|
||||||
|
|
||||||
|
Not built from source — `catatonit.SlackBuild` fetches and repackages
|
||||||
|
upstream's own prebuilt static x86_64 release binary. catatonit ships no
|
||||||
|
GitHub release asset for source-tarball builds that's meaningfully
|
||||||
|
different from just using the binary directly, and it's a small, purely
|
||||||
|
static ELF with zero runtime library dependencies (verified: `ldd` reports
|
||||||
|
"not a dynamic executable").
|
||||||
|
|
||||||
|
Required by `podman pod create` — without it, pod creation fails with
|
||||||
|
`finding catatonit binary: exec: catatonit: executable file not found in
|
||||||
|
$PATH`. Installed to `/usr/libexec/podman/catatonit`, alongside
|
||||||
|
netavark/aardvark-dns, which podman's `helper_binaries_dir` search already
|
||||||
|
covers.
|
||||||
|
|
||||||
|
Found by live-testing this plugin end-to-end against a real Unraid
|
||||||
|
install, not from reading podman's docs — see
|
||||||
|
[docs/ARCHITECTURE.md, section 8](../../docs/ARCHITECTURE.md#8-netzwerke).
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# packages/catatonit/catatonit.SlackBuild
|
||||||
|
#
|
||||||
|
# Packages the official prebuilt catatonit release binary — not built from
|
||||||
|
# source, see versions.env's CATATONIT_* block for why. catatonit is the
|
||||||
|
# init process podman runs inside every pod's infra container to reap
|
||||||
|
# zombies; without it, `podman pod create` fails outright with "finding
|
||||||
|
# catatonit binary: exec: catatonit: executable file not found in $PATH"
|
||||||
|
# (found by live-testing pod creation against a real Unraid install).
|
||||||
|
#
|
||||||
|
# Installed alongside netavark/aardvark-dns under /usr/libexec/podman/ —
|
||||||
|
# podman's helper_binaries_dir search path already covers that directory,
|
||||||
|
# matching where the other two helper binaries this project ships already
|
||||||
|
# live (see packages/netavark, packages/aardvark-dns).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/versions.env"
|
||||||
|
|
||||||
|
VERSION="$CATATONIT_VERSION"
|
||||||
|
ARCH="$PKG_ARCH"
|
||||||
|
BUILD="$PKG_BUILD"
|
||||||
|
TAG="$PKG_TAG"
|
||||||
|
|
||||||
|
sb_init "catatonit"
|
||||||
|
|
||||||
|
binary=$(sb_fetch_and_verify "$CATATONIT_SRC_URL" "$CATATONIT_SRC_SHA256" "catatonit-$VERSION")
|
||||||
|
|
||||||
|
install -D -m 0755 "$binary" "$PKG/usr/libexec/podman/catatonit"
|
||||||
|
|
||||||
|
# The release only ships the raw binary (+ checksum/signature files, no
|
||||||
|
# LICENSE/README asset) — write minimal doc metadata by hand instead of
|
||||||
|
# using sb_install_docs, which expects real files to copy from disk.
|
||||||
|
docdir="$PKG/usr/doc/catatonit-$VERSION"
|
||||||
|
mkdir -p "$docdir"
|
||||||
|
{
|
||||||
|
echo "catatonit $VERSION"
|
||||||
|
echo "https://github.com/openSUSE/catatonit"
|
||||||
|
echo "Prebuilt static binary, packaged as-is by unraid-podman — see"
|
||||||
|
echo "versions.env for the pinned release URL and SHA256."
|
||||||
|
} > "$docdir/README"
|
||||||
|
{
|
||||||
|
echo "Built by unraid-podman from upstream's prebuilt release binary."
|
||||||
|
echo "Package: catatonit $VERSION"
|
||||||
|
echo "Built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
} > "$docdir/unraid-podman.build-info"
|
||||||
|
|
||||||
|
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# HOW TO EDIT THIS FILE:
|
||||||
|
# The "handy ruler" below makes it easier to edit a package description.
|
||||||
|
# Line up the first '|' above the ':' following the base package name, and
|
||||||
|
# the '|' on the right side marks the last column you can put a character in.
|
||||||
|
# You must make exactly 11 lines for the formatting to be correct. It's also
|
||||||
|
# customary to leave one space after the ':' except on otherwise blank lines.
|
||||||
|
|
||||||
|
|-----handy-ruler------------------------------------------------|
|
||||||
|
catatonit: catatonit (init process for OCI pod infra containers)
|
||||||
|
catatonit:
|
||||||
|
catatonit: A minimal init that reaps zombie processes inside a pod's infra
|
||||||
|
catatonit: container. Required by `podman pod create` — packaged here from
|
||||||
|
catatonit: upstream's prebuilt static binary release for the unraid-podman
|
||||||
|
catatonit: plugin, not built from source (see versions.env).
|
||||||
|
catatonit:
|
||||||
|
catatonit: Homepage: https://github.com/openSUSE/catatonit
|
||||||
|
catatonit:
|
||||||
|
catatonit:
|
||||||
|
catatonit:
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# packages/docker-compose/
|
||||||
|
|
||||||
|
Pinned version: see `DOCKER_COMPOSE_VERSION` in [versions.env](../../versions.env).
|
||||||
|
|
||||||
|
Not built from source — `docker-compose.SlackBuild` fetches and repackages
|
||||||
|
upstream's own prebuilt static x86_64 release binary (`docker/compose`,
|
||||||
|
the Go-based Compose v2 CLI plugin — a different project from the older
|
||||||
|
Python `podman-compose`). It's a small, purely static ELF with zero
|
||||||
|
runtime library dependencies (verified: `ldd` reports "not a dynamic
|
||||||
|
executable"), so there's nothing meaningful to gain from a from-source
|
||||||
|
build.
|
||||||
|
|
||||||
|
`podman compose` (backing the WebUI's Compose panel, see
|
||||||
|
`webui/plugins/podman/ajax/compose.php`) has no compose implementation of
|
||||||
|
its own — it shells out to an "external compose provider" it discovers by
|
||||||
|
searching a fixed list of CLI-plugin directories for a binary named
|
||||||
|
`docker-compose`. Without one present, every Compose panel action fails
|
||||||
|
outright. Installed to `/usr/local/lib/docker/cli-plugins/docker-compose`
|
||||||
|
— one of podman's own search paths (extracted from the pinned podman
|
||||||
|
binary: `strings /usr/bin/podman | grep cli-plugins`), chosen specifically
|
||||||
|
under `/usr/local/` rather than `/usr/lib/docker/...` so this package
|
||||||
|
never collides with (or gets silently shadowed by) a genuine Docker
|
||||||
|
installation's own compose plugin on hosts that also run Unraid's
|
||||||
|
built-in Docker support.
|
||||||
|
|
||||||
|
Found by live-testing the Compose panel end-to-end against a real Unraid
|
||||||
|
install: it happened to work only because that particular host already
|
||||||
|
had Docker's own `docker-compose` plugin installed from an unrelated,
|
||||||
|
pre-existing Docker setup — a clean Unraid install has no compose
|
||||||
|
provider at all without this package. See
|
||||||
|
[docs/ARCHITECTURE.md, section 5.1](../../docs/ARCHITECTURE.md#51-zu-paketierende-komponenten).
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# packages/docker-compose/docker-compose.SlackBuild
|
||||||
|
#
|
||||||
|
# Packages the official prebuilt docker/compose v2 release binary — not
|
||||||
|
# built from source, see versions.env's DOCKER_COMPOSE_* block for why.
|
||||||
|
# `podman compose` (backing webui/plugins/podman/ajax/compose.php) has no
|
||||||
|
# compose implementation of its own; it shells out to an "external compose
|
||||||
|
# provider" it discovers by searching a fixed list of CLI-plugin
|
||||||
|
# directories for a binary literally named `docker-compose` (paths
|
||||||
|
# extracted from the pinned podman binary itself:
|
||||||
|
# `strings /usr/bin/podman | grep cli-plugins`). Without one present,
|
||||||
|
# every Compose panel action fails outright (found by live-testing against
|
||||||
|
# a real Unraid install — it only worked there because that host happened
|
||||||
|
# to already have Docker's own compose plugin from an unrelated setup).
|
||||||
|
#
|
||||||
|
# Installed to /usr/local/lib/docker/cli-plugins/docker-compose — one of
|
||||||
|
# podman's search paths, deliberately the /usr/local/ one rather than
|
||||||
|
# /usr/lib/docker/cli-plugins so this package never collides with (or
|
||||||
|
# gets silently shadowed by) a genuine Docker installation's own compose
|
||||||
|
# plugin on hosts that also run Unraid's built-in Docker support.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/versions.env"
|
||||||
|
|
||||||
|
VERSION="$DOCKER_COMPOSE_VERSION"
|
||||||
|
ARCH="$PKG_ARCH"
|
||||||
|
BUILD="$PKG_BUILD"
|
||||||
|
TAG="$PKG_TAG"
|
||||||
|
|
||||||
|
sb_init "docker-compose"
|
||||||
|
|
||||||
|
binary=$(sb_fetch_and_verify "$DOCKER_COMPOSE_SRC_URL" "$DOCKER_COMPOSE_SRC_SHA256" "docker-compose-$VERSION")
|
||||||
|
|
||||||
|
install -D -m 0755 "$binary" "$PKG/usr/local/lib/docker/cli-plugins/docker-compose"
|
||||||
|
|
||||||
|
# The release only ships the raw binary (+ checksum/signature files, no
|
||||||
|
# LICENSE/README asset) — write minimal doc metadata by hand instead of
|
||||||
|
# using sb_install_docs, which expects real files to copy from disk.
|
||||||
|
docdir="$PKG/usr/doc/docker-compose-$VERSION"
|
||||||
|
mkdir -p "$docdir"
|
||||||
|
{
|
||||||
|
echo "docker-compose (docker/compose v2 CLI plugin) $VERSION"
|
||||||
|
echo "https://github.com/docker/compose"
|
||||||
|
echo "Prebuilt static binary, packaged as-is by unraid-podman — see"
|
||||||
|
echo "versions.env for the pinned release URL and SHA256."
|
||||||
|
} > "$docdir/README"
|
||||||
|
{
|
||||||
|
echo "Built by unraid-podman from upstream's prebuilt release binary."
|
||||||
|
echo "Package: docker-compose $VERSION"
|
||||||
|
echo "Built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
} > "$docdir/unraid-podman.build-info"
|
||||||
|
|
||||||
|
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# HOW TO EDIT THIS FILE:
|
||||||
|
# The "handy ruler" below makes it easier to edit a package description.
|
||||||
|
# Line up the first '|' above the ':' following the base package name, and
|
||||||
|
# the '|' on the right side marks the last column you can put a character in.
|
||||||
|
# You must make exactly 11 lines for the formatting to be correct. It's also
|
||||||
|
# customary to leave one space after the ':' except on otherwise blank lines.
|
||||||
|
|
||||||
|
|-----handy-ruler------------------------------------------------|
|
||||||
|
docker-compose: docker-compose (external Compose provider for podman compose)
|
||||||
|
docker-compose:
|
||||||
|
docker-compose: The docker/compose v2 CLI-plugin binary, installed as a
|
||||||
|
docker-compose: CLI-plugin so `podman compose` can find it. Required by the
|
||||||
|
docker-compose: WebUI's Compose panel; packaged from upstream's prebuilt
|
||||||
|
docker-compose: static binary release, not built from source.
|
||||||
|
docker-compose:
|
||||||
|
docker-compose: Homepage: https://github.com/docker/compose
|
||||||
|
docker-compose:
|
||||||
|
docker-compose:
|
||||||
|
docker-compose:
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# packages/nftables/
|
||||||
|
|
||||||
|
Pinned version: see `NFTABLES_VERSION` in [versions.env](../../versions.env).
|
||||||
|
|
||||||
|
Not built from source, and no `slack-desc` here (unlike this project's
|
||||||
|
other packages) — `nftables.SlackBuild` fetches Slackware's own official
|
||||||
|
`nftables` package and re-hosts it as-is under this project's naming and
|
||||||
|
checksum convention. It's already a correctly-built Slackware package
|
||||||
|
(built by the Slackware team for exactly this OS/glibc/arch); the
|
||||||
|
slack-desc bundled inside it travels along unchanged.
|
||||||
|
|
||||||
|
netavark >= 2.0 dropped its iptables firewall driver entirely — nftables
|
||||||
|
(via the `nft` binary this package provides) is the only firewall backend
|
||||||
|
that works on Unraid (firewalld needs systemd/dbus, which Unraid has
|
||||||
|
neither of). Unraid OS itself ships no `nft` binary.
|
||||||
|
|
||||||
|
Found by live-testing this plugin end-to-end against a real Unraid
|
||||||
|
install, not from reading netavark's docs — see
|
||||||
|
[docs/ARCHITECTURE.md, section 8](../../docs/ARCHITECTURE.md#8-netzwerke).
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# packages/nftables/nftables.SlackBuild
|
||||||
|
#
|
||||||
|
# Vendors Slackware's own official nftables package as-is — not rebuilt
|
||||||
|
# from source, see versions.env's NFTABLES_* block for why. Unlike every
|
||||||
|
# other package here, there is no compile step: the fetched .txz is
|
||||||
|
# already a correctly-built Slackware package (built by the Slackware
|
||||||
|
# team for exactly this OS/glibc/arch), so it is re-hosted under this
|
||||||
|
# project's naming/checksum convention rather than unpacked and restaged
|
||||||
|
# through makepkg, which would add risk (differing compression/metadata)
|
||||||
|
# for no benefit.
|
||||||
|
#
|
||||||
|
# netavark >= 2.0 requires the nftables firewall driver (its iptables
|
||||||
|
# driver was removed entirely) but Unraid OS ships no `nft` binary — see
|
||||||
|
# config/containers.conf and docs/ARCHITECTURE.md section 8. Without this
|
||||||
|
# package, every `podman run`/`podman pod create` that touches networking
|
||||||
|
# fails with "netavark: Must provide a valid firewall backend" (found by
|
||||||
|
# live-testing against a real Unraid install).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$REPO_ROOT/versions.env"
|
||||||
|
|
||||||
|
VERSION="$NFTABLES_VERSION"
|
||||||
|
ARCH="$PKG_ARCH"
|
||||||
|
BUILD="$PKG_BUILD"
|
||||||
|
TAG="$PKG_TAG"
|
||||||
|
|
||||||
|
sb_init "nftables"
|
||||||
|
|
||||||
|
official_pkg=$(sb_fetch_and_verify "$NFTABLES_SRC_URL" "$NFTABLES_SRC_SHA256" "nftables-$VERSION-official.txz")
|
||||||
|
|
||||||
|
pkg_file="nftables-$VERSION-$ARCH-$BUILD$TAG.txz"
|
||||||
|
cp "$official_pkg" "$OUTPUT/$pkg_file"
|
||||||
|
|
||||||
|
( cd "$OUTPUT" && sha256sum "$pkg_file" > "$pkg_file.sha256" )
|
||||||
|
( cd "$OUTPUT" && md5sum "$pkg_file" > "$pkg_file.md5" )
|
||||||
|
|
||||||
|
echo "==> [nftables] vendored official Slackware package as $OUTPUT/$pkg_file"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# packages/unraid-podman
|
# packages/unraid-podman
|
||||||
|
|
||||||
Packaging recipe for the plugin's own scaffolding — **not** an upstream
|
Packaging recipe for the plugin's own scaffolding — **not** an upstream
|
||||||
component like the other seven package directories. See
|
component or vendored dependency like the other ten package directories. See
|
||||||
`unraid-podman.SlackBuild`'s header comment for the full rationale.
|
`unraid-podman.SlackBuild`'s header comment for the full rationale.
|
||||||
|
|
||||||
Bundles:
|
Bundles:
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# packages/unraid-podman/unraid-podman.SlackBuild
|
# packages/unraid-podman/unraid-podman.SlackBuild
|
||||||
#
|
#
|
||||||
# Unlike the other seven packages, this one does not compile anything from
|
# Unlike the other ten packages, this one does not fetch anything from
|
||||||
# an external upstream source — it packages THIS repository's own plugin
|
# an external upstream source at all — it packages THIS repository's own
|
||||||
# scaffolding (rc.podman, the sbin/ helper scripts, the official Unraid
|
# plugin scaffolding (rc.podman, the sbin/ helper scripts, the official
|
||||||
# event/ hooks, and the default config templates) into a single .txz,
|
# Unraid event/ hooks, and the default config templates) into a single
|
||||||
# exactly matching how real-world Unraid plugins bundle their own files
|
# .txz, exactly matching how real-world Unraid plugins bundle their own
|
||||||
# (verified against the actual unassigned.devices.plg / package layout —
|
# files (verified against the actual unassigned.devices.plg / package
|
||||||
# see docs/ARCHITECTURE.md section 3.2 for the reference check that led to
|
# layout — see docs/ARCHITECTURE.md section 3.2 for the reference check
|
||||||
# this design). plugin/podman.plg installs this alongside the seven
|
# that led to this design). plugin/podman.plg installs this alongside the
|
||||||
# compiled component packages, all via the same
|
# other ten packages, all via the same
|
||||||
# `upgradepkg --install-new --reinstall` mechanism.
|
# `upgradepkg --install-new --reinstall` mechanism.
|
||||||
#
|
#
|
||||||
# Version: taken directly from plugin/podman.plg's own <!ENTITY version>,
|
# Version: taken directly from plugin/podman.plg's own <!ENTITY version>,
|
||||||
|
|||||||
+66
-8
@@ -32,14 +32,16 @@
|
|||||||
|
|
||||||
Structure of this file:
|
Structure of this file:
|
||||||
1. DOCTYPE entity block — plugin metadata + one version/file/md5 triple
|
1. DOCTYPE entity block — plugin metadata + one version/file/md5 triple
|
||||||
per package (the seven upstream components plus this project's own
|
per package (the seven upstream components, catatonit/nftables/
|
||||||
"unraid-podman" scaffolding package, see packages/unraid-podman/).
|
docker-compose as vendored runtime dependencies, plus this
|
||||||
|
project's own "unraid-podman" scaffolding package, see
|
||||||
|
packages/unraid-podman/).
|
||||||
Entities are rewritten automatically by scripts/release.sh; never
|
Entities are rewritten automatically by scripts/release.sh; never
|
||||||
hand-edit a *_txz_version/_file/_md5 entity — see that script.
|
hand-edit a *_txz_version/_file/_md5 entity — see that script.
|
||||||
2. <PLUGIN> body:
|
2. <PLUGIN> body:
|
||||||
a. <CHANGES> — kept in sync with CHANGELOG.md by hand for now.
|
a. <CHANGES> — kept in sync with CHANGELOG.md by hand for now.
|
||||||
b. Pre-install architecture sanity check.
|
b. Pre-install architecture sanity check.
|
||||||
c. Eight <FILE> package install/update blocks.
|
c. Eleven <FILE> package install/update blocks.
|
||||||
d. Postinstall <FILE Run="/bin/bash"> — directory/config seeding,
|
d. Postinstall <FILE Run="/bin/bash"> — directory/config seeding,
|
||||||
install-manifest generation, first start.
|
install-manifest generation, first start.
|
||||||
e. <FILE Run="/bin/bash" Method="remove"> — uninstall.
|
e. <FILE Run="/bin/bash" Method="remove"> — uninstall.
|
||||||
@@ -65,8 +67,8 @@
|
|||||||
|
|
||||||
<!-- Slackware package naming components — must match versions.env's
|
<!-- Slackware package naming components — must match versions.env's
|
||||||
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
|
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
|
||||||
the eight removepkg calls in the Method="remove" block don't have to
|
the eleven removepkg calls in the Method="remove" block don't have to
|
||||||
repeat "x86_64-1_unraidpodman" eight times by hand. -->
|
repeat "x86_64-1_unraidpodman" eleven times by hand. -->
|
||||||
<!ENTITY pkgArch "x86_64">
|
<!ENTITY pkgArch "x86_64">
|
||||||
<!ENTITY pkgBuild "1">
|
<!ENTITY pkgBuild "1">
|
||||||
<!ENTITY pkgTag "_unraidpodman">
|
<!ENTITY pkgTag "_unraidpodman">
|
||||||
@@ -106,6 +108,27 @@
|
|||||||
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-&fuse_overlayfs_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-&fuse_overlayfs_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||||
<!ENTITY fuse_overlayfs_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY fuse_overlayfs_txz_md5 "00000000000000000000000000000000">
|
||||||
|
|
||||||
|
<!-- catatonit and nftables are runtime dependencies this plugin ships,
|
||||||
|
not upstream podman-ecosystem components — see packages/catatonit/
|
||||||
|
and packages/nftables/ READMEs for why each is needed (pod infra
|
||||||
|
container init; netavark's only viable firewall driver, since it
|
||||||
|
dropped iptables in 2.0 and Unraid has neither `nft` nor
|
||||||
|
systemd/dbus for firewalld) and why neither is built from source. -->
|
||||||
|
<!ENTITY catatonit_txz_version "0.0.0">
|
||||||
|
<!ENTITY catatonit_txz_file "catatonit-&catatonit_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||||
|
<!ENTITY catatonit_txz_md5 "00000000000000000000000000000000">
|
||||||
|
|
||||||
|
<!ENTITY nftables_txz_version "0.0.0">
|
||||||
|
<!ENTITY nftables_txz_file "nftables-&nftables_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||||
|
<!ENTITY nftables_txz_md5 "00000000000000000000000000000000">
|
||||||
|
|
||||||
|
<!-- docker-compose is the external Compose provider `podman compose`
|
||||||
|
shells out to (see packages/docker-compose/README.md) — without it
|
||||||
|
every Compose panel action fails outright on a clean install. -->
|
||||||
|
<!ENTITY docker_compose_txz_version "0.0.0">
|
||||||
|
<!ENTITY docker_compose_txz_file "docker-compose-&docker_compose_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
||||||
|
<!ENTITY docker_compose_txz_md5 "00000000000000000000000000000000">
|
||||||
|
|
||||||
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
|
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
|
||||||
sbin/ scripts, event/ hooks, config templates — see
|
sbin/ scripts, event/ hooks, config templates — see
|
||||||
packages/unraid-podman/README.md), not an upstream component. Its
|
packages/unraid-podman/README.md), not an upstream component. Its
|
||||||
@@ -145,7 +168,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Pre-install sanity check: this project only builds/ships x86_64 packages
|
Pre-install sanity check: this project only builds/ships x86_64 packages
|
||||||
(see versions.env's PKG_ARCH) — fail with a clear message on any other
|
(see versions.env's PKG_ARCH) — fail with a clear message on any other
|
||||||
architecture rather than letting eight package downloads 404 one by one.
|
architecture rather than letting eleven package downloads 404 one by one.
|
||||||
-->
|
-->
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
<INLINE>
|
<INLINE>
|
||||||
@@ -157,7 +180,9 @@ fi
|
|||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The seven upstream component packages. Each is downloaded straight into
|
The seven upstream component packages, plus catatonit, nftables, and
|
||||||
|
docker-compose (runtime dependencies vendored as-is — see the entity
|
||||||
|
block above for why). Each is downloaded straight into
|
||||||
its backup slot under /boot/config/plugins/&name;/backup/packages/&version;/
|
its backup slot under /boot/config/plugins/&name;/backup/packages/&version;/
|
||||||
(grouped by PLUGIN version, not each component's own version — a rollback
|
(grouped by PLUGIN version, not each component's own version — a rollback
|
||||||
targets "this plugin release" as one unit, see
|
targets "this plugin release" as one unit, see
|
||||||
@@ -232,6 +257,33 @@ fi
|
|||||||
</MD5>
|
</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&catatonit_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
|
<URL>
|
||||||
|
&baseURL;/&catatonit_txz_file;
|
||||||
|
</URL>
|
||||||
|
<MD5>
|
||||||
|
&catatonit_txz_md5;
|
||||||
|
</MD5>
|
||||||
|
</FILE>
|
||||||
|
|
||||||
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&nftables_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
|
<URL>
|
||||||
|
&baseURL;/&nftables_txz_file;
|
||||||
|
</URL>
|
||||||
|
<MD5>
|
||||||
|
&nftables_txz_md5;
|
||||||
|
</MD5>
|
||||||
|
</FILE>
|
||||||
|
|
||||||
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&docker_compose_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
|
<URL>
|
||||||
|
&baseURL;/&docker_compose_txz_file;
|
||||||
|
</URL>
|
||||||
|
<MD5>
|
||||||
|
&docker_compose_txz_md5;
|
||||||
|
</MD5>
|
||||||
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The plugin's own scaffolding package — rc.podman, sbin/ helper scripts,
|
The plugin's own scaffolding package — rc.podman, sbin/ helper scripts,
|
||||||
the event/ hooks below, and default config templates. See
|
the event/ hooks below, and default config templates. See
|
||||||
@@ -247,7 +299,7 @@ fi
|
|||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Postinstall: everything that has to happen AFTER the eight packages above
|
Postinstall: everything that has to happen AFTER the eleven packages above
|
||||||
are on disk, but isn't itself package content — directory/config
|
are on disk, but isn't itself package content — directory/config
|
||||||
seeding, the install-manifest that plugin/sbin/podman-verify-packages.sh
|
seeding, the install-manifest that plugin/sbin/podman-verify-packages.sh
|
||||||
and podman-update-packages.sh read, and the first start. Runs on both
|
and podman-update-packages.sh read, and the first start. Runs on both
|
||||||
@@ -291,6 +343,9 @@ echo "NETAVARK_INSTALLED_VERSION=\"&netavark_txz_version;\"" >> "$MANIFEST"
|
|||||||
echo "AARDVARK_DNS_INSTALLED_VERSION=\"&aardvark_dns_txz_version;\"" >> "$MANIFEST"
|
echo "AARDVARK_DNS_INSTALLED_VERSION=\"&aardvark_dns_txz_version;\"" >> "$MANIFEST"
|
||||||
echo "PASST_INSTALLED_VERSION=\"&passt_txz_version;\"" >> "$MANIFEST"
|
echo "PASST_INSTALLED_VERSION=\"&passt_txz_version;\"" >> "$MANIFEST"
|
||||||
echo "FUSE_OVERLAYFS_INSTALLED_VERSION=\"&fuse_overlayfs_txz_version;\"" >> "$MANIFEST"
|
echo "FUSE_OVERLAYFS_INSTALLED_VERSION=\"&fuse_overlayfs_txz_version;\"" >> "$MANIFEST"
|
||||||
|
echo "CATATONIT_INSTALLED_VERSION=\"&catatonit_txz_version;\"" >> "$MANIFEST"
|
||||||
|
echo "NFTABLES_INSTALLED_VERSION=\"&nftables_txz_version;\"" >> "$MANIFEST"
|
||||||
|
echo "DOCKER_COMPOSE_INSTALLED_VERSION=\"&docker_compose_txz_version;\"" >> "$MANIFEST"
|
||||||
echo "UNRAID_PODMAN_INSTALLED_VERSION=\"&unraid_podman_txz_version;\"" >> "$MANIFEST"
|
echo "UNRAID_PODMAN_INSTALLED_VERSION=\"&unraid_podman_txz_version;\"" >> "$MANIFEST"
|
||||||
|
|
||||||
echo "Seeding /boot/config/plugins/podman/ configuration (existing files left untouched)..."
|
echo "Seeding /boot/config/plugins/podman/ configuration (existing files left untouched)..."
|
||||||
@@ -351,6 +406,9 @@ removepkg &netavark_txz_file;
|
|||||||
removepkg &aardvark_dns_txz_file;
|
removepkg &aardvark_dns_txz_file;
|
||||||
removepkg &passt_txz_file;
|
removepkg &passt_txz_file;
|
||||||
removepkg &fuse_overlayfs_txz_file;
|
removepkg &fuse_overlayfs_txz_file;
|
||||||
|
removepkg &catatonit_txz_file;
|
||||||
|
removepkg &nftables_txz_file;
|
||||||
|
removepkg &docker_compose_txz_file;
|
||||||
removepkg &unraid_podman_txz_file;
|
removepkg &unraid_podman_txz_file;
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
# needed — see docs/ARCHITECTURE.md section 13.1.
|
# needed — see docs/ARCHITECTURE.md section 13.1.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# podman-update-packages.sh # reconcile all 7 packages
|
# podman-update-packages.sh # reconcile all 11 packages
|
||||||
# podman-update-packages.sh podman # reconcile a single package
|
# podman-update-packages.sh podman # reconcile a single package
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
. "$SCRIPT_DIR/podman-common.sh"
|
. "$SCRIPT_DIR/podman-common.sh"
|
||||||
|
|
||||||
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
||||||
ALL_PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
|
ALL_PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs catatonit nftables docker-compose unraid-podman"
|
||||||
|
|
||||||
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
|
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
|
||||||
podman_log_error "update-packages: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
|
podman_log_error "update-packages: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# plugin/sbin/podman-verify-packages.sh
|
# plugin/sbin/podman-verify-packages.sh
|
||||||
#
|
#
|
||||||
# "Pakete prüfen" — verifies the seven packages this plugin ships are
|
# "Pakete prüfen" — verifies the eleven packages this plugin ships are
|
||||||
# actually installed, at the version the plugin expects, and that the
|
# actually installed, at the version the plugin expects, and that the
|
||||||
# backed-up .txz copies (see podman-backup.sh) haven't bit-rotted on disk.
|
# backed-up .txz copies (see podman-backup.sh) haven't bit-rotted on disk.
|
||||||
#
|
#
|
||||||
@@ -34,7 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
. "$SCRIPT_DIR/podman-common.sh"
|
. "$SCRIPT_DIR/podman-common.sh"
|
||||||
|
|
||||||
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
|
||||||
PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
|
PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs catatonit nftables docker-compose unraid-podman"
|
||||||
|
|
||||||
QUIET=0
|
QUIET=0
|
||||||
[ "${1:-}" = "--quiet" ] && QUIET=1
|
[ "${1:-}" = "--quiet" ] && QUIET=1
|
||||||
@@ -95,7 +95,7 @@ for name in $PACKAGES; do
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# --- Check 3: backup artifact integrity, if present --------------------
|
# --- Check 3: backup artifact integrity, if present --------------------
|
||||||
# Packages of all 7 components released together as one plugin version
|
# Packages of all components released together as one plugin version
|
||||||
# are grouped under a single PLUGIN_VERSION directory (not per-component
|
# are grouped under a single PLUGIN_VERSION directory (not per-component
|
||||||
# version) — a rollback targets "go back to plugin release X", matching
|
# version) — a rollback targets "go back to plugin release X", matching
|
||||||
# podman-backup.sh's restore-packages <plugin-version>.
|
# podman-backup.sh's restore-packages <plugin-version>.
|
||||||
|
|||||||
+15
-10
@@ -2,10 +2,11 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# scripts/build-packages.sh
|
# scripts/build-packages.sh
|
||||||
#
|
#
|
||||||
# Orchestrates building all seven Slackware .txz packages this plugin ships
|
# Orchestrates building all Slackware .txz packages this plugin ships
|
||||||
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs), by
|
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs,
|
||||||
# running each package's <name>.SlackBuild in turn. See
|
# catatonit, nftables, docker-compose), by running each package's
|
||||||
# docs/ARCHITECTURE.md section 5.2 (Build-Strategie).
|
# <name>.SlackBuild in turn. See docs/ARCHITECTURE.md section 5.2
|
||||||
|
# (Build-Strategie).
|
||||||
#
|
#
|
||||||
# This script itself does not containerize anything — it assumes it is
|
# This script itself does not containerize anything — it assumes it is
|
||||||
# already running inside a Slackware-compatible build environment (see
|
# already running inside a Slackware-compatible build environment (see
|
||||||
@@ -37,13 +38,17 @@ DIST_DIR="$REPO_ROOT/dist"
|
|||||||
# see packages/unraid-podman/README.md) is built last since it's by far the
|
# see packages/unraid-podman/README.md) is built last since it's by far the
|
||||||
# fastest and has nothing useful to report on failure that earlier package
|
# fastest and has nothing useful to report on failure that earlier package
|
||||||
# failures wouldn't already explain.
|
# failures wouldn't already explain.
|
||||||
ALL_PACKAGES=(conmon crun netavark aardvark-dns passt fuse-overlayfs podman unraid-podman)
|
ALL_PACKAGES=(catatonit nftables docker-compose conmon crun netavark aardvark-dns passt fuse-overlayfs podman unraid-podman)
|
||||||
|
|
||||||
# Podman is listed second-to-last on purpose: among the seven upstream
|
# catatonit, nftables, and docker-compose are listed first since they're
|
||||||
# components it is the slowest build and the one most likely to fail on a
|
# all a plain fetch-and-repackage of an already-built upstream artifact
|
||||||
# dependency/tag mistake, so faster packages surface problems first during
|
# (see their own README.md for why) — no compiler, fastest possible
|
||||||
# local iteration. unraid-podman is genuinely last since it packages this
|
# signal if their pinned URL/checksum in versions.env ever goes stale.
|
||||||
# repo's own files and has no compile step at all.
|
# Podman is listed second-to-last on purpose: among the compiled
|
||||||
|
# components it is the slowest build and the one most likely to fail on
|
||||||
|
# a dependency/tag mistake, so faster packages surface problems first
|
||||||
|
# during local iteration. unraid-podman is genuinely last since it
|
||||||
|
# packages this repo's own files and has no compile step at all.
|
||||||
|
|
||||||
requested=("$@")
|
requested=("$@")
|
||||||
if [ "${#requested[@]}" -eq 0 ]; then
|
if [ "${#requested[@]}" -eq 0 ]; then
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# scripts/ci/setup-slackware-buildenv.sh
|
# scripts/ci/setup-slackware-buildenv.sh
|
||||||
#
|
#
|
||||||
# Prepares a Slackware container (see .github/workflows/build-packages.yml)
|
# Prepares a Slackware container (see .github/workflows/build-packages.yml)
|
||||||
# to build all seven packages under packages/. Idempotent and safe to re-run.
|
# to build all eleven packages under packages/. Idempotent and safe to re-run.
|
||||||
#
|
#
|
||||||
# Strategy: vbatts/slackware:15.0 (the image build-packages.yml runs this
|
# Strategy: vbatts/slackware:15.0 (the image build-packages.yml runs this
|
||||||
# in) is a minimal rootfs — it ships none of the 'D' (development) series,
|
# in) is a minimal rootfs — it ships none of the 'D' (development) series,
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
# Centralizing this logic means each individual SlackBuild only has to
|
# Centralizing this logic means each individual SlackBuild only has to
|
||||||
# describe *how to compile* its component — fetching, checksum verification,
|
# describe *how to compile* its component — fetching, checksum verification,
|
||||||
# and final .txz packaging are implemented once, here, and used the same way
|
# and final .txz packaging are implemented once, here, and used the same way
|
||||||
# by all seven packages. This is what keeps the seven build scripts
|
# by all ten packages (unraid-podman's own SlackBuild has no upstream
|
||||||
# consistent and short instead of each reinventing (and potentially
|
# source to fetch, so it doesn't need this). This is what keeps the build
|
||||||
|
# scripts consistent and short instead of each reinventing (and potentially
|
||||||
# forgetting) checksum verification or Slackware package metadata.
|
# forgetting) checksum verification or Slackware package metadata.
|
||||||
#
|
#
|
||||||
# Every SlackBuild is expected to:
|
# Every SlackBuild is expected to:
|
||||||
|
|||||||
+7
-5
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# Cuts a release of the plugin itself:
|
# Cuts a release of the plugin itself:
|
||||||
# 1. Bumps the &version; entity in plugin/podman.plg to <new-version>.
|
# 1. Bumps the &version; entity in plugin/podman.plg to <new-version>.
|
||||||
# 2. Builds all seven packages (scripts/build-packages.sh) unless
|
# 2. Builds all eleven packages (scripts/build-packages.sh) unless
|
||||||
# SKIP_BUILD=1 is set (useful when CI already built them in a prior job
|
# SKIP_BUILD=1 is set (useful when CI already built them in a prior job
|
||||||
# and only wants this script to do the .plg/CHANGELOG bookkeeping).
|
# and only wants this script to do the .plg/CHANGELOG bookkeeping).
|
||||||
# 3. Verifies + consolidates checksums (scripts/checksums.sh).
|
# 3. Verifies + consolidates checksums (scripts/checksums.sh).
|
||||||
@@ -54,10 +54,12 @@ RELEASE_BASE_URL="https://github.com/$REPO_SLUG/releases/download/$RELEASE_TAG"
|
|||||||
|
|
||||||
# Component name -> the entity name prefix used in podman.plg. Must match
|
# Component name -> the entity name prefix used in podman.plg. Must match
|
||||||
# plugin/podman.plg's <!ENTITY NAME_txz_...> declarations exactly.
|
# plugin/podman.plg's <!ENTITY NAME_txz_...> declarations exactly.
|
||||||
# unraid-podman is the plugin's own scaffolding package (see
|
# catatonit, nftables, and docker-compose are vendored runtime
|
||||||
# packages/unraid-podman/README.md), not an upstream component, but it's
|
# dependencies (not built from source, see their own packages/*/README.md)
|
||||||
# released and entity-updated exactly like the other seven.
|
# and unraid-podman is the plugin's own scaffolding package (see
|
||||||
COMPONENTS=(podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman)
|
# packages/unraid-podman/README.md), not an upstream component, but all
|
||||||
|
# four are released and entity-updated exactly like the seven upstream ones.
|
||||||
|
COMPONENTS=(podman conmon crun netavark aardvark-dns passt fuse-overlayfs catatonit nftables docker-compose unraid-podman)
|
||||||
|
|
||||||
echo "==> Releasing unraid-podman plugin v$NEW_VERSION (packages tag: $RELEASE_TAG)"
|
echo "==> Releasing unraid-podman plugin v$NEW_VERSION (packages tag: $RELEASE_TAG)"
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,50 @@ PASST_VERSION="git${PASST_COMMIT:0:7}"
|
|||||||
PASST_SRC_URL="https://passt.top/passt/snapshot/passt-${PASST_COMMIT}.tar.gz"
|
PASST_SRC_URL="https://passt.top/passt/snapshot/passt-${PASST_COMMIT}.tar.gz"
|
||||||
PASST_SRC_SHA256="4c58a77504a77d613464dddf22ae69d749a5ba64cb87e44c3b8c252333e209fc"
|
PASST_SRC_SHA256="4c58a77504a77d613464dddf22ae69d749a5ba64cb87e44c3b8c252333e209fc"
|
||||||
|
|
||||||
|
# --- catatonit -----------------------------------------------------------
|
||||||
|
# https://github.com/openSUSE/catatonit — the init process podman runs
|
||||||
|
# inside every pod's infra container to reap zombies; not built by
|
||||||
|
# podman's own Makefile, not packaged by Slackware, needed at runtime on
|
||||||
|
# every target Unraid install (found by live-testing `podman pod create`
|
||||||
|
# against a real install: "finding catatonit binary: exec: catatonit: no
|
||||||
|
# such file or directory"). Upstream publishes a prebuilt static
|
||||||
|
# (non-dynamic-linked) x86_64 binary release asset — no from-source build
|
||||||
|
# needed, no runtime library surprises like the crun/yajl chain had.
|
||||||
|
CATATONIT_VERSION="0.2.1"
|
||||||
|
CATATONIT_SRC_URL="https://github.com/openSUSE/catatonit/releases/download/v${CATATONIT_VERSION}/catatonit.x86_64"
|
||||||
|
CATATONIT_SRC_SHA256="8293951eaa7767fa411e3b89777bd01bc5e56db9ba6d145ad10cc4d05b01e961"
|
||||||
|
|
||||||
|
# --- nftables --------------------------------------------------------------
|
||||||
|
# netavark >= 2.0 dropped its iptables firewall driver entirely (see
|
||||||
|
# config/containers.conf and docs/ARCHITECTURE.md section 8) — nftables is
|
||||||
|
# now the only viable firewall backend on Unraid (no systemd/dbus for
|
||||||
|
# firewalld), but Unraid OS ships no `nft` binary. Slackware — Unraid's own
|
||||||
|
# base distro — already builds and ships this as an official package, so
|
||||||
|
# it is vendored through as-is (verified working live) rather than
|
||||||
|
# rebuilt from source: nftables pulls in its own dependency chain
|
||||||
|
# (libmnl, libnftnl, gmp, ...) for no benefit over the distro's own build,
|
||||||
|
# which is already correctly built for this exact environment.
|
||||||
|
NFTABLES_VERSION="1.0.1"
|
||||||
|
NFTABLES_SRC_URL="http://slackware.osuosl.org/slackware64-15.0/slackware64/n/nftables-${NFTABLES_VERSION}-x86_64-1.txz"
|
||||||
|
NFTABLES_SRC_SHA256="239e70d48edd6667ce875ff0d339b6f63c1fc94c472524d58772310b1006d31c"
|
||||||
|
|
||||||
|
# --- docker-compose ----------------------------------------------------------
|
||||||
|
# https://github.com/docker/compose — the Compose v2 CLI-plugin binary
|
||||||
|
# (Go, not the older Python podman-compose). `podman compose` (backing
|
||||||
|
# webui/plugins/podman/ajax/compose.php, the WebUI's Compose panel) has no
|
||||||
|
# compose implementation of its own — it searches a fixed set of
|
||||||
|
# CLI-plugin directories for a binary named exactly "docker-compose" and
|
||||||
|
# shells out to it. Without one present, every Compose panel action fails
|
||||||
|
# outright (found by live-testing: it only worked on the test host because
|
||||||
|
# that host happened to already have Docker's own compose plugin
|
||||||
|
# installed from an unrelated, pre-existing Docker setup — a clean Unraid
|
||||||
|
# install has none). Upstream publishes a prebuilt static x86_64 binary
|
||||||
|
# release asset (verified via `ldd`: "not a dynamic executable") plus a
|
||||||
|
# matching .sha256 sidecar — no from-source build needed.
|
||||||
|
DOCKER_COMPOSE_VERSION="5.3.1"
|
||||||
|
DOCKER_COMPOSE_SRC_URL="https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64"
|
||||||
|
DOCKER_COMPOSE_SRC_SHA256="f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Slackware package BUILD number (not upstream version). Bump this if a
|
# Slackware package BUILD number (not upstream version). Bump this if a
|
||||||
# package must be rebuilt without an upstream version change (e.g. a
|
# package must be rebuilt without an upstream version change (e.g. a
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ Dynamix-style WebUI pages, following Unraid's plugin GUI convention of
|
|||||||
`/usr/local/emhttp/plugins/<name>/`. `webui/plugins/podman/` is staged to
|
`/usr/local/emhttp/plugins/<name>/`. `webui/plugins/podman/` is staged to
|
||||||
that path by the `unraid-podman` scaffolding package (see
|
that path by the `unraid-podman` scaffolding package (see
|
||||||
`packages/unraid-podman/unraid-podman.SlackBuild`), which podman.plg installs
|
`packages/unraid-podman/unraid-podman.SlackBuild`), which podman.plg installs
|
||||||
alongside the seven compiled components.
|
alongside the other ten packages.
|
||||||
|
|
||||||
**Status: implemented**, covering all ten sections from
|
**Status: implemented**, covering all ten sections from
|
||||||
[docs/ARCHITECTURE.md, section 18](../docs/ARCHITECTURE.md#18-zukünftige-webui):
|
[docs/ARCHITECTURE.md, section 18](../docs/ARCHITECTURE.md#18-zukünftige-webui):
|
||||||
|
|||||||
@@ -16,8 +16,23 @@ Icon="podman"
|
|||||||
* page's markup mirrors (same structure, same CSS classes, real data
|
* page's markup mirrors (same structure, same CSS classes, real data
|
||||||
* instead of static samples).
|
* instead of static samples).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache-busts every static asset with its own on-disk mtime. Unraid's
|
||||||
|
* webserver sends no explicit no-cache headers for /plugins/ static
|
||||||
|
* files, so without this, browsers can keep serving a stale app.js/
|
||||||
|
* podman.css for a long time after a plugin update — verified live: a
|
||||||
|
* bugfix to app.js's CSRF handling silently kept failing in a real
|
||||||
|
* browser after redeploy until this was added, even though the deployed
|
||||||
|
* file on disk was byte-for-byte correct.
|
||||||
|
*/
|
||||||
|
function podman_asset_version(string $relPath): string
|
||||||
|
{
|
||||||
|
$full = __DIR__ . $relPath;
|
||||||
|
return is_file($full) ? (string) filemtime($full) : '0';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" type="text/css" href="/plugins/podman/styles/podman.css">
|
<link rel="stylesheet" type="text/css" href="/plugins/podman/styles/podman.css?v=<?=podman_asset_version('/styles/podman.css')?>">
|
||||||
|
|
||||||
<div class="podman-plugin">
|
<div class="podman-plugin">
|
||||||
|
|
||||||
@@ -73,6 +88,7 @@ Icon="podman"
|
|||||||
<button data-filter="running" id="containers-count-running">Running</button>
|
<button data-filter="running" id="containers-count-running">Running</button>
|
||||||
<button data-filter="stopped" id="containers-count-stopped">Stopped</button>
|
<button data-filter="stopped" id="containers-count-stopped">Stopped</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="podman-btn podman-btn-primary" id="containers-create-btn">+ New Container</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="podman-table-wrap">
|
<div class="podman-table-wrap">
|
||||||
<table>
|
<table>
|
||||||
@@ -243,14 +259,12 @@ Icon="podman"
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/plugins/podman/javascript/app.js"></script>
|
<?php
|
||||||
<script src="/plugins/podman/javascript/dashboard.js"></script>
|
foreach ([
|
||||||
<script src="/plugins/podman/javascript/containers.js"></script>
|
'app', 'dashboard', 'containers', 'pods', 'images', 'volumes',
|
||||||
<script src="/plugins/podman/javascript/pods.js"></script>
|
'networks', 'logs', 'terminal', 'compose', 'settings',
|
||||||
<script src="/plugins/podman/javascript/images.js"></script>
|
] as $podmanJsModule) {
|
||||||
<script src="/plugins/podman/javascript/volumes.js"></script>
|
$podmanJsPath = "/javascript/{$podmanJsModule}.js";
|
||||||
<script src="/plugins/podman/javascript/networks.js"></script>
|
echo '<script src="/plugins/podman' . $podmanJsPath . '?v=' . podman_asset_version($podmanJsPath) . '"></script>' . "\n";
|
||||||
<script src="/plugins/podman/javascript/logs.js"></script>
|
}
|
||||||
<script src="/plugins/podman/javascript/terminal.js"></script>
|
?>
|
||||||
<script src="/plugins/podman/javascript/compose.js"></script>
|
|
||||||
<script src="/plugins/podman/javascript/settings.js"></script>
|
|
||||||
|
|||||||
@@ -121,8 +121,19 @@ function compose_status(string $composeDir, string $project): string
|
|||||||
if ($result['exitCode'] !== 0) {
|
if ($result['exitCode'] !== 0) {
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
$decoded = json_decode($result['output'], true);
|
// `podman compose ps --format json` emits one JSON object PER LINE
|
||||||
return (is_array($decoded) && count($decoded) > 0) ? 'up' : 'down';
|
// (JSONL), not a single JSON array — decoding the whole blob in one
|
||||||
|
// json_decode() call fails silently (-> null) as soon as a project has
|
||||||
|
// more than one service (verified live with a 2-service project).
|
||||||
|
// stdout only, too: the "external compose provider" banner goes to
|
||||||
|
// stderr and would otherwise corrupt this either way.
|
||||||
|
$running = 0;
|
||||||
|
foreach (explode("\n", trim($result['stdout'])) as $line) {
|
||||||
|
if (trim($line) !== '' && is_array(json_decode($line, true))) {
|
||||||
|
$running++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $running > 0 ? 'up' : 'down';
|
||||||
}
|
}
|
||||||
|
|
||||||
function compose_read(string $composeDir, string $project): string
|
function compose_read(string $composeDir, string $project): string
|
||||||
@@ -155,7 +166,7 @@ function compose_run(string $composeDir, string $project, array $subcommand): ar
|
|||||||
* surface even though $project has already been validated above too).
|
* surface even though $project has already been validated above too).
|
||||||
*
|
*
|
||||||
* @param array<int,string> $subcommand
|
* @param array<int,string> $subcommand
|
||||||
* @return array{exitCode:int,output:string}
|
* @return array{exitCode:int,stdout:string,output:string}
|
||||||
*/
|
*/
|
||||||
function run_compose_command(string $composeDir, string $project, array $subcommand, int $timeoutSeconds): array
|
function run_compose_command(string $composeDir, string $project, array $subcommand, int $timeoutSeconds): array
|
||||||
{
|
{
|
||||||
@@ -165,7 +176,7 @@ function run_compose_command(string $composeDir, string $project, array $subcomm
|
|||||||
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||||
$process = proc_open($argv, $descriptors, $pipes, $composeDir . '/' . $project);
|
$process = proc_open($argv, $descriptors, $pipes, $composeDir . '/' . $project);
|
||||||
if (!is_resource($process)) {
|
if (!is_resource($process)) {
|
||||||
return ['exitCode' => 127, 'output' => 'Could not start podman compose process'];
|
return ['exitCode' => 127, 'stdout' => '', 'output' => 'Could not start podman compose process'];
|
||||||
}
|
}
|
||||||
|
|
||||||
stream_set_timeout($pipes[1], $timeoutSeconds);
|
stream_set_timeout($pipes[1], $timeoutSeconds);
|
||||||
@@ -175,5 +186,9 @@ function run_compose_command(string $composeDir, string $project, array $subcomm
|
|||||||
fclose($pipes[2]);
|
fclose($pipes[2]);
|
||||||
$exitCode = proc_close($process);
|
$exitCode = proc_close($process);
|
||||||
|
|
||||||
return ['exitCode' => $exitCode, 'output' => trim($stdout . $stderr)];
|
// 'stdout' (raw) for callers that need to parse machine-readable
|
||||||
|
// output (e.g. compose_status()'s JSON); 'output' (combined,
|
||||||
|
// trimmed) for human-facing success/error messages, where seeing
|
||||||
|
// podman's own stderr banner/warnings is actually useful context.
|
||||||
|
return ['exitCode' => $exitCode, 'stdout' => $stdout, 'output' => trim($stdout . $stderr)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
* restart POST {"id": "...", "timeout": 10}
|
* restart POST {"id": "...", "timeout": 10}
|
||||||
* remove POST {"id": "...", "force": false}
|
* remove POST {"id": "...", "force": false}
|
||||||
* logs GET (&id=...&tail=200) -> plain text
|
* logs GET (&id=...&tail=200) -> plain text
|
||||||
|
* create POST {"image": "...", "name": "...", "networkMode": "bridge"|"host"|"none"|"<custom-network-name>",
|
||||||
|
* "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);
|
declare(strict_types=1);
|
||||||
@@ -68,10 +73,129 @@ switch ($action) {
|
|||||||
podman_json_response(['status' => 'removed']);
|
podman_json_response(['status' => 'removed']);
|
||||||
break;
|
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:
|
default:
|
||||||
podman_json_error("Unknown action '{$action}'", 400);
|
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<string,mixed> $body
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $body */
|
/** @param array<string,mixed> $body */
|
||||||
function require_id(array $body): string
|
function require_id(array $body): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ function system_summary(PodmanClient $client, PodmanConfig $config): array
|
|||||||
return [
|
return [
|
||||||
'reachable' => true,
|
'reachable' => true,
|
||||||
'socketPath' => $config->socketPath,
|
'socketPath' => $config->socketPath,
|
||||||
'podmanVersion' => $info['Version']['Version'] ?? null,
|
// libpod's /info nests the version block under lowercase "version"
|
||||||
|
// (unlike most other libpod endpoints, which are PascalCase
|
||||||
|
// throughout) — verified live against a real podman system service.
|
||||||
|
'podmanVersion' => $info['version']['Version'] ?? null,
|
||||||
'containers' => [
|
'containers' => [
|
||||||
'total' => count($containers),
|
'total' => count($containers),
|
||||||
'running' => $running,
|
'running' => $running,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*
|
*
|
||||||
* Actions (?action=...):
|
* Actions (?action=...):
|
||||||
* list GET -> normalized volume list, with usedBy counts
|
* list GET -> normalized volume list, with usedBy counts
|
||||||
* create POST {"name": "...", "driver": "local"}
|
* create POST {"name": "...", "driver": "local", "path": "/mnt/cache/..." (optional)}
|
||||||
* remove POST {"name": "...", "force": false}
|
* remove POST {"name": "...", "force": false}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -30,7 +30,11 @@ switch ($action) {
|
|||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
podman_json_error('Missing name in request body', 400);
|
podman_json_error('Missing name in request body', 400);
|
||||||
}
|
}
|
||||||
podman_json_response($client->createVolume($name, (string) ($body['driver'] ?? 'local')));
|
$path = trim((string) ($body['path'] ?? ''));
|
||||||
|
if ($path !== '' && !str_starts_with($path, '/')) {
|
||||||
|
podman_json_error("Host path ({$path}) must be an absolute path.", 400);
|
||||||
|
}
|
||||||
|
podman_json_response($client->createVolume($name, (string) ($body['driver'] ?? 'local'), $path !== '' ? $path : null));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove':
|
case 'remove':
|
||||||
@@ -65,10 +69,19 @@ function volumes_list(PodmanClient $client): array
|
|||||||
$out = [];
|
$out = [];
|
||||||
foreach ($raw as $v) {
|
foreach ($raw as $v) {
|
||||||
$name = (string) ($v['Name'] ?? '');
|
$name = (string) ($v['Name'] ?? '');
|
||||||
|
$options = $v['Options'] ?? [];
|
||||||
|
// A volume created with our "Host path" field carries
|
||||||
|
// type=none,o=bind,device=<path> (see PodmanClient::createVolume)
|
||||||
|
// — surfaced separately from 'mountpoint' (podman's own internal
|
||||||
|
// storage path, which stays populated even for bind-backed
|
||||||
|
// volumes) so the UI can show users the host path they actually
|
||||||
|
// asked for.
|
||||||
|
$hostPath = (is_array($options) && ($options['o'] ?? '') === 'bind') ? (string) ($options['device'] ?? '') : null;
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'driver' => (string) ($v['Driver'] ?? 'local'),
|
'driver' => (string) ($v['Driver'] ?? 'local'),
|
||||||
'mountpoint' => (string) ($v['Mountpoint'] ?? ''),
|
'mountpoint' => (string) ($v['Mountpoint'] ?? ''),
|
||||||
|
'hostPath' => $hostPath,
|
||||||
'createdAt' => podman_parse_time($v['CreatedAt'] ?? null),
|
'createdAt' => podman_parse_time($v['CreatedAt'] ?? null),
|
||||||
'usedBy' => $usageCounts[$name] ?? 0,
|
'usedBy' => $usageCounts[$name] ?? 0,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -101,6 +101,23 @@ final class PodmanClient
|
|||||||
return $this->request('GET', '/containers/' . rawurlencode($id) . '/stats', ['stream' => 'false']);
|
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
|
public function startContainer(string $id): void
|
||||||
{
|
{
|
||||||
$this->request('POST', '/containers/' . rawurlencode($id) . '/start', [], true);
|
$this->request('POST', '/containers/' . rawurlencode($id) . '/start', [], true);
|
||||||
@@ -220,10 +237,54 @@ final class PodmanClient
|
|||||||
return $this->request('GET', '/images/json');
|
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
|
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
|
public function removeImage(string $id, bool $force = false): void
|
||||||
@@ -240,9 +301,21 @@ final class PodmanClient
|
|||||||
return $this->request('GET', '/volumes/json');
|
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
|
public function removeVolume(string $name, bool $force = false): void
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ window.Podman = (function () {
|
|||||||
opts.headers['Content-Type'] = 'application/json';
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
opts.body = JSON.stringify(body);
|
opts.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
|
// Unraid's own webGui/include/local_prepend.php (auto_prepend_file on
|
||||||
|
// every PHP request, not something this plugin controls) kills any
|
||||||
|
// POST request with no output at all unless it carries the page's
|
||||||
|
// CSRF token — either as a "csrf_token" POST field or this header.
|
||||||
|
// `csrf_token` itself is a global var HeadInlineJS.php sets on every
|
||||||
|
// Unraid page before plugin JS loads (verified live: without this
|
||||||
|
// header, every mutating action failed with "JSON.parse: unexpected
|
||||||
|
// end of data", i.e. an empty response body from csrf_terminate()).
|
||||||
|
if (method === 'POST' && typeof window.csrf_token === 'string') {
|
||||||
|
opts.headers['X-CSRF-Token'] = window.csrf_token;
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(url, opts)
|
return fetch(url, opts)
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
@@ -115,6 +126,109 @@ window.Podman = (function () {
|
|||||||
return '<tr><td colspan="' + colspan + '" class="podman-error">' + escapeHtml(message) + '</td></tr>';
|
return '<tr><td colspan="' + colspan + '" class="podman-error">' + escapeHtml(message) + '</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Modal form dialog -----------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a small form modal in place of browser-native prompt()/confirm()
|
||||||
|
* — needed for any action that takes more than one related value (e.g.
|
||||||
|
* "New Volume" wants a name AND an optional host path together; chaining
|
||||||
|
* prompt() calls for that is both bad UX and can't show both fields at
|
||||||
|
* once, or offer a hint under the path field explaining what it does).
|
||||||
|
*
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {string} opts.title
|
||||||
|
* @param {Array<{name:string, label:string, placeholder?:string, hint?:string, required?:boolean}>} opts.fields
|
||||||
|
* @param {string} [opts.submitLabel]
|
||||||
|
* @param {(values: Object<string,string>) => Promise<any>} opts.onSubmit
|
||||||
|
* Called with {fieldName: value}. Rejecting keeps the modal open and
|
||||||
|
* shows the error inline; resolving closes it.
|
||||||
|
*/
|
||||||
|
function openFormModal(opts) {
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'podman-modal-backdrop';
|
||||||
|
|
||||||
|
const fieldsHtml = opts.fields.map(function (f) {
|
||||||
|
return '' +
|
||||||
|
'<div class="podman-modal-field">' +
|
||||||
|
'<label for="podman-modal-' + f.name + '">' + escapeHtml(f.label) + '</label>' +
|
||||||
|
'<input type="text" id="podman-modal-' + f.name + '" name="' + f.name + '"' +
|
||||||
|
(f.placeholder ? ' placeholder="' + escapeHtml(f.placeholder) + '"' : '') + '>' +
|
||||||
|
(f.hint ? '<div class="hint">' + escapeHtml(f.hint) + '</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
backdrop.innerHTML = '' +
|
||||||
|
'<div class="podman-modal" role="dialog" aria-modal="true">' +
|
||||||
|
'<div class="podman-modal-head"><h3>' + escapeHtml(opts.title) + '</h3></div>' +
|
||||||
|
'<form class="podman-modal-body">' + fieldsHtml + '</form>' +
|
||||||
|
'<div class="podman-modal-actions">' +
|
||||||
|
'<button type="button" class="podman-btn" data-role="cancel">Cancel</button>' +
|
||||||
|
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">' +
|
||||||
|
escapeHtml(opts.submitLabel || 'Create') + '</button>' +
|
||||||
|
'</div></div>';
|
||||||
|
|
||||||
|
// Appended inside .podman-plugin, not document.body: the --surface/
|
||||||
|
// --border/etc. custom properties this modal's CSS relies on are
|
||||||
|
// scoped to .podman-plugin (see podman.css's token strategy comment),
|
||||||
|
// so a modal appended to body would resolve none of them — verified
|
||||||
|
// live: the backdrop dimming and card background were both missing,
|
||||||
|
// only the (inherited-from-body) text was visible. position:fixed
|
||||||
|
// still overlays the full viewport regardless of this nesting.
|
||||||
|
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||||
|
|
||||||
|
const firstInput = backdrop.querySelector('input');
|
||||||
|
if (firstInput) firstInput.focus();
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
backdrop.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const values = {};
|
||||||
|
opts.fields.forEach(function (f) {
|
||||||
|
values[f.name] = backdrop.querySelector('#podman-modal-' + f.name).value.trim();
|
||||||
|
});
|
||||||
|
for (const f of opts.fields) {
|
||||||
|
if (f.required && !values[f.name]) {
|
||||||
|
showError('"' + f.label + '" is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
Promise.resolve(opts.onSubmit(values)).then(close).catch(function (err) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
showError(err.message || String(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
let box = backdrop.querySelector('.podman-modal-error');
|
||||||
|
if (!box) {
|
||||||
|
box = document.createElement('div');
|
||||||
|
box.className = 'podman-modal-error';
|
||||||
|
backdrop.querySelector('.podman-modal-body').appendChild(box);
|
||||||
|
}
|
||||||
|
box.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
|
||||||
|
backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit);
|
||||||
|
backdrop.querySelector('form').addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
});
|
||||||
|
backdrop.addEventListener('click', function (e) {
|
||||||
|
if (e.target === backdrop) close();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function onKey(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
close();
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Panel router ----------------------------------------------------------
|
// --- Panel router ----------------------------------------------------------
|
||||||
|
|
||||||
const panelModules = {};
|
const panelModules = {};
|
||||||
@@ -180,6 +294,7 @@ window.Podman = (function () {
|
|||||||
stateChipClass: stateChipClass,
|
stateChipClass: stateChipClass,
|
||||||
loadingRow: loadingRow,
|
loadingRow: loadingRow,
|
||||||
errorRow: errorRow,
|
errorRow: errorRow,
|
||||||
|
openFormModal: openFormModal,
|
||||||
registerPanel: registerPanel,
|
registerPanel: registerPanel,
|
||||||
activatePanel: activatePanel,
|
activatePanel: activatePanel,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,6 +80,180 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Create Container -----------------------------------------------------
|
||||||
|
//
|
||||||
|
// Purpose-built modal (not app.js's generic openFormModal, which only
|
||||||
|
// supports flat text fields) — port/volume/env rows are dynamic
|
||||||
|
// add/remove groups, and network needs a <select> populated from the
|
||||||
|
// real network list, none of which fits the generic helper. Reuses its
|
||||||
|
// .podman-modal-* CSS classes for visual consistency.
|
||||||
|
|
||||||
|
function portRowHtml() {
|
||||||
|
return '' +
|
||||||
|
'<div class="podman-row-group-item">' +
|
||||||
|
'<input type="text" class="mono" data-field="hostPort" placeholder="Host port">' +
|
||||||
|
'<span>→</span>' +
|
||||||
|
'<input type="text" class="mono" data-field="containerPort" placeholder="Container port">' +
|
||||||
|
'<select data-field="protocol"><option value="tcp">TCP</option><option value="udp">UDP</option></select>' +
|
||||||
|
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeRowHtml() {
|
||||||
|
return '' +
|
||||||
|
'<div class="podman-row-group-item">' +
|
||||||
|
'<select data-field="kind"><option value="named">Volume</option><option value="path">Host path</option></select>' +
|
||||||
|
'<input type="text" class="mono" data-field="source" placeholder="my-volume or /mnt/cache/...">' +
|
||||||
|
'<span>→</span>' +
|
||||||
|
'<input type="text" class="mono" data-field="containerPath" placeholder="/data">' +
|
||||||
|
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function envRowHtml() {
|
||||||
|
return '' +
|
||||||
|
'<div class="podman-row-group-item">' +
|
||||||
|
'<input type="text" class="mono" data-field="key" placeholder="KEY">' +
|
||||||
|
'<span>=</span>' +
|
||||||
|
'<input type="text" class="mono" data-field="value" placeholder="value">' +
|
||||||
|
'<button type="button" class="podman-btn podman-btn-icon" data-remove-row title="Remove">×</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow(groupEl, rowHtmlFn) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = rowHtmlFn();
|
||||||
|
const row = div.firstElementChild;
|
||||||
|
row.querySelector('[data-remove-row]').addEventListener('click', function () { row.remove(); });
|
||||||
|
groupEl.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRows(groupEl) {
|
||||||
|
return Array.from(groupEl.children).map(function (row) {
|
||||||
|
const values = {};
|
||||||
|
row.querySelectorAll('[data-field]').forEach(function (input) {
|
||||||
|
values[input.dataset.field] = input.value.trim();
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateContainerModal() {
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'podman-modal-backdrop';
|
||||||
|
backdrop.innerHTML = '' +
|
||||||
|
'<div class="podman-modal podman-modal-wide" role="dialog" aria-modal="true">' +
|
||||||
|
'<div class="podman-modal-head"><h3>New Container</h3></div>' +
|
||||||
|
'<form class="podman-modal-body">' +
|
||||||
|
'<div class="podman-modal-field"><label>Image</label>' +
|
||||||
|
'<input type="text" id="cc-image" placeholder="docker.io/library/postgres:16"></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Name (optional)</label>' +
|
||||||
|
'<input type="text" id="cc-name" placeholder="my-container"></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Network</label>' +
|
||||||
|
'<select id="cc-network"><option value="bridge">Bridge (default)</option>' +
|
||||||
|
'<option value="host">Host</option><option value="none">None</option></select></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Port mappings</label>' +
|
||||||
|
'<div class="podman-row-group" id="cc-ports"></div>' +
|
||||||
|
'<button type="button" class="podman-btn" data-add="port">+ Add port</button></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Volumes</label>' +
|
||||||
|
'<div class="podman-row-group" id="cc-volumes"></div>' +
|
||||||
|
'<button type="button" class="podman-btn" data-add="volume">+ Add volume</button></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Environment variables</label>' +
|
||||||
|
'<div class="podman-row-group" id="cc-env"></div>' +
|
||||||
|
'<button type="button" class="podman-btn" data-add="env">+ Add variable</button></div>' +
|
||||||
|
'<div class="podman-modal-field"><label>Restart policy</label>' +
|
||||||
|
'<select id="cc-restart"><option value="no">No</option><option value="on-failure">On failure</option>' +
|
||||||
|
'<option value="always">Always</option><option value="unless-stopped">Unless stopped</option></select></div>' +
|
||||||
|
'<div class="podman-modal-field podman-modal-checkbox"><label>' +
|
||||||
|
'<input type="checkbox" id="cc-privileged"> Privileged</label></div>' +
|
||||||
|
'<div class="podman-modal-field podman-modal-checkbox"><label>' +
|
||||||
|
'<input type="checkbox" id="cc-start" checked> Start after create</label></div>' +
|
||||||
|
'</form>' +
|
||||||
|
'<div class="podman-modal-actions">' +
|
||||||
|
'<button type="button" class="podman-btn" data-role="cancel">Cancel</button>' +
|
||||||
|
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">Create</button>' +
|
||||||
|
'</div></div>';
|
||||||
|
|
||||||
|
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||||
|
|
||||||
|
const portsGroup = backdrop.querySelector('#cc-ports');
|
||||||
|
const volumesGroup = backdrop.querySelector('#cc-volumes');
|
||||||
|
const envGroup = backdrop.querySelector('#cc-env');
|
||||||
|
addRow(portsGroup, portRowHtml);
|
||||||
|
addRow(volumesGroup, volumeRowHtml);
|
||||||
|
addRow(envGroup, envRowHtml);
|
||||||
|
|
||||||
|
backdrop.querySelector('[data-add="port"]').addEventListener('click', function () { addRow(portsGroup, portRowHtml); });
|
||||||
|
backdrop.querySelector('[data-add="volume"]').addEventListener('click', function () { addRow(volumesGroup, volumeRowHtml); });
|
||||||
|
backdrop.querySelector('[data-add="env"]').addEventListener('click', function () { addRow(envGroup, envRowHtml); });
|
||||||
|
|
||||||
|
// Populate the network dropdown with any existing custom (non-default)
|
||||||
|
// podman networks, in addition to the built-in bridge/host/none modes
|
||||||
|
// — best-effort: if the list call fails, the three built-ins still work.
|
||||||
|
P.get('networks', 'list').then(function (networks) {
|
||||||
|
const select = backdrop.querySelector('#cc-network');
|
||||||
|
networks.filter(function (n) { return !n.isDefault; }).forEach(function (n) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = n.name;
|
||||||
|
opt.textContent = n.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
}).catch(function () { /* built-in modes still usable */ });
|
||||||
|
|
||||||
|
backdrop.querySelector('#cc-image').focus();
|
||||||
|
|
||||||
|
function close() { backdrop.remove(); }
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
let box = backdrop.querySelector('.podman-modal-error');
|
||||||
|
if (!box) {
|
||||||
|
box = document.createElement('div');
|
||||||
|
box.className = 'podman-modal-error';
|
||||||
|
backdrop.querySelector('.podman-modal-body').appendChild(box);
|
||||||
|
}
|
||||||
|
box.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const image = backdrop.querySelector('#cc-image').value.trim();
|
||||||
|
if (!image) {
|
||||||
|
showError('"Image" is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ports = readRows(portsGroup).filter(function (r) { return r.hostPort && r.containerPort; });
|
||||||
|
const volumes = readRows(volumesGroup).filter(function (r) { return r.source && r.containerPath; });
|
||||||
|
const env = readRows(envGroup).filter(function (r) { return r.key; });
|
||||||
|
|
||||||
|
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
P.post('containers', 'create', {
|
||||||
|
image: image,
|
||||||
|
name: backdrop.querySelector('#cc-name').value.trim(),
|
||||||
|
networkMode: backdrop.querySelector('#cc-network').value,
|
||||||
|
ports: ports,
|
||||||
|
volumes: volumes,
|
||||||
|
env: env,
|
||||||
|
restartPolicy: backdrop.querySelector('#cc-restart').value,
|
||||||
|
privileged: backdrop.querySelector('#cc-privileged').checked,
|
||||||
|
startAfterCreate: backdrop.querySelector('#cc-start').checked,
|
||||||
|
}).then(function () {
|
||||||
|
close();
|
||||||
|
return load();
|
||||||
|
}).catch(function (err) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
showError(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
|
||||||
|
backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit);
|
||||||
|
backdrop.querySelector('form').addEventListener('submit', function (e) { e.preventDefault(); submit(); });
|
||||||
|
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); });
|
||||||
|
document.addEventListener('keydown', function onKey(e) {
|
||||||
|
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleAction(id, action, btn) {
|
function handleAction(id, action, btn) {
|
||||||
const doIt = function (extra) {
|
const doIt = function (extra) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -97,6 +271,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
P.el('containers-create-btn').addEventListener('click', openCreateContainerModal);
|
||||||
|
|
||||||
P.el('containers-search').addEventListener('input', function (e) {
|
P.el('containers-search').addEventListener('input', function (e) {
|
||||||
searchTerm = e.target.value.trim().toLowerCase();
|
searchTerm = e.target.value.trim().toLowerCase();
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|||||||
@@ -43,10 +43,15 @@
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
P.el('images-pull-btn').addEventListener('click', function () {
|
P.el('images-pull-btn').addEventListener('click', function () {
|
||||||
const reference = prompt('Image to pull (e.g. docker.io/library/postgres:16):');
|
P.openFormModal({
|
||||||
if (!reference) return;
|
title: 'Pull Image',
|
||||||
P.post('images', 'pull', { reference: reference }).then(load).catch(function (err) {
|
submitLabel: 'Pull',
|
||||||
alert('Pull failed: ' + err.message);
|
fields: [
|
||||||
|
{ name: 'reference', label: 'Image reference', required: true, placeholder: 'docker.io/library/postgres:16' },
|
||||||
|
],
|
||||||
|
onSubmit: function (values) {
|
||||||
|
return P.post('images', 'pull', { reference: values.reference }).then(load);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,16 @@
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
P.el('networks-create-btn').addEventListener('click', function () {
|
P.el('networks-create-btn').addEventListener('click', function () {
|
||||||
const name = prompt('New network name:');
|
P.openFormModal({
|
||||||
if (!name) return;
|
title: 'New Network',
|
||||||
const subnet = prompt('Subnet (optional, e.g. 10.89.2.0/24):') || undefined;
|
submitLabel: 'Create',
|
||||||
P.post('networks', 'create', { name: name, driver: 'bridge', subnet: subnet }).then(load).catch(function (err) {
|
fields: [
|
||||||
alert('Create failed: ' + err.message);
|
{ name: 'name', label: 'Network name', required: true, placeholder: 'my-network' },
|
||||||
|
{ name: 'subnet', label: 'Subnet (optional)', placeholder: '10.89.2.0/24' },
|
||||||
|
],
|
||||||
|
onSubmit: function (values) {
|
||||||
|
return P.post('networks', 'create', { name: values.name, driver: 'bridge', subnet: values.subnet || undefined }).then(load);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
* javascript/volumes.js
|
* javascript/volumes.js
|
||||||
*
|
*
|
||||||
* Volumes panel: named-volume table + create/remove, backed by
|
* Volumes panel: named-volume table + create/remove, backed by
|
||||||
* ajax/volumes.php. Bind mounts are deliberately not shown here — see
|
* ajax/volumes.php. Container-level bind mounts (e.g. appdata under
|
||||||
* that file's header comment.
|
* /mnt/user/appdata/...) are deliberately not shown here, since they
|
||||||
|
* aren't a libpod-managed resource at all — see that file's header
|
||||||
|
* comment. A named volume created here WITH a host path (v.hostPath) IS
|
||||||
|
* still a real, listed podman volume, just backed by that path instead
|
||||||
|
* of podman's own internal storage — see PodmanClient::createVolume().
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -11,11 +15,14 @@
|
|||||||
let volumes = [];
|
let volumes = [];
|
||||||
|
|
||||||
function rowHtml(v) {
|
function rowHtml(v) {
|
||||||
|
const pathCell = v.hostPath
|
||||||
|
? P.escapeHtml(v.hostPath) + ' <span class="podman-chip podman-chip-neutral" title="Bind-mounted to this host path">bind</span>'
|
||||||
|
: P.escapeHtml(v.mountpoint);
|
||||||
return '' +
|
return '' +
|
||||||
'<tr data-name="' + P.escapeHtml(v.name) + '">' +
|
'<tr data-name="' + P.escapeHtml(v.name) + '">' +
|
||||||
'<td>' + P.escapeHtml(v.name) + '</td>' +
|
'<td>' + P.escapeHtml(v.name) + '</td>' +
|
||||||
'<td><span class="podman-chip podman-chip-neutral">' + P.escapeHtml(v.driver) + '</span></td>' +
|
'<td><span class="podman-chip podman-chip-neutral">' + P.escapeHtml(v.driver) + '</span></td>' +
|
||||||
'<td class="mono podman-row-sub">' + P.escapeHtml(v.mountpoint) + '</td>' +
|
'<td class="mono podman-row-sub">' + pathCell + '</td>' +
|
||||||
'<td class="tnum">' + v.usedBy + '</td>' +
|
'<td class="tnum">' + v.usedBy + '</td>' +
|
||||||
'<td class="podman-actions"><button class="podman-btn podman-btn-icon" data-action="remove"' +
|
'<td class="podman-actions"><button class="podman-btn podman-btn-icon" data-action="remove"' +
|
||||||
(v.usedBy > 0 ? ' disabled title="In use by a container"' : ' title="Remove"') + '>🗑</button></td>' +
|
(v.usedBy > 0 ? ' disabled title="In use by a container"' : ' title="Remove"') + '>🗑</button></td>' +
|
||||||
@@ -42,10 +49,20 @@
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
P.el('volumes-create-btn').addEventListener('click', function () {
|
P.el('volumes-create-btn').addEventListener('click', function () {
|
||||||
const name = prompt('New volume name:');
|
P.openFormModal({
|
||||||
if (!name) return;
|
title: 'New Volume',
|
||||||
P.post('volumes', 'create', { name: name }).then(load).catch(function (err) {
|
submitLabel: 'Create',
|
||||||
alert('Create failed: ' + err.message);
|
fields: [
|
||||||
|
{ name: 'name', label: 'Volume name', required: true, placeholder: 'my-volume' },
|
||||||
|
{
|
||||||
|
name: 'path', label: 'Host path (optional)', placeholder: '/mnt/cache/appdata/my-volume',
|
||||||
|
hint: 'Leave empty for a podman-managed volume. Set this to bind the volume ' +
|
||||||
|
'directly to an existing directory on disk (e.g. a cache pool path) instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSubmit: function (values) {
|
||||||
|
return P.post('volumes', 'create', { name: values.name, path: values.path || undefined }).then(load);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -222,4 +222,52 @@
|
|||||||
.podman-badge-update { font-size: 10px; font-weight: 700; color: var(--accent-strong); background: color-mix(in srgb, var(--accent) 16%, transparent); padding: 2px 7px; border-radius: 100px; margin-left: 8px; }
|
.podman-badge-update { font-size: 10px; font-weight: 700; color: var(--accent-strong); background: color-mix(in srgb, var(--accent) 16%, transparent); padding: 2px 7px; border-radius: 100px; margin-left: 8px; }
|
||||||
|
|
||||||
.podman-loading, .podman-error { padding: 32px 18px; text-align: center; color: var(--text-faint); font-size: 13px; }
|
.podman-loading, .podman-error { padding: 32px 18px; text-align: center; color: var(--text-faint); font-size: 13px; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal form dialog — replaces browser-native prompt()/confirm() for any
|
||||||
|
* action that needs more than a single yes/no (e.g. "New Volume" needs a
|
||||||
|
* name AND an optional host path together, which prompt() can't express
|
||||||
|
* as one coherent form). See app.js's openFormModal().
|
||||||
|
*/
|
||||||
|
.podman-modal-backdrop {
|
||||||
|
position: fixed; inset: 0; background: rgba(15, 17, 20, .55); z-index: 1000;
|
||||||
|
display: flex; align-items: center; justify-content: center; padding: 20px;
|
||||||
|
}
|
||||||
|
.podman-modal {
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow); width: 100%; max-width: 420px; max-height: calc(100vh - 40px);
|
||||||
|
overflow-y: auto; color: var(--text); font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
.podman-modal-head { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
||||||
|
.podman-modal-head h3 { font-size: 15px; }
|
||||||
|
.podman-modal-body { padding: 16px 20px; display: grid; gap: 14px; }
|
||||||
|
.podman-modal-field label { display: block; font-weight: 600; font-size: 12.5px; margin-bottom: 6px; }
|
||||||
|
.podman-modal-field input[type="text"] {
|
||||||
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px; padding: 8px 10px;
|
||||||
|
font-size: 13px; color: var(--text); width: 100%; font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
.podman-modal-field .hint { font-size: 11.5px; color: var(--text-faint); margin-top: 4px; }
|
||||||
|
.podman-modal-field select {
|
||||||
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px; padding: 8px 10px;
|
||||||
|
font-size: 13px; color: var(--text); font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
.podman-modal-checkbox label { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 12.5px; margin-bottom: 0; }
|
||||||
|
.podman-modal-error { font-size: 12.5px; color: var(--bad); background: var(--bad-bg); border-radius: 7px; padding: 8px 10px; }
|
||||||
|
.podman-modal-actions { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
|
||||||
.podman-error { color: var(--bad); }
|
.podman-error { color: var(--bad); }
|
||||||
|
|
||||||
|
/* Wider variant + repeatable row groups, for forms with more than 1-2 fields (e.g. Create Container). */
|
||||||
|
.podman-modal-wide { max-width: 640px; }
|
||||||
|
.podman-row-group { display: grid; gap: 8px; margin-bottom: 8px; }
|
||||||
|
.podman-row-group-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.podman-row-group-item input[type="text"] {
|
||||||
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px; padding: 7px 9px;
|
||||||
|
font-size: 12.5px; color: var(--text); font-family: var(--font-mono); flex: 1; min-width: 0;
|
||||||
|
}
|
||||||
|
.podman-row-group-item select {
|
||||||
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px; padding: 7px 9px;
|
||||||
|
font-size: 12.5px; color: var(--text); font-family: var(--font-ui); flex: none;
|
||||||
|
}
|
||||||
|
.podman-row-group-item span { color: var(--text-faint); font-size: 12px; flex: none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user