Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
166f0d96d1 | ||
|
|
9b9d1a05ac | ||
|
|
66ef830234 | ||
|
|
676fd8bc89 | ||
|
|
20d8686b61 | ||
|
|
9557f8c8f8 | ||
|
|
5fb7376b64 | ||
|
|
7f6fcb9166 |
@@ -1,6 +1,10 @@
|
||||
name: Release
|
||||
|
||||
# Publishes a GitHub Release for a version tag (vX.Y.Z).
|
||||
# Publishes a Gitea Release for a version tag (vX.Y.Z). This project lives
|
||||
# on a self-hosted Gitea instance (git.mp-mueller.de), not GitHub — despite
|
||||
# the .github/workflows/ path (kept there because Gitea Actions picks up
|
||||
# workflows from that path too, verified live: lint.yml/build-packages.yml
|
||||
# both already run from here without any .gitea/workflows/ copy).
|
||||
#
|
||||
# By design, this workflow does NOT bump versions or modify podman.plg
|
||||
# itself — that happens locally via `scripts/release.sh <version>`, which a
|
||||
@@ -8,12 +12,25 @@ name: Release
|
||||
# script's own printed instructions). This workflow's only job is to:
|
||||
# 1. Rebuild all packages from the tagged commit in a clean Slackware
|
||||
# container (reproducibility check + provenance — we don't trust
|
||||
# whatever a maintainer happened to have in their local dist/).
|
||||
# whatever a maintainer happened to have in their local dist/). This
|
||||
# only actually verifies anything because
|
||||
# scripts/lib/slackbuild-common.sh's sb_make_package() pins
|
||||
# SOURCE_DATE_EPOCH to the tagged commit's timestamp before calling
|
||||
# makepkg — without that, two separate builds of the identical commit
|
||||
# produce byte-different .txz files (different file mtimes baked into
|
||||
# the tar archive) and this whole verify step fails on the first
|
||||
# package it happens to check, every time (found live: aardvark-dns).
|
||||
# 2. Verify checksums match what's already committed in plugin/podman.plg
|
||||
# at this tag (catches a release.sh run that wasn't followed by a
|
||||
# matching commit — see the "Verify plg matches build" step).
|
||||
# 3. Create the GitHub Release and attach the .txz packages, checksum
|
||||
# manifests, and podman.plg.
|
||||
# 3. Create the Gitea Release and attach the .txz packages, checksum
|
||||
# manifests, and podman.plg, via Gitea's own REST API — NOT
|
||||
# softprops/action-gh-release, which talks to GitHub's API and simply
|
||||
# cannot publish anywhere else. Gitea Actions injects a real,
|
||||
# repo-scoped API token as secrets.GITHUB_TOKEN (the same env var
|
||||
# name GitHub Actions uses, for exactly this kind of drop-in
|
||||
# compatibility) — permissions: contents: write above is what scopes
|
||||
# that token to allow creating releases.
|
||||
#
|
||||
# See docs/ARCHITECTURE.md section 13 (Updates).
|
||||
|
||||
@@ -25,13 +42,17 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_HOST: git.mp-mueller.de
|
||||
GITEA_REPO: magges/unraid-podman
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build release packages
|
||||
uses: ./.github/workflows/build-packages.yml
|
||||
|
||||
publish:
|
||||
name: Publish GitHub Release
|
||||
name: Publish Gitea Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -79,18 +100,51 @@ jobs:
|
||||
' CHANGELOG.md > /tmp/release-notes.md
|
||||
echo "path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "unraid-podman v${{ steps.version.outputs.value }}"
|
||||
body_path: ${{ steps.changelog.outputs.path }}
|
||||
# v0.x tags are treated as pre-releases until the plugin reaches a
|
||||
# first stable 1.0.0 — see docs/ROADMAP.md.
|
||||
prerelease: ${{ startsWith(steps.version.outputs.value, '0.') }}
|
||||
files: |
|
||||
dist/*.txz
|
||||
dist/*.sha256
|
||||
dist/*.md5
|
||||
dist/CHECKSUMS.sha256
|
||||
dist/CHECKSUMS.md5
|
||||
plugin/podman.plg
|
||||
- name: Publish Gitea Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -eu
|
||||
API="https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}"
|
||||
case "${{ steps.version.outputs.value }}" in
|
||||
0.*) prerelease=true ;;
|
||||
*) prerelease=false ;;
|
||||
esac
|
||||
|
||||
# Idempotent: if a release for this tag already exists (e.g. a
|
||||
# re-run after a transient failure), delete it first rather than
|
||||
# erroring on Gitea's own duplicate-tag conflict.
|
||||
existing_id=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" "${API}/releases/tags/${TAG}" | jq -r '.id // empty')
|
||||
if [ -n "$existing_id" ]; then
|
||||
echo "==> Deleting existing release id=$existing_id for tag $TAG"
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" "${API}/releases/${existing_id}"
|
||||
fi
|
||||
|
||||
body_json=$(jq -Rs '.' < "${{ steps.changelog.outputs.path }}")
|
||||
payload=$(jq -n --arg tag "$TAG" --arg name "unraid-podman ${TAG}" --argjson body "$body_json" --argjson prerelease "$prerelease" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: $prerelease}')
|
||||
|
||||
release_id=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "$payload" "${API}/releases" | jq -r '.id')
|
||||
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
|
||||
echo "!! Could not create release (no id in response)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> Created release id=$release_id, uploading assets..."
|
||||
|
||||
for f in dist/*.txz dist/*.sha256 dist/*.md5 dist/CHECKSUMS.sha256 dist/CHECKSUMS.md5 plugin/podman.plg; do
|
||||
name=$(basename "$f")
|
||||
code=$(curl -s -o /tmp/upload_resp.json -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${f};filename=${name}" \
|
||||
"${API}/releases/${release_id}/assets?name=${name}")
|
||||
if [ "$code" != "201" ]; then
|
||||
echo "!! Upload failed ($code) for $name:" >&2
|
||||
cat /tmp/upload_resp.json >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " uploaded: $name"
|
||||
done
|
||||
|
||||
echo "==> Published: https://${GITEA_HOST}/${GITEA_REPO}/releases/tag/${TAG}"
|
||||
|
||||
+31
-1
@@ -9,7 +9,37 @@ see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#52-build-strategie)).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.1] - 2026-07-13
|
||||
## [0.1.3] - 2026-07-12
|
||||
|
||||
### Added
|
||||
- Settings: a "Podman Service" card with live status plus Start/Stop/Restart,
|
||||
each shown in a progress log modal — diagnosing and recovering a podman
|
||||
that failed to start no longer needs SSH/terminal access at all.
|
||||
- Settings: "Format a Disk for Podman Storage", for a single-disk system
|
||||
with no cache pool. Only ever lists disks with no existing partition,
|
||||
filesystem, or RAID/ZFS membership signature, and never the Unraid boot
|
||||
flash — both excluded by multiple independent checks, not just one.
|
||||
Formatted disks are remounted by UUID on every boot.
|
||||
|
||||
### Fixed
|
||||
- `STORAGE_PATH` was never auto-created, even when its parent (a real,
|
||||
already-mounted cache pool) existed — only failed if genuinely nothing
|
||||
was mounted at all. Root cause of a real "cannot reach the Podman API
|
||||
socket" report on a fresh install with a perfectly normal cache pool.
|
||||
- Cache-busting for the WebUI's own JS/CSS never actually worked, at all,
|
||||
the entire time — Unraid runs `.page` PHP through `eval()`, and
|
||||
`__DIR__` inside `eval()`'d code resolves to the *eval() call site's*
|
||||
directory, not the plugin's. Every "hard refresh" this project's own
|
||||
docs/commits ever recommended only worked because Ctrl+Shift+R bypasses
|
||||
the browser cache directly, independent of this (broken) mechanism.
|
||||
- Settings' service status chip showed "Not running" immediately after a
|
||||
successful Start, because the "is it running" check only recognized
|
||||
`rc.podman status`'s own wording, not `start`/`stop`/`restart`'s.
|
||||
- The Podman Service log showed `rc.podman`'s multi-line output as one
|
||||
run-together paragraph (missing `white-space: pre-wrap` on the pane
|
||||
itself, not just its per-line children).
|
||||
|
||||
## [0.1.1] - 2026-07-12
|
||||
|
||||
### Fixed
|
||||
- `plugin/podman.plg`'s `<URL>`/`<MD5>` entity values were split across their
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
# blocked on podman's full startup sequence (preflight, storage mount,
|
||||
# service start, autostart chain) — mirrors how unassigned.devices
|
||||
# backgrounds its own longer-running "started" hook.
|
||||
#
|
||||
# podman-mount-managed-disk.sh runs first, still within the same
|
||||
# backgrounded subshell: it's a no-op unless the WebUI's "Format a Disk
|
||||
# for Podman Storage" flow (ajax/disks.php) was ever used, and rc.podman
|
||||
# start's own storage step needs that disk already mounted at
|
||||
# $STORAGE_PATH to succeed — see that script's own header comment.
|
||||
# =============================================================================
|
||||
|
||||
/etc/rc.d/rc.podman start > /dev/null 2>&1 & disown
|
||||
(/usr/local/sbin/podman-mount-managed-disk.sh; /etc/rc.d/rc.podman start) > /dev/null 2>&1 & disown
|
||||
|
||||
+14
-14
@@ -50,7 +50,7 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "podman">
|
||||
<!ENTITY author "unraid-podman contributors">
|
||||
<!ENTITY version "0.1.1">
|
||||
<!ENTITY version "0.1.3">
|
||||
<!-- "Podman" (no parent) — Podman.page declares Menu="Podman", making it
|
||||
its own top-level nav tab next to Docker/VMs, not nested under
|
||||
Settings — see webui/plugins/podman/Podman.page. -->
|
||||
@@ -69,7 +69,7 @@
|
||||
<!-- Release asset base — matches scripts/release.sh's RELEASE_BASE_URL
|
||||
exactly; both must agree since release.sh is what publishes the
|
||||
packages this URL is expected to find. -->
|
||||
<!ENTITY baseURL "https://git.mp-mueller.de/magges/unraid-podman/releases/download/v0.1.1">
|
||||
<!ENTITY baseURL "https://git.mp-mueller.de/magges/unraid-podman/releases/download/v0.1.3">
|
||||
|
||||
<!-- Slackware package naming components — must match versions.env's
|
||||
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
|
||||
@@ -88,31 +88,31 @@
|
||||
|
||||
<!ENTITY podman_txz_version "6.0.1">
|
||||
<!ENTITY podman_txz_file "podman-6.0.1-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY podman_txz_md5 "4a9fdb25800fac506876903b09f64e7b">
|
||||
<!ENTITY podman_txz_md5 "f294f9eaf7139b43d153cafaed34de48">
|
||||
|
||||
<!ENTITY conmon_txz_version "2.2.1">
|
||||
<!ENTITY conmon_txz_file "conmon-2.2.1-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY conmon_txz_md5 "358136a2fbc8e629d50863466aed5cd3">
|
||||
<!ENTITY conmon_txz_md5 "e1e3b4b15398d965ee4efdfac93b1867">
|
||||
|
||||
<!ENTITY crun_txz_version "1.28">
|
||||
<!ENTITY crun_txz_file "crun-1.28-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY crun_txz_md5 "4108cca9a2e0673d1206e15a3d51cf73">
|
||||
<!ENTITY crun_txz_md5 "891914e9448a960dd28a9d1ea67e63b3">
|
||||
|
||||
<!ENTITY netavark_txz_version "2.0.0">
|
||||
<!ENTITY netavark_txz_file "netavark-2.0.0-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY netavark_txz_md5 "4970584505c056fd18995daf2728cb56">
|
||||
<!ENTITY netavark_txz_md5 "ef8509cc51d24ef1cae4d96f1fd92a01">
|
||||
|
||||
<!ENTITY aardvark_dns_txz_version "2.0.0">
|
||||
<!ENTITY aardvark_dns_txz_file "aardvark-dns-2.0.0-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY aardvark_dns_txz_md5 "168bbe9298db17fe51d6849b0b670dd4">
|
||||
<!ENTITY aardvark_dns_txz_md5 "2a5a5c2e3684b4130edd4b128484908f">
|
||||
|
||||
<!ENTITY passt_txz_version "git6ef3d1c">
|
||||
<!ENTITY passt_txz_file "passt-git6ef3d1c-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY passt_txz_md5 "cedd2d4b4ff1c2c22b13947bb754d427">
|
||||
<!ENTITY passt_txz_md5 "970a8f15257e5861933ae58459da2bd4">
|
||||
|
||||
<!ENTITY fuse_overlayfs_txz_version "1.17">
|
||||
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-1.17-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY fuse_overlayfs_txz_md5 "95bf694c5480be2069a581748b2dfb09">
|
||||
<!ENTITY fuse_overlayfs_txz_md5 "11f5aefe0372427ae194017f54af5d47">
|
||||
|
||||
<!-- catatonit and nftables are runtime dependencies this plugin ships,
|
||||
not upstream podman-ecosystem components — see packages/catatonit/
|
||||
@@ -122,7 +122,7 @@
|
||||
systemd/dbus for firewalld) and why neither is built from source. -->
|
||||
<!ENTITY catatonit_txz_version "0.2.1">
|
||||
<!ENTITY catatonit_txz_file "catatonit-0.2.1-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY catatonit_txz_md5 "a3fe1f08981a7fb07337838ba7d00de3">
|
||||
<!ENTITY catatonit_txz_md5 "e17faf29b0c618a73def017583697aca">
|
||||
|
||||
<!ENTITY nftables_txz_version "1.0.1">
|
||||
<!ENTITY nftables_txz_file "nftables-1.0.1-x86_64-1_unraidpodman.txz">
|
||||
@@ -133,7 +133,7 @@
|
||||
every Compose panel action fails outright on a clean install. -->
|
||||
<!ENTITY podman_compose_txz_version "1.6.0">
|
||||
<!ENTITY podman_compose_txz_file "podman-compose-1.6.0-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY podman_compose_txz_md5 "a4e1bdbfe397f1b693815e003e371990">
|
||||
<!ENTITY podman_compose_txz_md5 "2d7e21b071d40ab3518acbec67967777">
|
||||
|
||||
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
|
||||
sbin/ scripts, event/ hooks, config templates — see
|
||||
@@ -141,9 +141,9 @@
|
||||
version always equals the plugin's own &version; — see
|
||||
packages/unraid-podman/unraid-podman.SlackBuild, which reads it
|
||||
straight out of this very file rather than tracking it twice. -->
|
||||
<!ENTITY unraid_podman_txz_version "0.1.1">
|
||||
<!ENTITY unraid_podman_txz_file "unraid-podman-0.1.1-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY unraid_podman_txz_md5 "25e5933e113e09bfa07c0d5169ccac18">
|
||||
<!ENTITY unraid_podman_txz_version "0.1.3">
|
||||
<!ENTITY unraid_podman_txz_file "unraid-podman-0.1.3-x86_64-1_unraidpodman.txz">
|
||||
<!ENTITY unraid_podman_txz_md5 "ceeb31f08ad0df62dee1b6a6f1ed3bba">
|
||||
]>
|
||||
|
||||
<PLUGIN name="&name;"
|
||||
|
||||
@@ -168,6 +168,33 @@ podman_storage_path_is_safe() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_path_has_real_mount_ancestor <path>
|
||||
#
|
||||
# True if <path> itself, or its nearest EXISTING ancestor directory, lives
|
||||
# on a different filesystem than / (root) — i.e. something is genuinely
|
||||
# mounted along this path (a cache pool, a dedicated disk, ...), even if
|
||||
# the exact leaf directory doesn't exist yet. False only when nothing real
|
||||
# is mounted anywhere along the path (root/RAM all the way up), which is
|
||||
# the one case that's actually unsafe to silently `mkdir -p` into.
|
||||
#
|
||||
# This exists because this project never auto-created $STORAGE_PATH
|
||||
# itself (only podman.img inside it) — found live: a perfectly normal,
|
||||
# already-mounted cache pool still failed preflight/storage-create with
|
||||
# "does not exist", because the pool's own .../system/podman subdirectory
|
||||
# had simply never been created. "Does the exact leaf directory exist" was
|
||||
# always the wrong question; "is a real filesystem mounted somewhere along
|
||||
# this path" is the one that actually matters.
|
||||
# -----------------------------------------------------------------------------
|
||||
podman_path_has_real_mount_ancestor() {
|
||||
local path="$1"
|
||||
local parent="$path"
|
||||
while [ ! -d "$parent" ] && [ "$parent" != "/" ]; do
|
||||
parent="$(dirname "$parent")"
|
||||
done
|
||||
[ "$parent" != "/" ] && [ "$(stat -c %d "$parent")" != "$(stat -c %d /)" ]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# podman_require_command <binary>
|
||||
#
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# plugin/sbin/podman-mount-managed-disk.sh
|
||||
#
|
||||
# Remounts, on every boot, a disk the WebUI's "Format a Disk for Podman
|
||||
# Storage" flow (webui/plugins/podman/ajax/disks.php's "format" action)
|
||||
# formatted and mounted for a single-disk system with no cache pool —
|
||||
# that disk is deliberately outside Unraid's own array/cache pool
|
||||
# management (it's just a plain XFS filesystem on an otherwise-unassigned
|
||||
# disk), so nothing else on the system would remount it after a reboot.
|
||||
#
|
||||
# Called from plugin/event/disks_mounted, BEFORE rc.podman start, so
|
||||
# $STORAGE_PATH (pointed at this disk's mountpoint via Settings) is a real
|
||||
# mounted filesystem by the time podman-storage.sh's `create`/`mount`
|
||||
# steps run — see that script's "does not exist or is not mounted" check.
|
||||
#
|
||||
# Does nothing (exit 0) if the plugin was never used to format a disk —
|
||||
# /boot/config/plugins/podman/managed-disk.cfg only exists after that flow
|
||||
# has actually run at least once.
|
||||
# =============================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./podman-common.sh
|
||||
. "$SCRIPT_DIR/podman-common.sh"
|
||||
|
||||
MANAGED_DISK_CFG="$PODMAN_BOOT_DIR/managed-disk.cfg"
|
||||
[ -f "$MANAGED_DISK_CFG" ] || exit 0
|
||||
|
||||
UUID=""
|
||||
MOUNTPOINT=""
|
||||
# shellcheck source=/dev/null
|
||||
. "$MANAGED_DISK_CFG"
|
||||
|
||||
if [ -z "$UUID" ] || [ -z "$MOUNTPOINT" ]; then
|
||||
podman_log_error "mount-managed-disk: $MANAGED_DISK_CFG is missing UUID/MOUNTPOINT, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if mountpoint -q "$MOUNTPOINT" 2> /dev/null; then
|
||||
podman_log "mount-managed-disk: $MOUNTPOINT already mounted"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$MOUNTPOINT"
|
||||
if mount "UUID=$UUID" "$MOUNTPOINT"; then
|
||||
podman_log "mount-managed-disk: mounted UUID=$UUID at $MOUNTPOINT"
|
||||
else
|
||||
podman_log_error "mount-managed-disk: failed to mount UUID=$UUID at $MOUNTPOINT (disk removed/renamed?)"
|
||||
fi
|
||||
@@ -75,6 +75,14 @@ elif [ -d "$STORAGE_PATH" ]; then
|
||||
else
|
||||
fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem"
|
||||
fi
|
||||
elif podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then
|
||||
# The leaf directory doesn't exist yet, but a real filesystem IS mounted
|
||||
# somewhere along its path (e.g. the cache pool itself) — podman-storage.sh
|
||||
# create will mkdir -p it. Not a failure; see that check's own comment
|
||||
# for the live bug this used to cause (a normal, already-mounted cache
|
||||
# pool failing preflight just because its .../system/podman subdirectory
|
||||
# had never been created).
|
||||
ok "STORAGE_PATH ($STORAGE_PATH) doesn't exist yet, but resolves onto a mounted filesystem — will be created"
|
||||
else
|
||||
fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?"
|
||||
fi
|
||||
|
||||
@@ -46,9 +46,13 @@ cmd_create() {
|
||||
fi
|
||||
|
||||
if [ ! -d "$STORAGE_PATH" ]; then
|
||||
podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted."
|
||||
podman_log_error "storage: check that the configured cache pool/disk is present before starting podman."
|
||||
return 1
|
||||
if ! podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then
|
||||
podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted."
|
||||
podman_log_error "storage: check that the configured cache pool/disk is present before starting podman."
|
||||
return 1
|
||||
fi
|
||||
podman_log "storage: $STORAGE_PATH doesn't exist yet under an already-mounted filesystem — creating it"
|
||||
mkdir -p "$STORAGE_PATH"
|
||||
fi
|
||||
|
||||
# Free space check: refuse to create an image bigger than what's actually
|
||||
|
||||
@@ -173,6 +173,23 @@ sb_make_package() {
|
||||
find "$PKG" -type f \( -perm -u+x -o -name '*.so*' \) -exec sh -c \
|
||||
'file "$1" | grep -q ELF && strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \;
|
||||
|
||||
# Reproducible builds: Slackware's own makepkg already sorts its file
|
||||
# list (LC_COLLATE=C sort) before archiving, so member ORDER is already
|
||||
# deterministic — but it only clamps file mtimes in the resulting tar
|
||||
# when $SOURCE_DATE_EPOCH is set (verified by reading a real
|
||||
# /sbin/makepkg: `if [ -n "${SOURCE_DATE_EPOCH}" ]; then MTIME=
|
||||
# "--clamp-mtime --mtime=@${SOURCE_DATE_EPOCH}"; fi`). Without it, every
|
||||
# separate build run stamps freshly-compiled files with its own wall-clock
|
||||
# time, so two builds of the *same* source produce byte-different .txz
|
||||
# files — which is exactly what broke release.yml's "rebuild in CI and
|
||||
# verify it matches the checksums committed in podman.plg" step (found
|
||||
# live: aardvark-dns's checksum differed between two separate Gitea
|
||||
# Actions runs of the identical tagged commit). Deriving it from the
|
||||
# repo's last commit time (not `date`/a random per-build value) keeps it
|
||||
# stable across any number of rebuilds of the same commit, while still
|
||||
# changing whenever the source actually does.
|
||||
export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(cd "$CWD/../.." && git log -1 --format=%ct 2>/dev/null || echo 0)}"
|
||||
|
||||
local pkg_file="$PRGNAM-$version-$arch-$build$tag.txz"
|
||||
( cd "$PKG" && makepkg --linkadd y --chown y "$OUTPUT/$pkg_file" )
|
||||
|
||||
|
||||
@@ -23,14 +23,28 @@ Icon="podman"
|
||||
* 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.
|
||||
* podman.css for a long time after a plugin update.
|
||||
*
|
||||
* Deliberately a hardcoded absolute path, NOT __DIR__ — Unraid's own
|
||||
* PageBuilder runs every .page file's PHP through eval() (see
|
||||
* webGui/include/DefaultPageLayout/evalContent.php: "eval($evalContent)"),
|
||||
* and __DIR__/__FILE__ inside eval()'d code resolve to the eval() CALL
|
||||
* SITE (that file's own directory), not to Podman.page's real location —
|
||||
* a standard PHP eval() gotcha. Confirmed live: this made
|
||||
* podman_asset_version() return '0' for every single asset, always,
|
||||
* completely independent of any browser or PHP-FPM caching (ruled both
|
||||
* out first: neither a hard-reload nor a php-fpm restart changed the
|
||||
* result) — the version query string was never actually cache-busting
|
||||
* anything all session; every "hard refresh fixed it" moment was the
|
||||
* browser's own Ctrl+Shift+R bypass, not this mechanism. The rest of
|
||||
* this codebase already hardcodes this same install path elsewhere
|
||||
* (e.g. include/Config.php's $bootDir, plugin/podman.plg's event hook
|
||||
* paths) — Unraid plugins always install to /usr/local/emhttp/plugins/
|
||||
* <name>, so this isn't a new class of fragility.
|
||||
*/
|
||||
function podman_asset_version(string $relPath): string
|
||||
{
|
||||
$full = __DIR__ . $relPath;
|
||||
$full = '/usr/local/emhttp/plugins/podman' . $relPath;
|
||||
return is_file($full) ? (string) filemtime($full) : '0';
|
||||
}
|
||||
?>
|
||||
@@ -236,6 +250,23 @@ function podman_asset_version(string $relPath): string
|
||||
<button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button>
|
||||
</div>
|
||||
<div class="podman-grid">
|
||||
<div class="podman-card">
|
||||
<div class="podman-card-head">
|
||||
<div><h2>Podman Service</h2><div class="sub">Start, stop, restart, or check the podman.sock backend — no terminal needed.</div></div>
|
||||
</div>
|
||||
<div class="podman-card-pad">
|
||||
<div class="podman-service-row">
|
||||
<span class="podman-chip podman-chip-neutral" id="settings-service-chip"><span class="d"></span>Checking…</span>
|
||||
<button class="podman-btn" id="settings-service-status-btn">Refresh Status</button>
|
||||
<button class="podman-btn podman-btn-primary" id="settings-service-start-btn">▶ Start Podman</button>
|
||||
<button class="podman-btn podman-btn-ghost podman-btn-danger" id="settings-service-stop-btn">■ Stop Podman</button>
|
||||
<button class="podman-btn" id="settings-service-restart-btn">↻ Restart Podman</button>
|
||||
</div>
|
||||
<div class="hint">Stop/Restart first stop all running containers (each gets its own configured grace period) — not just the API service.</div>
|
||||
<div class="podman-log-pane" id="settings-service-log" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="podman-card">
|
||||
<div class="podman-card-head">
|
||||
<div><h2>Storage</h2><div class="sub">Where podman keeps images, containers and volumes on disk.</div></div>
|
||||
@@ -247,6 +278,13 @@ function podman_asset_version(string $relPath): string
|
||||
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="podman-field-row">
|
||||
<label>No cache pool?</label>
|
||||
<div>
|
||||
<button class="podman-btn" id="settings-format-disk-btn">Format a Disk for Podman Storage…</button>
|
||||
<div class="hint">Formats an unused disk with XFS and mounts it, for a single-disk system with no cache pool set up yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="podman-field-row">
|
||||
<label for="settings-storage-size">podman.img size</label>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
/**
|
||||
* ajax/disks.php
|
||||
*
|
||||
* Backs the Settings panel's "Format a Disk for Podman Storage" flow — for
|
||||
* a single-disk system with no cache pool set up yet, where podman's own
|
||||
* storage path (see include/Config.php's $storagePath default,
|
||||
* /mnt/cache/system/podman) has nowhere real to live, and podman never
|
||||
* fully starts as a result (found live: a fresh install on a system with
|
||||
* no cache pool failed with "cannot reach the Podman API socket" because
|
||||
* podman-storage.sh's `create` step refuses to run against a path that
|
||||
* isn't an actual mounted filesystem — see that script's header comment
|
||||
* for why /mnt/user/... FUSE paths don't work either).
|
||||
*
|
||||
* This is the one place in the plugin that formats a physical disk —
|
||||
* deliberately narrow and defensive: every candidate the "format" action
|
||||
* is asked to touch is re-derived server-side from the same safe-listing
|
||||
* logic "list_candidates" uses, never trusted from the request alone, and
|
||||
* only ever a whole, currently-unmounted, non-array disk with no existing
|
||||
* partitions is eligible in the first place.
|
||||
*
|
||||
* Actions (?action=...):
|
||||
* list_candidates GET -> [{device, sizeBytes, sizeFormatted, model, hasData}, ...]
|
||||
* format POST {"device": "/dev/sdc"} -> {"mountPath": "/mnt/disks/podman-storage"}
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../include/bootstrap.php';
|
||||
|
||||
const MOUNT_PATH = '/mnt/disks/podman-storage';
|
||||
const MANAGED_DISK_CFG = '/boot/config/plugins/podman/managed-disk.cfg';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'list_candidates':
|
||||
podman_json_response(list_candidate_disks());
|
||||
break;
|
||||
|
||||
case 'format':
|
||||
$body = podman_read_json_body();
|
||||
$device = (string) ($body['device'] ?? '');
|
||||
podman_json_response(format_disk($device));
|
||||
break;
|
||||
|
||||
default:
|
||||
podman_json_error("Unknown action '{$action}'", 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command and returns [exitCode, stdout+stderr combined]. Every
|
||||
* caller here passes a fixed argv array (never a shell string built from
|
||||
* request input), so there is no injection surface even before the
|
||||
* device-path validation in format_disk() below.
|
||||
*
|
||||
* @param array<int,string> $argv
|
||||
* @return array{0:int,1:string}
|
||||
*/
|
||||
function run(array $argv, int $timeoutSeconds = 30): array
|
||||
{
|
||||
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||
$process = proc_open($argv, $descriptors, $pipes);
|
||||
if (!is_resource($process)) {
|
||||
return [127, "could not start {$argv[0]}"];
|
||||
}
|
||||
stream_set_timeout($pipes[1], $timeoutSeconds);
|
||||
$out = (stream_get_contents($pipes[1]) ?: '') . (stream_get_contents($pipes[2]) ?: '');
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$code = proc_close($process);
|
||||
return [$code, trim($out)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every whole disk (not partition) libpod's host isn't already using —
|
||||
* mounted anywhere (itself or any partition), part of Unraid's own
|
||||
* array/cache pool (cross-checked against /var/local/emhttp/disks.ini,
|
||||
* the same state file Unraid's own array management writes — the boot
|
||||
* flash device is covered by this same check, since Unraid lists it there
|
||||
* too), or the disk backing the currently-booted root/flash filesystem.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
function list_candidate_disks(): array
|
||||
{
|
||||
// Nested partitions come back automatically as a "children" array in
|
||||
// -J's JSON tree — CHILDREN is not a real -o column (lsblk itself
|
||||
// rejects it: "unknown column: CHILDREN"; verified live against a
|
||||
// real host's lsblk). LABEL is included specifically to catch the
|
||||
// Unraid boot flash drive, which FAT-labels itself "UNRAID" — see the
|
||||
// safety note below for why that check exists at all.
|
||||
[$code, $out] = run(['lsblk', '-J', '-b', '-p', '-o', 'NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL,LABEL'], 10);
|
||||
if ($code !== 0) {
|
||||
podman_json_error('Could not list block devices: ' . $out, 500);
|
||||
}
|
||||
$tree = json_decode($out, true);
|
||||
if (!is_array($tree) || !isset($tree['blockdevices'])) {
|
||||
podman_json_error('Unexpected lsblk output', 500);
|
||||
}
|
||||
|
||||
$arrayDevices = unraid_array_device_names();
|
||||
|
||||
$candidates = [];
|
||||
foreach ($tree['blockdevices'] as $dev) {
|
||||
if (($dev['type'] ?? '') !== 'disk') {
|
||||
continue;
|
||||
}
|
||||
$name = (string) ($dev['name'] ?? '');
|
||||
$baseName = basename($name);
|
||||
if (in_array($baseName, $arrayDevices, true)) {
|
||||
continue;
|
||||
}
|
||||
// zram devices report TYPE "disk" too (RAM-backed, not persistent
|
||||
// storage — pointless and misleading to offer for this) and an
|
||||
// empty card-reader slot with no card inserted reports as a real
|
||||
// "disk" at 0 bytes; both are excluded outright rather than left
|
||||
// for the size-based hasData check below.
|
||||
if (str_starts_with($baseName, 'zram') || (int) ($dev['size'] ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (device_or_children_mounted($dev)) {
|
||||
continue;
|
||||
}
|
||||
if (device_or_children_labeled_unraid($dev)) {
|
||||
continue;
|
||||
}
|
||||
// Deliberately excludes (not just warns about) ANY disk that
|
||||
// already has a filesystem, partition, or other signature on it
|
||||
// or any of its partitions — not just ones lsblk reports as
|
||||
// currently mounted. Found live: a real host's cache pool disks
|
||||
// showed up here as "safe" with only a soft warning, because
|
||||
// they're ZFS pool members (fstype "zfs_member"/partition
|
||||
// present) rather than plain mounts, so the earlier
|
||||
// mounted-only check missed them entirely — the same blind spot
|
||||
// that also let the Unraid boot USB stick's own partition
|
||||
// through (FAT, not reported as "mounted" by lsblk either). A
|
||||
// disk being reused for podman storage must be genuinely blank;
|
||||
// asking a user to wipe it themselves first is a small price for
|
||||
// this being impossible to get wrong.
|
||||
if (!empty($dev['fstype']) || !empty($dev['children'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidates[] = [
|
||||
'device' => $name,
|
||||
'sizeBytes' => (int) ($dev['size'] ?? 0),
|
||||
'sizeFormatted' => podman_format_bytes((int) ($dev['size'] ?? 0)),
|
||||
'model' => trim((string) ($dev['model'] ?? '')) ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
function device_or_children_mounted(array $dev): bool
|
||||
{
|
||||
if (!empty($dev['mountpoint'])) {
|
||||
return true;
|
||||
}
|
||||
foreach ($dev['children'] ?? [] as $child) {
|
||||
if (device_or_children_mounted($child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the Unraid boot flash drive specifically: it FAT-labels itself
|
||||
* "UNRAID" (verified live: `blkid` on a real host's boot partition shows
|
||||
* LABEL_FATBOOT="UNRAID" LABEL="UNRAID") and — on at least one real,
|
||||
* modern Unraid setup — /boot is actually backed by a ZFS dataset
|
||||
* ("flash/boot"), not a direct mount of that partition, so it does NOT
|
||||
* show up as "mounted" via lsblk's own MOUNTPOINT column at all. This
|
||||
* label check is a second, independent layer specifically because that
|
||||
* gap meant the boot drive briefly passed every other check here during
|
||||
* development — never rely on a single signal for something this
|
||||
* destructive.
|
||||
*/
|
||||
function device_or_children_labeled_unraid(array $dev): bool
|
||||
{
|
||||
if (strtoupper(trim((string) ($dev['label'] ?? ''))) === 'UNRAID') {
|
||||
return true;
|
||||
}
|
||||
foreach ($dev['children'] ?? [] as $child) {
|
||||
if (device_or_children_labeled_unraid($child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string> bare device names ("sda", "nvme0n1", ...) Unraid
|
||||
* itself has assigned to the array or a cache pool, read from the same
|
||||
* /var/local/emhttp/disks.ini Unraid's own array management writes —
|
||||
* NOT parsed/guessed, so this stays correct across whatever array
|
||||
* layout a given host actually has.
|
||||
*/
|
||||
function unraid_array_device_names(): array
|
||||
{
|
||||
$path = '/var/local/emhttp/disks.ini';
|
||||
if (!is_readable($path)) {
|
||||
return [];
|
||||
}
|
||||
$ini = @parse_ini_file($path, true);
|
||||
if (!is_array($ini)) {
|
||||
return [];
|
||||
}
|
||||
$names = [];
|
||||
foreach ($ini as $section) {
|
||||
if (is_array($section) && !empty($section['device'])) {
|
||||
$names[] = basename((string) $section['device']);
|
||||
}
|
||||
}
|
||||
return $names;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
function format_disk(string $device): array
|
||||
{
|
||||
if (!preg_match('#^/dev/(sd[a-z]+|nvme\d+n\d+|vd[a-z]+)$#', $device)) {
|
||||
podman_json_error('Invalid or unsupported device path', 400);
|
||||
}
|
||||
|
||||
$allowed = array_column(list_candidate_disks(), 'device');
|
||||
if (!in_array($device, $allowed, true)) {
|
||||
podman_json_error("{$device} is not a currently-eligible disk (already in use, part of the array, or not found) — refusing to format it.", 400);
|
||||
}
|
||||
|
||||
$partDevice = preg_match('#nvme\d+n\d+$#', $device) ? "{$device}p1" : "{$device}1";
|
||||
|
||||
[$code, $out] = run(['wipefs', '-a', $device], 30);
|
||||
if ($code !== 0) {
|
||||
podman_json_error("wipefs failed: {$out}", 500);
|
||||
}
|
||||
|
||||
[$code, $out] = run(['parted', '-s', $device, 'mklabel', 'gpt', 'mkpart', 'primary', '1MiB', '100%'], 30);
|
||||
if ($code !== 0) {
|
||||
podman_json_error("parted failed: {$out}", 500);
|
||||
}
|
||||
|
||||
run(['partprobe', $device], 10);
|
||||
// Partition device nodes can take a moment to appear after partprobe.
|
||||
for ($i = 0; $i < 20 && !file_exists($partDevice); $i++) {
|
||||
usleep(250000);
|
||||
}
|
||||
if (!file_exists($partDevice)) {
|
||||
podman_json_error("Partition {$partDevice} did not appear after partitioning {$device}", 500);
|
||||
}
|
||||
|
||||
[$code, $out] = run(['mkfs.xfs', '-f', '-n', 'ftype=1', '-L', 'podmanstorage', $partDevice], 60);
|
||||
if ($code !== 0) {
|
||||
podman_json_error("mkfs.xfs failed: {$out}", 500);
|
||||
}
|
||||
|
||||
[$code, $uuid] = run(['blkid', '-s', 'UUID', '-o', 'value', $partDevice], 10);
|
||||
$uuid = trim($uuid);
|
||||
if ($code !== 0 || $uuid === '') {
|
||||
podman_json_error('Could not determine filesystem UUID after formatting', 500);
|
||||
}
|
||||
|
||||
if (!is_dir(MOUNT_PATH) && !mkdir(MOUNT_PATH, 0755, true) && !is_dir(MOUNT_PATH)) {
|
||||
podman_json_error('Could not create ' . MOUNT_PATH, 500);
|
||||
}
|
||||
[$code, $out] = run(['mount', 'UUID=' . $uuid, MOUNT_PATH], 15);
|
||||
if ($code !== 0) {
|
||||
podman_json_error("mount failed: {$out}", 500);
|
||||
}
|
||||
|
||||
// Persisted so plugin/sbin/podman-mount-managed-disk.sh (called from
|
||||
// the disks_mounted event hook, before rc.podman start) can remount
|
||||
// this same disk by UUID on every future boot — nothing else on the
|
||||
// system knows about this disk, since it's deliberately outside
|
||||
// Unraid's own array/cache pool management.
|
||||
$cfgDir = dirname(MANAGED_DISK_CFG);
|
||||
if (!is_dir($cfgDir) && !mkdir($cfgDir, 0755, true) && !is_dir($cfgDir)) {
|
||||
podman_json_error('Could not create ' . $cfgDir, 500);
|
||||
}
|
||||
$cfgContent = "# Written by the unraid-podman WebUI (ajax/disks.php) — do not edit by hand.\n"
|
||||
. 'UUID="' . $uuid . "\"\n"
|
||||
. 'MOUNTPOINT="' . MOUNT_PATH . "\"\n";
|
||||
if (file_put_contents(MANAGED_DISK_CFG, $cfgContent, LOCK_EX) === false) {
|
||||
podman_json_error('Formatted and mounted, but could not persist ' . MANAGED_DISK_CFG . ' for future boots', 500);
|
||||
}
|
||||
|
||||
return ['mountPath' => MOUNT_PATH];
|
||||
}
|
||||
@@ -16,12 +16,18 @@
|
||||
* save POST {"storagePath": "...", "storageImageSizeGb": 20,
|
||||
* "enabled": true, "stopTimeoutSeconds": 10}
|
||||
* autostart_save POST {"names": ["postgres", "nextcloud", ...]}
|
||||
* service_status GET -> {"running": bool, "output": "..."}
|
||||
* service_start POST -> {"running": bool, "output": "..."}
|
||||
* service_stop POST -> {"running": bool, "output": "..."}
|
||||
* service_restart POST -> {"running": bool, "output": "..."}
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../include/bootstrap.php';
|
||||
|
||||
const RC_PODMAN = '/etc/rc.d/rc.podman';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
@@ -45,10 +51,77 @@ switch ($action) {
|
||||
podman_json_response(['status' => 'saved']);
|
||||
break;
|
||||
|
||||
case 'service_status':
|
||||
podman_json_response(rc_podman('status', 15));
|
||||
break;
|
||||
|
||||
case 'service_start':
|
||||
podman_json_response(rc_podman('start', 120));
|
||||
break;
|
||||
|
||||
case 'service_stop':
|
||||
podman_json_response(rc_podman('stop', 120));
|
||||
break;
|
||||
|
||||
case 'service_restart':
|
||||
podman_json_response(rc_podman('restart', 120));
|
||||
break;
|
||||
|
||||
default:
|
||||
podman_json_error("Unknown action '{$action}'", 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shells out to /etc/rc.d/rc.podman <verb> — the plugin's own real
|
||||
* start/stop/status script, the SAME one the array-start event hook and
|
||||
* a terminal `rc.podman status` use (see plugin/rc.d/rc.podman's header
|
||||
* comment). This exists specifically so a fresh install where podman
|
||||
* failed to start (e.g. no cache pool configured yet, see
|
||||
* podman-storage.sh's "does not exist or is not mounted" error) can be
|
||||
* diagnosed and retried from the WebUI itself — no SSH/terminal access
|
||||
* needed, which is exactly what was missing when this was first needed
|
||||
* live (a fresh install on a different Unraid box with nobody able to
|
||||
* reach a terminal to run `rc.podman start` by hand).
|
||||
*
|
||||
* @return array{running: bool, output: string}
|
||||
*/
|
||||
function rc_podman(string $verb, int $timeoutSeconds): array
|
||||
{
|
||||
$output = run_rc_podman($verb, $timeoutSeconds);
|
||||
|
||||
// Only `rc.podman status` prints the "service: running
|
||||
// (pid ..., socket ...)" line this regex looks for — start/stop/
|
||||
// restart's OWN messages are worded differently ("start: already
|
||||
// running (pid ...)", "stop: stopped", ...), so relying on THIS same
|
||||
// regex against THEIR output silently reported "not running" right
|
||||
// after a successful start (found live: a start that printed "start:
|
||||
// already running" turned the status chip red). Always running a
|
||||
// fresh `status` afterward — regardless of which verb was actually
|
||||
// requested — is the one output format this check can trust.
|
||||
$statusOutput = $verb === 'status' ? $output : run_rc_podman('status', 15);
|
||||
$running = (bool) preg_match('/service:\s+running/', $statusOutput);
|
||||
|
||||
return ['running' => $running, 'output' => $output];
|
||||
}
|
||||
|
||||
function run_rc_podman(string $verb, int $timeoutSeconds): string
|
||||
{
|
||||
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||
$process = proc_open([RC_PODMAN, $verb], $descriptors, $pipes);
|
||||
if (!is_resource($process)) {
|
||||
podman_json_error("Could not run rc.podman {$verb}", 500);
|
||||
}
|
||||
|
||||
stream_set_timeout($pipes[1], $timeoutSeconds);
|
||||
$stdout = stream_get_contents($pipes[1]) ?: '';
|
||||
$stderr = stream_get_contents($pipes[2]) ?: '';
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($process);
|
||||
|
||||
return trim($stdout . $stderr);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
function settings_get(PodmanConfig $config): array
|
||||
{
|
||||
|
||||
@@ -57,6 +57,131 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- Podman service status/start/stop/restart ------------------------------
|
||||
|
||||
function renderServiceChip(data) {
|
||||
const chip = P.el('settings-service-chip');
|
||||
chip.className = 'podman-chip ' + (data.running ? 'podman-chip-good' : 'podman-chip-bad');
|
||||
chip.innerHTML = '<span class="d"></span>' + (data.running ? 'Running' : 'Not running');
|
||||
}
|
||||
|
||||
function serviceButtons() {
|
||||
return ['settings-service-status-btn', 'settings-service-start-btn', 'settings-service-stop-btn', 'settings-service-restart-btn']
|
||||
.map(function (id) { return P.el(id); });
|
||||
}
|
||||
|
||||
// "Refresh Status" is quick and read-only — shown inline, no modal needed.
|
||||
function refreshServiceStatus() {
|
||||
const buttons = serviceButtons();
|
||||
buttons.forEach(function (b) { b.disabled = true; });
|
||||
return P.get('settings', 'service_status').then(function (data) {
|
||||
renderServiceChip(data);
|
||||
const log = P.el('settings-service-log');
|
||||
log.style.display = '';
|
||||
log.textContent = data.output || '';
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}).catch(function (err) {
|
||||
P.el('settings-service-chip').className = 'podman-chip podman-chip-bad';
|
||||
P.el('settings-service-chip').innerHTML = '<span class="d"></span>Unknown';
|
||||
P.el('settings-service-log').style.display = '';
|
||||
P.el('settings-service-log').textContent = err.message;
|
||||
}).finally(function () {
|
||||
buttons.forEach(function (b) { b.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
// Start/Stop/Restart can take a while (storage checks, container
|
||||
// stop grace periods, ...) and are exactly the actions someone reaches
|
||||
// for when something's actually wrong — a small log modal (same pattern
|
||||
// as container update checking) shows what's happening instead of
|
||||
// leaving the button just spinning with no feedback.
|
||||
function runServiceAction(action, title) {
|
||||
const buttons = serviceButtons();
|
||||
buttons.forEach(function (b) { b.disabled = true; });
|
||||
const modal = P.openLogModal(title);
|
||||
return P.post('settings', action, {}).then(function (data) {
|
||||
renderServiceChip(data);
|
||||
(data.output || '').split('\n').forEach(function (line) { modal.log(line); });
|
||||
modal.done();
|
||||
}).catch(function (err) {
|
||||
modal.log('Error: ' + err.message);
|
||||
modal.done('Close');
|
||||
}).finally(function () {
|
||||
buttons.forEach(function (b) { b.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
// --- Format a disk for podman storage --------------------------------------
|
||||
|
||||
function openFormatDiskModal() {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'podman-modal-backdrop';
|
||||
backdrop.innerHTML = '' +
|
||||
'<div class="podman-modal" role="dialog" aria-modal="true">' +
|
||||
'<div class="podman-modal-head"><h3>Format a Disk for Podman Storage</h3></div>' +
|
||||
'<div class="podman-modal-body">' +
|
||||
'<div class="podman-modal-field"><label>Disk</label><select id="fd-device"><option value="">Loading…</option></select>' +
|
||||
'<div class="hint" id="fd-warning">Only disks with no existing partitions or filesystem are listed — anything already in use, part of the array/cache, or the boot flash is never shown here.</div></div>' +
|
||||
'<div class="podman-modal-field">' +
|
||||
'<label style="display:flex; align-items:flex-start; gap:8px; font-weight:400;">' +
|
||||
'<input type="checkbox" id="fd-confirm" style="margin-top:3px;">' +
|
||||
'<span>I understand this permanently erases all data on this disk, with no undo.</span>' +
|
||||
'</label></div>' +
|
||||
'</div>' +
|
||||
'<div class="podman-modal-actions">' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost podman-btn-danger" data-role="submit" disabled>Format Disk</button>' +
|
||||
'</div></div>';
|
||||
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||
|
||||
const select = backdrop.querySelector('#fd-device');
|
||||
const warning = backdrop.querySelector('#fd-warning');
|
||||
const confirmBox = backdrop.querySelector('#fd-confirm');
|
||||
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||
|
||||
function updateSubmitEnabled() {
|
||||
submitBtn.disabled = !(select.value && confirmBox.checked);
|
||||
}
|
||||
|
||||
P.get('disks', 'list_candidates').then(function (list) {
|
||||
select.innerHTML = list.length
|
||||
? list.map(function (d) {
|
||||
return '<option value="' + P.escapeHtml(d.device) + '">' + P.escapeHtml(d.device) +
|
||||
' — ' + P.escapeHtml(d.sizeFormatted) + (d.model ? ' (' + P.escapeHtml(d.model) + ')' : '') + '</option>';
|
||||
}).join('')
|
||||
: '<option value="">No eligible blank disks found</option>';
|
||||
updateSubmitEnabled();
|
||||
}).catch(function (err) {
|
||||
select.innerHTML = '<option value="">Error loading disks</option>';
|
||||
warning.textContent = err.message;
|
||||
});
|
||||
|
||||
select.addEventListener('change', updateSubmitEnabled);
|
||||
confirmBox.addEventListener('change', updateSubmitEnabled);
|
||||
|
||||
function close() { backdrop.remove(); }
|
||||
|
||||
submitBtn.addEventListener('click', function () {
|
||||
if (!select.value || !confirmBox.checked) return;
|
||||
if (!confirm('Format ' + select.value + '? This cannot be undone.')) return;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Formatting…';
|
||||
P.post('disks', 'format', { device: select.value }).then(function (data) {
|
||||
close();
|
||||
P.el('settings-storage-path').value = data.mountPath;
|
||||
alert('Formatted and mounted at ' + data.mountPath + '. Storage path has been filled in below — click "Save Settings" to use it, then Restart Podman.');
|
||||
}).catch(function (err) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Format Disk';
|
||||
alert('Format failed: ' + err.message);
|
||||
});
|
||||
});
|
||||
|
||||
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
|
||||
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); });
|
||||
}
|
||||
|
||||
function save() {
|
||||
const body = {
|
||||
storagePath: P.el('settings-storage-path').value.trim(),
|
||||
@@ -91,6 +216,19 @@
|
||||
saveAutostart();
|
||||
});
|
||||
|
||||
P.el('settings-service-status-btn').addEventListener('click', refreshServiceStatus);
|
||||
P.el('settings-service-start-btn').addEventListener('click', function () { runServiceAction('service_start', 'Starting Podman'); });
|
||||
P.el('settings-service-stop-btn').addEventListener('click', function () {
|
||||
if (!confirm('Stop podman? All running containers will be stopped first (each with its own configured grace period).')) return;
|
||||
runServiceAction('service_stop', 'Stopping Podman');
|
||||
});
|
||||
P.el('settings-service-restart-btn').addEventListener('click', function () {
|
||||
if (!confirm('Restart podman? All running containers will be stopped and podman.sock will be unavailable until it comes back up.')) return;
|
||||
runServiceAction('service_restart', 'Restarting Podman');
|
||||
});
|
||||
P.el('settings-format-disk-btn').addEventListener('click', openFormatDiskModal);
|
||||
|
||||
refreshServiceStatus();
|
||||
return load();
|
||||
}
|
||||
|
||||
|
||||
@@ -345,6 +345,12 @@
|
||||
.podman-log-pane {
|
||||
background: #0f1114; color: #c7ccd4; font-family: var(--font-mono); font-size: 12.3px;
|
||||
padding: 14px 16px; height: 400px; overflow-y: auto; line-height: 1.65;
|
||||
/* Settings' service log (and the log modal) set .textContent directly
|
||||
on this element rather than wrapping every line in a child .l div —
|
||||
needs its own white-space here too, or real newlines in rc.podman's
|
||||
output collapse into one run-together line (found live: the whole
|
||||
multi-line `rc.podman status` output rendered as a single paragraph). */
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.podman-log-pane .l { white-space: pre-wrap; word-break: break-word; }
|
||||
.podman-log-pane .ts { color: #6b7280; }
|
||||
@@ -418,6 +424,9 @@
|
||||
.podman-input-suffix input[type="number"] { max-width: 100px; width: auto; }
|
||||
.podman-input-suffix span { font-size: 12px; color: var(--text-dim); }
|
||||
|
||||
.podman-service-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.podman-service-row:last-child { margin-bottom: 0; }
|
||||
|
||||
/*
|
||||
* Toggle switch — a plain checkbox reads as a leftover form control next
|
||||
* to everything else in this panel getting a designed treatment; this
|
||||
|
||||
Reference in New Issue
Block a user