Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
166f0d96d1 | ||
|
|
9b9d1a05ac | ||
|
|
66ef830234 | ||
|
|
676fd8bc89 | ||
|
|
20d8686b61 | ||
|
|
9557f8c8f8 | ||
|
|
5fb7376b64 | ||
|
|
7f6fcb9166 | ||
|
|
80e006c73f | ||
|
|
b91bdb9810 | ||
|
|
6e36ee1aef | ||
|
|
18b414d7b5 |
@@ -1,6 +1,10 @@
|
|||||||
name: Release
|
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
|
# By design, this workflow does NOT bump versions or modify podman.plg
|
||||||
# itself — that happens locally via `scripts/release.sh <version>`, which a
|
# 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:
|
# script's own printed instructions). This workflow's only job is to:
|
||||||
# 1. Rebuild all packages from the tagged commit in a clean Slackware
|
# 1. Rebuild all packages from the tagged commit in a clean Slackware
|
||||||
# container (reproducibility check + provenance — we don't trust
|
# 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
|
# 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
|
# at this tag (catches a release.sh run that wasn't followed by a
|
||||||
# matching commit — see the "Verify plg matches build" step).
|
# matching commit — see the "Verify plg matches build" step).
|
||||||
# 3. Create the GitHub Release and attach the .txz packages, checksum
|
# 3. Create the Gitea Release and attach the .txz packages, checksum
|
||||||
# manifests, and podman.plg.
|
# 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).
|
# See docs/ARCHITECTURE.md section 13 (Updates).
|
||||||
|
|
||||||
@@ -25,13 +42,17 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_HOST: git.mp-mueller.de
|
||||||
|
GITEA_REPO: magges/unraid-podman
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build release packages
|
name: Build release packages
|
||||||
uses: ./.github/workflows/build-packages.yml
|
uses: ./.github/workflows/build-packages.yml
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish GitHub Release
|
name: Publish Gitea Release
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -79,18 +100,51 @@ jobs:
|
|||||||
' CHANGELOG.md > /tmp/release-notes.md
|
' CHANGELOG.md > /tmp/release-notes.md
|
||||||
echo "path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
|
echo "path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Publish Gitea Release
|
||||||
uses: softprops/action-gh-release@v2
|
env:
|
||||||
with:
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
name: "unraid-podman v${{ steps.version.outputs.value }}"
|
TAG: ${{ github.ref_name }}
|
||||||
body_path: ${{ steps.changelog.outputs.path }}
|
run: |
|
||||||
# v0.x tags are treated as pre-releases until the plugin reaches a
|
set -eu
|
||||||
# first stable 1.0.0 — see docs/ROADMAP.md.
|
API="https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}"
|
||||||
prerelease: ${{ startsWith(steps.version.outputs.value, '0.') }}
|
case "${{ steps.version.outputs.value }}" in
|
||||||
files: |
|
0.*) prerelease=true ;;
|
||||||
dist/*.txz
|
*) prerelease=false ;;
|
||||||
dist/*.sha256
|
esac
|
||||||
dist/*.md5
|
|
||||||
dist/CHECKSUMS.sha256
|
# Idempotent: if a release for this tag already exists (e.g. a
|
||||||
dist/CHECKSUMS.md5
|
# re-run after a transient failure), delete it first rather than
|
||||||
plugin/podman.plg
|
# 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}"
|
||||||
|
|||||||
@@ -9,6 +9,52 @@ see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#52-build-strategie)).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
||||||
|
own lines (`<URL>\n&baseURL;/...\n</URL>`) — Unraid's plugin manager passes
|
||||||
|
that text straight into a `wget ... -O <name> <url>` shell command without
|
||||||
|
trimming it, so the leading newline broke the command in two: `wget` saw no
|
||||||
|
URL argument at all, and the URL text ran on the next line as its own
|
||||||
|
(failing) shell command. Every real Unraid plugin (verified against
|
||||||
|
`unassigned.devices.plg` on a live host) keeps `<URL>...</URL>` on one
|
||||||
|
line — found by running the plugin installer's own CLI (`scripts/plugin
|
||||||
|
install`) directly on a real Unraid host and reading its raw output,
|
||||||
|
rather than trusting the webGUI's summarized install log.
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-07-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial repository scaffolding: directory structure, documentation skeleton,
|
- Initial repository scaffolding: directory structure, documentation skeleton,
|
||||||
CI workflow stubs, and community health files.
|
CI workflow stubs, and community health files.
|
||||||
|
|||||||
Regular → Executable
@@ -20,6 +20,12 @@
|
|||||||
# blocked on podman's full startup sequence (preflight, storage mount,
|
# blocked on podman's full startup sequence (preflight, storage mount,
|
||||||
# service start, autostart chain) — mirrors how unassigned.devices
|
# service start, autostart chain) — mirrors how unassigned.devices
|
||||||
# backgrounds its own longer-running "started" hook.
|
# 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
|
||||||
|
|||||||
+66
-104
@@ -50,20 +50,26 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "podman">
|
<!ENTITY name "podman">
|
||||||
<!ENTITY author "unraid-podman contributors">
|
<!ENTITY author "unraid-podman contributors">
|
||||||
<!ENTITY version "0.0.0">
|
<!ENTITY version "0.1.3">
|
||||||
<!-- "Podman" (no parent) — Podman.page declares Menu="Podman", making it
|
<!-- "Podman" (no parent) — Podman.page declares Menu="Podman", making it
|
||||||
its own top-level nav tab next to Docker/VMs, not nested under
|
its own top-level nav tab next to Docker/VMs, not nested under
|
||||||
Settings — see webui/plugins/podman/Podman.page. -->
|
Settings — see webui/plugins/podman/Podman.page. -->
|
||||||
<!ENTITY launch "Podman">
|
<!ENTITY launch "Podman">
|
||||||
<!ENTITY github "OWNER/unraid-podman">
|
<!-- This project is hosted on a self-hosted Gitea instance, not GitHub —
|
||||||
<!ENTITY gitURL "https://raw.githubusercontent.com/&github;/main">
|
&github; is kept as the entity name (widely referenced below) but
|
||||||
|
holds the Gitea owner/repo slug; gitURL/supportURL/baseURL all point
|
||||||
|
at git.mp-mueller.de using Gitea's own raw-file and release-asset URL
|
||||||
|
conventions (structurally the same shape as GitHub's, different host
|
||||||
|
and raw-file path segment: /raw/branch/<ref>/ instead of /<ref>/). -->
|
||||||
|
<!ENTITY github "magges/unraid-podman">
|
||||||
|
<!ENTITY gitURL "https://git.mp-mueller.de/&github;/raw/branch/main">
|
||||||
<!ENTITY pluginURL "&gitURL;/plugin/podman.plg">
|
<!ENTITY pluginURL "&gitURL;/plugin/podman.plg">
|
||||||
<!ENTITY supportURL "https://github.com/&github;/discussions">
|
<!ENTITY supportURL "https://git.mp-mueller.de/&github;/issues">
|
||||||
|
|
||||||
<!-- Release asset base — matches scripts/release.sh's RELEASE_BASE_URL
|
<!-- Release asset base — matches scripts/release.sh's RELEASE_BASE_URL
|
||||||
exactly; both must agree since release.sh is what publishes the
|
exactly; both must agree since release.sh is what publishes the
|
||||||
packages this URL is expected to find. -->
|
packages this URL is expected to find. -->
|
||||||
<!ENTITY baseURL "https://github.com/&github;/releases/download/v&version;">
|
<!ENTITY baseURL "https://git.mp-mueller.de/magges/unraid-podman/releases/download/v0.1.3">
|
||||||
|
|
||||||
<!-- 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
|
||||||
@@ -80,33 +86,33 @@
|
|||||||
fail to download anything — that is intentional; there is nothing to
|
fail to download anything — that is intentional; there is nothing to
|
||||||
install before the first tagged release. -->
|
install before the first tagged release. -->
|
||||||
|
|
||||||
<!ENTITY podman_txz_version "0.0.0">
|
<!ENTITY podman_txz_version "6.0.1">
|
||||||
<!ENTITY podman_txz_file "podman-&podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY podman_txz_file "podman-6.0.1-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY podman_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY podman_txz_md5 "f294f9eaf7139b43d153cafaed34de48">
|
||||||
|
|
||||||
<!ENTITY conmon_txz_version "0.0.0">
|
<!ENTITY conmon_txz_version "2.2.1">
|
||||||
<!ENTITY conmon_txz_file "conmon-&conmon_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY conmon_txz_file "conmon-2.2.1-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY conmon_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY conmon_txz_md5 "e1e3b4b15398d965ee4efdfac93b1867">
|
||||||
|
|
||||||
<!ENTITY crun_txz_version "0.0.0">
|
<!ENTITY crun_txz_version "1.28">
|
||||||
<!ENTITY crun_txz_file "crun-&crun_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY crun_txz_file "crun-1.28-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY crun_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY crun_txz_md5 "891914e9448a960dd28a9d1ea67e63b3">
|
||||||
|
|
||||||
<!ENTITY netavark_txz_version "0.0.0">
|
<!ENTITY netavark_txz_version "2.0.0">
|
||||||
<!ENTITY netavark_txz_file "netavark-&netavark_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY netavark_txz_file "netavark-2.0.0-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY netavark_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY netavark_txz_md5 "ef8509cc51d24ef1cae4d96f1fd92a01">
|
||||||
|
|
||||||
<!ENTITY aardvark_dns_txz_version "0.0.0">
|
<!ENTITY aardvark_dns_txz_version "2.0.0">
|
||||||
<!ENTITY aardvark_dns_txz_file "aardvark-dns-&aardvark_dns_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY aardvark_dns_txz_file "aardvark-dns-2.0.0-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY aardvark_dns_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY aardvark_dns_txz_md5 "2a5a5c2e3684b4130edd4b128484908f">
|
||||||
|
|
||||||
<!ENTITY passt_txz_version "0.0.0">
|
<!ENTITY passt_txz_version "git6ef3d1c">
|
||||||
<!ENTITY passt_txz_file "passt-&passt_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY passt_txz_file "passt-git6ef3d1c-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY passt_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY passt_txz_md5 "970a8f15257e5861933ae58459da2bd4">
|
||||||
|
|
||||||
<!ENTITY fuse_overlayfs_txz_version "0.0.0">
|
<!ENTITY fuse_overlayfs_txz_version "1.17">
|
||||||
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-&fuse_overlayfs_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-1.17-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY fuse_overlayfs_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY fuse_overlayfs_txz_md5 "11f5aefe0372427ae194017f54af5d47">
|
||||||
|
|
||||||
<!-- catatonit and nftables are runtime dependencies this plugin ships,
|
<!-- catatonit and nftables are runtime dependencies this plugin ships,
|
||||||
not upstream podman-ecosystem components — see packages/catatonit/
|
not upstream podman-ecosystem components — see packages/catatonit/
|
||||||
@@ -114,20 +120,20 @@
|
|||||||
container init; netavark's only viable firewall driver, since it
|
container init; netavark's only viable firewall driver, since it
|
||||||
dropped iptables in 2.0 and Unraid has neither `nft` nor
|
dropped iptables in 2.0 and Unraid has neither `nft` nor
|
||||||
systemd/dbus for firewalld) and why neither is built from source. -->
|
systemd/dbus for firewalld) and why neither is built from source. -->
|
||||||
<!ENTITY catatonit_txz_version "0.0.0">
|
<!ENTITY catatonit_txz_version "0.2.1">
|
||||||
<!ENTITY catatonit_txz_file "catatonit-&catatonit_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY catatonit_txz_file "catatonit-0.2.1-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY catatonit_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY catatonit_txz_md5 "e17faf29b0c618a73def017583697aca">
|
||||||
|
|
||||||
<!ENTITY nftables_txz_version "0.0.0">
|
<!ENTITY nftables_txz_version "1.0.1">
|
||||||
<!ENTITY nftables_txz_file "nftables-&nftables_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY nftables_txz_file "nftables-1.0.1-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY nftables_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY nftables_txz_md5 "d9bb93b0bdc061681ffbd42a243bb5fc">
|
||||||
|
|
||||||
<!-- podman-compose is the external Compose provider `podman compose`
|
<!-- podman-compose is the external Compose provider `podman compose`
|
||||||
shells out to (see packages/podman-compose/README.md) — without it
|
shells out to (see packages/podman-compose/README.md) — without it
|
||||||
every Compose panel action fails outright on a clean install. -->
|
every Compose panel action fails outright on a clean install. -->
|
||||||
<!ENTITY podman_compose_txz_version "0.0.0">
|
<!ENTITY podman_compose_txz_version "1.6.0">
|
||||||
<!ENTITY podman_compose_txz_file "podman-compose-&podman_compose_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY podman_compose_txz_file "podman-compose-1.6.0-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY podman_compose_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY podman_compose_txz_md5 "2d7e21b071d40ab3518acbec67967777">
|
||||||
|
|
||||||
<!-- 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
|
||||||
@@ -135,9 +141,9 @@
|
|||||||
version always equals the plugin's own &version; — see
|
version always equals the plugin's own &version; — see
|
||||||
packages/unraid-podman/unraid-podman.SlackBuild, which reads it
|
packages/unraid-podman/unraid-podman.SlackBuild, which reads it
|
||||||
straight out of this very file rather than tracking it twice. -->
|
straight out of this very file rather than tracking it twice. -->
|
||||||
<!ENTITY unraid_podman_txz_version "&version;">
|
<!ENTITY unraid_podman_txz_version "0.1.3">
|
||||||
<!ENTITY unraid_podman_txz_file "unraid-podman-&unraid_podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
|
<!ENTITY unraid_podman_txz_file "unraid-podman-0.1.3-x86_64-1_unraidpodman.txz">
|
||||||
<!ENTITY unraid_podman_txz_md5 "00000000000000000000000000000000">
|
<!ENTITY unraid_podman_txz_md5 "ceeb31f08ad0df62dee1b6a6f1ed3bba">
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<PLUGIN name="&name;"
|
<PLUGIN name="&name;"
|
||||||
@@ -195,93 +201,53 @@ fi
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&podman_txz_file;</URL>
|
||||||
&baseURL;/&podman_txz_file;
|
<MD5>&podman_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&podman_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&conmon_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&conmon_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&conmon_txz_file;</URL>
|
||||||
&baseURL;/&conmon_txz_file;
|
<MD5>&conmon_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&conmon_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&crun_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&crun_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&crun_txz_file;</URL>
|
||||||
&baseURL;/&crun_txz_file;
|
<MD5>&crun_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&crun_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&netavark_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&netavark_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&netavark_txz_file;</URL>
|
||||||
&baseURL;/&netavark_txz_file;
|
<MD5>&netavark_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&netavark_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&aardvark_dns_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&aardvark_dns_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&aardvark_dns_txz_file;</URL>
|
||||||
&baseURL;/&aardvark_dns_txz_file;
|
<MD5>&aardvark_dns_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&aardvark_dns_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&passt_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&passt_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&passt_txz_file;</URL>
|
||||||
&baseURL;/&passt_txz_file;
|
<MD5>&passt_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&passt_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&fuse_overlayfs_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&fuse_overlayfs_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&fuse_overlayfs_txz_file;</URL>
|
||||||
&baseURL;/&fuse_overlayfs_txz_file;
|
<MD5>&fuse_overlayfs_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&fuse_overlayfs_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&catatonit_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&catatonit_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&catatonit_txz_file;</URL>
|
||||||
&baseURL;/&catatonit_txz_file;
|
<MD5>&catatonit_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&catatonit_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&nftables_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&nftables_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&nftables_txz_file;</URL>
|
||||||
&baseURL;/&nftables_txz_file;
|
<MD5>&nftables_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&nftables_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_compose_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_compose_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&podman_compose_txz_file;</URL>
|
||||||
&baseURL;/&podman_compose_txz_file;
|
<MD5>&podman_compose_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&podman_compose_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -290,12 +256,8 @@ fi
|
|||||||
packages/unraid-podman/README.md.
|
packages/unraid-podman/README.md.
|
||||||
-->
|
-->
|
||||||
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&unraid_podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&unraid_podman_txz_file;" Run="upgradepkg --install-new --reinstall">
|
||||||
<URL>
|
<URL>&baseURL;/&unraid_podman_txz_file;</URL>
|
||||||
&baseURL;/&unraid_podman_txz_file;
|
<MD5>&unraid_podman_txz_md5;</MD5>
|
||||||
</URL>
|
|
||||||
<MD5>
|
|
||||||
&unraid_podman_txz_md5;
|
|
||||||
</MD5>
|
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -168,6 +168,33 @@ podman_storage_path_is_safe() {
|
|||||||
return 0
|
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>
|
# 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
|
else
|
||||||
fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem"
|
fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem"
|
||||||
fi
|
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
|
else
|
||||||
fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?"
|
fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -46,9 +46,13 @@ cmd_create() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d "$STORAGE_PATH" ]; then
|
if [ ! -d "$STORAGE_PATH" ]; then
|
||||||
podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted."
|
if ! podman_path_has_real_mount_ancestor "$STORAGE_PATH"; then
|
||||||
podman_log_error "storage: check that the configured cache pool/disk is present before starting podman."
|
podman_log_error "storage: $STORAGE_PATH does not exist or is not mounted."
|
||||||
return 1
|
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
|
fi
|
||||||
|
|
||||||
# Free space check: refuse to create an image bigger than what's actually
|
# 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 \
|
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' _ {} \;
|
'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"
|
local pkg_file="$PRGNAM-$version-$arch-$build$tag.txz"
|
||||||
( cd "$PKG" && makepkg --linkadd y --chown y "$OUTPUT/$pkg_file" )
|
( cd "$PKG" && makepkg --linkadd y --chown y "$OUTPUT/$pkg_file" )
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -45,12 +45,15 @@ if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The GitHub Release tag/URL this release's assets will be published under.
|
# The release tag/URL this release's assets will be published under. This
|
||||||
# Must match whatever .github/workflows/release.yml actually creates the
|
# project is hosted on a self-hosted Gitea instance (git.mp-mueller.de),
|
||||||
# release as (tag "v<version>") — see that workflow for the release job.
|
# not GitHub — REPO_SLUG/RELEASE_HOST are overridable via env vars for a
|
||||||
|
# future move, but default to where this repo actually lives today. Must
|
||||||
|
# match plugin/podman.plg's &baseURL; entity exactly (see that file).
|
||||||
RELEASE_TAG="v$NEW_VERSION"
|
RELEASE_TAG="v$NEW_VERSION"
|
||||||
REPO_SLUG="${GITHUB_REPOSITORY:-OWNER/unraid-podman}"
|
REPO_SLUG="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-magges/unraid-podman}}"
|
||||||
RELEASE_BASE_URL="https://github.com/$REPO_SLUG/releases/download/$RELEASE_TAG"
|
RELEASE_HOST="${RELEASE_HOST:-git.mp-mueller.de}"
|
||||||
|
RELEASE_BASE_URL="https://$RELEASE_HOST/$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.
|
||||||
@@ -142,7 +145,8 @@ echo " git add plugin/podman.plg CHANGELOG.md"
|
|||||||
echo " git commit -m \"release: v$NEW_VERSION\""
|
echo " git commit -m \"release: v$NEW_VERSION\""
|
||||||
echo " git tag $RELEASE_TAG"
|
echo " git tag $RELEASE_TAG"
|
||||||
echo " git push && git push origin $RELEASE_TAG"
|
echo " git push && git push origin $RELEASE_TAG"
|
||||||
echo "==> Pushing the tag triggers .github/workflows/release.yml, which"
|
echo "==> .github/workflows/release.yml (softprops/action-gh-release) only"
|
||||||
echo "==> rebuilds artifacts in CI (for reproducibility/provenance) and"
|
echo "==> knows how to publish to GitHub — this repo lives on Gitea"
|
||||||
echo "==> publishes the GitHub Release with dist/*.txz + CHECKSUMS.* + the"
|
echo "==> ($RELEASE_HOST), so for now, publish the Gitea Release and attach"
|
||||||
echo "==> updated podman.plg attached."
|
echo "==> dist/*.txz + CHECKSUMS.* + the updated podman.plg to it by hand"
|
||||||
|
echo "==> (or via the Gitea API) after pushing the tag."
|
||||||
|
|||||||
@@ -23,14 +23,28 @@ Icon="podman"
|
|||||||
* Cache-busts every static asset with its own on-disk mtime. Unraid's
|
* Cache-busts every static asset with its own on-disk mtime. Unraid's
|
||||||
* webserver sends no explicit no-cache headers for /plugins/ static
|
* webserver sends no explicit no-cache headers for /plugins/ static
|
||||||
* files, so without this, browsers can keep serving a stale app.js/
|
* 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
|
* podman.css for a long time after a plugin update.
|
||||||
* bugfix to app.js's CSRF handling silently kept failing in a real
|
*
|
||||||
* browser after redeploy until this was added, even though the deployed
|
* Deliberately a hardcoded absolute path, NOT __DIR__ — Unraid's own
|
||||||
* file on disk was byte-for-byte correct.
|
* 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
|
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';
|
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>
|
<button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="podman-grid">
|
<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">
|
||||||
<div class="podman-card-head">
|
<div class="podman-card-head">
|
||||||
<div><h2>Storage</h2><div class="sub">Where podman keeps images, containers and volumes on disk.</div></div>
|
<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 class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
|
||||||
</div>
|
</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">
|
<div class="podman-field-row">
|
||||||
<label for="settings-storage-size">podman.img size</label>
|
<label for="settings-storage-size">podman.img size</label>
|
||||||
<div>
|
<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,
|
* save POST {"storagePath": "...", "storageImageSizeGb": 20,
|
||||||
* "enabled": true, "stopTimeoutSeconds": 10}
|
* "enabled": true, "stopTimeoutSeconds": 10}
|
||||||
* autostart_save POST {"names": ["postgres", "nextcloud", ...]}
|
* 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);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require __DIR__ . '/../include/bootstrap.php';
|
require __DIR__ . '/../include/bootstrap.php';
|
||||||
|
|
||||||
|
const RC_PODMAN = '/etc/rc.d/rc.podman';
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
@@ -45,10 +51,77 @@ switch ($action) {
|
|||||||
podman_json_response(['status' => 'saved']);
|
podman_json_response(['status' => 'saved']);
|
||||||
break;
|
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:
|
default:
|
||||||
podman_json_error("Unknown action '{$action}'", 400);
|
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> */
|
/** @return array<string,mixed> */
|
||||||
function settings_get(PodmanConfig $config): array
|
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() {
|
function save() {
|
||||||
const body = {
|
const body = {
|
||||||
storagePath: P.el('settings-storage-path').value.trim(),
|
storagePath: P.el('settings-storage-path').value.trim(),
|
||||||
@@ -91,6 +216,19 @@
|
|||||||
saveAutostart();
|
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();
|
return load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,12 @@
|
|||||||
.podman-log-pane {
|
.podman-log-pane {
|
||||||
background: #0f1114; color: #c7ccd4; font-family: var(--font-mono); font-size: 12.3px;
|
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;
|
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 .l { white-space: pre-wrap; word-break: break-word; }
|
||||||
.podman-log-pane .ts { color: #6b7280; }
|
.podman-log-pane .ts { color: #6b7280; }
|
||||||
@@ -418,6 +424,9 @@
|
|||||||
.podman-input-suffix input[type="number"] { max-width: 100px; width: auto; }
|
.podman-input-suffix input[type="number"] { max-width: 100px; width: auto; }
|
||||||
.podman-input-suffix span { font-size: 12px; color: var(--text-dim); }
|
.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
|
* Toggle switch — a plain checkbox reads as a leftover form control next
|
||||||
* to everything else in this panel getting a designed treatment; this
|
* to everything else in this panel getting a designed treatment; this
|
||||||
|
|||||||
Reference in New Issue
Block a user