Add reproducible build system, native Unraid plugin, and WebUI
Build Packages / Build .txz packages (push) Failing after 9s
Lint / ShellCheck (push) Failing after 43s
Lint / Validate .plg XML (push) Successful in 10s
Lint / EditorConfig (push) Failing after 6s

- versions.env pins podman, conmon, crun, netavark, aardvark-dns, passt,
  and fuse-overlayfs to verified upstream source checksums; SlackBuild
  recipes, scripts/build-packages.sh, checksums.sh, release.sh, and
  update-versions.sh implement the reproducible pipeline; GitHub Actions
  workflows build in a Slackware container and publish releases without
  committing any binaries.

- plugin/podman.plg installs/updates/removes all eight packages (the
  seven components plus the plugin's own unraid-podman scaffolding
  package) via upgradepkg, using the official Unraid array-event hook
  mechanism (event/disks_mounted, event/stopping) instead of editing
  /boot/config/go. rc.podman and the sbin/ helper scripts implement
  storage creation, config seeding/sync, preflight checks, autostart
  with per-container Safe-Mode, and package verify/update/rollback.

- webui/plugins/podman implements the Dashboard, Containers, Pods,
  Images, Volumes, Networks, Logs, Terminal, Compose, and Settings
  panels against the approved mockup (webui/mockups/prototype.html),
  talking to podman system service exclusively via PodmanClient.php
  (libpod REST API over the Unix socket), with two documented
  exceptions: Terminal's one-shot exec model and Compose's use of the
  podman compose CLI, since libpod has no REST equivalent for either.

- docs/ARCHITECTURE.md and docs/ROADMAP.md record the design decisions
  and honest current status (syntax-checked, unit- and
  integration-tested against fake sockets/servers; not yet run against
  a real Unraid/Podman/Slackware system).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 10:51:14 +00:00
co-authored by Claude Sonnet 5
parent 58ffc0c226
commit e2fefcdf9c
124 changed files with 9611 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.sh]
indent_size = 2
[*.SlackBuild]
indent_size = 2
[Makefile]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
+12
View File
@@ -0,0 +1,12 @@
# Normalize line endings for all text files; Slackware/Unraid scripts and the
# .plg manifest must stay LF, never CRLF.
* text=auto eol=lf
*.sh text eol=lf
*.SlackBuild text eol=lf
*.plg text eol=lf
rc.* text eol=lf
*.png binary
*.jpg binary
*.ico binary
+9
View File
@@ -0,0 +1,9 @@
# Default owners for everything in the repo, until per-area owners are named.
# See https://docs.github.com/articles/about-codeowners for syntax.
* @OWNER
# Example of future area-based ownership once the team grows:
# /packages/ @OWNER @packaging-maintainer
# /webui/ @OWNER @webui-maintainer
# /plugin/ @OWNER
+51
View File
@@ -0,0 +1,51 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to a positive environment:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Focusing on what is best for the community
Examples of unacceptable behavior:
- The use of sexualized language or imagery, and unwelcome sexual attention
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information without explicit permission
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing standards of
acceptable behavior and will take appropriate corrective action in response to
any behavior deemed inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces (issues, pull requests,
discussions) and when an individual is officially representing the project in
public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the maintainers via the contact listed in
[SECURITY.md](SECURITY.md). All complaints will be reviewed and investigated
promptly and fairly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
+39
View File
@@ -0,0 +1,39 @@
---
name: Bug report
about: Report a problem with the plugin
title: "[Bug] "
labels: bug
assignees: ""
---
## Description
A clear description of what went wrong.
## Environment
- Unraid version:
- unraid-podman plugin version:
- Podman version (`podman version`, if available):
- Storage backend (Cache pool / Array disk), pool filesystem (XFS/BTRFS/ZFS):
- Docker also installed/running? (yes/no):
## Steps to reproduce
1. ...
2. ...
## Expected behavior
## Actual behavior
## Relevant logs
Attach or paste relevant excerpts from:
- `/mnt/*/system/podman/logs/podman-service.log`
- Unraid System Log (`Tools -> System Log`)
- `rc.podman status` output
Please redact any secrets (registry credentials, tokens) before posting.
## Additional context
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/OWNER/unraid-podman/security/advisories/new
about: Please report security issues privately — see SECURITY.md, not a public issue.
- name: General discussion / questions
url: https://github.com/OWNER/unraid-podman/discussions
about: Usage questions and design discussions that aren't a concrete bug or feature request.
+23
View File
@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an enhancement or new capability
title: "[Feature] "
labels: enhancement
assignees: ""
---
## Problem
What are you trying to do that isn't currently possible or is unnecessarily hard?
## Proposed solution
## Alternatives considered
## Which architecture phase does this fit?
See [docs/ROADMAP.md](../../docs/ROADMAP.md) — e.g. MVP / WebUI / Rootless /
Docker-optional / Pods. If unsure, leave blank; this helps triage against the
existing phased plan rather than scope-creeping the current phase.
## Additional context
+32
View File
@@ -0,0 +1,32 @@
## Summary
<!-- What does this PR change and why? -->
## Related issue(s)
<!-- Closes #... -->
## Type of change
- [ ] Documentation
- [ ] Packaging (`packages/`)
- [ ] Plugin core (`.plg`, `rc.podman`, `sbin/*`)
- [ ] WebUI (`webui/`)
- [ ] CI / tooling (`.github/`, `scripts/`)
- [ ] Other
## Checklist
- [ ] Changes follow the design in [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md);
any deliberate deviation is explained below
- [ ] Persistence discipline respected (state written under `/etc`, `/usr`, `/var`
at runtime is also mirrored to `/boot/config/plugins/podman/` or array/cache)
- [ ] No systemd usage introduced
- [ ] Docker coexistence preserved (no shared storage/network/iptables-chain names)
- [ ] `CHANGELOG.md` updated under `[Unreleased]` (if user-facing)
- [ ] Relevant docs under `docs/` updated
- [ ] Tested on a real or virtualized Unraid instance (describe below), where applicable
## How was this tested?
## Notes for reviewers
+37
View File
@@ -0,0 +1,37 @@
# Security Policy
## Threat model context
This plugin runs **rootful Podman**. Its API socket (`/var/run/podman/podman.sock`)
is root-equivalent on the host, in the same way Docker's `docker.sock` is. See
[docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md#19-sicherheitsbetrachtungen-phase-1-rootful)
for the full rationale. Treat any bug that affects socket permissions, WebUI
authentication, or container-to-host isolation as security-sensitive by default.
## Reporting a vulnerability
Please **do not** open a public GitHub issue for security vulnerabilities.
Instead, use one of:
- GitHub [private security advisories](https://github.com/OWNER/unraid-podman/security/advisories/new)
for this repository, or
- Email the maintainers at `security@OWNER-DOMAIN` (placeholder — update once a
contact address exists).
Please include:
- A description of the issue and its potential impact.
- Steps to reproduce (Unraid version, plugin version, storage backend).
- Whether the issue requires local access, network access, or a malicious
container image to trigger.
## Supported versions
This project has not yet cut a stable release. Until a `1.0.0` release, only the
latest `main` branch / most recent tag is supported with security fixes.
## Disclosure process
We aim to acknowledge reports within 5 business days and to agree on a
coordinated disclosure timeline before any public write-up.
+92
View File
@@ -0,0 +1,92 @@
name: Build Packages
# Builds the seven Slackware .txz packages defined under packages/
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs)
# inside a Slackware container, verifies + consolidates their checksums, and
# uploads the result as a workflow artifact.
#
# Intentionally does NOT commit any built binary back to the repository —
# packages/**, *.txz, dist/ are all git-ignored (see .gitignore). Artifacts
# only ever leave this workflow via the "Upload build artifacts" step below
# (retained by GitHub Actions, not the repo) or, for tagged releases, via
# release.yml attaching them to a GitHub Release.
#
# See docs/ARCHITECTURE.md section 5 (Paketmanagement).
on:
push:
branches: [main]
paths:
- "packages/**"
- "versions.env"
- "scripts/**"
- ".github/workflows/build-packages.yml"
pull_request:
paths:
- "packages/**"
- "versions.env"
- "scripts/**"
- ".github/workflows/build-packages.yml"
workflow_dispatch:
inputs:
packages:
description: >
Space-separated package names to build (default: all seven).
Example: "podman conmon"
required: false
default: ""
workflow_call:
outputs:
artifact-name:
description: "Name of the uploaded dist/ artifact"
value: ${{ jobs.build.outputs.artifact-name }}
# Pin the Slackware build image by tag here. vbatts/slackware is a
# long-standing, widely used Slackware Docker image; swap this (and ideally
# pin by digest) if the project standardizes on a different/self-hosted
# base image. See scripts/ci/setup-slackware-buildenv.sh for how missing
# build dependencies are bootstrapped on top of whatever this image ships.
env:
SLACKWARE_IMAGE: "vbatts/slackware:15.0"
jobs:
build:
name: Build .txz packages
runs-on: ubuntu-latest
container:
image: ${{ env.SLACKWARE_IMAGE }}
outputs:
artifact-name: ${{ steps.artifact-name.outputs.value }}
steps:
- name: Install git and tar (needed before actions/checkout can run)
run: |
(command -v git && command -v tar) || \
(echo "!! base image is missing git/tar — see SLACKWARE_IMAGE in this workflow" && exit 1)
- uses: actions/checkout@v4
- name: Set up Slackware build environment
run: scripts/ci/setup-slackware-buildenv.sh
- name: Build packages
run: scripts/build-packages.sh ${{ github.event.inputs.packages }}
- name: Verify and consolidate checksums
run: scripts/checksums.sh
- name: Compute artifact name
id: artifact-name
run: echo "value=podman-packages-${{ github.sha }}" >> "$GITHUB_OUTPUT"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.artifact-name.outputs.value }}
path: |
dist/*.txz
dist/*.sha256
dist/*.md5
dist/CHECKSUMS.sha256
dist/CHECKSUMS.md5
if-no-files-found: error
retention-days: 14
+53
View File
@@ -0,0 +1,53 @@
name: Lint
# Static analysis over shell scripts (ShellCheck) and the .plg/XML manifest
# (xmllint) on every push and pull request. Mirrors scripts/dev/lint.sh for
# local use.
on:
push:
branches: [main]
pull_request:
jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends shellcheck
- name: Run ShellCheck
run: |
set -eu
# shellcheck disable=SC2038 (find | xargs is fine here, filenames
# in this repo never contain spaces/newlines)
find plugin/rc.d plugin/sbin scripts -type f \
\( -name '*.sh' -o -name 'rc.*' -o -name '*.SlackBuild' \) \
-print0 \
| xargs -0 shellcheck --severity=warning --external-sources
xmllint:
name: Validate .plg XML
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install libxml2-utils
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libxml2-utils
- name: Validate podman.plg is well-formed XML
run: xmllint --noout plugin/podman.plg
editorconfig:
name: EditorConfig
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: editorconfig-checker/action-editorconfig-checker@main
+96
View File
@@ -0,0 +1,96 @@
name: Release
# Publishes a GitHub Release for a version tag (vX.Y.Z).
#
# By design, this workflow does NOT bump versions or modify podman.plg
# itself — that happens locally via `scripts/release.sh <version>`, which a
# maintainer reviews, commits, and tags *before* pushing the tag (see that
# 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/).
# 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.
#
# See docs/ARCHITECTURE.md section 13 (Updates).
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
build:
name: Build release packages
uses: ./.github/workflows/build-packages.yml
publish:
name: Publish GitHub Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download built packages
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact-name }}
path: dist
- name: Re-verify checksums
run: scripts/checksums.sh dist
- name: Verify plugin/podman.plg matches these artifacts
# scripts/release.sh should already have been run locally, and its
# resulting podman.plg changes committed as part of this tag, before
# the tag was pushed. This step fails the release loudly if that
# didn't happen, instead of publishing a release whose plg points at
# MD5s that don't match the .txz files actually attached below.
run: |
set -eu
for f in dist/*.txz.md5; do
expected_md5=$(awk '{print $1}' "$f")
txz_name=$(basename "${f%.md5}")
if ! grep -qF "$expected_md5" plugin/podman.plg; then
echo "!! $txz_name's checksum ($expected_md5) is not referenced in plugin/podman.plg." >&2
echo "!! Did you forget to run scripts/release.sh and commit its changes before tagging?" >&2
exit 1
fi
done
echo "All package checksums are referenced in plugin/podman.plg — OK."
- name: Extract version from tag
id: version
run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Extract changelog section for this version
id: changelog
run: |
awk -v ver="${{ steps.version.outputs.value }}" '
$0 ~ "^## \\[" ver "\\]" { found=1; print; next }
found && /^## \[/ { exit }
found { print }
' 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
+46
View File
@@ -0,0 +1,46 @@
# --- Slackware package build artifacts ---
packages/**/src/
packages/**/pkg/
packages/**/work/
packages/**/*.txz
packages/**/*.tgz
packages/**/*.txz.md5
packages/**/*.tgz.md5
# --- Build / release output ---
/dist/
/build/
/release/
*.plg.bak
# --- Logs ---
*.log
/scripts/dev/*.log
# --- Editor / IDE ---
.vscode/
.idea/
*.swp
*.swo
*~
# --- OS cruft ---
.DS_Store
Thumbs.db
# --- WebUI tooling (once JS/PHP tooling is added) ---
node_modules/
vendor/
composer.lock
package-lock.json
# --- Local/dev config overrides, never commit real secrets ---
*.local
.env
.env.*
config/*.secret
# --- Downloaded upstream sources used during package builds ---
packages/**/*.tar.gz
packages/**/*.tar.xz
packages/**/*.tar.bz2
+17
View File
@@ -0,0 +1,17 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
for the plugin version (independent of the bundled Podman/upstream package versions,
see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#52-build-strategie)).
## [Unreleased]
### Added
- Initial repository scaffolding: directory structure, documentation skeleton,
CI workflow stubs, and community health files.
- Architecture documentation (`docs/ARCHITECTURE.md`).
[Unreleased]: https://github.com/OWNER/unraid-podman/compare/HEAD...HEAD
+83
View File
@@ -0,0 +1,83 @@
# Contributing to unraid-podman
Thanks for your interest in contributing. This project is in an early,
architecture-first stage — please read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
before proposing structural changes, so new work stays consistent with the agreed
design (RAM-root persistence model, no systemd, Docker coexistence, etc.).
## Ground rules
- Discuss non-trivial changes in an issue before opening a PR — this repo makes
deliberate, sometimes non-obvious trade-offs (see the "Offene Fragen / Risiken"
section of the architecture doc) and we'd rather align early than rework a PR.
- Keep persistence discipline: anything written under `/etc`, `/usr`, `/var` on a
running Unraid system is lost on reboot. Any new config or state must be mirrored
to `/boot/config/plugins/podman/` or an array/cache path — see
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#7-persistenz-strategie-zusammenfassung-der-prinzipien).
- No systemd. Init/process management goes through `plugin/rc.d/rc.podman` in the
classic BSD `start|stop|restart|status` style.
- Don't introduce changes that could destabilize a coexisting Docker installation
(shared storage paths, shared bridge/network names, shared iptables chains).
## Repository layout
See the "Repository structure" section in [README.md](README.md). In short:
| Directory | What goes here |
|---|---|
| `packages/` | One subdirectory per Slackware `.txz` package (podman, conmon, crun, netavark, aardvark-dns, containers-common, fuse-overlayfs, passt) |
| `plugin/` | The `.plg` manifest, `rc.d/rc.podman`, `sbin/` helper scripts, and default files staged for `/boot/config/plugins/podman/` |
| `webui/` | Dynamix-style GUI pages under `plugins/podman/` (later phase) |
| `config/` | Default `containers.conf` / `storage.conf` / `registries.conf` / `policy.json` templates |
| `scripts/` | Build and release tooling, not shipped to end users |
| `docs/` | Architecture, install guide, FAQ, troubleshooting, roadmap |
## Development setup
Podman package builds must target Slackware compatibility with Unraid's current
base — this generally means building inside a Slackware-compatible container
rather than on your own distro. Run `scripts/build-packages.sh` (optionally
`scripts/build-packages.sh <name>` for a single package) inside such a
container; `scripts/ci/setup-slackware-buildenv.sh` bootstraps the toolchain
and C library dependencies the build needs. `.github/workflows/build-packages.yml`
runs the same scripts in CI and is the reference for the exact container image
and setup sequence.
For anything touching `plugin/rc.d/rc.podman`, `plugin/sbin/*.sh`, or `podman.plg`,
test on a real or virtualized Unraid instance — behavior around RAM-root, `/boot`
persistence, and array/cache availability at boot cannot be fully validated on a
generic Linux box.
## Coding conventions
- **Shell scripts** (`rc.podman`, `sbin/*.sh`, `scripts/*.sh`): POSIX-compatible
where practical, `set -eu` at minimum, and must pass
[ShellCheck](https://www.shellcheck.net/) (see `.github/workflows/lint.yml`).
- **Slackware package recipes** (`packages/*`): follow standard `SlackBuild`
conventions (`slack-desc` limited to 70 columns / 11 lines, `doinst.sh` for
post-install steps).
- **PHP/JS in `webui/`**: follow the conventions of Unraid's existing Dynamix
plugins for consistency with the rest of the GUI.
- Commit messages: [Conventional Commits](https://www.conventionalcommits.org/)
(`feat:`, `fix:`, `docs:`, `build:`, `ci:`, ...) so changelog generation and
history stay readable.
## Pull requests
1. Fork, branch off `main`, keep PRs focused on a single concern.
2. Update `CHANGELOG.md` under `[Unreleased]` for any user-facing change.
3. Update relevant docs under `docs/` if behavior or structure changes.
4. Make sure CI (lint / package build checks) passes.
5. Describe how you tested the change, especially for anything touching boot
behavior, persistence, or Docker coexistence.
## Reporting bugs / requesting features
Use the issue templates under `.github/ISSUE_TEMPLATE/`. For security-relevant
reports (rootful socket exposure, privilege escalation, etc.), follow
[SECURITY.md](.github/SECURITY.md) instead of a public issue.
## Code of Conduct
This project follows the [Code of Conduct](.github/CODE_OF_CONDUCT.md). By
participating, you agree to uphold it.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 unraid-podman contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+15
View File
@@ -0,0 +1,15 @@
# assets/
Branding and imagery used by the plugin manifest and (later) the WebUI.
```
assets/
├── icons/ Plugin icon(s) referenced by plugin/podman.plg (icon="podman")
│ and by webui/plugins/podman/. Unraid convention: PNG, square,
│ ~64x64 or SVG.
└── screenshots/ Screenshots used in README.md / docs/ once a WebUI exists.
```
No binary assets are checked in yet — this is scaffolding only. When adding the
real plugin icon, keep a source (SVG) alongside the exported PNG(s) so it can be
re-exported at other sizes later.
View File
View File
+23
View File
@@ -0,0 +1,23 @@
# Default containers.conf template for unraid-podman.
#
# This is a source template shipped in the repository/package. At install/boot
# time it is copied to /boot/config/plugins/podman/containers.conf (if not
# already present, never overwritten on update) and from there synced to
# /etc/containers/containers.conf by rc.podman start.
#
# See docs/ARCHITECTURE.md section 6.1 (Start-/Stop-Skripte) and section 7
# (Persistenz-Strategie). Placeholder — real defaults to be tuned during MVP
# implementation.
#
# Full reference: https://github.com/containers/common/blob/main/docs/containers.conf.5.md
[containers]
# TODO: default ulimits, log driver (k8s-file, see docs/ARCHITECTURE.md section 15), etc.
[engine]
# TODO: static_dir / volume_path / runroot overrides pointing at the cache-pool
# backed storage location instead of RAM-root defaults.
[network]
# TODO: default network backend (netavark), default subnet range distinct from
# Docker's docker0 range — see docs/ARCHITECTURE.md section 8.
+25
View File
@@ -0,0 +1,25 @@
# Example plugin settings file for unraid-podman.
#
# The real, user-editable copy of this file lives on the flash device at
# /boot/config/plugins/podman/podman.cfg and is the source of truth read by
# rc.podman at every start. See docs/ARCHITECTURE.md section 4.1 and 7.
#
# Format: simple KEY="value" pairs (bash-sourceable), matching the convention
# used by Unraid's own /boot/config/docker.cfg.
# Path to the directory holding podman.img (cache pool or array disk).
# TODO: finalize default; must NOT be a path under /mnt/user.
STORAGE_PATH="/mnt/cache/system/podman"
# Size of the podman.img loopback image, in GiB, used only on first creation.
STORAGE_IMAGE_SIZE_GB="20"
# Whether rc.podman should start automatically when the array starts.
PODMAN_ENABLED="yes"
# Per-container stop timeout, in seconds, used by `rc.podman stop`.
STOP_TIMEOUT="10"
# Config schema version, used by the migration logic described in
# docs/ARCHITECTURE.md section 13.1.
CONFIG_SCHEMA_VERSION="1"
+11
View File
@@ -0,0 +1,11 @@
{
"_comment": "Default container image signature verification policy. Placeholder: accepts all images without signature verification, matching Docker's default trust model on Unraid today. See docs/ARCHITECTURE.md section 10 (Images). Revisit before a 1.0 release if signed-image verification becomes a goal.",
"default": [
{ "type": "insecureAcceptAnything" }
],
"transports": {
"docker-daemon": {
"": [{ "type": "insecureAcceptAnything" }]
}
}
}
+17
View File
@@ -0,0 +1,17 @@
# Default registries.conf template for unraid-podman.
#
# Pre-configures unqualified-search registries explicitly, since there is no
# interactive TTY to prompt the user in a boot-script context.
# See docs/ARCHITECTURE.md section 10 (Images).
#
# Full reference: https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md
unqualified-search-registries = ["docker.io"]
# TODO: consider pre-listing common registries (ghcr.io, quay.io, lscr.io) as
# short-name aliases, matching what Unraid Community Applications users expect
# from Docker Hub-centric templates today.
[[registry]]
prefix = "docker.io"
location = "docker.io"
+21
View File
@@ -0,0 +1,21 @@
# Default storage.conf template for unraid-podman.
#
# Defines the Podman storage graph root/driver. On Unraid this MUST point at
# the mounted podman.img loopback (or equivalent cache-pool/disk path), never
# at a path under /mnt/user (FUSE) or RAM-root /var/lib/containers directly.
# See docs/ARCHITECTURE.md section 4.3 and section 10 (Images).
#
# Full reference: https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md
[storage]
driver = "overlay"
# runroot and graphroot are set at runtime by rc.podman based on
# /boot/config/plugins/podman/podman.cfg (configurable storage location),
# not hardcoded here. TODO: document the exact substitution mechanism once
# rc.podman is implemented.
# graphroot = "/var/lib/containers/storage"
# runroot = "/var/run/containers/storage"
[storage.options]
# TODO: overlay-specific mount options once the loopback filesystem
# (XFS with ftype=1, or BTRFS) is finalized.
+656
View File
@@ -0,0 +1,656 @@
# unraid-podman — Architekturplanung
Natives Unraid-Plugin zur vollständigen Integration von Podman (rootful, Phase 1) als
gleichberechtigte Alternative zu Docker, mit dem langfristigen Ziel, Docker optional
abschaltbar zu machen.
Status: Planungsdokument, kein Code. Dient als Grundlage für Implementierungs-Tickets.
---
## 0. Ziele & Nicht-Ziele
**Ziele (Phase 1 / MVP):**
- Podman rootful lauffähig unter Unraid, überlebt Reboots (Config, Images, Container, Volumes, Netzwerke).
- Koexistenz mit Docker ohne Konflikte (Storage, Netzwerk, Ports, iptables-Ketten).
- Start/Stop/Autostart analog zum bestehenden `rc.docker`-Muster, ohne systemd.
- Sauberer Update-/Rollback-Pfad für das Plugin selbst.
- Grundlegendes Logging & Fehlerdiagnose, das GUI-Meldungen von Unraid nutzt.
- Vorbereitung (nicht Umsetzung) einer WebUI-Anbindung.
**Nicht-Ziele (bewusst verschoben):**
- Rootless Podman (Phase 2+, andere Storage-/UID-Mapping-Anforderungen).
- Podman Pods / Kubernetes-YAML-Support (Phase 3, siehe Roadmap).
- 1:1-Ersatz der Community-Applications-Vorlagen (erfordert eigenen Übersetzungslayer, separat geplant).
- Abschalten von Docker (erst wenn Podman-Pfad stabil ist, siehe Abschnitt 17).
---
## 1. Rahmenbedingungen von Unraid, die die Architektur erzwingen
| Eigenschaft | Konsequenz für das Plugin |
|---|---|
| Unraid läuft komplett im RAM (SquashFS + tmpfs-Overlay via `unionfs`) | Alles, was `/`, `/etc`, `/usr`, `/var` betrifft, ist bei jedem Reboot verloren. Nichts darf implizit dort persistiert werden. |
| Persistenter Speicher nur unter `/boot` (USB-Stick, FAT32, klein, langsam, schreibintensiv riskant) und `/mnt/*` (Array/Cache/Pools) | Konfiguration klein & textbasiert auf `/boot/config/plugins/...`; große/volatile Daten (Images, Container-Storage, Logs) auf Array/Cache. |
| Kein systemd, sondern BSD-artige `/etc/rc.d/rc.*`-Skripte, angestoßen über `/boot/config/go` | Eigenes `rc.podman`-Init-Skript im klassischen `start|stop|restart|status`-Stil, keine `.service`-Units, keine `systemctl`-Aufrufe irgendwo im Code. |
| Slackware-Basis, Pakete als `.txz` (pkgtools: `installpkg`/`upgradepkg`/`removepkg`) | Podman + Abhängigkeiten müssen als Slackware-kompatible `.txz` gebaut werden, nicht als RPM/DEB. |
| Plugins werden über `.plg`-XML installiert (lädt Pakete, führt `<FILE>`-Blöcke mit Pre-/Post-Scripts aus) | Das `.plg` ist der zentrale Installations-/Update-/Deinstallations-Mechanismus, vergleichbar mit einem deklarativen Installer. |
| `/mnt/user` (shfs/FUSE) hat Einschränkungen für Overlay-Filesysteme (fehlendes `d_type` in manchen Konstellationen, FUSE-Overhead) | Podman-Storage (`overlay`-Driver) nicht direkt auf `/mnt/user/...` legen, sondern auf ein dediziertes Loopback-Image oder einen direkt gemounteten Pool/Disk-Pfad (Analogie zu `docker.img` bzw. Docker-„Directory“-Modus seit 6.12). |
| Array kann offline sein / erst spät im Boot verfügbar (`emhttpd`, Array-Start ist ein User-Trigger, kein automatischer Boot-Schritt) | `rc.podman` darf nicht blind beim Boot starten, sondern muss auf Array-/Pool-Verfügbarkeit warten bzw. vom `dynamix`-Event „array started“ getriggert werden, wie es `rc.docker` heute schon tut. |
| Unraid pflegt eigene Firewall-/NAT-Logik (`iptables`) für Docker-Netzwerke (`docker0`, custom bridges, macvlan) | Podman-Netzwerke (netavark) müssen eigene, klar abgegrenzte iptables-Ketten/Chains verwenden, um nicht mit Dockers Regeln zu kollidieren. |
---
## 2. Gesamtarchitektur (Überblick)
```
┌───────────────────────────────────────────┐
│ /boot (USB, FAT32) │
│ config/plugins/podman/ │
│ ├─ podman.cfg (Settings) │
│ ├─ autostart (Flat-File) │
│ ├─ network.json │
│ ├─ containers.conf / storage.conf (Vorlagen)
│ └─ backup/ (vorherige Paketversionen) │
└───────────────────┬─────────────────────────┘
│ beim Boot kopiert nach /etc, /var/lib
┌────────────────────────────────────────────────────────────────────┐
│ RAM-Root (tmpfs/unionfs) │
│ /etc/rc.d/rc.podman (Init-Skript) │
│ /etc/containers/* (aus /boot kopiert) │
│ /usr/bin/podman, conmon, crun, netavark, aardvark-dns, ... │
│ /usr/local/emhttp/plugins/podman/ (WebUI-Stub, PHP) │
└───────────────────────────┬───────────────────────────────────────┘
│ bind-mount / Storage-Root zeigt auf
┌────────────────────────────────────────────────────────────────────┐
│ Persistenter Bereich (Cache-Pool empfohlen) │
│ /mnt/cache/system/podman/ │
│ ├─ podman.img (Loopback, XFS/BTRFS, für overlay-Storage) │
│ │ → gemountet auf /var/lib/containers/storage │
│ ├─ logs/ │
│ └─ networks/ (netavark state, falls nicht im Image) │
└────────────────────────────────────────────────────────────────────┘
```
Kernprinzip: **Trennung von „Konfiguration“ (klein, Text, `/boot`) und „Nutzdaten“
(groß, binär, Array/Cache)** — exakt das bestehende Unraid-Muster, das auch
`dynamix.docker.manager` verwendet.
---
## 3. Pluginstruktur
### 3.1 Repository-Layout (Build-/Source-Repo, nicht das, was auf Unraid landet)
```
unraid-podman/
├── podman.plg # Haupt-Plugin-Manifest (XML)
├── packages/ # Slackware .txz Build-Rezepte
│ ├── podman/
│ ├── conmon/
│ ├── crun/
│ ├── netavark/
│ ├── aardvark-dns/
│ ├── containers-common/
│ ├── fuse-overlayfs/
│ └── passt/
├── source/
│ ├── rc.podman # Init-Skript
│ ├── podman-preflight.sh # Startup-Checks
│ ├── podman-autostart.sh
│ ├── podman-backup.sh
│ └── config-templates/
│ ├── containers.conf
│ ├── storage.conf
│ └── registries.conf
├── emhttp/
│ └── plugins/podman/ # WebUI (PHP/JS), Phase 2+
├── scripts/
│ ├── build-packages.sh
│ └── release.sh
└── CHANGELOG.md
```
### 3.2 `.plg`-Manifest — Verantwortlichkeiten
Das `.plg` ist eine deklarative XML-Datei, die von Unraids `plugin`-Kommando
interpretiert wird. Verantwortlich für:
- **Metadaten**: `<plugin name="podman" version="..." pluginURL="..." min="..." />` inkl.
Mindest-Unraid-Version (Kernel-/glibc-Kompatibilität der Podman-Binaries).
- **Entity-Variablen** (XML-Entities) für Basis-URL, Version, MD5-Summen — ermöglicht
Update-Checks über den Plugin-Manager der GUI.
- **`<FILE>`-Blöcke**:
1. Download & Installation aller acht `.txz`-Pakete (die sieben Komponenten
plus das plugin-eigene `unraid-podman`-Scaffolding-Paket) einheitlich per
`upgradepkg --install-new --reinstall` — funktioniert unverändert für
Erstinstallation und Update (bestätigtes Muster echter Unraid-Plugins,
siehe unten).
2. `<INLINE>`-Postinstall-Skript: Setzen von Berechtigungen, Anlegen der
Verzeichnisstruktur unter `/boot/config/plugins/podman/` (nur beim ersten
Install, idempotent; Configs nur kopiert, wenn nicht vorhanden — kein
Überschreiben bestehender Nutzerkonfiguration bei Updates), Schreiben der
Versions-Manifest-Datei, erster `rc.podman start`.
- **Array-Start/Stop-Hook**: **kein** Eintrag in `/boot/config/go`. Stattdessen
der offizielle Unraid-Plugin-Event-Mechanismus:
`/usr/local/emhttp/plugins/podman/event/disks_mounted` (startet `rc.podman`,
sobald Cache-Pools/Disks gemountet sind) und `.../event/stopping` (stoppt
`rc.podman` synchron als allererster Schritt der Shutdown-Sequenz, bevor
Unraid die zugrundeliegende Disk unmountet). Dieses Verzeichnis-Konvention
wurde gegen den echten Quellcode etablierter Plugins verifiziert (u. a.
`unassigned.devices`, das exakt dieselben `event/`-Hooks für
`disks_mounted`/`stopping_svcs` verwendet) — kein `/boot/config/go`-Edit
nötig, dadurch auch beim Deinstallieren nichts manuell rückgängig zu machen.
- **`<FILE Run="/bin/bash" Method="remove">`-Block für Deinstallation**: ruft
zuerst `podman-uninstall-cleanup.sh` auf (stoppt `rc.podman`, räumt
Laufzeitzustand auf), dann `removepkg` für alle acht Pakete. Lässt Nutzdaten
unter `/mnt/*` und `/boot/config/plugins/podman/` standardmäßig **stehen**
(explizite Option „Restlose Deinstallation inkl. Daten“ via
`--purge-data`-Flag, kein automatisches Löschen von Nutzdaten).
### 3.3 Namens- und Versionskonventionen
- Plugin-Name: `podman` (Namespace-Kollisionen mit CA-Plugins prüfen).
- Paket-Versionierung getrennt von Plugin-Versionierung: `podman.plg` trägt eine eigene
SemVer (`1.2.0`), referenziert aber exakte Upstream-Versionen der Binärpakete
(`podman-5.x.x`, `netavark-1.x.x`, ...) — erlaubt Plugin-Patches (z. B. am
Init-Skript) ohne Podman-Versionssprung.
---
## 4. Verzeichnislayout (vollständig)
### 4.1 Auf dem Flash-Device (`/boot`, persistent, klein)
```
/boot/config/plugins/podman/
├── podman.cfg # Key=Value, analog docker.cfg (Storage-Pfad, Enable, Optionen)
├── autostart # eine Container-ID/-Name pro Zeile, Reihenfolge = Startreihenfolge
├── autostart-delay # optionale Wartezeiten pro Container (Key=Sekunden)
├── containers.conf # Vorlage, wird nach /etc/containers/ kopiert
├── storage.conf # Vorlage, referenziert podman.img-Pfad
├── registries.conf
├── policy.json
├── networks/ # persistierte netavark-Netzwerkdefinitionen (JSON)
├── backup/
│ ├── packages/<version>/*.txz # vorherige Paketversionen für Rollback
│ └── config/<timestamp>/ # Config-Snapshots vor Updates
└── plugin.log # Install-/Update-Log des Plugins selbst
```
`/boot/config/plugins/podman/` ist bewusst analog zu
`/boot/config/plugins/dynamix.docker.manager/` gehalten (Wiedererkennung für
erfahrene Unraid-Nutzer, Support-Vergleichbarkeit).
### 4.2 Laufzeit (RAM-Root, bei jedem Boot neu aufgebaut)
```
/etc/rc.d/rc.podman # Init-Skript (Symlink-Ziel oder direkt kopiert)
/etc/containers/{containers,storage,registries}.conf # aus /boot kopiert
/etc/containers/policy.json
/etc/cni/ (falls CNI-Fallback statt netavark benötigt)
/usr/bin/{podman,conmon,crun,fuse-overlayfs,passt,pasta}
/usr/libexec/podman/{netavark,aardvark-dns} # matches upstream's own layout
/usr/local/emhttp/plugins/podman/ # WebUI-Stub (statisch aus Paket)
/var/run/podman/podman.sock # API-Socket (rootful)
/var/log/podman -> /mnt/cache/system/podman/logs # Symlink, siehe Abschnitt 13
```
### 4.3 Persistente Nutzdaten (Cache-Pool bevorzugt, Array als Fallback)
```
/mnt/cache/system/podman/ # analog /mnt/user/system/docker/
├── podman.img # Loopback-Datei, XFS (ftype=1) oder BTRFS
│ # → gemountet auf /var/lib/containers/storage
├── logs/
│ ├── containers/<id>.log
│ └── podman-service.log
└── networks/ # falls netavark-State nicht im Image liegt
```
**Warum Cache-Pool statt `/mnt/user`:** Der `overlay`-Storage-Driver benötigt native
Dateisystemsemantik (d_type, xattrs, Hardlinks) ohne den FUSE-Layer von `shfs`. Das
Docker-Vorbild löst das identisch mit `docker.img`/Directory-Modus auf einem echten
Mountpoint. Nutzer ohne Cache-Pool müssen einen Array-Disk-Pfad (`/mnt/diskX/system/podman`)
wählen können — mit deutlicher GUI-Warnung bzgl. Performance und Spin-up-Verhalten.
---
## 5. Paketmanagement
### 5.1 Zu paketierende Komponenten
| Paket | Zweck | Bemerkung |
|---|---|---|
| `podman` | Kern-Binary | Statisch gegen möglichst wenige glibc-Versionen bauen oder gegen Unraid-Slackware-Base kompilieren |
| `conmon` | Container-Monitor-Prozess | Pflicht |
| `crun` (empfohlen) / `runc` | OCI-Runtime | `crun` bevorzugt (leichter, cgroup v2-freundlich, kompatibel zu Unraids Kernel-Config) |
| `netavark` + `aardvark-dns` | Netzwerk-Backend + DNS | Ersetzt CNI-Plugins als Default seit Podman 4.x |
| `containers-common` | Default-Configs (`containers.conf`, `seccomp.json`, `registries.conf`) | Wird als Vorlage übernommen, nicht als aktive Config |
| `fuse-overlayfs` | Fallback-Storage-Driver | Für Rootless-Phase 2 vorbereitet, in Phase 1 optional |
| `passt`/`pasta` | Rootless-Networking | Nachfolger von slirp4netns, Phase 2, aber Paket schon mitbauen (geringe Kosten) |
| `catatonit` oder `tini` | Init-Prozess in Containern (optional, falls von Templates genutzt) | |
### 5.2 Build-Strategie
- Eigene Build-Pipeline (containerisiert, z. B. in einem Slackware-kompatiblen
Build-Container) statt manuellem Cross-Compile auf laufenden Unraid-Systemen.
- Zielarchitektur: `x86_64` (Unraid unterstützt aktuell keine anderen Architekturen
produktiv) — vereinfacht Matrix erheblich.
- Statisch oder minimal dynamisch gelinkt (Go-Binaries von Podman/netavark/aardvark-dns
sind ohnehin größtenteils statisch), um Abhängigkeits-Drift gegenüber der jeweils
aktuellen Unraid-Slackware-Basis zu minimieren.
- Jedes Paket als eigenständiges `.txz` mit Slackware-Standard-Metadaten
(`slack-desc`, `doinst.sh`), damit `removepkg`/`upgradepkg` korrekt funktionieren.
- Versions-Pinning: Das `.plg` referenziert exakte Paketversionen + MD5, kein
„latest“-Pull zur Laufzeit — reproduzierbare Installationen, Voraussetzung für Rollback.
### 5.3 Abhängigkeitsprüfung
- Preflight-Check im `.plg`-Postinstall: Kernel-Version, `cgroup v2` aktiv,
vorhandene `iptables`/`nftables`-Binaries, freier Platz auf Zielpfad für
`podman.img`. Bei Nichterfüllung: Installation abbrechen mit klarer GUI-Meldung
statt eines halb-funktionsfähigen Zustands.
---
## 6. Start-/Stop-Skripte
### 6.1 `rc.podman` — Design analog zu `rc.docker`
Klassisches Slackware-BSD-Init-Skript mit Case-Dispatch:
```
rc.podman start # Preflight → Storage mounten → API-Service starten → Autostart-Liste abarbeiten
rc.podman stop # Container geordnet stoppen (Timeout) → API-Service stoppen → Storage unmounten
rc.podman restart
rc.podman status # Health-Check-Ausgabe für GUI/CLI
```
Kernschritte von `start` (konzeptionell, kein Code):
1. **Preflight** (`podman-preflight.sh`): prüft, ob konfigurierter Storage-Pfad
(Cache-Pool/Disk) gemountet ist; falls `podman.img` fehlt → anlegen (Erstinstallation)
mit konfigurierter Größe; falls vorhanden → per Loopback mounten.
2. **Config-Sync**: Kopiert `/boot/config/plugins/podman/*.conf` nach
`/etc/containers/` (Boot-Config ist „Source of Truth“, Laufzeit-Kopie ist Cache).
3. **Socket-Service starten**: `podman system service` im Hintergrund (rootful Unix-Socket
unter `/var/run/podman/podman.sock`), damit spätere WebUI/API-Konsumenten und CLI
dieselbe Instanz sehen (statt pro Aufruf neuer Podman-Fork-Prozesse ohne gemeinsamen State
— State liegt zwar in `/var/lib/containers`, ein Service vereinfacht aber Health-Checks,
Event-Streaming und spätere WebUI-Anbindung).
4. **Netzwerke wiederherstellen**: netavark-Netzwerkdefinitionen aus
`/boot/config/plugins/podman/networks/` nach `/etc/containers/networks/` synchronisieren.
5. **Autostart** (`podman-autostart.sh`, siehe Abschnitt 10).
6. **Statusdatei** schreiben (für GUI-Polling), Event ins Unraid-Log (`logger`).
`stop` in umgekehrter Reihenfolge, mit konfigurierbarem Stop-Timeout pro Container
(analog Dockers „Stop timeout“-Einstellung), danach `podman system service` beenden,
zuletzt Loopback-Storage sauber unmounten (verhindert Dateisystemfehler im `podman.img`).
### 6.2 Boot-Integration
- **Offizieller Unraid-Plugin-Event-Mechanismus**, kein `/boot/config/go`-Edit:
Unraids `emhttpd` führt bei jedem Array-/Boot-Event automatisch jedes
ausführbare Skript unter `/usr/local/emhttp/plugins/<name>/event/<eventname>`
aus, sofern vorhanden. Dieses Verhalten wurde gegen den echten Quellcode
etablierter Plugins verifiziert (`unassigned.devices` nutzt exakt dieselbe
Konvention für seine `disks_mounted`/`started`/`stopping_svcs`-Hooks).
unraid-podman nutzt zwei Events:
- `event/disks_mounted` — feuert, sobald Array-Disks **und** Cache-Pools
gemountet sind (Unraids Event-Reihenfolge: `starting``array_started`
`disks_mounted``svcs_restarted``docker_started``libvirt_started`
`started`). Ruft `rc.podman start` im Hintergrund auf (`& disown`),
damit der Array-Start nicht blockiert.
- `event/stopping` — der **erste** Schritt der Shutdown-Sequenz (vor
`stopping_docker`, `stopping_svcs`, `unmounting_disks`, `stopping_array`).
Ruft `rc.podman stop` **synchron** auf, damit Container gestoppt und
`podman.img` sauber unmountet sind, bevor Unraid die zugrundeliegende
Disk/den Cache-Pool unmountet.
Dadurch kein Start vor Verfügbarkeit von Cache/Array, und beim Deinstallieren
ist nichts manuell rückgängig zu machen — die Hook-Skripte verschwinden
automatisch mit `removepkg` des `unraid-podman`-Pakets (siehe Abschnitt 3.2).
---
## 7. Persistenz-Strategie (Zusammenfassung der Prinzipien)
| Datenkategorie | Ablageort | Synchronisationsrichtung |
|---|---|---|
| Plugin-Settings (Storage-Pfad, Feature-Flags) | `/boot/config/plugins/podman/podman.cfg` | Boot → Laufzeit (read-only Kopie) |
| Container-/Netzwerk-/Registry-Config | `/boot/config/plugins/podman/*.conf` | Boot → Laufzeit bei jedem Start |
| Autostart-Liste & Reihenfolge | `/boot/config/plugins/podman/autostart` | Boot → Laufzeit; GUI schreibt zurück nach `/boot` |
| Images, Container, named Volumes, Layer-Cache | `podman.img` auf Cache-Pool | Laufzeit-only, Backup optional via Snapshot |
| Logs | Cache-Pool `logs/` | Laufzeit-only, Rotation |
| Netzwerk-Definitionen (netavark JSON) | Boot-Kopie + Laufzeit-Kopie | bidirektional bei Änderung über GUI/CLI (Watcher oder expliziter „Apply“-Schritt) |
Grundregel: **Jede Änderung, die ein Nutzer über CLI/GUI an persistenzrelevanten
Objekten vornimmt (neues Netzwerk, geänderte Autostart-Reihenfolge), muss explizit
oder per Watcher nach `/boot/config/plugins/podman/` zurückgeschrieben werden** —
sonst geht sie beim nächsten Reboot verloren. Das ist der Kernunterschied zu
Standard-Linux-Distributionen und der häufigste Fehlerquell-Kandidat bei Docker-artigen
Unraid-Plugins.
---
## 8. Netzwerke
- **Backend**: `netavark` + `aardvark-dns` (Podman-Default seit 4.x), kein CNI in Phase 1.
- **Default-Bridge**: eigene Bridge `podman0` (nicht `docker0`), eigener privater
Adressraum (konfigurierbar, Default-Vorschlag außerhalb von Dockers Default-Range,
um Kollisionen bei Parallelbetrieb zu vermeiden).
- **Custom Networks**: analog Docker-für-Unraid „Custom Networks“-Feature — GUI-Verwaltung
in Phase 2, Config-Persistenz-Mechanismus aber bereits in Phase 1 vorsehen (Abschnitt 7).
- **macvlan/ipvlan**: gleiche Kernel-Voraussetzungen wie bei Docker; bekannte Unraid-Falle
„macvlan call trap“ (Kernel-Panic bei bestimmten NIC-Treibern) muss dokumentiert und im
Preflight als Warnung ausgegeben werden, wenn macvlan-Netzwerke angelegt werden.
- **iptables/nftables-Koexistenz mit Docker**: netavark legt eigene Chains an
(`NETAVARK-*` Präfix); explizit verifizieren, dass diese nicht mit Dockers
`DOCKER`-Chains oder Unraids eigenen Firewall-Regeln interferieren. Firewall-Reload
durch Docker (bei dessen Start/Stop) darf Podman-Regeln nicht löschen und umgekehrt —
ggf. eigenes `iptables-restore`-Hook nach jedem `rc.podman start`.
- **DNS**: `aardvark-dns` für Container-zu-Container-Namensauflösung innerhalb
benutzerdefinierter Netzwerke, analog Dockers eingebettetem DNS-Server.
- **Port-Publishing**: Kollisionsprüfung mit bereits von Docker/anderen Diensten
belegten Ports als GUI-seitige Validierung (spätere Phase), technisch bereits
durch Kernel/`iptables` ohnehin erzwungen.
---
## 9. Volumes
- **Named Volumes**: liegen innerhalb von `podman.img` unter
`.../storage/volumes/`, damit sie vom selben Backup-/Rollback-Mechanismus wie Images
erfasst werden.
- **Bind-Mounts** auf Unraid-Shares (`/mnt/user/appdata/...`): empfohlener Standardweg
für Nutzerdaten (analog Dockers „Path“-Mappings in Community-Applications-Templates),
da `/mnt/user/appdata` bereits vom Nutzer im Backup-/Mover-/Cache-Konzept berücksichtigt
wird. Bind-Mounts direkt gegen `/mnt/user` (FUSE) sind für Applikationsdaten unkritisch
(kein Overlay-Storage-Anspruch), nur der Storage-Graph selbst braucht den echten Mount.
- **Permissions/UID-Mapping**: Phase 1 rootful → Container laufen mit denselben
UID/GID-Semantiken wie Docker rootful heute; keine zusätzliche User-Namespace-Remapping-
Komplexität. Wird in Phase 2 (rootless) relevant.
- **Migration-Hilfe (spätere Phase, nicht MVP)**: Werkzeug zum Referenzieren bestehender
Docker-Bind-Mount-Pfade beim Anlegen äquivalenter Podman-Container, um Parallelbetrieb
mit denselben Appdata-Verzeichnissen zu erleichtern (mit klarer Warnung vor
gleichzeitigem Schreibzugriff durch zwei Runtimes).
---
## 10. Images
- **Storage-Driver**: `overlay` (native, kein `fuse-overlayfs`) auf dem gemounteten
`podman.img` — Performance-Parität mit Dockers Standard-Setup.
- **Registries**: Default `registries.conf` mit Docker Hub + ggf. `ghcr.io`, `quay.io`
vorkonfiguriert, kurz-Namen-Auflösung (`unqualified-search-registries`) explizit
gesetzt statt interaktivem Prompt (der in einer daemonless/Skript-Umgebung nicht
funktioniert).
- **Pull/Push**: Standard-Podman-CLI-Semantik, keine Besonderheiten; Fortschrittsanzeige
später über API-Service-Events für die WebUI nutzbar.
- **Image-Größenmanagement**: `podman.img` ist eine feste/dynamisch wachsende
Loopback-Datei — GUI muss Füllstand anzeigen und Vergrößern anbieten (analog Dockers
„docker.img full“-Problem, das in der Community bekannt und schmerzhaft ist; hier von
Anfang an mit klarer Fehlermeldung statt stillem Fehlschlag lösen, siehe Abschnitt 16).
- **Community-Applications-Kompatibilität**: kein automatischer Import von
Docker-Templates in Phase 1 (eigenes, separates Vorhaben); Podman-Images werden
zunächst über CLI/eigene, minimale Template-Definition verwaltet.
---
## 11. Container (Lifecycle-Management)
- Lifecycle-Operationen (`create`, `start`, `stop`, `restart`, `remove`, `exec`,
`logs`, `inspect`) laufen über den in Abschnitt 6.1 gestarteten Podman-API-Service,
nicht über Ad-hoc-CLI-Aufrufe aus der GUI heraus — ein gemeinsamer Service reduziert
Race Conditions und ermöglicht Event-Streaming.
- **Labels für GUI-Integration**: eigenes Label-Schema (z. B. `net.unraid.podman.icon`,
`net.unraid.podman.webui`), angelehnt an, aber getrennt von Dockers
`net.unraid.docker.*`-Konventionen, um spätere GUI-Icons/WebUI-Links analog zur
Docker-Tab-Darstellung zu ermöglichen, ohne Namensraum-Kollisionen.
- **Update-Strategie für Container-Images**: „Check for Updates“-Mechanismus
(Digest-Vergleich gegen Registry) als spätere GUI-Funktion, technisch schon in
Phase 1 per `podman image inspect`/`skopeo`-Vergleich vorbereitbar.
---
## 12. Autostart
Da kein systemd existiert, wird Autostart **explizit vom Plugin verwaltet**, nicht von
Podman selbst (Podman hat kein natives „restart on boot“ ohne generierte Unit-Files):
- Flache Datei `/boot/config/plugins/podman/autostart`, eine Zeile pro Container
(Name oder ID), Reihenfolge = Zeilen-Reihenfolge = Startreihenfolge — direktes
Äquivalent zu Dockers `/boot/config/plugins/dynamix.docker.manager/docker-autostart`
bzw. der DB-gestützten Nachfolgelösung.
- Optionale, separate Delay-Datei für Wartezeiten zwischen Starts (Abhängigkeiten
zwischen Containern, z. B. DB vor App).
- `rc.podman start` liest die Liste sequenziell, startet jeden Container über den
API-Service, protokolliert Erfolg/Fehler pro Eintrag, bricht bei Einzelfehlern
**nicht** die gesamte Kette ab (ein defekter Container darf nicht alle anderen
blockieren).
- GUI-Checkbox „Autostart“ pro Container (spätere Phase) schreibt direkt in diese
Datei zurück — kein separates DB-Layer in Phase 1, um Komplexität niedrig zu halten
(kann bei Bedarf später durch strukturierteres Format, z. B. JSON, ersetzt werden).
---
## 13. Updates
### 13.1 Plugin-Update (Podman-Version, Init-Skripte)
- Über den regulären Unraid-Plugin-Update-Mechanismus: `.plg` mit neuer Version +
neuen Paket-URLs/MD5s wird erneut ausgeführt.
- Vor jedem Update: automatisches Backup (siehe Abschnitt 14) der aktuellen
`.txz`-Pakete und Config-Dateien.
- **Container laufen während des Plugin-Updates weiter**, sofern nur Binaries
ausgetauscht werden (laufende `conmon`/Container-Prozesse sind vom
Dateisystem-Austausch unter `/usr/bin` isoliert, solange der Prozess nicht neu
exec't wird) — erst ein nachfolgender `rc.podman restart` übernimmt die neue
Podman-Version für den API-Service. Klar kommunizieren: Update ≠ sofortiger
Container-Neustart.
- Config-Migrationen (z. B. neues Feld in `containers.conf`) über ein
Versions-gestempeltes Migrationsskript, das beim Postinstall geprüft wird
(`podman.cfg` enthält `CONFIG_SCHEMA_VERSION`).
### 13.2 Container-Image-Updates
- Getrennt vom Plugin-Update zu betrachten — reine Podman-Funktionalität
(`podman pull` + Recreate), GUI-Komfortfunktion für Phase 2.
---
## 14. Rollback
- **Paket-Rollback**: Vor jedem Update werden die aktuell installierten `.txz` nach
`/boot/config/plugins/podman/backup/packages/<alte-version>/` kopiert. Ein
„Rollback“-Kommando (Postinstall-Skript, später GUI-Button) installiert diese
Pakete erneut per `upgradepkg --reinstall`.
- **Config-Rollback**: Vor jeder strukturellen Config-Änderung (Plugin-Update mit
Schema-Migration) wird ein Snapshot nach
`/boot/config/plugins/podman/backup/config/<timestamp>/` geschrieben; Rollback
kopiert diesen Snapshot zurück und setzt `CONFIG_SCHEMA_VERSION` entsprechend zurück.
- **Storage-Rollback (Images/Container) ist explizit außerhalb des automatischen
Rollbacks**: `podman.img` wird nicht bei jedem Plugin-Update gesichert (zu groß,
zu I/O-intensiv). Stattdessen: dokumentierte, manuell auslösbare Snapshot-Funktion
(z. B. `cp --reflink` auf BTRFS/ZFS-Cache-Pools, wo Reflinks verfügbar sind) als
optionales Feature, kein Zwang.
- Rollback-Historie begrenzen (z. B. letzte 3 Versionen), um den knappen
Flash-Speicher nicht zu erschöpfen.
---
## 15. Logging
- **Problem**: `/var/log` liegt im RAM und ist bei Reboot weg; `journald` existiert
nicht, daher kann Podmans `journald`-Log-Driver nicht Default sein.
- **Lösung**: Default-Log-Driver `k8s-file` (JSON-per-Zeile, Podman-nativ) mit
Zielverzeichnis unter `/mnt/cache/system/podman/logs/containers/`, per
Symlink von `/var/log/podman` dorthin erreichbar (einheitlicher Pfad für Tools,
die `/var/log` erwarten, ohne RAM-Verlust).
- **Log-Rotation**: eigener Cron-Eintrag (Unraid nutzt `cron` bereits für andere
Plugins über `/boot/config/plugins/dynamix/`-Muster bzw. `/etc/cron.d`) mit
`logrotate`-Konfiguration oder einfachem Größen-/Alter-basiertem Skript, da
Flash/Cache-Platz begrenzt ist.
- **Service-Log** (`podman system service`, `rc.podman`-Ausführung) getrennt von
Container-Logs, ebenfalls auf Cache-Pool, zusätzlich Kurzfassung wichtiger
Ereignisse (Start/Stop/Fehler) über `logger` in Unraids zentrales Syslog, damit
sie in der Standard-„System Log“-GUI sichtbar sind.
- **GUI-Zugriff (spätere Phase)**: Log-Viewer analog Dockers Container-Log-Fenster,
liest direkt aus den `k8s-file`-Logs bzw. streamt über den API-Service.
---
## 16. Fehlerbehandlung
### 16.1 Startup-Robustheit
- **Preflight-Checks** vor jedem `rc.podman start` (siehe 6.1): Storage-Mount,
Speicherplatz auf `podman.img`, Kernel-Voraussetzungen, Socket-Verfügbarkeit
(kein verwaister Socket von vorherigem Absturz).
- **Storage-Korruption**: Falls `podman.img` beim Mount-Versuch Dateisystemfehler
zeigt (`xfs_repair`/`btrfs check` schlägt fehl oder wird nicht automatisch
ausgeführt) → Start abbrechen, klare GUI-Fehlermeldung mit Handlungsempfehlung
(Recovery-Tool ausführen, Backup einspielen), **kein** automatisches
Neuanlegen/Formatieren ohne Nutzerbestätigung (Datenverlustrisiko).
- **Autostart-Fehler pro Container**: einzelner fehlgeschlagener Container wird
geloggt und übersprungen, blockiert nicht die restliche Kette (siehe Abschnitt 12);
nach N gescheiterten Autostart-Versuchen in Folge wird der Container automatisch
aus der Autostart-Kette pausiert („Safe-Mode“ pro Container) mit GUI-Hinweis,
um Boot-Loops/Ressourcenverschwendung zu vermeiden.
### 16.2 Laufzeitfehler
- **Health-Check des API-Service**: `rc.podman status` prüft Socket-Erreichbarkeit
und meldet Diskrepanzen (Prozess läuft, Socket tot o. ä.) verständlich.
- **Firewall-/Netzwerk-Konflikte**: bei bekannten Fehlerbildern (z. B. Chain-Konflikt
mit Docker, Port bereits belegt) definierte, für Menschen lesbare Fehlermeldungen
statt roher `iptables`/Go-Stacktraces — Fehlerkatalog mit Klartext-Ursache +
nächstem Schritt.
- **Ressourcenerschöpfung** (`podman.img` voll): aktive Prüfung im Preflight und
periodisch (Cron), GUI-Warnung bei > 85 % Füllstand, klare Anleitung zum
Vergrößern des Loopback-Images (bekannte Docker-für-Unraid-Schwachstelle, hier
von Anfang an proaktiv statt reaktiv lösen).
- **Unraid-Benachrichtigungssystem**: kritische Fehler (Storage-Mount fehlgeschlagen,
Autostart-Kette größtenteils fehlgeschlagen, `podman.img` > Schwellwert) werden
über Unraids Standard-Notify-Mechanismus (`/usr/local/emhttp/webGui/scripts/notify`)
ausgelöst, damit sie in GUI-Toasts, optional E-Mail/Pushover (falls vom Nutzer
konfiguriert) erscheinen — kein Parallel-Notify-System erfinden.
---
## 17. Docker-Parallelbetrieb & spätere Deaktivierung
### Phase 1 — Parallelbetrieb (Pflicht)
- Getrennte Storage-Roots (`docker.img` vs. `podman.img`), getrennte Default-Bridges,
getrennte iptables-Chain-Präfixe, getrennte Log-Verzeichnisse — keine gemeinsam
genutzten mutable Ressourcen außer read-only Kernel-Features (cgroups, netfilter).
- Ressourcen-Konkurrenz (CPU/RAM/Disk-IO) ist erwartet und wird nicht technisch
verhindert, aber in der GUI/Doku transparent gemacht.
- Beide Runtimes dürfen gleichzeitig laufen, ohne dass Start/Stop des einen den
anderen beeinflusst (insbesondere Firewall-Reloads, siehe Abschnitt 8).
### Phase 2+ — Docker optional deaktivierbar
- Neuer Schalter (z. B. in den bestehenden Docker-Settings oder einem neuen
„Container Engine“-Auswahlbereich): „Docker aktiviert / Podman aktiviert / beide“.
- Deaktivierung von Docker bedeutet: `rc.docker stop` + Entfernen des
Autostart-Hooks aus `/boot/config/go` für Docker, **kein** Deinstallieren des
Docker-Plugins selbst (reversibel, Datenerhalt).
- Voraussetzung für diese Phase: Podman-Pfad muss funktional äquivalent zu den von
Nutzern tatsächlich genutzten Docker-Features sein (mind. Custom Networks, Autostart,
Log-Viewer, Update-Check) — technisch als Gate, nicht nur als Empfehlung, im
Freigabeprozess verankern.
- CA-Kompatibilität (Community Applications erwartet aktuell Docker) ist der
wahrscheinlich größte Blocker für „Docker komplett aus“ und sollte als eigenes
Arbeitspaket (Template-Übersetzung Docker→Podman) vor Freigabe dieser Option
behandelt werden.
---
## 18. Zukünftige WebUI
### 18.1 Architektur (Zielbild, Umsetzung nach MVP)
- Eigene GUI-Seite unter `/usr/local/emhttp/plugins/podman/`, PHP + JS im
bestehenden Unraid-„Dynamix“-Stil (Konsistenz mit restlicher GUI, wiederverwendbare
CSS-/JS-Assets), analog zur Docker-Tab-Struktur (`dynamix.docker.manager`).
- Backend-Kommunikation **nicht** über Shell-Exec von `podman`-CLI-Aufrufen aus PHP
(fragil, schwer zu parallelisieren), sondern über den in Abschnitt 6.1 laufenden
Podman-API-Service (Unix-Socket, REST, Docker-kompatible Teilmenge der API) —
ermöglicht später auch Wiederverwendung bestehender Docker-API-kompatibler
Frontend-Bibliotheken.
- Echtzeit-Updates (Container-Status, Logs) über Server-Sent Events oder Polling
gegen den API-Service, kein WebSocket-Zwang in Phase 1 der WebUI (geringerer
Implementierungsaufwand, ausreichend für Status-Refresh im Sekundenbereich).
### 18.2 Funktionsumfang, gestaffelt
1. **Stufe 1**: Read-only Übersicht (laufende Container, Images, Netzwerke),
Start/Stop/Restart einzelner Container.
2. **Stufe 2**: Container-Erstellung über einfache Formulare (Image, Ports, Volumes,
Env, Labels), Autostart-Verwaltung direkt in GUI (schreibt in Abschnitt-12-Datei).
3. **Stufe 3**: Log-Viewer, Netzwerk-Verwaltung (Custom Networks/macvlan-Anlage),
Update-Check pro Container.
4. **Stufe 4**: Community-Applications-Anbindung/Template-Import (separates Vorhaben).
### 18.3 Sicherheitsaspekt der WebUI
- API-Service-Socket nur lokal (`/var/run/podman/podman.sock`), keine TCP-Exposition
nach außen ohne explizite, abgesicherte Opt-in-Konfiguration (TLS + Auth), da
rootful Podman-API faktisch Root-Äquivalent ist — gleiche Vorsicht wie beim
Docker-Socket.
---
## 19. Sicherheitsbetrachtungen (Phase 1, rootful)
- Rootful Podman-API-Socket ist funktional gleichwertig zu `docker.sock` bzgl.
Angriffsfläche (Zugriff = Root auf dem Host) — Berechtigungen auf den Socket
(`0660`, Gruppe analog `docker`-Gruppe, z. B. neue Gruppe `podman`) restriktiv setzen.
Achtung Docker Sock: root Äquivalenz, daher keine ungeprüfte Gruppenmitgliedschaft
für WebUI-Prozesse vergeben, die nicht ohnehin schon rootäquivalent laufen (`emhttpd`
läuft unter Unraid ohnehin als root, insofern kein zusätzliches Risiko gegenüber
Docker heute — aber explizit dokumentieren, nicht stillschweigend voraussetzen).
- Keine automatische, ungefragte Netzwerk-Exposition von Container-Ports über die
Standard-Bridge hinaus.
- Rollback-/Backup-Mechanismus darf keine Zugangsdaten (Registry-Logins in
`auth.json`) ungesichert auf dem FAT32-Flash ablegen, ohne dass sich der Nutzer
dessen bewusst ist (Flash ist physisch leicht auslesbar) — ggf. Hinweis, sensible
Registry-Credentials bevorzugt nicht dauerhaft dort zu speichern, oder Ablage
ausschließlich auf dem Cache-Pool statt `/boot`.
---
## 20. Phasenplan
| Phase | Inhalt |
|---|---|
| **MVP (Phase 1)** | Plugin-Grundgerüst, Pakete, `rc.podman`, Storage auf Cache-Pool, Autostart-Datei, Basis-Logging, Preflight/Fehlerbehandlung, Rollback für Pakete/Config, **keine WebUI** (CLI-only, `podman` direkt nutzbar) |
| **Phase 2** | WebUI Stufe 12, Custom-Networks-Verwaltung, Log-Viewer, Storage-Snapshot-Feature |
| **Phase 3** | Rootless-Option, Docker-Deaktivierungs-Schalter, CA-Template-Kompatibilitätsschicht |
| **Phase 4** | Pods/Quadlet-artige Konzepte (ohne systemd nur eingeschränkt sinnvoll — ggf. eigenes Skript-Äquivalent statt echter Quadlet/systemd-Generatoren) |
---
## 21. Offene Fragen / Risiken (zur Klärung vor Implementierungsstart)
1. Ziel-Podman-Version und Update-Kadenz (Tracking von Upstream-CVEs erfordert
Paket-Update-Prozess, nicht nur Erstrelease).
2. Cache-Pool-Pflicht vs. Array-Fallback für `podman.img` — Nutzer ohne Cache-Pool
sind bei Docker heute ebenfalls im Nachteil; gleiche Community-Erwartungshaltung
übernehmen oder verbessern?
2a. Verhalten bei mehreren Cache-Pools (Unraid 6.9+ unterstützt mehrere benannte
Pools) — welchen Pool per Default vorschlagen, wie in GUI wählbar machen.
3. `podman system service` als Dauerprozess vs. Socket-Activation-artiges
On-Demand-Start (ohne systemd nur eingeschränkt nachbaubar) — Dauerprozess ist
der pragmatischere Start, sollte aber gegen Ressourcenverbrauch im Idle
evaluiert werden.
4. Genaues Verhalten bei gleichzeitigem Boot von Docker und Podman bzgl.
iptables-Reihenfolge/Locking (`iptables`-Aufrufe sind nicht atomar über
mehrere Prozesse hinweg) — ggf. Locking-Strategie (`xtables_lock`) explizit prüfen.
5. Lizenz-/Signatur-Anforderungen für Community-Verbreitung des Plugins
(Unraid-Plugin-Repository-Richtlinien, Signierung der `.plg`/Pakete).
+30
View File
@@ -0,0 +1,30 @@
# FAQ
**Q: Does this replace Docker on Unraid?**
Not initially. Phase 1 runs Podman alongside Docker with fully separate storage,
networking, and firewall rules. A later phase adds an option to disable Docker
once feature parity is reached — see [ROADMAP.md](ROADMAP.md).
**Q: Is this rootless or rootful Podman?**
Phase 1 is rootful only, matching Docker's current trust model on Unraid.
Rootless is planned for a later phase — see
[ARCHITECTURE.md](ARCHITECTURE.md#0-ziele--nicht-ziele).
**Q: Will my existing Docker containers/appdata work with Podman?**
Bind-mounted appdata under `/mnt/user/appdata/...` can generally be reused, but
there is no automatic Docker→Podman template migration in Phase 1. See
[ARCHITECTURE.md, section 9 (Volumes)](ARCHITECTURE.md#9-volumes).
**Q: Why isn't there a WebUI yet?**
The MVP intentionally ships CLI-only to validate the persistence, packaging, and
init model first, before investing in GUI work. See
[ARCHITECTURE.md, section 18](ARCHITECTURE.md#18-zukünftige-webui).
**Q: Why does this need a cache pool / dedicated disk instead of just using
`/mnt/user`?**
Podman's `overlay` storage driver needs real filesystem semantics that the
`/mnt/user` FUSE layer (`shfs`) doesn't reliably provide. See
[ARCHITECTURE.md, section 4.3](ARCHITECTURE.md#43-persistente-nutzdaten-cache-pool-bevorzugt-array-als-fallback).
**Q: Is this affiliated with Unraid, Inc. or the Podman project?**
No. This is an independent, community-driven plugin project.
+39
View File
@@ -0,0 +1,39 @@
# Installation Guide (draft)
> This guide is a placeholder. Nothing described here is functional yet — the
> project is at the architecture/scaffolding stage. See
> [ARCHITECTURE.md](ARCHITECTURE.md) and [ROADMAP.md](ROADMAP.md).
## Planned installation flow
Once a release exists, installation will follow standard Unraid conventions:
1. In the Unraid WebUI, go to **Plugins → Install Plugin**.
2. Paste the `.plg` URL (or install via Community Applications, once listed).
3. Unraid downloads and runs `podman.plg`, which:
- installs the Slackware `.txz` packages under `packages/`,
- stages default configuration under `/boot/config/plugins/podman/`,
- registers the boot hook in `/boot/config/go`,
- starts the Podman service for the first time.
## Planned requirements
- Unraid version: TBD minimum (depends on kernel/cgroup v2 requirements — see
[ARCHITECTURE.md, section 5.3](ARCHITECTURE.md#53-abhängigkeitsprüfung)).
- A cache pool (recommended) or a dedicated array disk path for
`podman.img` — see [ARCHITECTURE.md, section 4.3](ARCHITECTURE.md#43-persistente-nutzdaten-cache-pool-bevorzugt-array-als-fallback).
- Sufficient free space on that pool/disk for the container storage image.
## Uninstallation (planned)
Removing the plugin removes the installed packages and init hooks but
**preserves** data under `/mnt/*/system/podman/` and
`/boot/config/plugins/podman/` by default. A separate, explicit option will be
provided for full data removal.
## Verifying the installation (planned)
```sh
rc.podman status
podman info
```
+100
View File
@@ -0,0 +1,100 @@
# Roadmap
This roadmap mirrors the phase plan in
[ARCHITECTURE.md](ARCHITECTURE.md#20-phasenplan). It will be kept in sync as
scope shifts; treat ARCHITECTURE.md as the source of truth for *why*, and this
file as the tracking view for *what/when*.
## Phase 1 — MVP (CLI core)
- [x] Repository scaffolding
- [x] Reproducible build system: `versions.env` pins, per-package
`SlackBuild` scripts, `scripts/build-packages.sh` orchestrator,
`scripts/checksums.sh`, `scripts/release.sh`,
`scripts/update-versions.sh`, GitHub Actions
(`build-packages.yml`, `release.yml`, `lint.yml`) — builds podman,
conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs, and the
plugin's own `unraid-podman` scaffolding package. Not yet run
end-to-end in a real Slackware environment (see note below).
- [x] `podman.plg` install/update/remove manifest — validated well-formed
(including full DTD entity expansion inside INLINE script blocks) and
release-round-tripped against `scripts/release.sh` with synthetic
build artifacts; not yet installed on a real Unraid system.
- [x] `rc.podman` init script (start/stop/restart/status)
- [x] Storage on cache pool (`podman.img`, XFS ftype=1, overlay driver) —
`podman-storage.sh`
- [x] Config sync (`/boot/config/plugins/podman/``/etc/containers/`) —
`podman-config.sh` (seed/sync logic sandbox-tested)
- [x] Autostart flat-file mechanism — `podman-autostart.sh`, including
per-container Safe-Mode after repeated failures (parsing logic
unit-tested)
- [x] Preflight checks & error handling (storage mount, disk space, kernel
reqs) — `podman-preflight.sh`
- [x] Basic logging — `podman_log`/`podman_log_error` in `podman-common.sh`;
log rotation still open (cron-based, see ARCHITECTURE.md section 15)
- [x] Package + config rollback (backup previous `.txz` / config snapshots)
`podman-backup.sh`; plus `podman-verify-packages.sh` and
`podman-update-packages.sh` for integrity checking and reconciliation
- [x] Official Unraid array-event hooks (`event/disks_mounted`,
`event/stopping`) instead of editing `/boot/config/go` — verified
against real-world plugin source (`unassigned.devices`)
- [ ] Real-world install/uninstall/update test on an actual (or virtualized)
Unraid system — everything above has been syntax-checked, unit-tested
in isolation, and XML-validated, but not run against real Unraid/
Podman/Slackware yet
## Phase 2 — WebUI & network management
Built ahead of the original phase order (see [webui/README.md](../webui/README.md)
and the mockup at `webui/mockups/prototype.html`, which was reviewed before
implementation started) — PHP/JS/AJAX, talking to `podman system service`
exclusively via `PodmanClient` (two documented, deliberate exceptions: the
Terminal panel's one-shot exec model, and the Compose panel's use of the
`podman compose` CLI, since libpod has no REST endpoint for either true PTY
sessions or Compose — see those ajax/*.php files' header comments).
- [x] WebUI stage 1: read-only overview (Dashboard), start/stop/restart/remove
for Containers/Pods, backend logic unit- and integration-tested against
a fake podman.sock server (real Unix socket, real cURL transport)
- [x] Images, Volumes, Networks panels (list + create/remove/pull) — not part
of the original "stage 2/3" split, built alongside stage 1 since the
backend pattern (PodmanClient + one ajax/*.php file) is identical
- [x] Autostart management in GUI (Settings panel — reorder/remove, writes
the same `/boot/config/plugins/podman/autostart` file
`podman-autostart.sh` reads)
- [x] Log viewer (Logs panel, polling-based "Follow")
- [x] Terminal panel — one-shot exec, not a true interactive PTY (see
`ajax/exec.php`); a real PTY would need a WebSocket bridge, tracked
as a follow-up, not implemented as a shell-out workaround
- [x] Compose panel — project list + YAML view + up/down/pull via the
`podman compose` CLI (the one deliberate CLI exception, see
`ajax/compose.php`)
- [ ] Container **creation** forms — the Containers panel manages the
lifecycle of existing containers only; there is no "new container"
form yet (the mockup shows one; the real implementation doesn't wire
it up)
- [ ] Custom network / macvlan creation UI beyond the basic name+subnet form
already in the Networks panel
- [ ] Real interactive PTY terminal (WebSocket bridge)
- [ ] Storage snapshot feature (reflink-based, BTRFS/ZFS cache pools)
- [ ] Run against a real Unraid + Podman install — the backend has been
tested against a hand-written fake podman.sock server (verifying the
transport layer, JSON shaping, and error handling genuinely work),
not against real libpod
## Phase 3 — Rootless & Docker-optional
- [ ] Rootless Podman option
- [ ] Docker enable/disable switch
- [ ] Community Applications template compatibility layer
## Phase 4 — Pods
- [ ] Pod support (Quadlet-equivalent without systemd — approach TBD, see
ARCHITECTURE.md open question)
## Open questions blocking later phases
See [ARCHITECTURE.md, section 21](ARCHITECTURE.md#21-offene-fragen--risiken) for
the current list (Podman version/update cadence, cache-pool requirement, service
vs. on-demand start, iptables locking with Docker, release signing).
+41
View File
@@ -0,0 +1,41 @@
# Troubleshooting (draft)
> Placeholder. Will be filled in as real failure modes are observed post-MVP.
> Cross-reference [ARCHITECTURE.md, section 16 (Fehlerbehandlung)](ARCHITECTURE.md#16-fehlerbehandlung)
> for the error-handling design these entries should map to.
## How to gather diagnostics
```sh
rc.podman status
podman info
podman system df
```
Relevant log locations:
- Container logs: `/mnt/*/system/podman/logs/containers/`
- Service log: `/mnt/*/system/podman/logs/podman-service.log`
- Unraid System Log: `Tools → System Log` in the WebUI
Please redact registry credentials/tokens before sharing logs in an issue.
## Known problem categories (to be populated)
### Storage
- `podman.img` full / near-full
- Loopback mount failures after unclean shutdown
### Networking
- Custom network / macvlan issues
- Conflicts with Docker's `iptables` chains
### Autostart
- Container stuck in a failed autostart loop
### Plugin update / rollback
- Config migration failures after an update
+59
View File
@@ -0,0 +1,59 @@
# packages/
One subdirectory per Slackware `.txz` package required by the plugin, built
against Unraid's Slackware base. See
[docs/ARCHITECTURE.md, section 5](../docs/ARCHITECTURE.md#5-paketmanagement)
for the full rationale (why these components, static linking preference,
version pinning, build strategy).
## Packages
| Directory | Upstream project | Purpose |
|---|---|---|
| `podman/` | [containers/podman](https://github.com/containers/podman) | Core container engine binary |
| `conmon/` | [containers/conmon](https://github.com/containers/conmon) | Container monitor process |
| `crun/` | [containers/crun](https://github.com/containers/crun) | OCI runtime (preferred over runc) |
| `netavark/` | [containers/netavark](https://github.com/containers/netavark) | Network backend |
| `aardvark-dns/` | [containers/aardvark-dns](https://github.com/containers/aardvark-dns) | DNS for container-to-container name resolution |
| `containers-common/` | [containers/common](https://github.com/containers/common) | Default config/seccomp/registries templates |
| `fuse-overlayfs/` | [containers/fuse-overlayfs](https://github.com/containers/fuse-overlayfs) | Fallback storage driver (rootless, Phase 2) |
| `passt/` | [passt.top](https://passt.top/passt/about/) (no GitHub mirror) | Rootless networking (Phase 2), successor to slirp4netns |
| `unraid-podman/` | this repository (not an upstream project) | Plugin's own scaffolding — rc.podman, sbin/ scripts, event/ hooks, config templates (see [its README](unraid-podman/README.md)) |
`podman`, `conmon`, `crun`, `netavark`, `aardvark-dns`, `passt`,
`fuse-overlayfs`, and `unraid-podman` are built and released automatically by
`.github/workflows/build-packages.yml`. `containers-common` currently only
supplies reference config (see [config/](../config)) and is not yet wired
into the automated build.
## Standard layout per package
Each package directory follows the same skeleton:
```
packages/<name>/
├── <name>.SlackBuild # Slackware build script (source download, build, packaging)
├── slack-desc # Slackware package description (max 70 cols / 11 lines)
├── patches/ # Optional patches applied during the build
└── README.md # Upstream version pinned, build notes, patch rationale
```
## Build pipeline
Packages are built via `scripts/build-packages.sh`, which runs each
package's `<name>.SlackBuild` in turn (see `scripts/lib/slackbuild-common.sh`
for the shared fetch/verify/package helpers they all use). This must run
inside a Slackware-compatible build environment — never on an arbitrary host
distro, to avoid glibc/ABI drift against Unraid's base.
`.github/workflows/build-packages.yml` is the CI entry point: it runs the
same script inside a pinned Slackware container
(`scripts/ci/setup-slackware-buildenv.sh` bootstraps any build-time
dependency the base image doesn't already ship) and uploads the results as a
workflow artifact — nothing built is ever committed to this repository.
Exact upstream versions and source checksums are pinned centrally in
[versions.env](../versions.env) (update via `scripts/update-versions.sh`,
never by hand) and, at release time, mirrored into the top-level
`plugin/podman.plg` manifest together with MD5 checksums by
`scripts/release.sh`, so installs are reproducible and rollback-able — see
[docs/ARCHITECTURE.md, section 14 (Rollback)](../docs/ARCHITECTURE.md#14-rollback).
+15
View File
@@ -0,0 +1,15 @@
# packages/aardvark-dns
Slackware package build recipe for **aardvark-dns**.
- Upstream: https://github.com/containers/aardvark-dns
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `aardvark-dns.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# =============================================================================
# packages/aardvark-dns/aardvark-dns.SlackBuild
#
# Builds the aardvark-dns .txz package from upstream source (Rust).
# aardvark-dns provides container-to-container DNS name resolution within
# netavark-managed networks (see docs/ARCHITECTURE.md section 8). Installed
# as a libexec helper binary, same rationale as netavark.
#
# Requires (in the build environment): rustc, cargo.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$AARDVARK_DNS_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "aardvark-dns"
tarball=$(sb_fetch_and_verify "$AARDVARK_DNS_SRC_URL" "$AARDVARK_DNS_SRC_SHA256" "aardvark-dns-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [aardvark-dns] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
echo "==> [aardvark-dns] cargo build --release"
cargo build --release --locked
echo "==> [aardvark-dns] install into \$PKG"
install -D -m 0755 target/release/aardvark-dns "$PKG/usr/libexec/podman/aardvark-dns"
sb_install_docs "$srcdir/LICENSE" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
aardvark-dns: aardvark-dns (TODO: one-line summary)
aardvark-dns:
aardvark-dns: TODO: two-to-three sentence description of aardvark-dns, its role in the
aardvark-dns: unraid-podman plugin, and a link to the upstream project.
aardvark-dns:
aardvark-dns: Homepage: https://github.com/containers/aardvark-dns
aardvark-dns:
aardvark-dns:
aardvark-dns:
aardvark-dns:
aardvark-dns:
+15
View File
@@ -0,0 +1,15 @@
# packages/conmon
Slackware package build recipe for **conmon**.
- Upstream: https://github.com/containers/conmon
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `conmon.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# =============================================================================
# packages/conmon/conmon.SlackBuild
#
# Builds the conmon .txz package from upstream source (C, glib-based).
# conmon is the per-container monitor process podman/crun rely on to keep a
# container's stdio and exit status attached even if podman itself restarts.
#
# Requires (in the build environment): gcc, make, pkg-config, glib2-dev,
# libseccomp-dev.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$CONMON_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "conmon"
tarball=$(sb_fetch_and_verify "$CONMON_SRC_URL" "$CONMON_SRC_SHA256" "conmon-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [conmon] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
echo "==> [conmon] make"
make PREFIX=/usr
echo "==> [conmon] make install into \$PKG"
make install PREFIX=/usr DESTDIR="$PKG"
sb_install_docs "$srcdir/LICENSE" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
View File
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
conmon: conmon (TODO: one-line summary)
conmon:
conmon: TODO: two-to-three sentence description of conmon, its role in the
conmon: unraid-podman plugin, and a link to the upstream project.
conmon:
conmon: Homepage: https://github.com/containers/conmon
conmon:
conmon:
conmon:
conmon:
conmon:
+15
View File
@@ -0,0 +1,15 @@
# packages/containers-common
Slackware package build recipe for **containers-common**.
- Upstream: https://github.com/containers/common
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `containers-common.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# SlackBuild for containers-common — unraid-podman
#
# Builds a Slackware-compatible .txz package for containers-common, pinned to a specific
# upstream release from https://github.com/containers/common.
# See docs/ARCHITECTURE.md section 5 (Paketmanagement) and packages/README.md.
#
# STATUS: placeholder — not yet implemented.
set -eu
PRGNAM=containers-common
VERSION=${VERSION:-0.0.0} # TODO: pin exact upstream version
ARCH=${ARCH:-x86_64}
BUILD=${BUILD:-1}
TAG=${TAG:-_unraidpodman}
CWD=$(pwd)
TMP=${TMP:-/tmp/SBo}
PKG=$TMP/package-$PRGNAM
OUTPUT=${OUTPUT:-/tmp}
# TODO:
# 1. Fetch source tarball for $PRGNAM $VERSION from
# https://github.com/containers/common
# 2. Apply patches from packages/containers-common/patches/, if any
# 3. Build (Go build for Go-based components, make for C-based components)
# 4. Install into $PKG following Slackware package filesystem layout
# 5. Copy packages/containers-common/slack-desc into $PKG/install/slack-desc
# 6. makepkg to produce $OUTPUT/$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.txz
echo "TODO: implement containers-common.SlackBuild"
exit 1
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
containers-common: containers-common (TODO: one-line summary)
containers-common:
containers-common: TODO: two-to-three sentence description of containers-common, its role in the
containers-common: unraid-podman plugin, and a link to the upstream project.
containers-common:
containers-common: Homepage: https://github.com/containers/common
containers-common:
containers-common:
containers-common:
containers-common:
containers-common:
+15
View File
@@ -0,0 +1,15 @@
# packages/crun
Slackware package build recipe for **crun**.
- Upstream: https://github.com/containers/crun
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `crun.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# =============================================================================
# packages/crun/crun.SlackBuild
#
# Builds the crun .txz package from upstream source (C, autotools). crun is
# the OCI runtime this project uses (preferred over runc — lighter, cgroup
# v2-friendly, see docs/ARCHITECTURE.md section 5.1).
#
# Requires (in the build environment): gcc, make, autoconf, automake,
# libtool, pkg-config, libcap-dev, libseccomp-dev, libyajl-dev, python3
# (used by crun's build-time code generator).
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$CRUN_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "crun"
tarball=$(sb_fetch_and_verify "$CRUN_SRC_URL" "$CRUN_SRC_SHA256" "crun-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [crun] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
# GitHub's source archive does not include vendored git submodules
# (libocispec) that crun's release tarballs normally bundle; init/update
# them explicitly.
if [ -f .gitmodules ]; then
echo "==> [crun] fetching submodules"
git -c protocol.file.allow=always submodule update --init --recursive || true
fi
echo "==> [crun] autogen + configure"
./autogen.sh
./configure --prefix=/usr --disable-systemd
echo "==> [crun] make"
make
echo "==> [crun] make install into \$PKG"
make install DESTDIR="$PKG"
sb_install_docs "$srcdir/COPYING" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
View File
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
crun: crun (TODO: one-line summary)
crun:
crun: TODO: two-to-three sentence description of crun, its role in the
crun: unraid-podman plugin, and a link to the upstream project.
crun:
crun: Homepage: https://github.com/containers/crun
crun:
crun:
crun:
crun:
crun:
+15
View File
@@ -0,0 +1,15 @@
# packages/fuse-overlayfs
Slackware package build recipe for **fuse-overlayfs**.
- Upstream: https://github.com/containers/fuse-overlayfs
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `fuse-overlayfs.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# =============================================================================
# packages/fuse-overlayfs/fuse-overlayfs.SlackBuild
#
# Builds the fuse-overlayfs .txz package from upstream source (C, autotools).
# Not used by the rootful storage path in Phase 1 (which uses the native
# overlay driver on the podman.img loopback filesystem — see
# docs/ARCHITECTURE.md section 10), but required ahead of time for the
# rootless Podman phase, where unprivileged overlay mounts generally aren't
# available (see docs/ARCHITECTURE.md section 0, "Nicht-Ziele" / ROADMAP.md
# Phase 3).
#
# Requires (in the build environment): gcc, make, autoconf, automake,
# libtool, pkg-config, libfuse3-dev.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$FUSE_OVERLAYFS_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "fuse-overlayfs"
tarball=$(sb_fetch_and_verify "$FUSE_OVERLAYFS_SRC_URL" "$FUSE_OVERLAYFS_SRC_SHA256" "fuse-overlayfs-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [fuse-overlayfs] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
echo "==> [fuse-overlayfs] autogen + configure"
./autogen.sh
./configure --prefix=/usr
echo "==> [fuse-overlayfs] make"
make
echo "==> [fuse-overlayfs] make install into \$PKG"
make install DESTDIR="$PKG"
sb_install_docs "$srcdir/COPYING" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
fuse-overlayfs: fuse-overlayfs (TODO: one-line summary)
fuse-overlayfs:
fuse-overlayfs: TODO: two-to-three sentence description of fuse-overlayfs, its role in the
fuse-overlayfs: unraid-podman plugin, and a link to the upstream project.
fuse-overlayfs:
fuse-overlayfs: Homepage: https://github.com/containers/fuse-overlayfs
fuse-overlayfs:
fuse-overlayfs:
fuse-overlayfs:
fuse-overlayfs:
fuse-overlayfs:
+15
View File
@@ -0,0 +1,15 @@
# packages/netavark
Slackware package build recipe for **netavark**.
- Upstream: https://github.com/containers/netavark
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `netavark.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# =============================================================================
# packages/netavark/netavark.SlackBuild
#
# Builds the netavark .txz package from upstream source (Rust). netavark is
# this project's container networking backend (see docs/ARCHITECTURE.md
# section 8) — it is installed as a libexec helper binary, not a $PATH tool,
# matching upstream's own layout so podman finds it via
# `netavark_binary_dir` in containers.conf.
#
# Requires (in the build environment): rustc, cargo (a recent stable
# toolchain — check upstream's Cargo.toml `rust-version` for the current
# minimum).
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$NETAVARK_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "netavark"
tarball=$(sb_fetch_and_verify "$NETAVARK_SRC_URL" "$NETAVARK_SRC_SHA256" "netavark-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [netavark] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
echo "==> [netavark] cargo build --release"
cargo build --release --locked
echo "==> [netavark] install into \$PKG"
install -D -m 0755 target/release/netavark "$PKG/usr/libexec/podman/netavark"
sb_install_docs "$srcdir/LICENSE" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
View File
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
netavark: netavark (TODO: one-line summary)
netavark:
netavark: TODO: two-to-three sentence description of netavark, its role in the
netavark: unraid-podman plugin, and a link to the upstream project.
netavark:
netavark: Homepage: https://github.com/containers/netavark
netavark:
netavark:
netavark:
netavark:
netavark:
+21
View File
@@ -0,0 +1,21 @@
# packages/passt
Slackware package build recipe for **passt** (and its `pasta` counterpart).
- Upstream: https://passt.top/passt/about/ (cgit, no GitHub mirror)
- Pinned commit: see `PASST_COMMIT` in [versions.env](../../versions.env)
- Patches: see `patches/` (currently none)
- Build entry point: `passt.SlackBuild`
## Notes
Unlike the other six packages, passt has no tagged releases or semver
versioning upstream — distributions package it straight from a pinned git
commit (this is also how Fedora/Debian build it). `scripts/update-versions.sh`
handles re-pinning to a newer commit and recomputing the snapshot checksum;
see [versions.env](../../versions.env) for the exact snapshot URL pattern.
Not required for the Phase 1 (rootful) MVP — passt/pasta become relevant once
rootless Podman support is added (see
[docs/ROADMAP.md](../../docs/ROADMAP.md), Phase 3). It is built now so the
package pipeline covers the full dependency set from day one.
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# =============================================================================
# packages/passt/passt.SlackBuild
#
# Builds the passt/pasta .txz package from upstream source (C, plain
# Makefile — no autotools). passt is Podman's modern unprivileged
# user-mode network transport for rootless containers, superseding
# slirp4netns (see versions.env for why this project pins it via a git
# commit snapshot rather than a tagged release: upstream has no GitHub
# mirror and does not use semver tags).
#
# Requires (in the build environment): gcc, make. No external library
# dependencies beyond libc — passt deliberately avoids them.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$PASST_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "passt"
tarball=$(sb_fetch_and_verify "$PASST_SRC_URL" "$PASST_SRC_SHA256" "passt-$PASST_COMMIT.tar.gz")
srcdir=$(sb_extract "$tarball")
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [passt] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
echo "==> [passt] make"
make
echo "==> [passt] make install into \$PKG"
make install prefix=/usr DESTDIR="$PKG"
sb_install_docs "$srcdir/LICENSES/GPL-2.0-or-later.txt" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
View File
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
passt: passt (unprivileged user-mode networking for VMs/containers)
passt:
passt: passt and pasta provide unprivileged (rootless) user-mode network
passt: connectivity for containers and VMs, without requiring a tap device
passt: or elevated capabilities. Podman uses it as the rootless network
passt: transport, superseding slirp4netns.
passt:
passt: Homepage: https://passt.top/passt/about/
passt:
passt:
passt:
+15
View File
@@ -0,0 +1,15 @@
# packages/podman
Slackware package build recipe for **podman**.
- Upstream: https://github.com/containers/podman
- Pinned version: TODO
- Patches: see `patches/` (currently none)
- Build entry point: `podman.SlackBuild` (placeholder, not yet implemented)
## Notes
TODO: document any Unraid/Slackware-specific build considerations (static
linking, cgroup v2 assumptions, kernel feature requirements) once the build
script is implemented. See
[docs/ARCHITECTURE.md, section 5.2 (Build-Strategie)](../../docs/ARCHITECTURE.md#52-build-strategie).
View File
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# =============================================================================
# packages/podman/podman.SlackBuild
#
# Builds the podman .txz package from upstream source (Go). Version and
# checksum are pinned centrally in versions.env — see
# scripts/lib/slackbuild-common.sh for the shared fetch/verify/package
# helpers used below, and docs/ARCHITECTURE.md section 5 for why the
# graphdriver build tags below are what they are (this project only ever
# uses the overlay storage driver, see docs/ARCHITECTURE.md section 10).
#
# Requires (in the build environment): go >= 1.22, make, gcc, pkg-config,
# libseccomp-dev. GPG signature verification support is intentionally left
# out to keep the dependency footprint minimal for a from-scratch Slackware
# build.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION="$PODMAN_VERSION"
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "podman"
tarball=$(sb_fetch_and_verify "$PODMAN_SRC_URL" "$PODMAN_SRC_SHA256" "podman-$VERSION.tar.gz")
srcdir=$(sb_extract "$tarball")
# Apply any local patches (none currently — see packages/podman/patches/).
for patch in "$CWD"/patches/*.patch; do
[ -e "$patch" ] || continue
echo "==> [podman] applying $(basename "$patch")"
patch -d "$srcdir" -p1 < "$patch"
done
cd "$srcdir"
# Build tags: exclude storage backends this project doesn't use (btrfs,
# devicemapper — see docs/ARCHITECTURE.md section 10, overlay-only), exclude
# systemd integration (Unraid has no systemd — see docs/ARCHITECTURE.md
# "Rahmenbedingungen"), keep seccomp support.
export BUILDTAGS="seccomp exclude_graphdriver_btrfs exclude_graphdriver_devicemapper containers_image_openpgp"
export CGO_ENABLED=1
export GOFLAGS="${GOFLAGS:--mod=mod}"
echo "==> [podman] make (BUILDTAGS=$BUILDTAGS)"
make BUILDTAGS="$BUILDTAGS" GO_BUILD_FLAGS="-ldflags -s"
echo "==> [podman] make install into \$PKG"
make install \
DESTDIR="$PKG" \
PREFIX=/usr \
ETCDIR="$PKG/etc" \
BUILDTAGS="$BUILDTAGS"
# We ship our own containers.conf/storage.conf/registries.conf/policy.json
# templates (see config/) staged via plugin/podman.plg instead of upstream's
# defaults, so drop the ones `make install` places under $PKG/etc to avoid
# ambiguity about which file is authoritative on a running system.
rm -rf "$PKG/etc/containers"
# No systemd units — Unraid has no systemd (see docs/ARCHITECTURE.md,
# "Rahmenbedingungen"); process lifecycle is entirely owned by rc.podman.
rm -rf "$PKG/usr/lib/systemd" "$PKG/lib/systemd"
sb_install_docs "$srcdir/LICENSE" "$srcdir/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
podman: podman (TODO: one-line summary)
podman:
podman: TODO: two-to-three sentence description of podman, its role in the
podman: unraid-podman plugin, and a link to the upstream project.
podman:
podman: Homepage: https://github.com/containers/podman
podman:
podman:
podman:
podman:
podman:
+16
View File
@@ -0,0 +1,16 @@
# packages/unraid-podman
Packaging recipe for the plugin's own scaffolding — **not** an upstream
component like the other seven package directories. See
`unraid-podman.SlackBuild`'s header comment for the full rationale.
Bundles:
- `plugin/rc.d/rc.podman``/etc/rc.d/rc.podman`
- `plugin/sbin/*.sh``/usr/local/sbin/`
- `plugin/event/*``/usr/local/emhttp/plugins/podman/event/` (official
Unraid array-event hooks — see those files' own header comments)
- `config/*.conf`, `config/podman.cfg.example``/usr/local/share/unraid-podman/templates/`
- `webui/plugins/podman/``/usr/local/emhttp/plugins/podman/` (once populated, see [docs/ROADMAP.md](../../docs/ROADMAP.md))
Version is read directly from `plugin/podman.plg`'s `<!ENTITY version>`
there's no separate version to keep in sync.
+19
View File
@@ -0,0 +1,19 @@
# HOW TO EDIT THIS FILE:
# The "handy ruler" below makes it easier to edit a package description.
# Line up the first '|' above the ':' following the base package name, and
# the '|' on the right side marks the last column you can put a character in.
# You must make exactly 11 lines for the formatting to be correct. It's also
# customary to leave one space after the ':' except on otherwise blank lines.
|-----handy-ruler------------------------------------------------|
unraid-podman: unraid-podman (plugin scaffolding: rc.podman, hooks, config)
unraid-podman:
unraid-podman: This package contains the unraid-podman plugin's own files —
unraid-podman: the rc.podman init script, helper scripts under
unraid-podman: /usr/local/sbin, the official Unraid array event hooks, and
unraid-podman: default configuration templates. It does not contain Podman
unraid-podman: itself (see the separate podman/conmon/crun/... packages).
unraid-podman:
unraid-podman: Homepage: https://github.com/OWNER/unraid-podman
unraid-podman:
unraid-podman:
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# =============================================================================
# packages/unraid-podman/unraid-podman.SlackBuild
#
# Unlike the other seven packages, this one does not compile anything from
# an external upstream source — it packages THIS repository's own plugin
# scaffolding (rc.podman, the sbin/ helper scripts, the official Unraid
# event/ hooks, and the default config templates) into a single .txz,
# exactly matching how real-world Unraid plugins bundle their own files
# (verified against the actual unassigned.devices.plg / package layout —
# see docs/ARCHITECTURE.md section 3.2 for the reference check that led to
# this design). plugin/podman.plg installs this alongside the seven
# compiled component packages, all via the same
# `upgradepkg --install-new --reinstall` mechanism.
#
# Version: taken directly from plugin/podman.plg's own <!ENTITY version>,
# so there is exactly one place a release's plugin version is defined —
# scripts/release.sh already updates that entity, and this script just
# reads it back rather than duplicating it in versions.env (which is
# reserved for pinning EXTERNAL upstream sources, see that file's header).
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/lib/slackbuild-common.sh"
# shellcheck source=/dev/null
. "$REPO_ROOT/versions.env"
VERSION=$(grep -oP '(?<=<!ENTITY version[[:space:]]{1,10}")[^"]+' "$REPO_ROOT/plugin/podman.plg" | head -n1)
if [ -z "$VERSION" ]; then
echo "!! Could not read <!ENTITY version> from $REPO_ROOT/plugin/podman.plg" >&2
exit 1
fi
ARCH="$PKG_ARCH"
BUILD="$PKG_BUILD"
TAG="$PKG_TAG"
sb_init "unraid-podman"
# --- Stage rc.podman -----------------------------------------------------
install -D -m 0755 "$REPO_ROOT/plugin/rc.d/rc.podman" "$PKG/etc/rc.d/rc.podman"
# --- Stage sbin helper scripts ---------------------------------------------
for f in "$REPO_ROOT"/plugin/sbin/*.sh; do
install -D -m 0755 "$f" "$PKG/usr/local/sbin/$(basename "$f")"
done
# --- Stage official Unraid plugin event hooks -------------------------------
# See plugin/event/*/'s own header comments: this is the officially
# documented mechanism (verified against unassigned.devices), NOT a
# /boot/config/go edit.
for f in "$REPO_ROOT"/plugin/event/*; do
install -D -m 0755 "$f" "$PKG/usr/local/emhttp/plugins/podman/event/$(basename "$f")"
done
# --- Stage default config templates -----------------------------------------
# Consumed by podman-config.sh's `seed` command at install/first-boot time —
# see that script's header comment for the seed-only-if-missing contract.
for f in containers.conf storage.conf registries.conf policy.json; do
install -D -m 0644 "$REPO_ROOT/config/$f" "$PKG/usr/local/share/unraid-podman/templates/$f"
done
install -D -m 0644 "$REPO_ROOT/config/podman.cfg.example" \
"$PKG/usr/local/share/unraid-podman/templates/podman.cfg.example"
# --- Stage WebUI files, if present ------------------------------------------
# webui/plugins/podman/ is populated starting in a later development phase
# (see docs/ROADMAP.md, Phase 2) — copy it in wholesale when it exists so
# this SlackBuild doesn't need to change again once it does.
if [ -d "$REPO_ROOT/webui/plugins/podman" ]; then
mkdir -p "$PKG/usr/local/emhttp/plugins/podman"
cp -a "$REPO_ROOT/webui/plugins/podman/." "$PKG/usr/local/emhttp/plugins/podman/"
# event/ was already staged above with correct permissions — avoid
# clobbering it with a possibly-non-executable copy from webui/ (which
# has no reason to contain its own event/ directory, but be defensive).
for f in "$REPO_ROOT"/plugin/event/*; do
install -D -m 0755 "$f" "$PKG/usr/local/emhttp/plugins/podman/event/$(basename "$f")"
done
fi
sb_install_docs "$REPO_ROOT/LICENSE" "$REPO_ROOT/README.md"
sb_make_package "$VERSION" "$ARCH" "$BUILD" "$TAG"
@@ -0,0 +1,27 @@
# plugin/boot-config/plugins/podman/
This directory is a staged mirror of what `/boot/config/plugins/podman/` should
look like immediately after a first install — see
[docs/ARCHITECTURE.md, section 4.1](../../../../docs/ARCHITECTURE.md#41-auf-dem-flash-device-boot-persistent-klein).
The `.plg` postinstall step (see `plugin/podman.plg`) copies these files to
`/boot/config/plugins/podman/` **only if they don't already exist**, so
re-running an install/update never clobbers a user's existing configuration.
Config file *content* templates (`containers.conf`, `storage.conf`,
`registries.conf`, `policy.json`) are sourced from the top-level [config/](../../../../config)
directory rather than duplicated here — this directory only defines the
directory skeleton and the plugin-specific settings files (`podman.cfg`,
`autostart`, ...) that have no equivalent elsewhere.
```
plugins/podman/
├── podman.cfg # copied from config/podman.cfg.example
├── autostart # empty by default, see docs/ARCHITECTURE.md section 12
├── autostart-delay # empty by default
├── networks/ # empty, populated as custom networks are created
├── backup/
│ ├── packages/ # empty, populated on first update
│ └── config/ # empty, populated on first update
└── plugin.log # empty, install/update log
```
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# =============================================================================
# plugin/event/disks_mounted
#
# Official Unraid plugin event hook — NOT a custom mechanism. Unraid's
# emhttpd fires every executable found at
# /usr/local/emhttp/plugins/<name>/event/<eventname> as each boot/array
# event happens (see e.g. the real-world unassigned.devices plugin, which
# uses this same convention for its own disks_mounted/started/stopping_svcs
# hooks). This project relies exclusively on this official mechanism —
# specifically NOT on hand-editing /boot/config/go, which is unnecessary
# and easy to get wrong across plugin updates/uninstalls.
#
# "disks_mounted" fires once array disks AND cache pools are mounted —
# exactly the precondition unraid-podman needs before it can create/mount
# podman.img on the configured cache pool/disk (see
# docs/ARCHITECTURE.md section 4.3 and section 6.2).
#
# Backgrounded (& disown) so array startup / the Main GUI page isn't
# 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.
# =============================================================================
/etc/rc.d/rc.podman start > /dev/null 2>&1 & disown
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# =============================================================================
# plugin/event/stopping
#
# Official Unraid plugin event hook (see plugin/event/disks_mounted for the
# full explanation of this mechanism). "stopping" is the FIRST event fired
# in Unraid's shutdown sequence — before stopping_docker, stopping_svcs,
# unmounting_disks, stopping_array — which gives unraid-podman the largest
# possible window to stop containers and unmount podman.img BEFORE Unraid
# unmounts the underlying cache pool/disk out from under it.
#
# Run synchronously (no backgrounding, unlike disks_mounted) — shutdown
# must actually wait for containers to stop and storage to unmount
# cleanly; racing the array-stop sequence here risks filesystem corruption
# on podman.img (see docs/ARCHITECTURE.md section 16.1 on why this project
# treats storage integrity conservatively).
# =============================================================================
/etc/rc.d/rc.podman stop
+365
View File
@@ -0,0 +1,365 @@
<?xml version="1.0" standalone="yes"?>
<!--
podman.plg — Unraid plugin manifest for unraid-podman.
This file is the single entry point Unraid's Plugin Manager uses to
install, update, and remove the plugin. Everything it does is built on
OFFICIAL Unraid plugin mechanisms only — verified directly against the
real source of two long-established, widely used Unraid plugins
(unraid/community.applications and unraid/unassigned.devices) rather than
guessed:
- A FILE block with a Name attribute, an upgradepkg Run command (using
its install new and reinstall flags together), and a URL/MD5 child
pair is the standard way to download and install a Slackware .txz
package, and the same command works for both a fresh install and an
update in one step (confirmed: both reference plugins use exactly
this pattern rather than separate installpkg/upgradepkg branches).
- Plugin uninstall logic lives in a <FILE Run="/bin/bash" Method="remove">
block — this exact `Method="remove"` attribute is what Unraid's
plugin manager looks for when the user clicks "Remove" (confirmed
against community.applications.plg).
- Boot/shutdown integration uses the official Unraid plugin EVENT
mechanism: any executable at
/usr/local/emhttp/plugins/<name>/event/<eventname> is run automatically
by emhttpd as the corresponding event fires (confirmed against
unassigned.devices, which uses this exact convention for its own
disks_mounted/started/stopping_svcs hooks). This project deliberately
does NOT hand-edit /boot/config/go — see plugin/event/disks_mounted
and plugin/event/stopping for the two hooks used here, and
docs/ARCHITECTURE.md section 6.2 for the full event-ordering rationale.
Structure of this file:
1. DOCTYPE entity block — plugin metadata + one version/file/md5 triple
per package (the seven upstream components plus this project's own
"unraid-podman" scaffolding package, see packages/unraid-podman/).
Entities are rewritten automatically by scripts/release.sh; never
hand-edit a *_txz_version/_file/_md5 entity — see that script.
2. <PLUGIN> body:
a. <CHANGES> — kept in sync with CHANGELOG.md by hand for now.
b. Pre-install architecture sanity check.
c. Eight <FILE> package install/update blocks.
d. Postinstall <FILE Run="/bin/bash"> — directory/config seeding,
install-manifest generation, first start.
e. <FILE Run="/bin/bash" Method="remove"> — uninstall.
-->
<!DOCTYPE PLUGIN [
<!ENTITY name "podman">
<!ENTITY author "unraid-podman contributors">
<!ENTITY version "0.0.0">
<!-- "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. -->
<!ENTITY launch "Podman">
<!ENTITY github "OWNER/unraid-podman">
<!ENTITY gitURL "https://raw.githubusercontent.com/&github;/main">
<!ENTITY pluginURL "&gitURL;/plugin/podman.plg">
<!ENTITY supportURL "https://github.com/&github;/discussions">
<!-- 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://github.com/&github;/releases/download/v&version;">
<!-- Slackware package naming components — must match versions.env's
PKG_ARCH/PKG_BUILD/PKG_TAG (see that file). Kept as entities here so
the eight removepkg calls in the Method="remove" block don't have to
repeat "x86_64-1_unraidpodman" eight times by hand. -->
<!ENTITY pkgArch "x86_64">
<!ENTITY pkgBuild "1">
<!ENTITY pkgTag "_unraidpodman">
<!-- One version/file/md5 entity triple per package, filled in by
scripts/release.sh from dist/*.txz.md5 at release time (see that
script's "Update per-package entities" step). Until a release has been
cut, these are placeholders and the FILE blocks below will correctly
fail to download anything — that is intentional; there is nothing to
install before the first tagged release. -->
<!ENTITY podman_txz_version "0.0.0">
<!ENTITY podman_txz_file "podman-&podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY podman_txz_md5 "00000000000000000000000000000000">
<!ENTITY conmon_txz_version "0.0.0">
<!ENTITY conmon_txz_file "conmon-&conmon_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY conmon_txz_md5 "00000000000000000000000000000000">
<!ENTITY crun_txz_version "0.0.0">
<!ENTITY crun_txz_file "crun-&crun_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY crun_txz_md5 "00000000000000000000000000000000">
<!ENTITY netavark_txz_version "0.0.0">
<!ENTITY netavark_txz_file "netavark-&netavark_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY netavark_txz_md5 "00000000000000000000000000000000">
<!ENTITY aardvark_dns_txz_version "0.0.0">
<!ENTITY aardvark_dns_txz_file "aardvark-dns-&aardvark_dns_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY aardvark_dns_txz_md5 "00000000000000000000000000000000">
<!ENTITY passt_txz_version "0.0.0">
<!ENTITY passt_txz_file "passt-&passt_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY passt_txz_md5 "00000000000000000000000000000000">
<!ENTITY fuse_overlayfs_txz_version "0.0.0">
<!ENTITY fuse_overlayfs_txz_file "fuse-overlayfs-&fuse_overlayfs_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY fuse_overlayfs_txz_md5 "00000000000000000000000000000000">
<!-- unraid-podman is this project's OWN scaffolding package (rc.podman,
sbin/ scripts, event/ hooks, config templates — see
packages/unraid-podman/README.md), not an upstream component. Its
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 "&version;">
<!ENTITY unraid_podman_txz_file "unraid-podman-&unraid_podman_txz_version;-&pkgArch;-&pkgBuild;&pkgTag;.txz">
<!ENTITY unraid_podman_txz_md5 "00000000000000000000000000000000">
]>
<PLUGIN name="&name;"
author="&author;"
version="&version;"
launch="&launch;"
pluginURL="&pluginURL;"
support="&supportURL;"
icon="cubes"
min="6.12.0">
<!--
min="6.12.0": Unraid 6.12 introduced folder-based (non-loopback) Docker
storage and is a reasonably conservative baseline for the cgroup v2 /
kernel networking features netavark, aardvark-dns, and passt assume. See
docs/ARCHITECTURE.md section 21 (open question — revisit if real-world
testing shows an earlier or later minimum is actually required).
-->
<CHANGES>
##podman
###0.0.0
- Initial scaffolding. Not a functional release — see CHANGELOG.md for the
authoritative, up-to-date history; this block is kept in sync with it by
hand at release time (scripts/release.sh does not touch this section).
</CHANGES>
<!--
Pre-install sanity check: this project only builds/ships x86_64 packages
(see versions.env's PKG_ARCH) — fail with a clear message on any other
architecture rather than letting eight package downloads 404 one by one.
-->
<FILE Run="/bin/bash">
<INLINE>
if [ "$(uname -m)" != "x86_64" ]; then
echo "unraid-podman only supports x86_64 (detected: $(uname -m)) - aborting install."
exit 1
fi
</INLINE>
</FILE>
<!--
The seven upstream component packages. Each is downloaded straight into
its backup slot under /boot/config/plugins/&name;/backup/packages/&version;/
(grouped by PLUGIN version, not each component's own version — a rollback
targets "this plugin release" as one unit, see
plugin/sbin/podman-backup.sh's header comment) and installed from there
in place via upgradepkg's install new / reinstall flags, which handle
both a fresh install and an update to a newer version identically (see
file header comment). This download-destination-doubles-as-backup approach is
what makes plugin/sbin/podman-backup.sh's restore-packages command work
with no separate copy step.
-->
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&podman_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&podman_txz_file;
</URL>
<MD5>
&podman_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&conmon_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&conmon_txz_file;
</URL>
<MD5>
&conmon_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&crun_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&crun_txz_file;
</URL>
<MD5>
&crun_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&netavark_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&netavark_txz_file;
</URL>
<MD5>
&netavark_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&aardvark_dns_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&aardvark_dns_txz_file;
</URL>
<MD5>
&aardvark_dns_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&passt_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&passt_txz_file;
</URL>
<MD5>
&passt_txz_md5;
</MD5>
</FILE>
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&fuse_overlayfs_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&fuse_overlayfs_txz_file;
</URL>
<MD5>
&fuse_overlayfs_txz_md5;
</MD5>
</FILE>
<!--
The plugin's own scaffolding package — rc.podman, sbin/ helper scripts,
the event/ hooks below, and default config templates. See
packages/unraid-podman/README.md.
-->
<FILE Name="/boot/config/plugins/&name;/backup/packages/&version;/&unraid_podman_txz_file;" Run="upgradepkg --install-new --reinstall">
<URL>
&baseURL;/&unraid_podman_txz_file;
</URL>
<MD5>
&unraid_podman_txz_md5;
</MD5>
</FILE>
<!--
Postinstall: everything that has to happen AFTER the eight packages above
are on disk, but isn't itself package content — directory/config
seeding, the install-manifest that plugin/sbin/podman-verify-packages.sh
and podman-update-packages.sh read, and the first start. Runs on both
fresh install and every update (Unraid re-runs the whole .plg either way)
— every step here is written to be idempotent, per
docs/ARCHITECTURE.md section 7.
-->
<!--
IMPORTANT for anyone editing this block: it is deliberately NOT wrapped
in <![CDATA[ ]]>. CDATA sections suppress XML entity expansion entirely,
which would leave literal, unexpanded text like "&version;" in the
generated installed-versions.env instead of the real version number —
confirmed by direct testing against this exact file. This is also why
real Unraid plugins (community.applications, unassigned.devices) never
wrap their entity-referencing INLINE scripts in CDATA either. The
practical consequence: avoid literal '<' in this script (heredocs
included) — a sequence of individual "echo >>" lines is used below
instead for exactly that reason. A lone '>' is fine unescaped in XML
content (only '<' and '&' are not) — verified empirically, not assumed.
-->
<FILE Run="/bin/bash">
<INLINE>
set -u
echo "Setting permissions..."
chmod 0755 /etc/rc.d/rc.podman
chmod 0755 /usr/local/sbin/podman-*.sh
chmod 0755 /usr/local/emhttp/plugins/podman/event/disks_mounted
chmod 0755 /usr/local/emhttp/plugins/podman/event/stopping
echo "Writing installed-package manifest..."
mkdir -p /usr/local/share/unraid-podman
MANIFEST=/usr/local/share/unraid-podman/installed-versions.env
echo "# Generated by podman.plg postinstall - do not edit by hand." > "$MANIFEST"
echo "# Read by podman-verify-packages.sh and podman-update-packages.sh." >> "$MANIFEST"
echo "PLUGIN_VERSION=\"&version;\"" >> "$MANIFEST"
echo "PODMAN_INSTALLED_VERSION=\"&podman_txz_version;\"" >> "$MANIFEST"
echo "CONMON_INSTALLED_VERSION=\"&conmon_txz_version;\"" >> "$MANIFEST"
echo "CRUN_INSTALLED_VERSION=\"&crun_txz_version;\"" >> "$MANIFEST"
echo "NETAVARK_INSTALLED_VERSION=\"&netavark_txz_version;\"" >> "$MANIFEST"
echo "AARDVARK_DNS_INSTALLED_VERSION=\"&aardvark_dns_txz_version;\"" >> "$MANIFEST"
echo "PASST_INSTALLED_VERSION=\"&passt_txz_version;\"" >> "$MANIFEST"
echo "FUSE_OVERLAYFS_INSTALLED_VERSION=\"&fuse_overlayfs_txz_version;\"" >> "$MANIFEST"
echo "UNRAID_PODMAN_INSTALLED_VERSION=\"&unraid_podman_txz_version;\"" >> "$MANIFEST"
echo "Seeding /boot/config/plugins/podman/ configuration (existing files left untouched)..."
/usr/local/sbin/podman-config.sh seed
echo "Verifying package installation..."
if /usr/local/sbin/podman-verify-packages.sh --quiet; then
echo "Package verification OK."
else
echo "WARNING: package verification reported problems - see /boot/config/plugins/podman/plugin.log"
fi
echo "Starting podman..."
if /etc/rc.d/rc.podman start; then
echo ""
echo "----------------------------------------------------"
echo " unraid-podman &version; has been installed and started."
echo " CLI: podman --url unix:///var/run/podman/podman.sock ..."
echo " Docs: https://github.com/&github;"
echo "----------------------------------------------------"
echo ""
else
echo ""
echo "----------------------------------------------------"
echo " unraid-podman &version; was installed but did not start cleanly."
echo " It will be retried automatically the next time the array starts"
echo " (see /usr/local/emhttp/plugins/podman/event/disks_mounted)."
echo " Check /boot/config/plugins/podman/plugin.log for details."
echo "----------------------------------------------------"
echo ""
fi
</INLINE>
</FILE>
<!--
Uninstall. Triggered by Unraid's Plugin Manager when the user clicks
"Remove" — the Method="remove" attribute is what makes this block
(rather than any of the install blocks above) run in that case.
Order matters: podman-uninstall-cleanup.sh runs FIRST, while the
unraid-podman package (which contains it, at /usr/local/sbin/) is still
on disk — removepkg-ing it out from under itself mid-script would be a
mistake. See that script's own header comment for exactly what is and
is not removed by default (user data is preserved unless the purge-data
flag is passed, which this normal removal flow deliberately never does).
-->
<!-- Not CDATA-wrapped — see the comment above the postinstall block for why: this content needs its &..._txz_file; entity references expanded, and CDATA would suppress that. Nothing here needs an unescaped '<'. -->
<FILE Run="/bin/bash" Method="remove">
<INLINE>
echo "Running unraid-podman cleanup..."
/usr/local/sbin/podman-uninstall-cleanup.sh
echo "Removing packages..."
removepkg &podman_txz_file;
removepkg &conmon_txz_file;
removepkg &crun_txz_file;
removepkg &netavark_txz_file;
removepkg &aardvark_dns_txz_file;
removepkg &passt_txz_file;
removepkg &fuse_overlayfs_txz_file;
removepkg &unraid_podman_txz_file;
echo ""
echo "unraid-podman removed. Your configuration, backups, and container"
echo "storage under /boot/config/plugins/podman/ and the configured"
echo "STORAGE_PATH were left in place - see docs/INSTALL.md if you want"
echo "to remove them too."
echo ""
</INLINE>
</FILE>
</PLUGIN>
+244
View File
@@ -0,0 +1,244 @@
#!/bin/bash
# =============================================================================
# plugin/rc.d/rc.podman
#
# Init script for unraid-podman, in the classic Slackware BSD
# start|stop|restart|status style — Unraid has no systemd, so this script
# alone owns the entire process lifecycle (see docs/ARCHITECTURE.md
# section 6, "Start-/Stop-Skripte", and the top-level "Rahmenbedingungen"
# table on why that's the case).
#
# Staged by plugin/podman.plg to /etc/rc.d/rc.podman. Its boot-time
# invocation is registered (also by podman.plg) in /boot/config/go, gated on
# Unraid's "array started" event rather than running unconditionally at
# boot — the configured storage path (cache pool/disk) is not guaranteed to
# be available before that point. See ARCHITECTURE.md section 6.2.
#
# The individual responsibilities below are deliberately split into small,
# single-purpose scripts under /usr/local/sbin/ (staged from plugin/sbin/)
# rather than inlined here — see each script's own header comment for why
# it's separate:
# podman-preflight.sh startup validation
# podman-config.sh config seeding + sync
# podman-storage.sh podman.img create/mount/unmount
# podman-autostart.sh autostart chain
# podman-backup.sh config/package snapshots for rollback
# podman-verify-packages.sh package integrity checks
# podman-update-packages.sh package reconciliation/update
# podman-uninstall-cleanup.sh called from podman.plg's removepkg block
# =============================================================================
set -u
SBIN_DIR="/usr/local/sbin"
# shellcheck source=../sbin/podman-common.sh
. "$SBIN_DIR/podman-common.sh"
podman_load_cfg
# -----------------------------------------------------------------------------
# podman_start
#
# See file header for the full sequence. Every step is expected to be
# idempotent (safe to re-run `rc.podman start` against an already-running
# instance without breaking anything) — preflight and podman-storage.sh
# both already implement this on their own, but we also short-circuit here
# if the service is clearly already up, to avoid doing redundant work.
# -----------------------------------------------------------------------------
podman_start() {
if [ "${PODMAN_ENABLED:-yes}" != "yes" ]; then
podman_log "start: PODMAN_ENABLED is not 'yes' in podman.cfg, not starting"
return 0
fi
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
podman_log "start: already running (pid $(cat "$PODMAN_SERVICE_PID_FILE"))"
return 0
fi
podman_log "start: beginning startup sequence"
# 1. Preflight — aborts loudly (and already notified) on failure.
if ! "$SBIN_DIR/podman-preflight.sh"; then
podman_log_error "start: preflight checks failed, aborting"
return 1
fi
# 2. Ensure config exists (no-op if already seeded) and sync it to the
# RAM-root /etc/containers/ — see podman-config.sh.
"$SBIN_DIR/podman-config.sh" seed
if ! "$SBIN_DIR/podman-config.sh" sync; then
podman_log_error "start: config sync failed, aborting"
return 1
fi
# 3. Storage: create the image if this is a first start, then mount it.
if ! "$SBIN_DIR/podman-storage.sh" create; then
podman_log_error "start: storage creation failed, aborting"
return 1
fi
if ! "$SBIN_DIR/podman-storage.sh" mount; then
podman_log_error "start: storage mount failed, aborting"
return 1
fi
# 4. Restore persisted netavark network definitions (see
# docs/ARCHITECTURE.md section 8, Netzwerke) from the boot-persistent
# copy into the RAM-root config directory netavark reads from.
mkdir -p "$PODMAN_ETC_DIR/networks"
if [ -d "$PODMAN_NETWORKS_BOOT_DIR" ] && [ -n "$(ls -A "$PODMAN_NETWORKS_BOOT_DIR" 2> /dev/null)" ]; then
cp -a "$PODMAN_NETWORKS_BOOT_DIR"/. "$PODMAN_ETC_DIR/networks/"
podman_log "start: restored custom network definitions"
fi
# 5. Start the Podman API service, rootful, on a unix socket — see
# docs/ARCHITECTURE.md section 6.1 for why this runs as a persistent
# service rather than being started fresh per CLI/WebUI call:
# a shared process gives consistent state and event streaming.
# --time=0 disables the idle-shutdown timeout (this is a long-running
# daemon under our process management, not an on-demand activation).
mkdir -p "$PODMAN_RUN_DIR"
podman_log "start: starting podman system service on unix://$PODMAN_SOCKET"
nohup podman system service --time=0 "unix://$PODMAN_SOCKET" \
> "$PODMAN_LOG_DIR/podman-service.log" 2>&1 &
local service_pid=$!
echo "$service_pid" > "$PODMAN_SERVICE_PID_FILE"
# Wait for the socket to actually appear rather than assuming the fork
# succeeded instantly — up to 15s, polled every 200ms.
local waited=0
while [ ! -S "$PODMAN_SOCKET" ] && [ "$waited" -lt 15000 ]; do
sleep 0.2
waited=$((waited + 200))
if ! kill -0 "$service_pid" 2> /dev/null; then
podman_log_error "start: podman system service exited immediately — see $PODMAN_LOG_DIR/podman-service.log"
"$SBIN_DIR/podman-storage.sh" unmount || true
podman_notify "Podman failed to start" \
"podman system service exited immediately. See $PODMAN_LOG_DIR/podman-service.log." \
"alert"
return 1
fi
done
if [ ! -S "$PODMAN_SOCKET" ]; then
podman_log_error "start: timed out waiting for $PODMAN_SOCKET to appear"
return 1
fi
podman_log "start: podman system service is up (pid $service_pid)"
# 6. Autostart. A failure here is logged/notified by the script itself
# and does not abort rc.podman start — the service is already usable.
"$SBIN_DIR/podman-autostart.sh" || podman_log_error "start: autostart chain reported errors (see above)"
echo "running" > "$PODMAN_STATUS_FILE"
podman_log "start: startup sequence complete"
return 0
}
# -----------------------------------------------------------------------------
# podman_stop
#
# Stops containers first (each with its own STOP_TIMEOUT grace period),
# then the API service, then unmounts storage — the reverse of start, so
# nothing is torn down out from under something still using it.
# -----------------------------------------------------------------------------
podman_stop() {
if [ ! -S "$PODMAN_SOCKET" ]; then
podman_log "stop: not running (no socket at $PODMAN_SOCKET)"
# Still attempt an unmount in case a prior stop was interrupted after
# the service went down but before the unmount completed.
"$SBIN_DIR/podman-storage.sh" unmount || true
rm -f "$PODMAN_STATUS_FILE"
return 0
fi
podman_log "stop: stopping running containers (timeout ${STOP_TIMEOUT:-10}s each)"
local running_ids
running_ids=$(podman --url "unix://$PODMAN_SOCKET" ps -q 2> /dev/null || true)
if [ -n "$running_ids" ]; then
local id
for id in $running_ids; do
podman --url "unix://$PODMAN_SOCKET" stop -t "${STOP_TIMEOUT:-10}" "$id" \
> /dev/null 2>&1 \
|| podman_log_error "stop: failed to stop container $id within timeout"
done
fi
podman_log "stop: stopping podman system service"
if [ -f "$PODMAN_SERVICE_PID_FILE" ]; then
local pid
pid=$(cat "$PODMAN_SERVICE_PID_FILE")
if kill -0 "$pid" 2> /dev/null; then
kill "$pid" 2> /dev/null || true
local waited=0
while kill -0 "$pid" 2> /dev/null && [ "$waited" -lt 10 ]; do
sleep 1
waited=$((waited + 1))
done
kill -0 "$pid" 2> /dev/null && kill -9 "$pid" 2> /dev/null || true
fi
rm -f "$PODMAN_SERVICE_PID_FILE"
fi
rm -f "$PODMAN_SOCKET"
"$SBIN_DIR/podman-storage.sh" unmount || podman_log_error "stop: storage unmount failed"
rm -f "$PODMAN_STATUS_FILE"
podman_log "stop: stopped"
return 0
}
# -----------------------------------------------------------------------------
# podman_status
#
# Human-readable health summary — see docs/ARCHITECTURE.md section 16.2.
# -----------------------------------------------------------------------------
podman_status() {
echo "PODMAN_ENABLED: ${PODMAN_ENABLED:-yes}"
if [ -S "$PODMAN_SOCKET" ] && [ -f "$PODMAN_SERVICE_PID_FILE" ] \
&& kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE" 2> /dev/null)" 2> /dev/null; then
echo "service: running (pid $(cat "$PODMAN_SERVICE_PID_FILE"), socket $PODMAN_SOCKET)"
else
echo "service: stopped"
fi
echo
"$SBIN_DIR/podman-storage.sh" status
echo
if [ -f "$PODMAN_AUTOSTART_FILE" ]; then
local count
count=$(grep -vcE '^\s*(#|$)' "$PODMAN_AUTOSTART_FILE" 2> /dev/null || echo 0)
echo "autostart entries: $count (see $PODMAN_AUTOSTART_FILE)"
fi
if [ -S "$PODMAN_SOCKET" ]; then
echo
echo "podman info:"
podman --url "unix://$PODMAN_SOCKET" info --format \
' containers: {{.Store.ContainerStore.Number}} images: {{.Store.ImageStore.Number}}' \
2> /dev/null || echo " (failed to query — service may still be initializing)"
fi
}
case "${1:-}" in
start)
podman_start
;;
stop)
podman_stop
;;
restart)
podman_stop
podman_start
;;
status)
podman_status
;;
*)
echo "usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-autostart.sh
#
# "Autostart einrichten" — starts containers listed in
# /boot/config/plugins/podman/autostart, in file order, via the running
# Podman API service. Run by `rc.podman start` after the service is up.
# See docs/ARCHITECTURE.md section 12 (Autostart).
#
# Design constraints from the architecture doc, both implemented below:
# - A single container failing to start must NOT abort the rest of the
# chain (one broken container shouldn't take down everything else).
# - After repeated consecutive failures, a container is automatically
# paused out of the autostart chain ("Safe-Mode pro Container") so a
# persistently crash-looping container doesn't waste boot time or
# resources forever — with a GUI notification explaining why.
#
# File formats:
# autostart one container name per line; '#' starts a comment;
# blank lines ignored. Order = start order.
# autostart-delay optional "<container-name>=<seconds>" lines — sleep
# that many seconds AFTER starting that container
# before moving to the next (for startup dependencies,
# e.g. a database before the app that needs it).
#
# Failure tracking: a small per-container counter file under
# $PODMAN_BOOT_DIR/autostart-failures/<name> holds the consecutive-failure
# count. Reset to 0 on any successful start.
# =============================================================================
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
podman_load_cfg
# After this many consecutive failed autostart attempts, a container is
# skipped and flagged rather than retried again on the next boot, until a
# human clears it (see clear_failure_flag below).
MAX_CONSECUTIVE_FAILURES=3
FAILURES_DIR="$PODMAN_BOOT_DIR/autostart-failures"
mkdir -p "$FAILURES_DIR"
# -----------------------------------------------------------------------------
# get_delay <container-name>
#
# Looks up an optional post-start delay for a container from
# autostart-delay ("name=seconds" lines). Returns 0 if not configured.
# -----------------------------------------------------------------------------
get_delay() {
local name="$1"
if [ -f "$PODMAN_AUTOSTART_DELAY_FILE" ]; then
awk -F= -v n="$name" '$1 == n { print $2; found=1 } END { if (!found) print 0 }' \
"$PODMAN_AUTOSTART_DELAY_FILE"
else
echo 0
fi
}
failure_count() {
local name="$1"
local f="$FAILURES_DIR/$name"
[ -f "$f" ] && cat "$f" || echo 0
}
record_failure() {
local name="$1"
local count
count=$(($(failure_count "$name") + 1))
echo "$count" > "$FAILURES_DIR/$name"
echo "$count"
}
clear_failure_flag() {
local name="$1"
rm -f "$FAILURES_DIR/$name"
}
# -----------------------------------------------------------------------------
# start_one <container-name>
#
# Starts a single container via `podman start`, which talks to the already
# running API service over $PODMAN_SOCKET rather than spawning an unrelated
# podman process tree — see docs/ARCHITECTURE.md section 11 (Container).
# -----------------------------------------------------------------------------
start_one() {
local name="$1"
local prior_failures
prior_failures=$(failure_count "$name")
if [ "$prior_failures" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
podman_log_error "autostart: skipping '$name' — $prior_failures consecutive prior failures (Safe-Mode). Remove $FAILURES_DIR/$name to re-enable."
return 1
fi
podman_log "autostart: starting '$name'"
if podman --url "unix://$PODMAN_SOCKET" start "$name" > /tmp/podman-autostart-"$name".log 2>&1; then
clear_failure_flag "$name"
podman_log "autostart: '$name' started successfully"
return 0
fi
local new_count
new_count=$(record_failure "$name")
podman_log_error "autostart: '$name' failed to start (attempt $new_count/$MAX_CONSECUTIVE_FAILURES) — see /tmp/podman-autostart-$name.log"
if [ "$new_count" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
podman_notify "Podman container disabled from autostart" \
"'$name' failed to start $new_count times in a row and has been paused from autostart. Fix the underlying issue, then remove $FAILURES_DIR/$name to re-enable." \
"warning"
fi
return 1
}
# -----------------------------------------------------------------------------
# main
# -----------------------------------------------------------------------------
if [ ! -f "$PODMAN_AUTOSTART_FILE" ]; then
podman_log "autostart: no autostart file at $PODMAN_AUTOSTART_FILE, nothing to do"
exit 0
fi
if [ ! -S "$PODMAN_SOCKET" ]; then
podman_log_error "autostart: podman API socket ($PODMAN_SOCKET) not present — is the service running?"
exit 1
fi
started=0
skipped=0
failed=0
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
# Strip comments and surrounding whitespace; skip blank lines.
line="${raw_line%%#*}"
line="$(echo "$line" | xargs || true)"
[ -z "$line" ] && continue
if start_one "$line"; then
started=$((started + 1))
else
prior=$(failure_count "$line")
if [ "$prior" -ge "$MAX_CONSECUTIVE_FAILURES" ]; then
skipped=$((skipped + 1))
else
failed=$((failed + 1))
fi
# A single container's failure does not abort the loop — see file
# header. Continue to the next entry.
continue
fi
delay=$(get_delay "$line")
if [ "$delay" -gt 0 ] 2> /dev/null; then
podman_log "autostart: waiting ${delay}s after '$line' (configured dependency delay)"
sleep "$delay"
fi
done < "$PODMAN_AUTOSTART_FILE"
podman_log "autostart: complete (started=$started failed=$failed skipped-safe-mode=$skipped)"
# Exit non-zero only if EVERY entry failed outright (as opposed to a mix,
# or entries already in Safe-Mode) — rc.podman treats that as worth
# flagging loudly, whereas a partial failure is already individually
# notified above.
if [ "$started" -eq 0 ] && [ "$failed" -gt 0 ]; then
exit 1
fi
exit 0
+168
View File
@@ -0,0 +1,168 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-backup.sh
#
# Rollback support — see docs/ARCHITECTURE.md section 14 (Rollback). Two
# independent kinds of backup, matching the two kinds of thing an update can
# break:
#
# PACKAGES plugin/podman.plg downloads each release's .txz files
# straight into $PODMAN_BOOT_DIR/backup/packages/<plugin-version>/
# — all 7 component packages of one plugin release share a
# single directory named after the PLUGIN version (not each
# component's own version), since a rollback means "go back to
# plugin release X" as one unit — rather than into a scratch
# dir that gets discarded, so the previously installed
# version's packages are automatically still on disk after an
# update, with no separate copy step. `restore-packages
# <plugin-version>` reinstalls every package from one of those
# directories.
#
# CONFIG Before a config schema migration (see podman.cfg's
# CONFIG_SCHEMA_VERSION), the plg's postinstall calls
# `snapshot-config` to copy the current *.conf + podman.cfg
# into $PODMAN_BOOT_DIR/backup/config/<timestamp>/.
# `restore-config <timestamp>` copies them back.
#
# `prune` bounds how many old snapshots of each kind are kept, since flash
# space is limited (see ARCHITECTURE.md section 14, "Rollback-Historie
# begrenzen").
#
# Usage:
# podman-backup.sh snapshot-config
# podman-backup.sh restore-config <timestamp>
# podman-backup.sh restore-packages <version>
# podman-backup.sh list
# podman-backup.sh prune [--keep N]
# =============================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
DEFAULT_KEEP=3
cmd_snapshot_config() {
local ts
ts=$(date -u +%Y%m%dT%H%M%SZ)
local dest="$PODMAN_BACKUP_DIR/config/$ts"
mkdir -p "$dest"
local f
for f in podman.cfg containers.conf storage.conf registries.conf policy.json; do
[ -f "$PODMAN_BOOT_DIR/$f" ] && cp "$PODMAN_BOOT_DIR/$f" "$dest/"
done
podman_log "backup: config snapshot saved to $dest"
echo "$ts"
}
cmd_restore_config() {
local ts="${1:?usage: podman-backup.sh restore-config <timestamp>}"
local src="$PODMAN_BACKUP_DIR/config/$ts"
if [ ! -d "$src" ]; then
podman_log_error "backup: no config snapshot found at $src"
exit 1
fi
local f
for f in "$src"/*; do
[ -f "$f" ] || continue
cp "$f" "$PODMAN_BOOT_DIR/$(basename "$f")"
done
podman_log "backup: config restored from snapshot $ts — restart podman (rc.podman restart) to apply"
}
cmd_restore_packages() {
local version="${1:?usage: podman-backup.sh restore-packages <version>}"
local src="$PODMAN_BACKUP_DIR/packages/$version"
if [ ! -d "$src" ]; then
podman_log_error "backup: no package backup found for version $version at $src"
exit 1
fi
podman_require_command upgradepkg
local txz
local restored=0
for txz in "$src"/*.txz; do
[ -f "$txz" ] || continue
podman_log "backup: reinstalling $(basename "$txz") from backup (version $version)"
upgradepkg --reinstall --install-new "$txz"
restored=$((restored + 1))
done
if [ "$restored" -eq 0 ]; then
podman_log_error "backup: no .txz files found in $src"
exit 1
fi
podman_notify "Podman packages rolled back" \
"Reinstalled $restored package(s) from the version $version backup. Run 'rc.podman restart' to apply." \
"warning"
podman_log "backup: package rollback to version $version complete ($restored package(s))"
}
cmd_list() {
echo "Config snapshots ($PODMAN_BACKUP_DIR/config/):"
if [ -d "$PODMAN_BACKUP_DIR/config" ]; then
find "$PODMAN_BACKUP_DIR/config" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' 2> /dev/null | sort -r
fi
echo
echo "Package backups ($PODMAN_BACKUP_DIR/packages/):"
if [ -d "$PODMAN_BACKUP_DIR/packages" ]; then
find "$PODMAN_BACKUP_DIR/packages" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' 2> /dev/null | sort -r
fi
}
cmd_prune() {
local keep="$DEFAULT_KEEP"
if [ "${1:-}" = "--keep" ] && [ -n "${2:-}" ]; then
keep="$2"
fi
local kind
for kind in config packages; do
local dir="$PODMAN_BACKUP_DIR/$kind"
[ -d "$dir" ] || continue
# Sort newest-first by directory name (timestamps and semver both sort
# correctly lexicographically here — timestamps are zero-padded ISO8601,
# versions are compared via `sort -V`), then remove everything beyond
# $keep.
local sorted
if [ "$kind" = "config" ]; then
sorted=$(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -r)
else
sorted=$(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -rV)
fi
local i=0
local name
while IFS= read -r name; do
[ -z "$name" ] && continue
i=$((i + 1))
if [ "$i" -gt "$keep" ]; then
podman_log "backup: pruning old $kind backup: $name"
rm -rf "${dir:?}/$name"
fi
done <<< "$sorted"
done
}
case "${1:-}" in
snapshot-config) cmd_snapshot_config ;;
restore-config) shift; cmd_restore_config "$@" ;;
restore-packages) shift; cmd_restore_packages "$@" ;;
list) cmd_list ;;
prune) shift; cmd_prune "$@" ;;
*)
echo "usage: $0 {snapshot-config|restore-config <ts>|restore-packages <version>|list|prune [--keep N]}" >&2
exit 1
;;
esac
+173
View File
@@ -0,0 +1,173 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-common.sh
#
# Shared constants and helper functions sourced by every other script under
# plugin/sbin/ and by plugin/rc.d/rc.podman. Centralizing these here means:
# - every script agrees on the same paths (no risk of one script writing
# config to a path another script reads from a slightly different one),
# - logging/notification behavior is consistent everywhere,
# - each individual script stays focused on its one job instead of
# re-implementing "how do I log this" or "where is podman.cfg".
#
# This file is NOT meant to be executed directly — it only defines
# functions/variables for other scripts to source:
# . "$(dirname "${BASH_SOURCE[0]}")/podman-common.sh"
#
# See docs/ARCHITECTURE.md sections 4 (Verzeichnislayout), 7 (Persistenz),
# 15 (Logging), 16 (Fehlerbehandlung) for the design this implements.
# =============================================================================
# We deliberately do NOT `set -e` in this file: it is sourced by scripts
# that set their own error-handling mode, and a library changing its
# caller's shell options would be a surprising action at a distance.
# -----------------------------------------------------------------------------
# Path constants.
#
# PODMAN_BOOT_DIR is the persistent, source-of-truth configuration directory
# on the Unraid flash device (survives reboots — see ARCHITECTURE.md 4.1).
# Everything under /etc, /var, /usr is RAM-root and rebuilt from here (or
# from the array/cache-backed storage dir) on every boot.
# -----------------------------------------------------------------------------
PODMAN_BOOT_DIR="/boot/config/plugins/podman"
PODMAN_CFG_FILE="$PODMAN_BOOT_DIR/podman.cfg"
PODMAN_AUTOSTART_FILE="$PODMAN_BOOT_DIR/autostart"
PODMAN_AUTOSTART_DELAY_FILE="$PODMAN_BOOT_DIR/autostart-delay"
PODMAN_NETWORKS_BOOT_DIR="$PODMAN_BOOT_DIR/networks"
PODMAN_BACKUP_DIR="$PODMAN_BOOT_DIR/backup"
PODMAN_PLUGIN_LOG="$PODMAN_BOOT_DIR/plugin.log"
# Runtime (RAM-root) locations rebuilt/synced on every rc.podman start.
PODMAN_ETC_DIR="/etc/containers"
PODMAN_RUN_DIR="/var/run/podman"
PODMAN_SOCKET="$PODMAN_RUN_DIR/podman.sock"
PODMAN_STATUS_FILE="$PODMAN_RUN_DIR/rc.podman.status"
PODMAN_SERVICE_PID_FILE="$PODMAN_RUN_DIR/podman-service.pid"
# -----------------------------------------------------------------------------
# podman_load_cfg
#
# Sources /boot/config/plugins/podman/podman.cfg (the user-editable settings
# file, see config/podman.cfg.example) and applies safe defaults for any
# setting that file doesn't define — so every other script can simply
# reference $STORAGE_PATH, $PODMAN_ENABLED, etc. after calling this, without
# each script having its own copy of the defaults.
# -----------------------------------------------------------------------------
podman_load_cfg() {
# Defaults, applied BEFORE sourcing podman.cfg so the file only needs to
# override what the user actually wants to change.
STORAGE_PATH="/mnt/cache/system/podman"
STORAGE_IMAGE_SIZE_GB="20"
PODMAN_ENABLED="yes"
STOP_TIMEOUT="10"
CONFIG_SCHEMA_VERSION="1"
if [ -f "$PODMAN_CFG_FILE" ]; then
# shellcheck source=/dev/null
. "$PODMAN_CFG_FILE"
fi
# Values derived from STORAGE_PATH, computed after sourcing so a custom
# STORAGE_PATH is honored.
PODMAN_STORAGE_IMAGE="$STORAGE_PATH/podman.img"
PODMAN_LOG_DIR="$STORAGE_PATH/logs"
PODMAN_GRAPHROOT="/var/lib/containers/storage"
}
# -----------------------------------------------------------------------------
# podman_log <message>
#
# Writes a timestamped line to both stdout (so it shows up in `rc.podman`
# invocations and CI/manual runs) and to the persistent plugin log on flash
# (so it survives a reboot — see ARCHITECTURE.md section 15, Logging). Kept
# deliberately terse (no log levels/rotation logic here) — this is for
# install/lifecycle events, not container output, which lives under
# $PODMAN_LOG_DIR instead.
# -----------------------------------------------------------------------------
podman_log() {
local msg="$1"
local line
line="$(date -u +'%Y-%m-%dT%H:%M:%SZ') [podman] $msg"
echo "$line"
# Best-effort: the boot directory should always exist post-install, but
# never let logging itself fail a caller that has `set -e`.
mkdir -p "$PODMAN_BOOT_DIR" 2> /dev/null || true
echo "$line" >> "$PODMAN_PLUGIN_LOG" 2> /dev/null || true
}
# -----------------------------------------------------------------------------
# podman_log_error <message>
#
# Like podman_log, but also mirrors the message to the system log via
# `logger`, so it is visible in Unraid's Tools -> System Log GUI without the
# user having to know where the plugin's own log lives. Reserved for
# conditions the user should actually notice (see ARCHITECTURE.md 16.2).
# -----------------------------------------------------------------------------
podman_log_error() {
local msg="$1"
podman_log "ERROR: $msg"
if command -v logger > /dev/null 2>&1; then
logger -t podman-plugin "$msg"
fi
}
# -----------------------------------------------------------------------------
# podman_notify <subject> <description> [importance]
#
# Surfaces a message via Unraid's own GUI notification system
# (/usr/local/emhttp/webGui/scripts/notify) instead of inventing a parallel
# notification mechanism — see ARCHITECTURE.md section 16.2. Silently
# no-ops if that script isn't present (e.g. when running outside a real
# Unraid system, such as in CI/lint contexts).
#
# importance: "normal" (default), "warning", or "alert".
# -----------------------------------------------------------------------------
podman_notify() {
local subject="$1"
local description="$2"
local importance="${3:-normal}"
local notify_bin="/usr/local/emhttp/webGui/scripts/notify"
podman_log "notify [$importance] $subject: $description"
if [ -x "$notify_bin" ]; then
"$notify_bin" -e "unraid-podman" -s "$subject" -d "$description" -i "$importance" \
> /dev/null 2>&1 || true
fi
}
# -----------------------------------------------------------------------------
# podman_storage_path_is_safe
#
# Refuses storage paths under /mnt/user (FUSE/shfs) — see
# ARCHITECTURE.md section 4.3 for why the overlay storage driver must live
# on a real mounted filesystem (cache pool or a specific disk), not shfs.
# Returns 0 (safe) or 1 (unsafe) and prints a reason on failure.
# -----------------------------------------------------------------------------
podman_storage_path_is_safe() {
local path="$1"
case "$path" in
/mnt/user/*|/mnt/user)
echo "STORAGE_PATH ($path) is under /mnt/user (FUSE/shfs)." >&2
echo "The overlay storage driver needs a real mounted filesystem —" >&2
echo "use a cache pool or a specific disk path instead, e.g. /mnt/cache/system/podman." >&2
return 1
;;
esac
return 0
}
# -----------------------------------------------------------------------------
# podman_require_command <binary>
#
# Fails loudly (rather than letting a script continue and fail confusingly
# three steps later) if a required binary isn't on PATH.
# -----------------------------------------------------------------------------
podman_require_command() {
local bin="$1"
if ! command -v "$bin" > /dev/null 2>&1; then
podman_log_error "required command not found: $bin"
return 1
fi
}
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-config.sh
#
# "Konfiguration anlegen" — creates and synchronizes unraid-podman's
# configuration. Has two responsibilities, run as two subcommands:
#
# podman-config.sh seed
# First-install (or "file went missing") step: ensures
# /boot/config/plugins/podman/ exists with its full directory skeleton,
# and copies default config templates into it — but ONLY for files that
# don't already exist. This is what makes plugin updates safe: an
# existing user configuration is never overwritten (see
# docs/ARCHITECTURE.md section 3.2 and section 7).
#
# podman-config.sh sync
# Boot-time step: copies the current, authoritative config from
# /boot/config/plugins/podman/*.conf into /etc/containers/ (which lives
# on Unraid's RAM-root and is empty again after every reboot). Run by
# rc.podman on every `start`.
#
# Template source: the plugin ships its default config templates (from this
# repo's config/) to /usr/local/share/unraid-podman/templates/ at install
# time (see plugin/podman.plg) — that is what `seed` copies FROM, and
# /boot/config/plugins/podman/ is what it copies TO.
#
# Usage:
# podman-config.sh seed
# podman-config.sh sync
# =============================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
TEMPLATES_DIR="/usr/local/share/unraid-podman/templates"
# -----------------------------------------------------------------------------
# seed_file <template-filename> <dest-path>
#
# Copies a template into place only if the destination doesn't already
# exist. This single rule is what protects user customizations across
# plugin updates — see docs/ARCHITECTURE.md section 7, "Persistenz-Strategie".
# -----------------------------------------------------------------------------
seed_file() {
local template_name="$1"
local dest_path="$2"
local src_path="$TEMPLATES_DIR/$template_name"
if [ -f "$dest_path" ]; then
podman_log "config: $dest_path already exists, leaving untouched"
return 0
fi
if [ ! -f "$src_path" ]; then
podman_log_error "config: template missing: $src_path (plugin install incomplete?)"
return 1
fi
mkdir -p "$(dirname "$dest_path")"
cp "$src_path" "$dest_path"
podman_log "config: seeded $dest_path from template $template_name"
}
cmd_seed() {
podman_log "config: seeding /boot/config/plugins/podman/ (first install or repair)"
# Full directory skeleton — see docs/ARCHITECTURE.md section 4.1 and
# plugin/boot-config/plugins/podman/README.md for the reference layout.
mkdir -p \
"$PODMAN_BOOT_DIR" \
"$PODMAN_NETWORKS_BOOT_DIR" \
"$PODMAN_BACKUP_DIR/packages" \
"$PODMAN_BACKUP_DIR/config"
seed_file "podman.cfg.example" "$PODMAN_CFG_FILE"
seed_file "containers.conf" "$PODMAN_BOOT_DIR/containers.conf"
seed_file "storage.conf" "$PODMAN_BOOT_DIR/storage.conf"
seed_file "registries.conf" "$PODMAN_BOOT_DIR/registries.conf"
seed_file "policy.json" "$PODMAN_BOOT_DIR/policy.json"
# Autostart files: empty by default, `touch` is the "seed" here (no
# template content to copy — see docs/ARCHITECTURE.md section 12).
[ -f "$PODMAN_AUTOSTART_FILE" ] || { touch "$PODMAN_AUTOSTART_FILE"; podman_log "config: created empty $PODMAN_AUTOSTART_FILE"; }
[ -f "$PODMAN_AUTOSTART_DELAY_FILE" ] || { touch "$PODMAN_AUTOSTART_DELAY_FILE"; podman_log "config: created empty $PODMAN_AUTOSTART_DELAY_FILE"; }
[ -f "$PODMAN_PLUGIN_LOG" ] || touch "$PODMAN_PLUGIN_LOG"
podman_log "config: seeding complete"
}
cmd_sync() {
podman_load_cfg
if [ ! -d "$PODMAN_BOOT_DIR" ]; then
podman_log_error "config: $PODMAN_BOOT_DIR does not exist — run 'podman-config.sh seed' first"
return 1
fi
podman_log "config: syncing $PODMAN_BOOT_DIR/*.conf -> $PODMAN_ETC_DIR/"
mkdir -p "$PODMAN_ETC_DIR"
local any_missing=0
for f in containers.conf storage.conf registries.conf policy.json; do
if [ ! -f "$PODMAN_BOOT_DIR/$f" ]; then
podman_log_error "config: missing $PODMAN_BOOT_DIR/$f (run 'podman-config.sh seed')"
any_missing=1
continue
fi
cp "$PODMAN_BOOT_DIR/$f" "$PODMAN_ETC_DIR/$f"
done
if [ "$any_missing" -ne 0 ]; then
return 1
fi
# storage.conf's graphroot/runroot are environment-specific (depend on
# STORAGE_PATH from podman.cfg), so they are appended here at sync time
# rather than hardcoded in the template — see config/storage.conf's own
# comment on this split.
{
echo ""
echo "# --- appended at boot by podman-config.sh sync, from podman.cfg ---"
echo "[storage]"
echo "driver = \"overlay\""
echo "graphroot = \"$PODMAN_GRAPHROOT\""
echo "runroot = \"/var/run/containers/storage\""
} >> "$PODMAN_ETC_DIR/storage.conf"
podman_log "config: sync complete"
}
case "${1:-}" in
seed) cmd_seed ;;
sync) cmd_sync ;;
*)
echo "usage: $0 {seed|sync}" >&2
exit 1
;;
esac
+150
View File
@@ -0,0 +1,150 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-preflight.sh
#
# Startup validation, run by `rc.podman start` BEFORE anything is mounted or
# started. The goal is to fail fast with one clear message, instead of
# letting `podman system service` fail three steps later with a cryptic
# error. See docs/ARCHITECTURE.md section 16.1 (Startup-Robustheit).
#
# Every check below is independent and all of them run even if an earlier
# one fails, so a single invocation reports every problem at once rather
# than forcing the user through a fix-one-rerun-find-the-next loop.
#
# Exit code: 0 if every check passed, 1 if any failed (with all failures
# already logged/notified by that point).
# =============================================================================
set -u # deliberately not -e: see the "run every check" note above
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
podman_load_cfg
FAILURES=0
fail() {
podman_log_error "preflight: $1"
FAILURES=$((FAILURES + 1))
}
ok() {
podman_log "preflight: OK - $1"
}
# -----------------------------------------------------------------------------
# 1. Required binaries are actually installed.
# -----------------------------------------------------------------------------
for bin in podman conmon crun mount umount xfs_repair mkfs.xfs; do
if command -v "$bin" > /dev/null 2>&1; then
ok "$bin found"
else
fail "required binary '$bin' not found on PATH — is the plugin fully installed?"
fi
done
for helper in /usr/libexec/podman/netavark /usr/libexec/podman/aardvark-dns; do
if [ -x "$helper" ]; then
ok "$helper found"
else
fail "required helper '$helper' not found — is the plugin fully installed?"
fi
done
# -----------------------------------------------------------------------------
# 2. Storage path configured, safe, and mounted (cache pool / disk, not
# /mnt/user — see docs/ARCHITECTURE.md section 4.3).
# -----------------------------------------------------------------------------
if podman_storage_path_is_safe "$STORAGE_PATH" 2> /tmp/podman-preflight-storage-safety.log; then
ok "STORAGE_PATH ($STORAGE_PATH) is not under /mnt/user"
else
fail "$(cat /tmp/podman-preflight-storage-safety.log)"
fi
rm -f /tmp/podman-preflight-storage-safety.log
if [ -d "$STORAGE_PATH" ] && mountpoint -q "$STORAGE_PATH" 2> /dev/null; then
ok "STORAGE_PATH ($STORAGE_PATH) is a mounted filesystem"
elif [ -d "$STORAGE_PATH" ]; then
# Not every valid target is a mountpoint itself (e.g. a subdirectory of a
# cache pool root) — warn rather than fail, but only if the parent chain
# is mounted somewhere.
if findmnt -T "$STORAGE_PATH" > /dev/null 2>&1; then
ok "STORAGE_PATH ($STORAGE_PATH) resolves onto a mounted filesystem"
else
fail "STORAGE_PATH ($STORAGE_PATH) does not appear to be on a mounted filesystem"
fi
else
fail "STORAGE_PATH ($STORAGE_PATH) does not exist — is the configured cache pool/disk present and started?"
fi
# -----------------------------------------------------------------------------
# 3. Free space check (only meaningful once STORAGE_PATH exists).
# -----------------------------------------------------------------------------
if [ -d "$STORAGE_PATH" ]; then
available_kb=$(df --output=avail -k "$STORAGE_PATH" 2> /dev/null | tail -n1 | tr -d '[:space:]')
if [ -n "${available_kb:-}" ]; then
available_gb=$((available_kb / 1024 / 1024))
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
# Image already exists — just warn if the pool itself is nearly full,
# since podman.img growth or new image pulls need headroom too.
if [ "$available_gb" -lt 2 ]; then
fail "less than 2G free on $STORAGE_PATH (${available_gb}G) — image pulls will likely fail"
else
ok "${available_gb}G free on $STORAGE_PATH"
fi
elif [ "$available_gb" -lt "$STORAGE_IMAGE_SIZE_GB" ]; then
fail "not enough free space on $STORAGE_PATH to create a ${STORAGE_IMAGE_SIZE_GB}G podman.img (only ${available_gb}G free)"
else
ok "${available_gb}G free on $STORAGE_PATH (enough for a fresh ${STORAGE_IMAGE_SIZE_GB}G podman.img)"
fi
fi
fi
# -----------------------------------------------------------------------------
# 4. Kernel supports cgroup v2 (required by crun/netavark's expectations).
# -----------------------------------------------------------------------------
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
ok "cgroup v2 unified hierarchy is active"
else
fail "cgroup v2 unified hierarchy not detected (/sys/fs/cgroup/cgroup.controllers missing) — check Unraid's syslinux cgroup boot parameters"
fi
# -----------------------------------------------------------------------------
# 5. No orphaned socket/pidfile from a previous unclean shutdown.
# -----------------------------------------------------------------------------
if [ -S "$PODMAN_SOCKET" ]; then
if [ -f "$PODMAN_SERVICE_PID_FILE" ] && kill -0 "$(cat "$PODMAN_SERVICE_PID_FILE")" 2> /dev/null; then
fail "podman system service already appears to be running (pid $(cat "$PODMAN_SERVICE_PID_FILE")) — is rc.podman already started?"
else
podman_log "preflight: removing orphaned socket $PODMAN_SOCKET from a previous unclean shutdown"
rm -f "$PODMAN_SOCKET"
ok "cleared orphaned socket"
fi
else
ok "no orphaned podman.sock"
fi
# -----------------------------------------------------------------------------
# 6. Boot-config directory exists (i.e. 'seed' has run at least once).
# -----------------------------------------------------------------------------
if [ -f "$PODMAN_CFG_FILE" ]; then
ok "$PODMAN_CFG_FILE present"
else
fail "$PODMAN_CFG_FILE missing — run 'podman-config.sh seed' (should happen automatically on install)"
fi
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
if [ "$FAILURES" -gt 0 ]; then
podman_notify "Podman preflight checks failed" \
"$FAILURES check(s) failed — podman was not started. See $PODMAN_PLUGIN_LOG for details." \
"alert"
podman_log_error "preflight: $FAILURES check(s) failed, aborting start"
exit 1
fi
podman_log "preflight: all checks passed"
exit 0
+180
View File
@@ -0,0 +1,180 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-storage.sh
#
# "Container Storage erstellen" — creates, mounts, and unmounts the
# podman.img loopback filesystem that backs Podman's overlay storage graph
# (images, containers, named volumes). See docs/ARCHITECTURE.md section 4.3
# for why this needs to be a real mounted filesystem (XFS with ftype=1, on a
# cache pool or dedicated disk) rather than a directory under /mnt/user —
# the FUSE (shfs) layer behind /mnt/user does not reliably support the
# overlay storage driver's filesystem requirements (d_type, etc).
#
# Usage:
# podman-storage.sh create # create podman.img if it doesn't exist yet
# podman-storage.sh mount # mount it at $PODMAN_GRAPHROOT (idempotent)
# podman-storage.sh unmount # cleanly unmount
# podman-storage.sh status # report existence/mount/usage
# =============================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
podman_load_cfg
# -----------------------------------------------------------------------------
# cmd_create
#
# Creates podman.img at the configured size if it doesn't already exist,
# and formats it XFS with ftype=1 (required for the overlay storage driver
# to see correct directory entry types — without it, podman's overlay
# backend silently falls back to a slower/less-capable mode or fails
# outright, depending on version). Does nothing (and exits 0) if the image
# already exists — this script is meant to be safe to call on every boot.
# -----------------------------------------------------------------------------
cmd_create() {
if ! podman_storage_path_is_safe "$STORAGE_PATH"; then
return 1
fi
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
podman_log "storage: $PODMAN_STORAGE_IMAGE already exists, not recreating"
return 0
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
fi
# Free space check: refuse to create an image bigger than what's actually
# available, with a small safety margin, rather than letting truncate
# silently create a sparse file that will fail unpredictably later once
# it's actually written to (see docs/ARCHITECTURE.md section 16.2).
local available_kb required_kb
available_kb=$(df --output=avail -k "$STORAGE_PATH" | tail -n1 | tr -d '[:space:]')
required_kb=$((STORAGE_IMAGE_SIZE_GB * 1024 * 1024))
if [ "$available_kb" -lt "$required_kb" ]; then
podman_log_error "storage: not enough free space on $STORAGE_PATH (need ${STORAGE_IMAGE_SIZE_GB}G, have $((available_kb / 1024 / 1024))G)"
podman_notify "Podman storage creation failed" \
"Not enough free space on $STORAGE_PATH for a ${STORAGE_IMAGE_SIZE_GB}G podman.img." \
"alert"
return 1
fi
podman_log "storage: creating ${STORAGE_IMAGE_SIZE_GB}G image at $PODMAN_STORAGE_IMAGE"
mkdir -p "$STORAGE_PATH"
# Sparse file: only actually consumes disk space as data is written,
# matching Docker-for-Unraid's docker.img behavior that users already
# understand.
truncate -s "${STORAGE_IMAGE_SIZE_GB}G" "$PODMAN_STORAGE_IMAGE"
podman_require_command mkfs.xfs
# -n ftype=1 is the whole reason this has to be a purpose-made image
# rather than a plain directory — see file header comment.
mkfs.xfs -n ftype=1 -q "$PODMAN_STORAGE_IMAGE"
podman_log "storage: created and formatted $PODMAN_STORAGE_IMAGE"
}
# -----------------------------------------------------------------------------
# cmd_mount
#
# Idempotent: does nothing if $PODMAN_GRAPHROOT is already mounted from
# podman.img. Runs a read-only integrity check (xfs_repair -n) before
# mounting and refuses to proceed if it reports corruption — per
# docs/ARCHITECTURE.md section 16.1, this project does not auto-repair
# storage without the user's explicit action, to avoid silent data loss.
# -----------------------------------------------------------------------------
cmd_mount() {
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
podman_log "storage: $PODMAN_GRAPHROOT already mounted"
return 0
fi
if [ ! -f "$PODMAN_STORAGE_IMAGE" ]; then
podman_log_error "storage: $PODMAN_STORAGE_IMAGE does not exist — run 'podman-storage.sh create' first"
return 1
fi
podman_require_command xfs_repair
podman_log "storage: checking filesystem integrity of $PODMAN_STORAGE_IMAGE"
if ! xfs_repair -n "$PODMAN_STORAGE_IMAGE" > /tmp/podman-xfs-repair.log 2>&1; then
podman_log_error "storage: filesystem check failed for $PODMAN_STORAGE_IMAGE — refusing to mount."
podman_log_error "storage: see /tmp/podman-xfs-repair.log. Run 'xfs_repair $PODMAN_STORAGE_IMAGE' manually to attempt repair, or restore from backup."
podman_notify "Podman storage corruption detected" \
"$PODMAN_STORAGE_IMAGE failed an integrity check and was not mounted. Manual recovery required — see plugin.log." \
"alert"
return 1
fi
mkdir -p "$PODMAN_GRAPHROOT"
podman_log "storage: mounting $PODMAN_STORAGE_IMAGE at $PODMAN_GRAPHROOT"
mount -o loop "$PODMAN_STORAGE_IMAGE" "$PODMAN_GRAPHROOT"
# Runtime state (runroot) is tmpfs-backed and fine to live on RAM-root —
# only the persistent graphroot needs the loopback filesystem.
mkdir -p /var/run/containers/storage
podman_log "storage: mounted"
}
# -----------------------------------------------------------------------------
# cmd_unmount
#
# Unmounts cleanly. Retries briefly if the mount is momentarily busy (a
# container process exiting can hold a reference for a few hundred ms),
# rather than immediately failing rc.podman's stop sequence.
# -----------------------------------------------------------------------------
cmd_unmount() {
if ! mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
podman_log "storage: $PODMAN_GRAPHROOT not mounted, nothing to do"
return 0
fi
podman_log "storage: unmounting $PODMAN_GRAPHROOT"
local attempt
for attempt in 1 2 3 4 5; do
if umount "$PODMAN_GRAPHROOT" 2> /dev/null; then
podman_log "storage: unmounted"
return 0
fi
sleep 1
done
podman_log_error "storage: failed to unmount $PODMAN_GRAPHROOT after 5 attempts (still busy?)"
return 1
}
cmd_status() {
echo "storage image: $PODMAN_STORAGE_IMAGE"
if [ -f "$PODMAN_STORAGE_IMAGE" ]; then
echo " exists: yes ($(du -h "$PODMAN_STORAGE_IMAGE" | cut -f1) allocated / ${STORAGE_IMAGE_SIZE_GB}G nominal)"
else
echo " exists: no"
fi
echo "graphroot: $PODMAN_GRAPHROOT"
if mountpoint -q "$PODMAN_GRAPHROOT" 2> /dev/null; then
echo " mounted: yes"
df -h "$PODMAN_GRAPHROOT" | tail -n1 | awk '{print " usage: " $3 " used / " $2 " total (" $5 " full)"}'
else
echo " mounted: no"
fi
}
case "${1:-}" in
create) cmd_create ;;
mount) cmd_mount ;;
unmount) cmd_unmount ;;
status) cmd_status ;;
*)
echo "usage: $0 {create|mount|unmount|status}" >&2
exit 1
;;
esac
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-uninstall-cleanup.sh
#
# "Beim Entfernen sauber aufräumen" — run by plugin/podman.plg's <FILE
# Run="removepkg"> block when the plugin is uninstalled via Unraid's Plugins
# page. Two-tier cleanup, matching docs/ARCHITECTURE.md section 3.2 /
# section 17:
#
# ALWAYS removed (installed software, safe to regenerate):
# - running containers/service stopped cleanly
# - runtime config under /etc/containers, /var/run/podman
# - staged helper scripts, templates, WebUI files, event hooks
#
# NOT applicable: this project has no /boot/config/go entry to clean up
# in the first place — it hooks the official Unraid plugin event
# mechanism (/usr/local/emhttp/plugins/podman/event/*, see
# plugin/event/disks_mounted's header comment) instead of editing go,
# so removepkg-ing the unraid-podman package (done by podman.plg right
# after this script runs) already removes those hooks along with
# everything else under /usr/local/emhttp/plugins/podman/.
#
# PRESERVED BY DEFAULT (user data — see docs/INSTALL.md "Uninstallation"):
# - /boot/config/plugins/podman/ (settings, autostart list, backups)
# - $STORAGE_PATH (podman.img — every image/container/volume the user
# has)
#
# Full data removal is opt-in only, via:
# podman-uninstall-cleanup.sh --purge-data
# never the default, and never triggered automatically — deleting a user's
# containers/images without an explicit, separate confirmation is exactly
# the kind of destructive-by-surprise behavior this project avoids (see
# docs/ARCHITECTURE.md "Nicht-Ziele" around data safety).
# =============================================================================
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
PURGE_DATA=0
[ "${1:-}" = "--purge-data" ] && PURGE_DATA=1
podman_log "uninstall: starting cleanup (purge-data=$PURGE_DATA)"
# -----------------------------------------------------------------------------
# 1. Stop podman cleanly if it's running, so containers shut down properly
# and the storage loopback is unmounted before we remove anything else.
# -----------------------------------------------------------------------------
if [ -x /etc/rc.d/rc.podman ]; then
podman_log "uninstall: stopping podman"
/etc/rc.d/rc.podman stop || podman_log_error "uninstall: rc.podman stop reported an error (continuing cleanup anyway)"
fi
# -----------------------------------------------------------------------------
# 2. Remove runtime state (RAM-root anyway, but tidy up now rather than
# waiting for the next reboot to implicitly clear it).
# -----------------------------------------------------------------------------
podman_log "uninstall: removing runtime state"
rm -rf "$PODMAN_ETC_DIR" "$PODMAN_RUN_DIR"
# -----------------------------------------------------------------------------
# 3. Remove staged files this plugin installed outside of package-managed
# paths. This is technically redundant with podman.plg's removepkg of
# the unraid-podman package that immediately follows this script (see
# that package's manifest — it owns exactly these paths), but doing it
# explicitly here too means this script is also safe to run manually as
# a standalone repair tool without relying on removepkg bookkeeping.
# -----------------------------------------------------------------------------
podman_log "uninstall: removing plugin scaffolding"
rm -rf /usr/local/share/unraid-podman
rm -rf /usr/local/emhttp/plugins/podman
rm -f /etc/rc.d/rc.podman
# -----------------------------------------------------------------------------
# 4. User data — preserved unless --purge-data was explicitly passed.
# -----------------------------------------------------------------------------
if [ "$PURGE_DATA" -eq 1 ]; then
podman_load_cfg
podman_log "uninstall: --purge-data given, removing user data"
if [ -n "${STORAGE_PATH:-}" ] && [ -d "$STORAGE_PATH" ]; then
podman_log "uninstall: removing $STORAGE_PATH (images, containers, volumes, logs)"
rm -rf "${STORAGE_PATH:?}"
fi
podman_log "uninstall: removing $PODMAN_BOOT_DIR (settings, autostart, backups)"
rm -rf "${PODMAN_BOOT_DIR:?}"
else
podman_log "uninstall: preserving $PODMAN_BOOT_DIR and podman's storage path (pass --purge-data to remove them too)"
fi
podman_log "uninstall: cleanup complete"
+134
View File
@@ -0,0 +1,134 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-update-packages.sh
#
# "Pakete aktualisieren" — reconciles installed packages with what the
# currently installed plugin version expects, per
# /usr/local/share/unraid-podman/installed-versions.env (written by
# plugin/podman.plg's postinstall — see podman-verify-packages.sh for the
# same manifest used the other direction, to *check* rather than *fix*).
#
# This is intentionally idempotent and safe to run any time, not just
# during a plugin update: if every package already matches, it's a no-op.
# It exists as a separate script (rather than being inline in podman.plg)
# for two reasons:
# 1. plugin/podman.plg's own <FILE Run="upgradepkg"> blocks handle the
# *normal* update path (new .txz already downloaded by the plg,
# installed as part of the same transaction) — this script is the
# *repair* path for when that didn't fully complete (e.g. Unraid was
# rebooted mid-update), and can be re-run safely from the command line
# or from a future WebUI "check for package issues" action.
# 2. It calls podman-backup.sh snapshot-config first, since any package
# swap is exactly the moment a config schema migration might be
# needed — see docs/ARCHITECTURE.md section 13.1.
#
# Usage:
# podman-update-packages.sh # reconcile all 7 packages
# podman-update-packages.sh podman # reconcile a single package
# =============================================================================
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
ALL_PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
podman_log_error "update-packages: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
exit 1
fi
# shellcheck source=/dev/null
. "$INSTALLED_VERSIONS_FILE"
if [ -z "${PLUGIN_VERSION:-}" ]; then
podman_log_error "update-packages: PLUGIN_VERSION not recorded in $INSTALLED_VERSIONS_FILE"
exit 1
fi
targets="${*:-$ALL_PACKAGES}"
podman_require_command upgradepkg
podman_require_command installpkg
updated=0
already_current=0
errors=0
for name in $targets; do
entity_prefix=$(echo "$name" | tr '[:lower:]-' '[:upper:]_')
expected_version_var="${entity_prefix}_INSTALLED_VERSION"
expected_version="${!expected_version_var:-}"
if [ -z "$expected_version" ]; then
podman_log_error "update-packages: no expected version recorded for '$name', skipping"
errors=$((errors + 1))
continue
fi
installed_record=$(find /var/log/packages -maxdepth 1 -name "${name}-*" -print 2> /dev/null | head -n1)
installed_basename=$(basename "${installed_record:-__none__}")
case "$installed_basename" in
"${name}-${expected_version}-"*)
podman_log "update-packages: $name already at expected version $expected_version"
already_current=$((already_current + 1))
continue
;;
esac
# The package for the currently expected version was downloaded straight
# into its backup slot by podman.plg (grouped by PLUGIN_VERSION, since a
# rollback targets "this plugin release" — see podman-backup.sh's header
# comment for why that's also where we install FROM.
txz=$(find "$PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION" -maxdepth 1 -name "${name}-*.txz" 2> /dev/null | head -n1)
if [ -z "$txz" ]; then
podman_log_error "update-packages: no package file found for $name (expected version $expected_version) in $PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION/"
errors=$((errors + 1))
continue
fi
podman_log "update-packages: installing $name -> $expected_version ($txz)"
# Snapshot config once, before the first actual package change — a
# version bump is exactly when a schema migration might be needed (see
# file header). Only do this once per run, not once per package.
if [ "$updated" -eq 0 ]; then
"$SCRIPT_DIR/podman-backup.sh" snapshot-config > /dev/null
fi
if [ -n "$installed_record" ]; then
if upgradepkg --install-new "$txz"; then
updated=$((updated + 1))
else
podman_log_error "update-packages: upgradepkg failed for $name ($txz)"
errors=$((errors + 1))
fi
else
if installpkg "$txz"; then
updated=$((updated + 1))
else
podman_log_error "update-packages: installpkg failed for $name ($txz)"
errors=$((errors + 1))
fi
fi
done
podman_log "update-packages: done (updated=$updated already-current=$already_current errors=$errors)"
if [ "$errors" -gt 0 ]; then
podman_notify "Podman package update had errors" \
"$errors package(s) failed to update — see $PODMAN_PLUGIN_LOG." \
"alert"
exit 1
fi
if [ "$updated" -gt 0 ]; then
podman_notify "Podman packages updated" \
"$updated package(s) updated. Run 'rc.podman restart' to apply." \
"normal"
fi
exit 0
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# =============================================================================
# plugin/sbin/podman-verify-packages.sh
#
# "Pakete prüfen" — verifies the seven packages this plugin ships are
# actually installed, at the version the plugin expects, and that the
# backed-up .txz copies (see podman-backup.sh) haven't bit-rotted on disk.
#
# Three checks per package:
# 1. Installed: Slackware records every installed package under
# /var/log/packages/<name>-<version>-<arch>-<build><tag> — its mere
# existence IS the install record (standard Slackware pkgtools
# convention, nothing custom here).
# 2. Expected version: compared against
# /usr/local/share/unraid-podman/installed-versions.env, written by
# plugin/podman.plg's postinstall step at install/update time.
# 3. Backup integrity: if a backed-up .txz exists for the installed
# version (see podman-backup.sh), its current SHA256 is re-checked
# against the .sha256 sidecar recorded at build time — catches flash
# storage corruption on the backup copy before it's needed for a
# rollback.
#
# Usage:
# podman-verify-packages.sh # human-readable report
# podman-verify-packages.sh --quiet # exit code only, minimal output
#
# Exit code: 0 if everything checks out, 1 if any package has a problem.
# =============================================================================
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./podman-common.sh
. "$SCRIPT_DIR/podman-common.sh"
INSTALLED_VERSIONS_FILE="/usr/local/share/unraid-podman/installed-versions.env"
PACKAGES="podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman"
QUIET=0
[ "${1:-}" = "--quiet" ] && QUIET=1
report() {
[ "$QUIET" -eq 0 ] && echo "$1"
}
if [ ! -f "$INSTALLED_VERSIONS_FILE" ]; then
podman_log_error "verify: $INSTALLED_VERSIONS_FILE missing — plugin install metadata not found"
exit 1
fi
# shellcheck source=/dev/null
. "$INSTALLED_VERSIONS_FILE"
if [ -z "${PLUGIN_VERSION:-}" ]; then
podman_log_error "verify: PLUGIN_VERSION not recorded in $INSTALLED_VERSIONS_FILE"
exit 1
fi
PROBLEMS=0
for name in $PACKAGES; do
entity_prefix=$(echo "$name" | tr '[:lower:]-' '[:upper:]_')
expected_version_var="${entity_prefix}_INSTALLED_VERSION"
expected_version="${!expected_version_var:-}"
report "== $name =="
if [ -z "$expected_version" ]; then
report " expected version: UNKNOWN (not recorded in $INSTALLED_VERSIONS_FILE)"
PROBLEMS=$((PROBLEMS + 1))
continue
fi
report " expected version: $expected_version"
# --- Check 1: installed -----------------------------------------------
installed_record=$(find /var/log/packages -maxdepth 1 -name "${name}-*" -print 2> /dev/null | head -n1)
if [ -z "$installed_record" ]; then
report " installed: NO"
podman_log_error "verify: $name is not installed (no /var/log/packages/$name-* record)"
PROBLEMS=$((PROBLEMS + 1))
continue
fi
installed_basename=$(basename "$installed_record")
report " installed package: $installed_basename"
# --- Check 2: version matches expectation ------------------------------
case "$installed_basename" in
"${name}-${expected_version}-"*)
report " version match: OK"
;;
*)
report " version match: MISMATCH (installed record does not match expected $expected_version)"
podman_log_error "verify: $name installed record '$installed_basename' does not match expected version $expected_version"
PROBLEMS=$((PROBLEMS + 1))
;;
esac
# --- Check 3: backup artifact integrity, if present --------------------
# Packages of all 7 components released together as one plugin version
# are grouped under a single PLUGIN_VERSION directory (not per-component
# version) — a rollback targets "go back to plugin release X", matching
# podman-backup.sh's restore-packages <plugin-version>.
backup_dir="$PODMAN_BACKUP_DIR/packages/$PLUGIN_VERSION"
backup_txz=$(find "$backup_dir" -maxdepth 1 -name "${name}-*.txz" 2> /dev/null | head -n1)
if [ -n "$backup_txz" ] && [ -f "$backup_txz.sha256" ]; then
if ( cd "$(dirname "$backup_txz")" && sha256sum -c "$(basename "$backup_txz").sha256" > /dev/null 2>&1 ); then
report " backup integrity: OK ($backup_txz)"
else
report " backup integrity: CORRUPT ($backup_txz)"
podman_log_error "verify: backup artifact $backup_txz failed checksum verification"
PROBLEMS=$((PROBLEMS + 1))
fi
else
report " backup integrity: no backup artifact on file (nothing to verify)"
fi
done
report ""
if [ "$PROBLEMS" -gt 0 ]; then
report "$PROBLEMS problem(s) found."
podman_notify "Podman package verification found problems" \
"$PROBLEMS issue(s) found — see $PODMAN_PLUGIN_LOG for details." \
"warning"
exit 1
fi
report "All packages verified OK."
exit 0
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# =============================================================================
# scripts/build-packages.sh
#
# Orchestrates building all seven Slackware .txz packages this plugin ships
# (podman, conmon, crun, netavark, aardvark-dns, passt, fuse-overlayfs), by
# running each package's <name>.SlackBuild in turn. See
# docs/ARCHITECTURE.md section 5.2 (Build-Strategie).
#
# This script itself does not containerize anything — it assumes it is
# already running inside a Slackware-compatible build environment (see
# .github/workflows/build-packages.yml, which runs it inside a pinned
# Slackware Docker image). Running it on a non-Slackware host will likely
# still compile successfully for most components but the resulting .txz
# should not be trusted for an actual Unraid install — see the container
# image note in the CI workflow.
#
# Usage:
# scripts/build-packages.sh # build all packages
# scripts/build-packages.sh podman conmon # build only the named ones
#
# Output: $REPO_ROOT/dist/<name>-<version>-<arch>-<build><tag>.txz
# plus a .sha256 and .md5 sidecar file per package (see
# scripts/lib/slackbuild-common.sh, sb_make_package).
# Nothing under dist/ is committed to git — see .gitignore.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PACKAGES_DIR="$REPO_ROOT/packages"
DIST_DIR="$REPO_ROOT/dist"
# The full, ordered set of packages this project builds automatically.
# containers-common is intentionally excluded for now — see packages/README.md.
# unraid-podman (the plugin's own scaffolding, not an upstream component —
# see packages/unraid-podman/README.md) is built last since it's by far the
# fastest and has nothing useful to report on failure that earlier package
# failures wouldn't already explain.
ALL_PACKAGES=(conmon crun netavark aardvark-dns passt fuse-overlayfs podman unraid-podman)
# Podman is listed second-to-last on purpose: among the seven upstream
# components it is the slowest build and the one most likely to fail on a
# dependency/tag mistake, so faster packages surface problems first during
# local iteration. unraid-podman is genuinely last since it packages this
# repo's own files and has no compile step at all.
requested=("$@")
if [ "${#requested[@]}" -eq 0 ]; then
targets=("${ALL_PACKAGES[@]}")
else
targets=("${requested[@]}")
fi
mkdir -p "$DIST_DIR"
echo "==> Building packages: ${targets[*]}"
echo "==> Output directory: $DIST_DIR"
echo
failed=()
built=()
for name in "${targets[@]}"; do
slackbuild="$PACKAGES_DIR/$name/$name.SlackBuild"
if [ ! -x "$slackbuild" ]; then
echo "!! No SlackBuild found/executable for '$name' ($slackbuild)" >&2
failed+=("$name")
continue
fi
echo "############################################################"
echo "## Building: $name"
echo "############################################################"
# Run each SlackBuild with OUTPUT pointed at the shared dist/ directory,
# so every package's .txz ends up in one place regardless of the
# per-package TMP/PKG scratch dirs it uses internally.
if OUTPUT="$DIST_DIR" "$slackbuild"; then
built+=("$name")
else
echo "!! Build failed: $name" >&2
failed+=("$name")
fi
echo
done
echo "============================================================"
echo "Build summary"
echo "============================================================"
echo "Succeeded (${#built[@]}): ${built[*]:-none}"
echo "Failed (${#failed[@]}): ${failed[*]:-none}"
if [ "${#failed[@]}" -gt 0 ]; then
echo
echo "!! One or more packages failed to build — see log above." >&2
exit 1
fi
echo
echo "All packages built successfully. Artifacts in $DIST_DIR:"
ls -1 "$DIST_DIR"
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# =============================================================================
# scripts/checksums.sh
#
# Verifies and consolidates checksums for everything in dist/. Each package
# already gets its own <file>.sha256 / <file>.md5 sidecar file from
# sb_make_package (scripts/lib/slackbuild-common.sh) at build time — this
# script:
# 1. Re-verifies every .txz against its own sidecar checksum (defense in
# depth: catches disk corruption or a tampered artifact between the
# build job and the release job in CI).
# 2. Writes a single consolidated CHECKSUMS.sha256 manifest covering all
# built packages, suitable for attaching to a GitHub Release so users
# can verify the whole set with one `sha256sum -c CHECKSUMS.sha256`.
#
# Usage:
# scripts/checksums.sh [dist-dir] # defaults to <repo>/dist
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="${1:-$REPO_ROOT/dist}"
if [ ! -d "$DIST_DIR" ]; then
echo "!! No such directory: $DIST_DIR (nothing built yet?)" >&2
exit 1
fi
shopt -s nullglob
txz_files=("$DIST_DIR"/*.txz)
shopt -u nullglob
if [ "${#txz_files[@]}" -eq 0 ]; then
echo "!! No .txz files found in $DIST_DIR" >&2
exit 1
fi
echo "==> Verifying per-package checksums"
verify_failed=0
for f in "${txz_files[@]}"; do
base=$(basename "$f")
sidecar="$f.sha256"
if [ ! -f "$sidecar" ]; then
echo "!! Missing $sidecar for $base" >&2
verify_failed=1
continue
fi
if ( cd "$DIST_DIR" && sha256sum -c "$(basename "$sidecar")" > /dev/null 2>&1 ); then
echo "OK $base"
else
echo "FAIL $base" >&2
verify_failed=1
fi
done
if [ "$verify_failed" -ne 0 ]; then
echo "!! Checksum verification failed for one or more packages." >&2
exit 1
fi
echo
echo "==> Writing consolidated manifest: $DIST_DIR/CHECKSUMS.sha256"
( cd "$DIST_DIR" && sha256sum ./*.txz > CHECKSUMS.sha256 )
echo "==> Writing consolidated MD5 manifest: $DIST_DIR/CHECKSUMS.md5"
( cd "$DIST_DIR" && md5sum ./*.txz > CHECKSUMS.md5 )
echo "==> Done."
cat "$DIST_DIR/CHECKSUMS.sha256"
+46
View File
@@ -0,0 +1,46 @@
# =============================================================================
# scripts/ci/buildenv-versions.env
#
# Version pins for the BUILD ENVIRONMENT itself — the toolchains and C
# libraries needed to compile the seven packages in packages/, but which are
# not themselves shipped as part of the plugin. Kept separate from the
# top-level versions.env, which pins only what actually gets packaged and
# installed on an Unraid system (see that file's header comment).
#
# Consumed by scripts/ci/setup-slackware-buildenv.sh.
# =============================================================================
# Go toolchain (builds podman). Official upstream tarball, not a distro
# package — Slackware ships no Go toolchain in a stock install.
GO_VERSION="1.26.5"
GO_SRC_URL="https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
# Checksum as published by go.dev itself (`curl -s https://go.dev/dl/?mode=json`)
# — must be re-derived from the same source whenever GO_VERSION changes.
GO_SRC_SHA256="88c162b204e6eefcc32499453b492e80209f4a4c78c33092636901c540fb0d05"
# Rust toolchain (builds netavark, aardvark-dns) — installed via rustup
# rather than a pinned tarball, since rustup itself provides reproducible,
# checksummed component installation. We pin the *channel*, not an exact
# rustc build; per-crate reproducibility comes from each Rust package's
# Cargo.lock (built with `cargo build --locked`, see the netavark/
# aardvark-dns SlackBuilds) rather than from the compiler version.
RUST_CHANNEL="stable"
RUSTUP_INIT_URL="https://sh.rustup.rs"
# --- C library build-time dependencies ---------------------------------------
# These are expected to already be present in the Slackware base image (part
# of a stock "full" Slackware 15.0 install): glib2 (conmon), libcap (crun),
# fuse3 (fuse-overlayfs). setup-slackware-buildenv.sh fails fast with a clear
# message if any of these are missing, rather than silently vendoring them.
#
# libseccomp and yajl are NOT part of a stock Slackware install and are
# built from source by setup-slackware-buildenv.sh if pkg-config doesn't
# find them.
LIBSECCOMP_VERSION="2.6.1"
LIBSECCOMP_SRC_URL="https://github.com/seccomp/libseccomp/archive/refs/tags/v${LIBSECCOMP_VERSION}.tar.gz"
LIBSECCOMP_SRC_SHA256="f9a13e4c633d319a9240189760ca348caa0837c0ebe2a09b17061da8ceaf60f0"
YAJL_VERSION="2.1.0"
YAJL_SRC_URL="https://github.com/lloyd/yajl/archive/refs/tags/${YAJL_VERSION}.tar.gz"
YAJL_SRC_SHA256="3fb73364a5a30efe615046d07e6db9d09fd2b41c763c5f7d3bfb121cd5c5ac5a"
+152
View File
@@ -0,0 +1,152 @@
#!/bin/bash
# =============================================================================
# scripts/ci/setup-slackware-buildenv.sh
#
# Prepares a Slackware container (see .github/workflows/build-packages.yml)
# to build all seven packages under packages/. Idempotent and safe to re-run.
#
# Strategy: detect what's already present (a stock "full" Slackware 15.0
# install already provides gcc/make/autotools/glib2/libcap/fuse3) and only
# bootstrap what's genuinely missing (libseccomp, yajl — neither ships in
# stock Slackware — plus the Go and Rust toolchains, which no Slackware
# install ships). This makes the script tolerant of small differences
# between Slackware base image variants instead of assuming one exact image
# layout, while still failing loudly if something we cannot self-provision
# (a C compiler, basically) is missing.
#
# Exits non-zero with a clear message if a required tool cannot be found or
# provisioned — this script is meant to run early in CI so failures surface
# immediately, not halfway through a 20-minute podman build.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=/dev/null
. "$REPO_ROOT/scripts/ci/buildenv-versions.env"
WORK="/tmp/unraid-podman-buildenv"
mkdir -p "$WORK"
require_binary() {
local bin="$1" hint="$2"
if ! command -v "$bin" > /dev/null 2>&1; then
echo "!! Required tool '$bin' not found in the build image." >&2
echo "!! $hint" >&2
exit 1
fi
echo "==> found $bin: $(command -v "$bin")"
}
require_pkgconfig() {
local module="$1" hint="$2"
if ! pkg-config --exists "$module" 2>/dev/null; then
echo "!! Required library '$module' not found via pkg-config." >&2
echo "!! $hint" >&2
return 1
fi
echo "==> found pkg-config module: $module ($(pkg-config --modversion "$module"))"
return 0
}
# -----------------------------------------------------------------------------
# 1. Baseline toolchain expected to already be present in the base image.
# -----------------------------------------------------------------------------
require_binary gcc "Use a Slackware base image with the 'D' (development) series installed."
require_binary make "Use a Slackware base image with the 'D' (development) series installed."
require_binary autoconf "Needed by crun/fuse-overlayfs; part of Slackware's 'D' series."
require_binary automake "Needed by crun/fuse-overlayfs; part of Slackware's 'D' series."
require_binary libtool "Needed by crun/fuse-overlayfs; part of Slackware's 'D' series."
require_binary pkg-config "Needed to locate C library dependencies."
require_binary git "Needed to fetch crun's git submodules."
require_binary curl "Needed to fetch pinned source tarballs."
require_binary makepkg "Slackware's own packaging tool (pkgtools); should always be present."
require_binary strip "Part of binutils; part of Slackware's 'D' series."
# -----------------------------------------------------------------------------
# 2. C library dependencies expected to already be present.
# -----------------------------------------------------------------------------
require_pkgconfig glib-2.0 "Install Slackware's glib2 package (needed by conmon)."
require_pkgconfig libcap "Install Slackware's libcap package (needed by crun)." || true
require_pkgconfig fuse3 "Install Slackware's fuse3 package (needed by fuse-overlayfs)." || true
# -----------------------------------------------------------------------------
# 3. libseccomp — not part of stock Slackware, build from source if missing.
# -----------------------------------------------------------------------------
if ! pkg-config --exists libseccomp 2>/dev/null; then
echo "==> libseccomp not found, building v$LIBSECCOMP_VERSION from source"
d="$WORK/libseccomp"
mkdir -p "$d"
curl -fL --retry 3 -o "$d/src.tar.gz" "$LIBSECCOMP_SRC_URL"
actual=$(sha256sum "$d/src.tar.gz" | awk '{print $1}')
[ "$actual" = "$LIBSECCOMP_SRC_SHA256" ] || {
echo "!! libseccomp checksum mismatch (expected $LIBSECCOMP_SRC_SHA256, got $actual)" >&2
exit 1
}
mkdir -p "$d/src" && tar -xf "$d/src.tar.gz" -C "$d/src" --strip-components=1
( cd "$d/src" && ./autogen.sh && ./configure --prefix=/usr && make -j"$(nproc)" && make install )
else
echo "==> libseccomp already present, skipping bootstrap build"
fi
# -----------------------------------------------------------------------------
# 4. yajl — not part of stock Slackware, build from source if missing.
# -----------------------------------------------------------------------------
if ! pkg-config --exists yajl 2>/dev/null; then
echo "==> yajl not found, building v$YAJL_VERSION from source"
d="$WORK/yajl"
mkdir -p "$d"
curl -fL --retry 3 -o "$d/src.tar.gz" "$YAJL_SRC_URL"
actual=$(sha256sum "$d/src.tar.gz" | awk '{print $1}')
[ "$actual" = "$YAJL_SRC_SHA256" ] || {
echo "!! yajl checksum mismatch (expected $YAJL_SRC_SHA256, got $actual)" >&2
exit 1
}
mkdir -p "$d/src" && tar -xf "$d/src.tar.gz" -C "$d/src" --strip-components=1
# yajl uses its own cmake-free ./configure wrapper script.
( cd "$d/src" && ./configure -p /usr && make -C build install )
ldconfig 2>/dev/null || true
else
echo "==> yajl already present, skipping bootstrap build"
fi
# -----------------------------------------------------------------------------
# 5. Go toolchain (podman) — official upstream tarball.
# -----------------------------------------------------------------------------
if ! command -v go > /dev/null 2>&1; then
echo "==> Go not found, installing $GO_VERSION"
curl -fL --retry 3 -o "$WORK/go.tar.gz" "$GO_SRC_URL"
actual=$(sha256sum "$WORK/go.tar.gz" | awk '{print $1}')
[ "$actual" = "$GO_SRC_SHA256" ] || {
echo "!! Go toolchain checksum mismatch (expected $GO_SRC_SHA256, got $actual)" >&2
exit 1
}
rm -rf /usr/local/go
tar -C /usr/local -xf "$WORK/go.tar.gz"
export PATH="/usr/local/go/bin:$PATH"
# Persist PATH for subsequent steps in the same GitHub Actions job.
if [ -n "${GITHUB_PATH:-}" ]; then
echo "/usr/local/go/bin" >> "$GITHUB_PATH"
fi
else
echo "==> Go already present: $(go version)"
fi
# -----------------------------------------------------------------------------
# 6. Rust toolchain (netavark, aardvark-dns) — via rustup.
# -----------------------------------------------------------------------------
if ! command -v cargo > /dev/null 2>&1; then
echo "==> Rust/cargo not found, installing via rustup ($RUST_CHANNEL channel)"
curl -fL --retry 3 --proto '=https' --tlsv1.2 -sSf "$RUSTUP_INIT_URL" \
| sh -s -- -y --default-toolchain "$RUST_CHANNEL" --profile minimal
# shellcheck source=/dev/null
. "$HOME/.cargo/env"
if [ -n "${GITHUB_PATH:-}" ]; then
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
else
echo "==> Rust already present: $(cargo --version)"
fi
echo
echo "==> Build environment ready."
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# =============================================================================
# scripts/dev/lint.sh
#
# Local developer entry point mirroring .github/workflows/lint.yml: runs
# ShellCheck over shell scripts/SlackBuilds and xmllint over plugin/podman.plg.
# Requires `shellcheck` and `xmllint` (libxml2) to be installed locally.
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"
status=0
if command -v shellcheck > /dev/null 2>&1; then
echo "==> ShellCheck"
find plugin/rc.d plugin/sbin scripts -type f \
\( -name '*.sh' -o -name 'rc.*' -o -name '*.SlackBuild' \) \
-print0 \
| xargs -0 shellcheck --severity=warning --external-sources || status=1
else
echo "!! shellcheck not installed, skipping (install it to match CI: https://www.shellcheck.net/)" >&2
fi
if command -v xmllint > /dev/null 2>&1; then
echo "==> xmllint (plugin/podman.plg)"
xmllint --noout plugin/podman.plg || status=1
else
echo "!! xmllint not installed, skipping (part of libxml2-utils)" >&2
fi
exit "$status"
+176
View File
@@ -0,0 +1,176 @@
#!/bin/bash
# =============================================================================
# scripts/lib/slackbuild-common.sh
#
# Shared helper functions sourced by every packages/*/*.SlackBuild script.
# Centralizing this logic means each individual SlackBuild only has to
# describe *how to compile* its component — fetching, checksum verification,
# and final .txz packaging are implemented once, here, and used the same way
# by all seven packages. This is what keeps the seven build scripts
# consistent and short instead of each reinventing (and potentially
# forgetting) checksum verification or Slackware package metadata.
#
# Every SlackBuild is expected to:
# 1. `. "$(dirname "$0")/../../scripts/lib/slackbuild-common.sh"`
# 2. `. "$(dirname "$0")/../../versions.env"`
# 3. Call sb_init, sb_fetch_and_verify, do its own compile steps into
# $PKG, then call sb_make_package.
#
# Not meant to be run directly.
# =============================================================================
set -eu
# -----------------------------------------------------------------------------
# sb_init <pkg-name>
#
# Sets up the standard SlackBuild working directories and exports the
# variables every subsequent helper (and the calling SlackBuild) relies on:
# CWD - directory the SlackBuild itself lives in (packages/<name>/)
# TMP - scratch/build directory (source is extracted and compiled here)
# PKG - staging directory that becomes the .txz payload
# OUTPUT - where the finished .txz + checksum files are written
# All three of TMP/PKG/OUTPUT are safe to delete/recreate; nothing outside
# of them is ever touched, and nothing under them is ever committed to git
# (see .gitignore: packages/**/src, packages/**/pkg, packages/**/work).
# -----------------------------------------------------------------------------
sb_init() {
local pkg_name="$1"
PRGNAM="$pkg_name"
CWD="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)"
TMP="${TMP:-$CWD/work}"
PKG="${PKG:-$TMP/package-$PRGNAM}"
OUTPUT="${OUTPUT:-$CWD/../../dist}"
export PRGNAM CWD TMP PKG OUTPUT
rm -rf "$TMP"
mkdir -p "$TMP" "$PKG" "$OUTPUT"
echo "==> [$PRGNAM] TMP=$TMP"
echo "==> [$PRGNAM] PKG=$PKG"
echo "==> [$PRGNAM] OUTPUT=$OUTPUT"
}
# -----------------------------------------------------------------------------
# sb_fetch_and_verify <url> <expected-sha256> <dest-filename>
#
# Downloads a source tarball into $TMP and verifies it against the SHA256
# pinned in versions.env before anything is extracted or built. Aborts the
# build loudly on any mismatch — a checksum mismatch means either
# versions.env is stale (a legitimate new upstream release) or the source is
# not what it claims to be; either way, an unreviewed build must not proceed.
# -----------------------------------------------------------------------------
sb_fetch_and_verify() {
local url="$1"
local expected_sha256="$2"
local dest_name="$3"
local dest_path="$TMP/$dest_name"
echo "==> [$PRGNAM] Fetching $url"
curl -fL --retry 3 --retry-delay 2 -o "$dest_path" "$url"
local actual_sha256
actual_sha256=$(sha256sum "$dest_path" | awk '{print $1}')
if [ "$actual_sha256" != "$expected_sha256" ]; then
echo "!! [$PRGNAM] SHA256 MISMATCH for $dest_name" >&2
echo "!! expected: $expected_sha256" >&2
echo "!! actual: $actual_sha256" >&2
echo "!! Refusing to build against unverified source. If upstream" >&2
echo "!! genuinely released a new version, update versions.env via" >&2
echo "!! scripts/update-versions.sh instead of editing the hash by hand." >&2
exit 1
fi
echo "==> [$PRGNAM] SHA256 verified ($actual_sha256)"
echo "$dest_path"
}
# -----------------------------------------------------------------------------
# sb_extract <tarball-path> [strip-components]
#
# Extracts a verified tarball into $TMP/src, normalizing away the
# "<repo>-<version>/" wrapper directory GitHub/cgit archives always contain,
# so every SlackBuild can `cd "$TMP/src"` regardless of upstream's archive
# layout.
# -----------------------------------------------------------------------------
sb_extract() {
local tarball="$1"
local strip="${2:-1}"
mkdir -p "$TMP/src"
tar -xf "$tarball" -C "$TMP/src" --strip-components="$strip"
echo "$TMP/src"
}
# -----------------------------------------------------------------------------
# sb_install_docs <license-file> [readme-file]
#
# Installs upstream's license (and optionally README) into the standard
# Slackware package documentation location, plus this project's own build
# metadata, so `installpkg` output and /usr/doc/<pkg>-<version>/ are
# populated per Slackware convention.
# -----------------------------------------------------------------------------
sb_install_docs() {
local license_file="$1"
local readme_file="${2:-}"
local docdir="$PKG/usr/doc/$PRGNAM-$VERSION"
mkdir -p "$docdir"
[ -f "$license_file" ] && cp "$license_file" "$docdir/"
[ -n "$readme_file" ] && [ -f "$readme_file" ] && cp "$readme_file" "$docdir/"
{
echo "Built by unraid-podman from upstream source."
echo "Package: $PRGNAM $VERSION"
echo "Built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$docdir/unraid-podman.build-info"
}
# -----------------------------------------------------------------------------
# sb_make_package
#
# Finalizes the Slackware package: installs slack-desc + doinst.sh (if
# present) into $PKG/install/, strips binaries, sets root:root ownership,
# runs makepkg to produce the .txz, then writes SHA256 + MD5 sidecar files
# next to it.
#
# - SHA256 is used by scripts/build-packages.sh / CI to verify build
# artifacts between jobs.
# - MD5 is additionally generated because Unraid's .plg <FILE> mechanism
# verifies downloads via MD5 by convention (see
# docs/ARCHITECTURE.md section 3.2) — scripts/release.sh reads the .md5
# file to populate plugin/podman.plg's entities.
#
# Produces: $OUTPUT/<name>-<version>-<arch>-<build><tag>.txz (+ .sha256, .md5)
# -----------------------------------------------------------------------------
sb_make_package() {
local version="$1"
local arch="$2"
local build="$3"
local tag="$4"
mkdir -p "$PKG/install"
if [ -f "$CWD/slack-desc" ]; then
cp "$CWD/slack-desc" "$PKG/install/slack-desc"
else
echo "!! [$PRGNAM] missing packages/$PRGNAM/slack-desc" >&2
exit 1
fi
[ -f "$CWD/doinst.sh" ] && cp "$CWD/doinst.sh" "$PKG/install/doinst.sh"
# Strip debug symbols where possible to keep package size down; ignore
# failures (e.g. static/stripped-already binaries, non-ELF files).
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' _ {} \;
local pkg_file="$PRGNAM-$version-$arch-$build$tag.txz"
( cd "$PKG" && makepkg --linkadd y --chown y "$OUTPUT/$pkg_file" )
( cd "$OUTPUT" && sha256sum "$pkg_file" > "$pkg_file.sha256" )
( cd "$OUTPUT" && md5sum "$pkg_file" > "$pkg_file.md5" )
echo "==> [$PRGNAM] built $OUTPUT/$pkg_file"
}
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
# =============================================================================
# scripts/release.sh
#
# Cuts a release of the plugin itself:
# 1. Bumps the &version; entity in plugin/podman.plg to <new-version>.
# 2. Builds all seven packages (scripts/build-packages.sh) unless
# SKIP_BUILD=1 is set (useful when CI already built them in a prior job
# and only wants this script to do the .plg/CHANGELOG bookkeeping).
# 3. Verifies + consolidates checksums (scripts/checksums.sh).
# 4. Rewrites the per-package <!ENTITY ..._txz_version/_txz_file/_txz_md5>
# entities in plugin/podman.plg from the freshly built dist/*.txz.md5
# sidecar files, and the &baseURL; entity to point at the GitHub
# Release that will hold these assets.
# 5. Moves CHANGELOG.md's [Unreleased] section under a new dated heading.
#
# This script deliberately does NOT `git commit`, `git tag`, or `gh release
# create` — it only prepares files. Committing/tagging/publishing is left to
# the caller (locally) or to .github/workflows/release.yml (in CI), so a
# human always reviews the diff before anything becomes public. See
# docs/ARCHITECTURE.md section 13 (Updates).
#
# Usage:
# scripts/release.sh <new-version> # e.g. scripts/release.sh 0.2.0
#
# Env vars:
# SKIP_BUILD=1 Skip scripts/build-packages.sh (assume dist/ already built)
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PLG_FILE="$REPO_ROOT/plugin/podman.plg"
CHANGELOG_FILE="$REPO_ROOT/CHANGELOG.md"
DIST_DIR="$REPO_ROOT/dist"
NEW_VERSION="${1:-}"
if [ -z "$NEW_VERSION" ]; then
echo "usage: $0 <new-version>" >&2
exit 1
fi
if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "!! <new-version> must be plain SemVer (X.Y.Z), got: $NEW_VERSION" >&2
exit 1
fi
# The GitHub Release tag/URL this release's assets will be published under.
# Must match whatever .github/workflows/release.yml actually creates the
# release as (tag "v<version>") — see that workflow for the release job.
RELEASE_TAG="v$NEW_VERSION"
REPO_SLUG="${GITHUB_REPOSITORY:-OWNER/unraid-podman}"
RELEASE_BASE_URL="https://github.com/$REPO_SLUG/releases/download/$RELEASE_TAG"
# Component name -> the entity name prefix used in podman.plg. Must match
# plugin/podman.plg's <!ENTITY NAME_txz_...> declarations exactly.
# unraid-podman is the plugin's own scaffolding package (see
# packages/unraid-podman/README.md), not an upstream component, but it's
# released and entity-updated exactly like the other seven.
COMPONENTS=(podman conmon crun netavark aardvark-dns passt fuse-overlayfs unraid-podman)
echo "==> Releasing unraid-podman plugin v$NEW_VERSION (packages tag: $RELEASE_TAG)"
# --- 1. Build ----------------------------------------------------------------
if [ "${SKIP_BUILD:-0}" != "1" ]; then
echo "==> Building all packages"
"$REPO_ROOT/scripts/build-packages.sh"
else
echo "==> SKIP_BUILD=1, assuming $DIST_DIR is already populated"
fi
"$REPO_ROOT/scripts/checksums.sh" "$DIST_DIR"
# --- 2. Update plugin version entity ------------------------------------------
echo "==> Setting <!ENTITY version> to $NEW_VERSION in $PLG_FILE"
sed -i -E "s|(<!ENTITY version[[:space:]]+\")[^\"]*(\">)|\1${NEW_VERSION}\2|" "$PLG_FILE"
sed -i -E "s|(<!ENTITY baseURL[[:space:]]+\")[^\"]*(\">)|\1${RELEASE_BASE_URL}\2|" "$PLG_FILE"
# --- 3. Update per-package entities from dist/*.txz.md5 -----------------------
for name in "${COMPONENTS[@]}"; do
# dist/ contains files like podman-6.0.1-x86_64-1_unraidpodman.txz — find
# the one for this component (there should be exactly one per release).
txz_path=$(find "$DIST_DIR" -maxdepth 1 -name "${name}-*-*-*.txz" | head -n1)
if [ -z "$txz_path" ]; then
echo "!! No built .txz found for component '$name' in $DIST_DIR" >&2
echo "!! Did scripts/build-packages.sh run successfully for it?" >&2
exit 1
fi
txz_file=$(basename "$txz_path")
md5_path="$txz_path.md5"
if [ ! -f "$md5_path" ]; then
echo "!! Missing $md5_path" >&2
exit 1
fi
md5=$(awk '{print $1}' "$md5_path")
# Component version is everything between "<name>-" and the next "-<arch>-"
# e.g. "podman-6.0.1-x86_64-1_unraidpodman.txz" -> "6.0.1"
txz_version=$(echo "$txz_file" | sed -E "s/^${name}-(.+)-[^-]+-[0-9]+[^-]*\.txz\$/\1/")
entity_prefix=$(echo "$name" | tr '-' '_')
echo "==> [$name] $txz_file (md5=$md5)"
sed -i -E "s|(<!ENTITY ${entity_prefix}_txz_version[[:space:]]+\")[^\"]*(\">)|\1${txz_version}\2|" "$PLG_FILE"
sed -i -E "s|(<!ENTITY ${entity_prefix}_txz_file[[:space:]]+\")[^\"]*(\">)|\1${txz_file}\2|" "$PLG_FILE"
sed -i -E "s|(<!ENTITY ${entity_prefix}_txz_md5[[:space:]]+\")[^\"]*(\">)|\1${md5}\2|" "$PLG_FILE"
done
# --- 4. Update CHANGELOG.md ---------------------------------------------------
# Pure awk (no python/perl dependency — this script also has to run inside
# the minimal Slackware build container, see .github/workflows/release.yml):
# inserts a fresh "## [<version>] - <date>" heading right after the existing
# "## [Unreleased]" marker, leaving [Unreleased] itself empty and at the top
# for the next round of changes.
echo "==> Moving CHANGELOG.md [Unreleased] section under [$NEW_VERSION]"
TODAY=$(date -u +%Y-%m-%d)
if ! grep -q '^## \[Unreleased\]$' "$CHANGELOG_FILE"; then
echo "!! No '## [Unreleased]' heading found in $CHANGELOG_FILE" >&2
exit 1
fi
awk -v ver="$NEW_VERSION" -v date="$TODAY" '
!done && $0 == "## [Unreleased]" {
print $0
print ""
print "## [" ver "] - " date
done = 1
next
}
{ print $0 }
' "$CHANGELOG_FILE" > "$CHANGELOG_FILE.tmp"
mv "$CHANGELOG_FILE.tmp" "$CHANGELOG_FILE"
echo
echo "==> Release preparation complete for v$NEW_VERSION."
echo "==> Review the diff, then:"
echo " git add plugin/podman.plg CHANGELOG.md"
echo " git commit -m \"release: v$NEW_VERSION\""
echo " git tag $RELEASE_TAG"
echo " git push && git push origin $RELEASE_TAG"
echo "==> Pushing the tag triggers .github/workflows/release.yml, which"
echo "==> rebuilds artifacts in CI (for reproducibility/provenance) and"
echo "==> publishes the GitHub Release with dist/*.txz + CHECKSUMS.* + the"
echo "==> updated podman.plg attached."
+151
View File
@@ -0,0 +1,151 @@
#!/bin/bash
# =============================================================================
# scripts/update-versions.sh
#
# Re-pins one (or all) upstream components in versions.env to their current
# latest release, recomputing the SHA256 checksum against the freshly
# downloaded source tarball. This is the ONLY supported way to change a
# version/checksum pair in versions.env — never hand-edit a checksum, since
# that defeats the entire point of pinning it (see the header comment in
# versions.env).
#
# Usage:
# scripts/update-versions.sh # check/update all components
# scripts/update-versions.sh podman crun # only these components
#
# This script only rewrites versions.env. It does not build anything, and it
# does not commit — review the diff (`git diff versions.env`) before
# committing, ideally by also running a build to confirm the new source
# still compiles (scripts/build-packages.sh <name>).
# =============================================================================
set -eu
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSIONS_FILE="$REPO_ROOT/versions.env"
# Maps our internal component name -> GitHub "owner/repo", for every
# component that actually has GitHub-tagged releases. passt is handled
# separately below (see versions.env for why).
declare -A GITHUB_REPO=(
[podman]="containers/podman"
[conmon]="containers/conmon"
[crun]="containers/crun"
[netavark]="containers/netavark"
[aardvark-dns]="containers/aardvark-dns"
[fuse-overlayfs]="containers/fuse-overlayfs"
)
# Maps our internal component name -> the *_VERSION variable prefix used in
# versions.env (uppercased, hyphens -> underscores).
env_prefix() {
echo "$1" | tr '[:lower:]-' '[:upper:]_'
}
update_github_component() {
local name="$1"
local repo="${GITHUB_REPO[$name]}"
local prefix
prefix=$(env_prefix "$name")
echo "==> [$name] checking latest release for $repo"
local api_response
api_response=$(curl -sL --max-time 15 "https://api.github.com/repos/$repo/releases/latest")
local tag
tag=$(echo "$api_response" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/')
if [ -z "$tag" ]; then
echo "!! [$name] could not determine latest tag (rate-limited or repo has no releases?)" >&2
return 1
fi
# Strip a leading "v" for the version we store, but keep it for the URL
# since GitHub tags for these projects are inconsistent about it (crun
# tags plain "1.28", others tag "v1.28").
local version="${tag#v}"
local url="https://github.com/$repo/archive/refs/tags/$tag.tar.gz"
echo "==> [$name] latest = $version, downloading to verify + checksum"
local tmpfile
tmpfile=$(mktemp)
curl -fL --max-time 120 -o "$tmpfile" "$url"
local sha256
sha256=$(sha256sum "$tmpfile" | awk '{print $1}')
rm -f "$tmpfile"
echo "==> [$name] sha256=$sha256"
apply_update "$prefix" "$version" "$url" "$sha256"
}
apply_update() {
local prefix="$1" version="$2" url="$3" sha256="$4"
# In-place rewrite of the three lines for this component. Using distinct
# sed expressions per variable (rather than one blanket substitution)
# keeps this safe even if variable order in versions.env changes.
sed -i \
-e "s|^${prefix}_VERSION=.*|${prefix}_VERSION=\"${version}\"|" \
-e "s|^${prefix}_SRC_SHA256=.*|${prefix}_SRC_SHA256=\"${sha256}\"|" \
"$VERSIONS_FILE"
# The *_SRC_URL line is templated against *_VERSION (e.g.
# ".../v${PODMAN_VERSION}.tar.gz") in most cases, so it doesn't need
# rewriting — only touch it if it isn't already parameterized.
if ! grep -q "^${prefix}_SRC_URL=.*\${${prefix}_VERSION}" "$VERSIONS_FILE" \
&& ! grep -q "^${prefix}_SRC_URL=.*\$${prefix}_VERSION" "$VERSIONS_FILE"; then
sed -i -e "s|^${prefix}_SRC_URL=.*|${prefix}_SRC_URL=\"${url}\"|" "$VERSIONS_FILE"
fi
echo "==> updated ${prefix}_VERSION / ${prefix}_SRC_SHA256 in $VERSIONS_FILE"
}
update_passt() {
echo "==> [passt] checking latest master commit at https://passt.top/passt/"
local atom
atom=$(curl -sL --max-time 15 "https://passt.top/passt/atom/?h=master")
local commit
commit=$(echo "$atom" | grep -m1 -oE '<id>[a-f0-9]{40}</id>' | sed -E 's/<\/?id>//g')
if [ -z "$commit" ]; then
echo "!! [passt] could not determine latest commit" >&2
return 1
fi
local url="https://passt.top/passt/snapshot/passt-${commit}.tar.gz"
echo "==> [passt] latest commit = $commit, downloading to verify + checksum"
local tmpfile
tmpfile=$(mktemp)
curl -fL --max-time 120 -o "$tmpfile" "$url"
local sha256
sha256=$(sha256sum "$tmpfile" | awk '{print $1}')
rm -f "$tmpfile"
sed -i \
-e "s|^PASST_COMMIT=.*|PASST_COMMIT=\"${commit}\"|" \
-e "s|^PASST_VERSION=.*|PASST_VERSION=\"git${commit:0:7}\"|" \
-e "s|^PASST_SRC_SHA256=.*|PASST_SRC_SHA256=\"${sha256}\"|" \
"$VERSIONS_FILE"
echo "==> updated PASST_COMMIT / PASST_VERSION / PASST_SRC_SHA256 in $VERSIONS_FILE"
}
requested=("$@")
if [ "${#requested[@]}" -eq 0 ]; then
requested=("${!GITHUB_REPO[@]}" passt)
fi
for name in "${requested[@]}"; do
if [ "$name" = "passt" ]; then
update_passt
elif [ -n "${GITHUB_REPO[$name]:-}" ]; then
update_github_component "$name"
else
echo "!! Unknown component: $name" >&2
exit 1
fi
done
echo
echo "==> Done. Review the diff before committing:"
echo " git -C \"$REPO_ROOT\" diff versions.env"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
#
# version-bump.sh — bumps the plugin version entity in plugin/podman.plg
# without performing a full release (useful for pre-release testing builds).
#
# Usage (planned): scripts/version-bump.sh <new-version>
#
# STATUS: placeholder skeleton, not yet functional.
set -eu
NEW_VERSION="${1:-}"
if [ -z "$NEW_VERSION" ]; then
echo "usage: $0 <new-version>" >&2
exit 1
fi
# TODO: update <!ENTITY version "..."> in plugin/podman.plg
echo "TODO: implement version-bump.sh for version $NEW_VERSION"
exit 1
+86
View File
@@ -0,0 +1,86 @@
# =============================================================================
# versions.env — single source of truth for upstream component versions.
#
# Every SlackBuild under packages/*/ and the orchestrator
# (scripts/build-packages.sh) source this file instead of hardcoding a
# version or URL. This is what makes the build reproducible: given the same
# versions.env, the same source tarballs (verified by SHA256) are fetched and
# built, every time.
#
# To bump a component's version, run scripts/update-versions.sh <component>
# (fetches the new upstream release, recomputes the checksum, rewrites the
# corresponding block below) rather than editing hashes by hand.
#
# SHA256 sums below were computed directly against the upstream source
# tarball/snapshot at the time of pinning (see the fetch command in each
# comment). GitHub's auto-generated "archive/refs/tags" tarballs are stable
# in practice but are NOT cryptographically signed by upstream — treat this
# checksum as tamper-evidence against a compromised mirror/CDN, not as a
# replacement for verifying upstream's own release signing where available.
# =============================================================================
# --- podman ------------------------------------------------------------------
# https://github.com/containers/podman
PODMAN_VERSION="6.0.1"
PODMAN_SRC_URL="https://github.com/containers/podman/archive/refs/tags/v${PODMAN_VERSION}.tar.gz"
PODMAN_SRC_SHA256="4829d7c1423523a6a4d5537dea7968ae7f6c22ed7f1d5f416638fd81c83caa47"
# --- conmon --------------------------------------------------------------
# https://github.com/containers/conmon
CONMON_VERSION="2.2.1"
CONMON_SRC_URL="https://github.com/containers/conmon/archive/refs/tags/v${CONMON_VERSION}.tar.gz"
CONMON_SRC_SHA256="814fb5979a3a4b8576b1f901e606b482bebb41cb7e57926e6d5765ee786b96d3"
# --- crun ----------------------------------------------------------------
# https://github.com/containers/crun
CRUN_VERSION="1.28"
CRUN_SRC_URL="https://github.com/containers/crun/archive/refs/tags/${CRUN_VERSION}.tar.gz"
CRUN_SRC_SHA256="90284c7f097f8ee72a6447978c263e1b1355727c2f2ca0ac667e6d57788f46f5"
# --- netavark --------------------------------------------------------------
# https://github.com/containers/netavark
NETAVARK_VERSION="2.0.0"
NETAVARK_SRC_URL="https://github.com/containers/netavark/archive/refs/tags/v${NETAVARK_VERSION}.tar.gz"
NETAVARK_SRC_SHA256="031aeeacc930382e8635d40a885798eff1da164dfcf9024b698f822e5995d9c8"
# --- aardvark-dns ----------------------------------------------------------
# https://github.com/containers/aardvark-dns
AARDVARK_DNS_VERSION="2.0.0"
AARDVARK_DNS_SRC_URL="https://github.com/containers/aardvark-dns/archive/refs/tags/v${AARDVARK_DNS_VERSION}.tar.gz"
AARDVARK_DNS_SRC_SHA256="d3f5d6b3be3c2d80e8257fb9467e34ff104f299474427979454034dca6dc88cc"
# --- fuse-overlayfs --------------------------------------------------------
# https://github.com/containers/fuse-overlayfs
FUSE_OVERLAYFS_VERSION="1.17"
FUSE_OVERLAYFS_SRC_URL="https://github.com/containers/fuse-overlayfs/archive/refs/tags/v${FUSE_OVERLAYFS_VERSION}.tar.gz"
FUSE_OVERLAYFS_SRC_SHA256="cefffecfbb001b2784f19af344f27eae07b31a4faa38d345b738af96b2bec59e"
# --- passt -------------------------------------------------------------------
# https://passt.top/passt/about/ — "Plug A Simple Socket Transport". Podman's
# modern (post-slirp4netns) rootless network transport. Upstream has NO
# GitHub mirror and no semver tags; it is released continuously from the
# cgit-hosted git repository at https://passt.top/passt/, identified by full
# git commit hash. We pin to a specific commit snapshot for reproducibility,
# fetched via cgit's snapshot endpoint:
# https://passt.top/passt/snapshot/passt-<commit>.tar.gz
PASST_COMMIT="6ef3d1c86ffc690a17a9a4445df4a741446bcd44"
PASST_VERSION="git${PASST_COMMIT:0:7}"
PASST_SRC_URL="https://passt.top/passt/snapshot/passt-${PASST_COMMIT}.tar.gz"
PASST_SRC_SHA256="4c58a77504a77d613464dddf22ae69d749a5ba64cb87e44c3b8c252333e209fc"
# =============================================================================
# Slackware package BUILD number (not upstream version). Bump this if a
# package must be rebuilt without an upstream version change (e.g. a
# packaging-only fix). Reset to 1 whenever *_VERSION above changes.
# =============================================================================
PKG_BUILD="1"
# Slackware package architecture. Unraid is x86_64-only today; kept as a
# variable rather than hardcoded so the build scripts don't need a second
# source of truth if that ever changes.
PKG_ARCH="x86_64"
# Suffix appended to every package's tag field (Slackware convention:
# <name>-<version>-<arch>-<build><tag>.txz), identifies packages built by
# this project as opposed to a stock Slackware/SBo package of the same name.
PKG_TAG="_unraidpodman"
+72
View File
@@ -0,0 +1,72 @@
# webui/
Dynamix-style WebUI pages, following Unraid's plugin GUI convention of
`/usr/local/emhttp/plugins/<name>/`. `webui/plugins/podman/` is staged to
that path by the `unraid-podman` scaffolding package (see
`packages/unraid-podman/unraid-podman.SlackBuild`), which podman.plg installs
alongside the seven compiled components.
**Status: implemented**, covering all ten sections from
[docs/ARCHITECTURE.md, section 18](../docs/ARCHITECTURE.md#18-zukünftige-webui):
Dashboard, Containers, Pods, Images, Volumes, Networks, Logs, Terminal,
Compose, Settings. Not yet exercised against a real Unraid/Podman install —
see [docs/ROADMAP.md](../docs/ROADMAP.md) for what "implemented" does and
doesn't cover yet.
`webui/mockups/prototype.html` is the static, non-PHP clickable mockup this
implementation was built against — kept as the visual reference; it is not
staged into the package.
## Structure
```
webui/
├── mockups/
│ └── prototype.html # static approved mockup, not shipped
└── plugins/
└── podman/
├── Podman.page # page shell: header, sub-nav, one container per panel
├── include/
│ ├── PodmanClient.php # libpod REST API client (talks to podman.sock only)
│ ├── Config.php # reads podman.cfg (mirrors podman-common.sh)
│ ├── bootstrap.php # shared include + error handling for ajax/*.php
│ └── helpers.php # formatting + JSON-response helpers
├── ajax/ # one endpoint per resource, each require()s bootstrap.php
│ ├── containers.php # list/start/stop/restart/remove/logs
│ ├── pods.php
│ ├── images.php
│ ├── volumes.php
│ ├── networks.php
│ ├── exec.php # Terminal — see its header comment for API scope
│ ├── compose.php # Compose — the one deliberate CLI exception, see header
│ ├── settings.php # plugin's own config, not a libpod resource
│ └── system.php # Dashboard aggregation
├── javascript/
│ ├── app.js # shared AJAX helper + sub-tab router
│ └── <panel>.js # one module per panel, registers with app.js
├── styles/podman.css # design tokens ported 1:1 from the mockup
├── event/ # official Unraid array-event hooks (see plugin/event/)
└── images/ # plugin icon assets
```
## Design constraints (see ARCHITECTURE.md for full rationale)
- Every panel talks to `podman system service` over its Unix socket via
`PodmanClient` — no `exec()`/`shell_exec()` of the `podman` binary
anywhere in `include/` or in the Containers/Pods/Images/Volumes/Networks/
Logs endpoints.
- **Two documented, deliberate exceptions**, not oversights:
- `ajax/exec.php` (Terminal) uses the real exec REST API, but as
one-command-in/output-out rather than a true interactive PTY — libpod's
interactive exec needs a persistent hijacked connection that doesn't
fit PHP-FPM's request lifecycle. See that file's header comment.
- `ajax/compose.php` (Compose) shells out to the `podman compose` CLI via
`proc_open()` with an argv array (never a shell string) — because no
REST endpoint for Compose exists in libpod at all. See that file's
header comment.
- The API socket is root-equivalent; no unauthenticated network exposure
beyond what Unraid's own WebUI auth already provides — see
[.github/SECURITY.md](../.github/SECURITY.md).
- Dark/light mode via CSS custom properties (`prefers-color-scheme` +
`[data-theme]` override), matching Unraid's own theme mechanism — no
separate theme toggle inside the plugin page.
+949
View File
@@ -0,0 +1,949 @@
<title>unraid-podman — WebUI Mockup</title>
<style>
/* =========================================================================
Design tokens
Grounded in Unraid's real webGUI chrome: near-black masthead, a warm
construction-orange brand accent, dense admin-tool typography via the
OS's own system font stack (Unraid itself ships no custom webfont).
Semantic status colors (good/warn/bad/neutral) are kept separate from
the brand accent, per the container-fleet domain's own vocabulary.
========================================================================= */
:root {
--bg: #eef0f2;
--surface: #ffffff;
--surface-2: #f4f5f7;
--surface-3: #e9ebee;
--border: #dde1e6;
--text: #1c2024;
--text-dim: #5b6572;
--text-faint: #8a94a1;
--accent: #d8541a;
--accent-strong: #b8420f;
--accent-contrast: #fff8f3;
--good: #1a8f4c;
--good-bg: #e4f6ea;
--warn: #9a6b00;
--warn-bg: #fdf1d6;
--bad: #c22b3a;
--bad-bg: #fbe6e8;
--neutral: #5b6572;
--neutral-bg: #e9ebee;
--shadow: 0 1px 2px rgba(20, 22, 26, 0.06), 0 4px 12px rgba(20, 22, 26, 0.05);
--mast-bg: #16181c;
--mast-fg: #d7dae0;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Consolas, "Liberation Mono", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #15171b;
--surface: #1c1f24;
--surface-2: #23262c;
--surface-3: #2b2f36;
--border: #34383f;
--text: #e7e9ec;
--text-dim: #9aa1ab;
--text-faint: #6b7280;
--accent: #ef7f3f;
--accent-strong: #f6975f;
--accent-contrast: #1a1002;
--good: #4cc785;
--good-bg: #123322;
--warn: #e0b23d;
--warn-bg: #3a2e0d;
--bad: #ef6470;
--bad-bg: #3a1519;
--neutral: #9aa1ab;
--neutral-bg: #2b2f36;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.35);
--mast-bg: #0e0f11;
--mast-fg: #cdd1d7;
}
}
:root[data-theme="dark"] {
--bg: #15171b; --surface: #1c1f24; --surface-2: #23262c; --surface-3: #2b2f36;
--border: #34383f; --text: #e7e9ec; --text-dim: #9aa1ab; --text-faint: #6b7280;
--accent: #ef7f3f; --accent-strong: #f6975f; --accent-contrast: #1a1002;
--good: #4cc785; --good-bg: #123322; --warn: #e0b23d; --warn-bg: #3a2e0d;
--bad: #ef6470; --bad-bg: #3a1519; --neutral: #9aa1ab; --neutral-bg: #2b2f36;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 8px 24px rgba(0,0,0,.35);
--mast-bg: #0e0f11; --mast-fg: #cdd1d7;
}
:root[data-theme="light"] {
--bg: #eef0f2; --surface: #ffffff; --surface-2: #f4f5f7; --surface-3: #e9ebee;
--border: #dde1e6; --text: #1c2024; --text-dim: #5b6572; --text-faint: #8a94a1;
--accent: #d8541a; --accent-strong: #b8420f; --accent-contrast: #fff8f3;
--good: #1a8f4c; --good-bg: #e4f6ea; --warn: #9a6b00; --warn-bg: #fdf1d6;
--bad: #c22b3a; --bad-bg: #fbe6e8; --neutral: #5b6572; --neutral-bg: #e9ebee;
--shadow: 0 1px 2px rgba(20,22,26,.06), 0 4px 12px rgba(20,22,26,.05);
--mast-bg: #16181c; --mast-fg: #d7dae0;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-ui);
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 { text-wrap: balance; margin: 0; font-weight: 600; }
::selection { background: var(--accent); color: var(--accent-contrast); }
a { color: inherit; }
button, input, select { font-family: inherit; font-size: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.tnum { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--font-mono); }
/* ---- Masthead (real Unraid top bar, non-interactive context) ---- */
.mast {
background: var(--mast-bg);
color: var(--mast-fg);
display: flex;
align-items: center;
gap: 14px;
padding: 0 16px;
height: 46px;
}
.mast .brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: .02em; }
.mast .brand .mark {
width: 22px; height: 22px; border-radius: 5px;
background: linear-gradient(155deg, var(--accent), var(--accent-strong));
display: grid; place-items: center; color: #fff; font-size: 11px; font-weight: 800;
}
.mast .server { font-size: 12.5px; color: var(--mast-fg); opacity: .75; }
.mast .spacer { flex: 1; }
.mast .mast-icons { display: flex; gap: 16px; font-size: 13px; opacity: .7; }
.mainnav {
background: color-mix(in srgb, var(--mast-bg) 88%, var(--surface));
display: flex;
gap: 2px;
padding: 0 10px;
overflow-x: auto;
}
.mainnav a {
display: block;
padding: 10px 14px 8px;
font-size: 12px;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--mast-fg);
opacity: .55;
text-decoration: none;
border-bottom: 2px solid transparent;
white-space: nowrap;
cursor: default;
}
.mainnav a.active {
opacity: 1;
color: var(--accent-strong);
border-bottom-color: var(--accent);
font-weight: 700;
}
/* ---- Page header ---- */
.pagehead {
max-width: 1280px;
margin: 0 auto;
padding: 22px 24px 0;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.pagehead .titlewrap { display: flex; align-items: center; gap: 12px; }
.pagehead .icon {
width: 38px; height: 38px; border-radius: 9px;
background: var(--surface-3);
display: grid; place-items: center;
color: var(--accent);
border: 1px solid var(--border);
}
.pagehead h1 { font-size: 21px; }
.pagehead .meta { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; }
.pagehead .meta .dot { color: var(--good); }
.btn {
appearance: none;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
padding: 8px 14px;
border-radius: 7px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: border-color .12s, background .12s;
}
.btn:hover { border-color: var(--text-faint); }
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-contrast);
}
.btn-primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn-danger { color: var(--bad); }
.btn-danger:hover { border-color: var(--bad); }
.btn-icon { padding: 6px 8px; }
.btn[disabled] { opacity: .4; cursor: not-allowed; }
/* ---- Sub navigation (the 10 sections) ---- */
.subnav {
max-width: 1280px;
margin: 14px auto 0;
padding: 0 24px;
display: flex;
gap: 4px;
border-bottom: 1px solid var(--border);
overflow-x: auto;
}
.subnav button {
appearance: none;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-dim);
padding: 10px 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
display: flex; align-items: center; gap: 6px;
}
.subnav button:hover { color: var(--text); }
.subnav button.active { color: var(--accent-strong); border-bottom-color: var(--accent); }
.subnav button .n {
font-size: 10.5px; padding: 1px 5px; border-radius: 8px;
background: var(--surface-3); color: var(--text-dim);
}
.main { max-width: 1280px; margin: 0 auto; padding: 20px 24px 64px; }
.panel { display: none; animation: fade .15s ease; }
.panel.active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .panel { animation: none; } }
/* ---- Generic building blocks ---- */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: var(--shadow);
}
.card-pad { padding: 16px 18px; }
.card-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid var(--border);
}
.card-head h2 { font-size: 14.5px; }
.card-head .sub { font-size: 12px; color: var(--text-faint); font-weight: 500; }
.grid { display: grid; gap: 14px; }
.stat-grid { grid-template-columns: repeat(6, 1fr); }
@media (max-width: 1080px) { .stat-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 620px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } }
.stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
box-shadow: var(--shadow);
}
.stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); font-weight: 700; }
.stat .value { font-size: 24px; font-weight: 700; margin-top: 6px; }
.stat .value small { font-size: 13px; color: var(--text-dim); font-weight: 600; }
.stat .bar { height: 5px; border-radius: 3px; background: var(--surface-3); margin-top: 10px; overflow: hidden; }
.stat .bar > span { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
.chip {
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 9px; border-radius: 100px;
font-size: 11.5px; font-weight: 700; letter-spacing: .01em;
white-space: nowrap;
}
.chip .d { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.chip-good { background: var(--good-bg); color: var(--good); }
.chip-warn { background: var(--warn-bg); color: var(--warn); }
.chip-bad { background: var(--bad-bg); color: var(--bad); }
.chip-neutral { background: var(--neutral-bg); color: var(--neutral); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
color: var(--text-faint); font-weight: 700;
padding: 10px 18px; border-bottom: 1px solid var(--border);
}
tbody td { padding: 11px 18px; border-bottom: 1px solid var(--border); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--surface-2); }
.table-wrap { overflow-x: auto; }
.row-name { display: flex; align-items: center; gap: 10px; font-weight: 600; }
.row-name .ico {
width: 26px; height: 26px; border-radius: 6px; flex: none;
background: var(--surface-3); display: grid; place-items: center; font-size: 12px;
border: 1px solid var(--border); color: var(--text-dim);
}
.row-sub { font-size: 11.5px; color: var(--text-faint); font-weight: 500; margin-top: 1px; }
.actions { display: flex; gap: 4px; justify-content: flex-end; }
.usage-mini { display: flex; align-items: center; gap: 8px; min-width: 110px; }
.usage-mini .track { flex: 1; height: 5px; border-radius: 3px; background: var(--surface-3); overflow: hidden; }
.usage-mini .track > span { display: block; height: 100%; background: var(--accent); }
.usage-mini .num { font-size: 11.5px; color: var(--text-dim); width: 34px; text-align: right; }
.toolbar { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.search {
flex: 1; min-width: 180px;
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px;
padding: 7px 11px; font-size: 13px; color: var(--text);
}
.search::placeholder { color: var(--text-faint); }
.filterset { display: flex; gap: 4px; background: var(--surface-2); padding: 3px; border-radius: 8px; }
.filterset button {
appearance: none; border: none; background: none; color: var(--text-dim);
font-size: 12px; font-weight: 700; padding: 5px 10px; border-radius: 6px; cursor: pointer;
}
.filterset button.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
.two-col { display: grid; grid-template-columns: 1.3fr 1fr; gap: 14px; align-items: start; }
@media (max-width: 920px) { .two-col { grid-template-columns: 1fr; } }
.activity-list { list-style: none; margin: 0; padding: 6px 0; }
.activity-list li { display: flex; gap: 10px; padding: 9px 18px; border-bottom: 1px solid var(--border); font-size: 12.5px; }
.activity-list li:last-child { border-bottom: none; }
.activity-list .dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 5px; flex: none; }
.activity-list .txt strong { font-weight: 600; }
.activity-list .when { color: var(--text-faint); font-size: 11px; margin-top: 1px; }
.empty-note { padding: 28px 18px; text-align: center; color: var(--text-faint); font-size: 13px; }
/* ---- Pods ---- */
.pod-card { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 14px; background: var(--surface); box-shadow: var(--shadow); }
.pod-card:last-child { margin-bottom: 0; }
.pod-head { display: flex; align-items: center; gap: 10px; padding: 13px 16px; background: var(--surface-2); border-bottom: 1px solid var(--border); }
.pod-head .name { font-weight: 700; font-size: 13.5px; }
.pod-head .infra { font-size: 11.5px; color: var(--text-faint); }
.pod-body table { font-size: 12.5px; }
.pod-body td, .pod-body th { padding: 8px 16px 8px 40px; }
/* ---- Logs ---- */
.logs-layout { display: grid; grid-template-columns: 200px 1fr; min-height: 460px; }
@media (max-width: 760px) { .logs-layout { grid-template-columns: 1fr; } }
.logs-side { border-right: 1px solid var(--border); }
.logs-side .item { padding: 10px 14px; font-size: 12.5px; border-bottom: 1px solid var(--border); cursor: default; display: flex; align-items: center; gap: 8px; }
.logs-side .item.active { background: var(--surface-2); font-weight: 700; box-shadow: inset 2px 0 0 var(--accent); }
.log-toolbar { display: flex; gap: 8px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); }
.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;
}
:root:not([data-theme="dark"]) .log-pane { background: #14161a; }
.log-pane .l { white-space: pre-wrap; word-break: break-word; }
.log-pane .ts { color: #6b7280; }
.log-pane .lvl-info { color: #6fb2f5; }
.log-pane .lvl-warn { color: #e0b23d; }
.log-pane .lvl-error { color: #ef6470; }
/* ---- Terminal ---- */
.term-wrap { padding: 16px 18px; }
.term-select { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; font-size: 12.5px; color: var(--text-dim); }
.term-select select {
background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; color: var(--text);
}
.term {
background: #0f1114; color: #d7dbe0; font-family: var(--font-mono); font-size: 12.6px;
border-radius: 8px; padding: 14px 16px; height: 380px; overflow-y: auto; line-height: 1.7;
}
.term .prompt { color: #4cc785; }
.term .path { color: #6fb2f5; }
.term .cur { display: inline-block; width: 7px; height: 14px; background: #d7dbe0; vertical-align: text-bottom; animation: blink 1s step-end infinite; }
@media (prefers-reduced-motion: reduce) { .term .cur { animation: none; } }
@keyframes blink { 50% { opacity: 0; } }
/* ---- Compose ---- */
.compose-layout { display: grid; grid-template-columns: 230px 1fr; min-height: 480px; }
@media (max-width: 800px) { .compose-layout { grid-template-columns: 1fr; } }
.compose-side { border-right: 1px solid var(--border); }
.compose-proj { padding: 12px 14px; border-bottom: 1px solid var(--border); cursor: default; }
.compose-proj.active { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
.compose-proj .name { font-weight: 700; font-size: 13px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.compose-proj .path { font-size: 11px; color: var(--text-faint); margin-top: 2px; font-family: var(--font-mono); }
.compose-body-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); gap: 10px; flex-wrap: wrap; }
.yaml { background: #0f1114; color: #c7ccd4; font-family: var(--font-mono); font-size: 12.4px; padding: 16px 18px; height: 420px; overflow: auto; line-height: 1.7; }
:root:not([data-theme="dark"]) .yaml { background: #14161a; }
.yaml .k { color: #ef9f76; }
.yaml .s { color: #9fd88a; }
.yaml .c { color: #6b7280; font-style: italic; }
.yaml .p { color: #6fb2f5; }
/* ---- Settings ---- */
.settings-grid { display: grid; gap: 14px; }
.field-row { display: grid; grid-template-columns: 220px 1fr; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border); align-items: start; }
.field-row:last-child { border-bottom: none; }
.field-row label { font-weight: 600; font-size: 13px; }
.field-row .hint { font-size: 11.5px; color: var(--text-faint); margin-top: 4px; max-width: 46ch; }
.field-row input[type="text"], .field-row input[type="number"], .field-row select {
background: var(--surface-2); border: 1px solid var(--border); border-radius: 7px;
padding: 8px 10px; font-size: 13px; color: var(--text); width: 100%; max-width: 340px;
}
.field-row .unit { font-size: 12px; color: var(--text-dim); margin-left: 8px; }
.switch { position: relative; display: inline-block; width: 38px; height: 22px; }
.switch input { display: none; }
.switch .track { position: absolute; inset: 0; background: var(--surface-3); border: 1px solid var(--border); border-radius: 20px; transition: .15s; }
.switch .track::after { content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%; background: var(--surface); top: 2px; left: 2px; transition: .15s; box-shadow: 0 1px 2px rgba(0,0,0,.3); }
.switch input:checked + .track { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .track::after { transform: translateX(16px); background: #fff; }
.danger-card { border-color: color-mix(in srgb, var(--bad) 40%, var(--border)); }
.danger-card .card-head { border-bottom-color: color-mix(in srgb, var(--bad) 30%, var(--border)); }
.danger-card .card-head h2 { color: var(--bad); }
.badge-update { font-size: 10px; font-weight: 700; color: var(--accent-strong); background: color-mix(in srgb, var(--accent) 16%, transparent); padding: 2px 7px; border-radius: 100px; margin-left: 8px; }
footer.note { max-width: 1280px; margin: 0 auto; padding: 4px 24px 40px; font-size: 11.5px; color: var(--text-faint); }
</style>
<!-- =========================================================================
Real Unraid chrome (masthead + main nav) — recreated for context, not
interactive. Only "Podman" is the actual subject of this mockup.
========================================================================= -->
<div class="mast">
<div class="brand"><span class="mark">U</span> unraid</div>
<div class="server">TOWER · 14d 6h uptime · Array Started</div>
<div class="spacer"></div>
<div class="mast-icons"><span>&#9432; Updates</span><span>&#128276;</span><span>&#128100; root</span></div>
</div>
<nav class="mainnav">
<a>Dashboard</a><a>Main</a><a>Shares</a><a>Users</a><a>Settings</a><a>Plugins</a><a>Docker</a><a>VMs</a>
<a class="active">Podman</a><a>Tools</a>
</nav>
<header class="pagehead">
<div class="titlewrap">
<div class="icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 2l8 4.5v9L12 20l-8-4.5v-9L12 2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 11l8-4.5M12 11v9M12 11L4 6.5" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
</div>
<div>
<h1>Podman</h1>
<div class="meta"><span class="dot">&#9679;</span> podman.sock connected &nbsp;·&nbsp; 5 of 8 containers running &nbsp;·&nbsp; v6.0.1</div>
</div>
</div>
<div style="display:flex; gap:8px;">
<button class="btn">&#8635; Refresh</button>
<button class="btn btn-primary">+ New Container</button>
</div>
</header>
<nav class="subnav" id="subnav">
<button class="active" data-panel="dashboard">Dashboard</button>
<button data-panel="containers">Containers <span class="n">8</span></button>
<button data-panel="pods">Pods <span class="n">2</span></button>
<button data-panel="images">Images <span class="n">14</span></button>
<button data-panel="volumes">Volumes <span class="n">9</span></button>
<button data-panel="networks">Networks <span class="n">3</span></button>
<button data-panel="logs">Logs</button>
<button data-panel="terminal">Terminal</button>
<button data-panel="compose">Compose</button>
<button data-panel="settings">Settings</button>
</nav>
<main class="main">
<!-- ============================= DASHBOARD ============================= -->
<section class="panel active" id="panel-dashboard">
<div class="grid stat-grid">
<div class="stat"><div class="label">Running</div><div class="value tnum">5 <small>/ 8</small></div></div>
<div class="stat"><div class="label">Pods</div><div class="value tnum">2</div></div>
<div class="stat"><div class="label">Images</div><div class="value tnum">14</div></div>
<div class="stat"><div class="label">Volumes</div><div class="value tnum">9</div></div>
<div class="stat"><div class="label">Networks</div><div class="value tnum">3</div></div>
<div class="stat">
<div class="label">Storage Used</div>
<div class="value tnum">62.4 <small>/ 100 GB</small></div>
<div class="bar"><span style="width:62%"></span></div>
</div>
</div>
<div class="two-col" style="margin-top:14px;">
<div class="card">
<div class="card-head"><h2>Resource Usage</h2><span class="sub">last 60 min</span></div>
<div class="card-pad">
<svg viewBox="0 0 560 140" width="100%" height="140" preserveAspectRatio="none" role="img" aria-label="CPU and memory usage over the last 60 minutes">
<defs>
<linearGradient id="cpuFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="var(--accent)" stop-opacity="0.35"/>
<stop offset="1" stop-color="var(--accent)" stop-opacity="0"/>
</linearGradient>
</defs>
<g stroke="var(--border)" stroke-width="1">
<line x1="0" y1="10" x2="560" y2="10"/><line x1="0" y1="45" x2="560" y2="45"/>
<line x1="0" y1="80" x2="560" y2="80"/><line x1="0" y1="115" x2="560" y2="115"/>
</g>
<path d="M0,100 L40,92 L80,96 L120,70 L160,74 L200,60 L240,66 L280,50 L320,58 L360,42 L400,48 L440,36 L480,40 L520,30 L560,34 L560,140 L0,140 Z" fill="url(#cpuFill)"/>
<path d="M0,100 L40,92 L80,96 L120,70 L160,74 L200,60 L240,66 L280,50 L320,58 L360,42 L400,48 L440,36 L480,40 L520,30 L560,34" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="560" cy="34" r="3.5" fill="var(--accent)"/>
<path d="M0,120 L40,118 L80,121 L120,112 L160,116 L200,108 L240,110 L280,104 L320,109 L360,100 L400,103 L440,96 L480,99 L520,92 L560,95" fill="none" stroke="var(--text-faint)" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="1 4"/>
<circle cx="560" cy="95" r="3" fill="var(--text-faint)"/>
</svg>
<div style="display:flex; gap:18px; margin-top:6px; font-size:11.5px; color:var(--text-dim);">
<span><span style="color:var(--accent);">&#9679;</span> CPU · 24%</span>
<span><span style="color:var(--text-faint);">&#9679;</span> Memory · 6.1 / 16 GB</span>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h2>Recent Activity</h2></div>
<ul class="activity-list">
<li><span class="dot" style="background:var(--good)"></span><div class="txt"><div><strong>sonarr</strong> started</div><div class="when">2 min ago</div></div></li>
<li><span class="dot" style="background:var(--bad)"></span><div class="txt"><div><strong>postgres</strong> health check failed</div><div class="when">11 min ago</div></div></li>
<li><span class="dot" style="background:var(--accent)"></span><div class="txt"><div>Pulled <strong>radarr:latest</strong> (image updated)</div><div class="when">38 min ago</div></div></li>
<li><span class="dot" style="background:var(--good)"></span><div class="txt"><div>Autostart chain completed (7/8)</div><div class="when">6h ago · array start</div></div></li>
<li><span class="dot" style="background:var(--warn)"></span><div class="txt"><div><strong>watchtower</strong> paused (autostart Safe-Mode)</div><div class="when">6h ago</div></div></li>
<li><span class="dot" style="background:var(--neutral)"></span><div class="txt"><div>Network <strong>media-net</strong> created</div><div class="when">2d ago</div></div></li>
</ul>
</div>
</div>
<div class="card" style="margin-top:14px;">
<div class="card-head"><h2>Autostart Queue</h2><span class="sub">/boot/config/plugins/podman/autostart</span></div>
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Container</th><th>Delay</th><th>Last Result</th></tr></thead>
<tbody>
<tr><td class="tnum">1</td><td>postgres</td><td class="tnum"></td><td><span class="chip chip-good"><span class="d"></span>started</span></td></tr>
<tr><td class="tnum">2</td><td>nextcloud</td><td class="tnum">5s</td><td><span class="chip chip-good"><span class="d"></span>started</span></td></tr>
<tr><td class="tnum">3</td><td>sonarr</td><td class="tnum"></td><td><span class="chip chip-good"><span class="d"></span>started</span></td></tr>
<tr><td class="tnum">4</td><td>radarr</td><td class="tnum"></td><td><span class="chip chip-good"><span class="d"></span>started</span></td></tr>
<tr><td class="tnum">5</td><td>watchtower</td><td class="tnum"></td><td><span class="chip chip-warn"><span class="d"></span>Safe-Mode (3 failures)</span></td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============================= CONTAINERS ============================= -->
<section class="panel" id="panel-containers">
<div class="card">
<div class="toolbar">
<input class="search" type="text" placeholder="Search containers by name or image…">
<div class="filterset">
<button class="active">All 8</button><button>Running 5</button><button>Stopped 2</button><button>Unhealthy 1</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Status</th><th>Name</th><th>Image</th><th>CPU</th><th>Memory</th><th>Ports</th><th>Uptime</th><th></th></tr></thead>
<tbody>
<tr>
<td><span class="chip chip-good"><span class="d"></span>running</span></td>
<td><div class="row-name"><span class="ico">Px</span>plex</div></td>
<td class="mono row-sub">lscr.io/linuxserver/plex:latest</td>
<td class="tnum">18%</td>
<td><div class="usage-mini"><div class="track"><span style="width:41%"></span></div><span class="num tnum">1.6G</span></div></td>
<td class="mono row-sub">32400/tcp</td>
<td class="tnum">14d 6h</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
<tr>
<td><span class="chip chip-good"><span class="d"></span>running</span></td>
<td><div class="row-name"><span class="ico">So</span>sonarr</div></td>
<td class="mono row-sub">lscr.io/linuxserver/sonarr:4.0</td>
<td class="tnum">3%</td>
<td><div class="usage-mini"><div class="track"><span style="width:22%"></span></div><span class="num tnum">420M</span></div></td>
<td class="mono row-sub">8989/tcp</td>
<td class="tnum">6h 12m</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
<tr>
<td><span class="chip chip-good"><span class="d"></span>running</span></td>
<td><div class="row-name"><span class="ico">Ra</span>radarr <span class="badge-update">update</span></div></td>
<td class="mono row-sub">lscr.io/linuxserver/radarr:5.2</td>
<td class="tnum">2%</td>
<td><div class="usage-mini"><div class="track"><span style="width:19%"></span></div><span class="num tnum">380M</span></div></td>
<td class="mono row-sub">7878/tcp</td>
<td class="tnum">6h 12m</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
<tr>
<td><span class="chip chip-bad"><span class="d"></span>unhealthy</span></td>
<td><div class="row-name"><span class="ico">Pg</span>postgres</div></td>
<td class="mono row-sub">postgres:16-alpine</td>
<td class="tnum">1%</td>
<td><div class="usage-mini"><div class="track"><span style="width:34%"></span></div><span class="num tnum">210M</span></div></td>
<td class="mono row-sub">5432/tcp</td>
<td class="tnum">2d 1h</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
<tr>
<td><span class="chip chip-good"><span class="d"></span>running</span></td>
<td><div class="row-name"><span class="ico">Nc</span>nextcloud</div></td>
<td class="mono row-sub">nextcloud:29-apache</td>
<td class="tnum">6%</td>
<td><div class="usage-mini"><div class="track"><span style="width:58%"></span></div><span class="num tnum">890M</span></div></td>
<td class="mono row-sub">443/tcp, 80/tcp</td>
<td class="tnum">6h 12m</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
<tr>
<td><span class="chip chip-neutral"><span class="d"></span>exited</span></td>
<td><div class="row-name"><span class="ico">Pr</span>prowlarr</div></td>
<td class="mono row-sub">lscr.io/linuxserver/prowlarr:1</td>
<td class="tnum"></td>
<td class="tnum"></td>
<td class="mono row-sub">9696/tcp</td>
<td class="tnum"></td>
<td class="actions"><button class="btn btn-icon" title="Start">&#9654;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Remove">&#128465;</button></td>
</tr>
<tr>
<td><span class="chip chip-warn"><span class="d"></span>Safe-Mode</span></td>
<td><div class="row-name"><span class="ico">Wt</span>watchtower</div></td>
<td class="mono row-sub">containrrr/watchtower:latest</td>
<td class="tnum"></td>
<td class="tnum"></td>
<td class="mono row-sub"></td>
<td class="tnum"></td>
<td class="actions"><button class="btn btn-icon" title="Start">&#9654;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Remove">&#128465;</button></td>
</tr>
<tr>
<td><span class="chip chip-good"><span class="d"></span>running</span></td>
<td><div class="row-name"><span class="ico">Ha</span>homeassistant</div></td>
<td class="mono row-sub">ghcr.io/home-assistant/home-assistant</td>
<td class="tnum">4%</td>
<td><div class="usage-mini"><div class="track"><span style="width:30%"></span></div><span class="num tnum">610M</span></div></td>
<td class="mono row-sub">8123/tcp</td>
<td class="tnum">3d 4h</td>
<td class="actions"><button class="btn btn-icon" title="Restart">&#8635;</button><button class="btn btn-icon" title="Logs">&#128196;</button><button class="btn btn-icon" title="Shell">&#9099;</button><button class="btn btn-icon" title="Stop">&#9632;</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============================= PODS ============================= -->
<section class="panel" id="panel-pods">
<div class="pod-card">
<div class="pod-head">
<span class="chip chip-good"><span class="d"></span>running</span>
<span class="name">media-stack</span>
<span class="infra">infra: localhost/podman-pause · shared network namespace</span>
</div>
<div class="pod-body table-wrap">
<table>
<thead><tr><th>Container</th><th>Image</th><th>Status</th><th>Ports</th><th></th></tr></thead>
<tbody>
<tr><td>sonarr</td><td class="mono row-sub">linuxserver/sonarr:4.0</td><td><span class="chip chip-good"><span class="d"></span>running</span></td><td class="mono row-sub">8989/tcp</td><td class="actions"><button class="btn btn-icon">&#128196;</button></td></tr>
<tr><td>radarr</td><td class="mono row-sub">linuxserver/radarr:5.2</td><td><span class="chip chip-good"><span class="d"></span>running</span></td><td class="mono row-sub">7878/tcp</td><td class="actions"><button class="btn btn-icon">&#128196;</button></td></tr>
<tr><td>prowlarr</td><td class="mono row-sub">linuxserver/prowlarr:1</td><td><span class="chip chip-neutral"><span class="d"></span>exited</span></td><td class="mono row-sub">9696/tcp</td><td class="actions"><button class="btn btn-icon">&#128196;</button></td></tr>
</tbody>
</table>
</div>
</div>
<div class="pod-card">
<div class="pod-head">
<span class="chip chip-good"><span class="d"></span>running</span>
<span class="name">home-stack</span>
<span class="infra">infra: localhost/podman-pause · shared network namespace</span>
</div>
<div class="pod-body table-wrap">
<table>
<thead><tr><th>Container</th><th>Image</th><th>Status</th><th>Ports</th><th></th></tr></thead>
<tbody>
<tr><td>homeassistant</td><td class="mono row-sub">ghcr.io/home-assistant/home-assistant</td><td><span class="chip chip-good"><span class="d"></span>running</span></td><td class="mono row-sub">8123/tcp</td><td class="actions"><button class="btn btn-icon">&#128196;</button></td></tr>
<tr><td>mosquitto</td><td class="mono row-sub">eclipse-mosquitto:2</td><td><span class="chip chip-good"><span class="d"></span>running</span></td><td class="mono row-sub">1883/tcp</td><td class="actions"><button class="btn btn-icon">&#128196;</button></td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============================= IMAGES ============================= -->
<section class="panel" id="panel-images">
<div class="card">
<div class="toolbar">
<input class="search" type="text" placeholder="Search images…">
<button class="btn">&#11015; Pull Image</button>
<button class="btn">Prune Unused</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Repository</th><th>Tag</th><th>Image ID</th><th>Size</th><th>Created</th><th>Used By</th><th></th></tr></thead>
<tbody>
<tr><td>lscr.io/linuxserver/plex</td><td class="mono">latest</td><td class="mono row-sub">a3f9c2e1b7d4</td><td class="tnum">1.42 GB</td><td class="tnum">3d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>lscr.io/linuxserver/sonarr</td><td class="mono">4.0</td><td class="mono row-sub">7e21ac9f0b12</td><td class="tnum">612 MB</td><td class="tnum">6h ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>lscr.io/linuxserver/radarr <span class="badge-update">update</span></td><td class="mono">5.2</td><td class="mono row-sub">44b6f1d8ce90</td><td class="tnum">598 MB</td><td class="tnum">38m ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>postgres</td><td class="mono">16-alpine</td><td class="mono row-sub">c1a08e4477fd</td><td class="tnum">243 MB</td><td class="tnum">9d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>nextcloud</td><td class="mono">29-apache</td><td class="mono row-sub">9d3c72b1a5e6</td><td class="tnum">890 MB</td><td class="tnum">12d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>containrrr/watchtower</td><td class="mono">latest</td><td class="mono row-sub">0f5b9a2d7c31</td><td class="tnum">28 MB</td><td class="tnum">40d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>ghcr.io/home-assistant/home-assistant</td><td class="mono">2026.7</td><td class="mono row-sub">b7e4f0a19c22</td><td class="tnum">1.1 GB</td><td class="tnum">3d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>eclipse-mosquitto</td><td class="mono">2</td><td class="mono row-sub">2a6d0e5f8b41</td><td class="tnum">14 MB</td><td class="tnum">40d ago</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>lscr.io/linuxserver/prowlarr</td><td class="mono">1</td><td class="mono row-sub">d8f231b9a706</td><td class="tnum">340 MB</td><td class="tnum">11d ago</td><td class="tnum">0</td><td class="actions"><button class="btn btn-icon">&#128465;</button></td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============================= VOLUMES ============================= -->
<section class="panel" id="panel-volumes">
<div class="card">
<div class="toolbar">
<input class="search" type="text" placeholder="Search volumes…">
<button class="btn btn-primary">+ New Volume</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Driver</th><th>Mountpoint</th><th>Size</th><th>Used By</th><th></th></tr></thead>
<tbody>
<tr><td>postgres-data</td><td class="chip chip-neutral">local</td><td class="mono row-sub">…/storage/volumes/postgres-data/_data</td><td class="tnum">1.8 GB</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128193;</button><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>nextcloud-data</td><td class="chip chip-neutral">local</td><td class="mono row-sub">…/storage/volumes/nextcloud-data/_data</td><td class="tnum">14.2 GB</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128193;</button><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>ha-config</td><td class="chip chip-neutral">local</td><td class="mono row-sub">…/storage/volumes/ha-config/_data</td><td class="tnum">340 MB</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128193;</button><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>mosquitto-data</td><td class="chip chip-neutral">local</td><td class="mono row-sub">…/storage/volumes/mosquitto-data/_data</td><td class="tnum">4 MB</td><td class="tnum">1</td><td class="actions"><button class="btn btn-icon">&#128193;</button><button class="btn btn-icon">&#128465;</button></td></tr>
<tr><td>unused-cache</td><td class="chip chip-neutral">local</td><td class="mono row-sub">…/storage/volumes/unused-cache/_data</td><td class="tnum">610 MB</td><td class="tnum">0</td><td class="actions"><button class="btn btn-icon">&#128193;</button><button class="btn btn-icon">&#128465;</button></td></tr>
</tbody>
</table>
</div>
<div class="empty-note" style="border-top:1px solid var(--border); text-align:left; padding:12px 18px;">
App data mapped from Unraid shares (e.g. <span class="mono">/mnt/user/appdata/…</span>) is managed as bind mounts per-container, not listed here — see each container's edit dialog.
</div>
</div>
</section>
<!-- ============================= NETWORKS ============================= -->
<section class="panel" id="panel-networks">
<div class="card">
<div class="toolbar">
<input class="search" type="text" placeholder="Search networks…">
<button class="btn btn-primary">+ New Network</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Driver</th><th>Subnet</th><th>Gateway</th><th>Containers</th><th></th></tr></thead>
<tbody>
<tr><td><strong>podman0</strong> <span class="row-sub">(default)</span></td><td class="chip chip-neutral">bridge</td><td class="mono">10.89.0.0/24</td><td class="mono">10.89.0.1</td><td class="tnum">3</td><td class="actions"><button class="btn btn-icon">&#9998;</button></td></tr>
<tr><td>media-net</td><td class="chip chip-neutral">bridge</td><td class="mono">10.89.1.0/24</td><td class="mono">10.89.1.1</td><td class="tnum">3</td><td class="actions"><button class="btn btn-icon">&#9998;</button><button class="btn btn-icon">&#128465;</button></td></tr>
<tr>
<td>iot-macvlan</td><td class="chip chip-neutral">macvlan</td><td class="mono">192.168.10.0/24</td><td class="mono">192.168.10.1</td><td class="tnum">1</td>
<td class="actions"><button class="btn btn-icon">&#9998;</button><button class="btn btn-icon">&#128465;</button></td>
</tr>
</tbody>
</table>
</div>
<div class="empty-note" style="border-top:1px solid var(--border); text-align:left; padding:12px 18px; display:flex; gap:8px; align-items:flex-start;">
<span class="chip chip-warn" style="margin-top:1px;"><span class="d"></span>note</span>
<span>macvlan networks can trigger a known kernel issue with some NIC drivers on Unraid ("macvlan call trap"). See the docs before attaching containers that need host-network access on the same interface.</span>
</div>
</div>
</section>
<!-- ============================= LOGS ============================= -->
<section class="panel" id="panel-logs">
<div class="card">
<div class="logs-layout">
<div class="logs-side">
<div class="item active">&#9679; postgres</div>
<div class="item">&#9679; plex</div>
<div class="item">&#9679; sonarr</div>
<div class="item">&#9679; radarr</div>
<div class="item">&#9679; nextcloud</div>
<div class="item">&#9679; homeassistant</div>
<div class="item">&#9679; mosquitto</div>
</div>
<div>
<div class="log-toolbar">
<input class="search" type="text" placeholder="Filter log output…" style="max-width:280px;">
<span class="filterset"><button class="active">Follow</button><button>Paused</button></span>
<div class="spacer" style="flex:1;"></div>
<button class="btn btn-icon" title="Download">&#8681;</button>
</div>
<div class="log-pane" role="log">
<div class="l"><span class="ts">2026-07-11T09:58:02Z</span> <span class="lvl-info">INFO</span> database system is ready to accept connections</div>
<div class="l"><span class="ts">2026-07-11T09:58:14Z</span> <span class="lvl-info">INFO</span> checkpoint starting: time</div>
<div class="l"><span class="ts">2026-07-11T10:04:41Z</span> <span class="lvl-warn">WARN</span> could not receive data from client: Connection reset by peer</div>
<div class="l"><span class="ts">2026-07-11T10:07:52Z</span> <span class="lvl-error">ERROR</span> connection to server was lost</div>
<div class="l"><span class="ts">2026-07-11T10:07:53Z</span> <span class="lvl-info">INFO</span> restartpoint starting: time</div>
<div class="l"><span class="ts">2026-07-11T10:07:58Z</span> <span class="lvl-info">INFO</span> database system is ready to accept connections</div>
<div class="l"><span class="ts">2026-07-11T10:11:03Z</span> <span class="lvl-warn">WARN</span> autovacuum: found orphan temp table in database "app"</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================= TERMINAL ============================= -->
<section class="panel" id="panel-terminal">
<div class="card">
<div class="term-wrap">
<div class="term-select">
Exec into:
<select><option>sonarr</option><option>plex</option><option>postgres</option><option>nextcloud</option></select>
Shell: <select><option>/bin/bash</option><option>/bin/sh</option></select>
<button class="btn" style="margin-left:auto;">New Session</button>
</div>
<div class="term">
<div><span class="prompt">root@sonarr</span>:<span class="path">/config</span>$ podman exec -it sonarr /bin/bash</div>
<div>root@sonarr:/config# ls -la</div>
<div>drwxr-xr-x 6 abc abc 4096 Jul 11 09:12 .</div>
<div>drwxr-xr-x 1 root root 4096 Jun 02 17:03 ..</div>
<div>-rw-r--r-- 1 abc abc 22016 Jul 11 09:58 sonarr.db</div>
<div>drwxr-xr-x 2 abc abc 4096 Jul 11 06:00 logs</div>
<div>&nbsp;</div>
<div>root@sonarr:/config# tail -n 3 logs/sonarr.txt</div>
<div>10:11:02|Info|RssSyncService: Starting RSS Sync</div>
<div>10:11:04|Info|RssSyncService: RSS Sync Completed. Reported 12 releases</div>
<div>10:11:04|Info|DownloadService: Report sent to SABnzbd</div>
<div>&nbsp;</div>
<div><span class="prompt">root@sonarr</span>:<span class="path">/config</span># <span class="cur"></span></div>
</div>
</div>
</div>
</section>
<!-- ============================= COMPOSE ============================= -->
<section class="panel" id="panel-compose">
<div class="card">
<div class="compose-layout">
<div class="compose-side">
<div class="compose-proj active">
<div class="name">media-stack <span class="chip chip-good"><span class="d"></span>up</span></div>
<div class="path">/boot/config/plugins/podman/compose/media-stack</div>
</div>
<div class="compose-proj">
<div class="name">home-stack <span class="chip chip-good"><span class="d"></span>up</span></div>
<div class="path">/boot/config/plugins/podman/compose/home-stack</div>
</div>
<div class="compose-proj">
<div class="name">staging <span class="chip chip-neutral"><span class="d"></span>down</span></div>
<div class="path">/boot/config/plugins/podman/compose/staging</div>
</div>
</div>
<div>
<div class="compose-body-head">
<strong>media-stack / compose.yaml</strong>
<div style="display:flex; gap:8px;">
<button class="btn">&#11015; Pull</button>
<button class="btn">&#9632; Down</button>
<button class="btn btn-primary">&#9654; Up</button>
</div>
</div>
<div class="yaml"><span class="c"># managed via podman kube play — see docs/ARCHITECTURE.md</span>
<span class="k">services</span>:
<span class="k">sonarr</span>:
<span class="k">image</span>: <span class="s">lscr.io/linuxserver/sonarr:4.0</span>
<span class="k">ports</span>:
- <span class="s">"8989:8989"</span>
<span class="k">volumes</span>:
- <span class="p">/mnt/user/appdata/sonarr</span>:<span class="p">/config</span>
- <span class="p">/mnt/user/media</span>:<span class="p">/media</span>
<span class="k">environment</span>:
<span class="k">PUID</span>: <span class="s">"99"</span>
<span class="k">PGID</span>: <span class="s">"100"</span>
<span class="k">restart</span>: <span class="s">unless-stopped</span>
<span class="k">radarr</span>:
<span class="k">image</span>: <span class="s">lscr.io/linuxserver/radarr:5.2</span>
<span class="k">ports</span>:
- <span class="s">"7878:7878"</span>
<span class="k">volumes</span>:
- <span class="p">/mnt/user/appdata/radarr</span>:<span class="p">/config</span>
- <span class="p">/mnt/user/media</span>:<span class="p">/media</span>
<span class="k">restart</span>: <span class="s">unless-stopped</span>
<span class="k">prowlarr</span>:
<span class="k">image</span>: <span class="s">lscr.io/linuxserver/prowlarr:1</span>
<span class="k">ports</span>:
- <span class="s">"9696:9696"</span>
<span class="k">restart</span>: <span class="s">unless-stopped</span></div>
</div>
</div>
</div>
</section>
<!-- ============================= SETTINGS ============================= -->
<section class="panel" id="panel-settings">
<div class="settings-grid">
<div class="card">
<div class="card-head"><h2>Storage</h2></div>
<div class="field-row">
<label>Storage path</label>
<div>
<input type="text" value="/mnt/cache/system/podman" class="mono">
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE). See Architecture docs.</div>
</div>
</div>
<div class="field-row">
<label>podman.img size</label>
<div><input type="number" value="20" style="max-width:100px;"><span class="unit">GB</span>
<div class="bar" style="max-width:340px; margin-top:10px;"><span style="width:62%"></span></div>
<div class="hint">62.4 GB used of 20 GB nominal — image will need to grow soon.</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><h2>Networking</h2></div>
<div class="field-row"><label>Default bridge subnet</label><div><input type="text" value="10.89.0.0/24" class="mono"></div></div>
<div class="field-row"><label>DNS backend</label><div><select><option>aardvark-dns (recommended)</option><option>none</option></select></div></div>
</div>
<div class="card">
<div class="card-head"><h2>Autostart &amp; Lifecycle</h2></div>
<div class="field-row"><label>Start podman on array start</label><div><label class="switch"><input type="checkbox" checked><span class="track"></span></label></div></div>
<div class="field-row"><label>Container stop timeout</label><div><input type="number" value="10" style="max-width:100px;"><span class="unit">seconds</span></div></div>
<div class="field-row"><label>Autostart Safe-Mode threshold</label><div><input type="number" value="3" style="max-width:100px;"><span class="unit">consecutive failures</span>
<div class="hint">watchtower is currently paused — see Dashboard.</div></div></div>
</div>
<div class="card">
<div class="card-head"><h2>Updates</h2><span class="sub">unraid-podman 0.1.0</span></div>
<div class="field-row"><label>Update channel</label><div><select><option>Stable</option><option>Pre-release</option></select></div></div>
<div class="field-row"><label>Package versions</label>
<div class="hint mono" style="max-width:none;">
podman 6.0.1 · conmon 2.2.1 · crun 1.28 · netavark 2.0.0 · aardvark-dns 2.0.0 · passt git6ef3d1c · fuse-overlayfs 1.17
</div>
</div>
</div>
<div class="card danger-card">
<div class="card-head"><h2>Danger Zone</h2></div>
<div class="field-row">
<label>Roll back plugin</label>
<div><button class="btn">Restore previous version (0.0.9)</button></div>
</div>
<div class="field-row">
<label>Remove plugin</label>
<div>
<button class="btn btn-danger">Uninstall unraid-podman…</button>
<div class="hint">Your configuration, backups, and container storage are preserved by default. Full data removal requires a separate confirmation.</div>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="note">Mockup only — static sample data, not connected to a live podman.sock. See webui/README.md.</footer>
<script>
// Tab switching: the only interactive behavior this static mockup needs.
const buttons = document.querySelectorAll('#subnav button');
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
buttons.forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
document.querySelectorAll('.panel').forEach(function (p) { p.classList.remove('active'); });
const target = document.getElementById('panel-' + btn.dataset.panel);
if (target) target.classList.add('active');
});
});
</script>
+256
View File
@@ -0,0 +1,256 @@
Menu="Podman"
Title="Podman"
Icon="podman"
---
<?php
/**
* Podman.page — Unraid Dynamix-style GUI page for unraid-podman.
*
* This file only renders the page shell: the header, the sub-navigation
* for the ten sections, and one (mostly empty) container per panel. Every
* panel fills itself in via AJAX after load — see javascript/app.js's
* router and each javascript/<panel>.js module — so this file has no
* business logic of its own and does not talk to PodmanClient directly.
* See docs/ARCHITECTURE.md section 18 for the overall WebUI design and
* webui/mockups/prototype.html for the approved visual reference this
* page's markup mirrors (same structure, same CSS classes, real data
* instead of static samples).
*/
?>
<link rel="stylesheet" type="text/css" href="/plugins/podman/styles/podman.css">
<div class="podman-plugin">
<header class="podman-pagehead">
<div class="titlewrap">
<div class="icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 2l8 4.5v9L12 20l-8-4.5v-9L12 2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 11l8-4.5M12 11v9M12 11L4 6.5" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
</div>
<div>
<h1>Podman</h1>
<div class="meta" id="podman-header-meta">Connecting…</div>
</div>
</div>
<div style="display:flex; gap:8px;">
<button class="podman-btn" id="podman-refresh-all">&#8635; Refresh</button>
</div>
</header>
<nav class="podman-subnav">
<button class="active" data-panel="dashboard">Dashboard</button>
<button data-panel="containers">Containers</button>
<button data-panel="pods">Pods</button>
<button data-panel="images">Images</button>
<button data-panel="volumes">Volumes</button>
<button data-panel="networks">Networks</button>
<button data-panel="logs">Logs</button>
<button data-panel="terminal">Terminal</button>
<button data-panel="compose">Compose</button>
<button data-panel="settings">Settings</button>
</nav>
<main class="podman-main">
<!-- ============================= DASHBOARD ============================= -->
<section class="podman-panel active" id="podman-panel-dashboard">
<div class="podman-grid podman-stat-grid">
<div class="podman-stat"><div class="label">Running</div><div class="value tnum"><span id="stat-running">—</span> <small id="stat-running-total"></small></div></div>
<div class="podman-stat"><div class="label">Pods</div><div class="value tnum" id="stat-pods">—</div></div>
<div class="podman-stat"><div class="label">Images</div><div class="value tnum" id="stat-images">—</div></div>
<div class="podman-stat"><div class="label">Volumes</div><div class="value tnum" id="stat-volumes">—</div></div>
<div class="podman-stat"><div class="label">Networks</div><div class="value tnum" id="stat-networks">—</div></div>
<div class="podman-stat"><div class="label">Images on Disk</div><div class="value tnum" id="stat-images-size">—</div></div>
</div>
</section>
<!-- ============================= CONTAINERS ============================= -->
<section class="podman-panel" id="podman-panel-containers">
<div class="podman-card">
<div class="podman-toolbar">
<input class="podman-search" id="containers-search" type="text" placeholder="Search containers by name or image…">
<div class="filterset" id="containers-filterset" style="display:flex; gap:4px; background:var(--surface-2); padding:3px; border-radius:8px;">
<button class="active" data-filter="all" id="containers-count-all">All</button>
<button data-filter="running" id="containers-count-running">Running</button>
<button data-filter="stopped" id="containers-count-stopped">Stopped</button>
</div>
</div>
<div class="podman-table-wrap">
<table>
<thead><tr><th>Status</th><th>Name</th><th>Image</th><th>CPU/Mem</th><th>Ports</th><th>Uptime</th><th></th></tr></thead>
<tbody id="containers-tbody"></tbody>
</table>
</div>
</div>
</section>
<!-- ============================= PODS ============================= -->
<section class="podman-panel" id="podman-panel-pods"></section>
<!-- ============================= IMAGES ============================= -->
<section class="podman-panel" id="podman-panel-images">
<div class="podman-card">
<div class="podman-toolbar">
<input class="podman-search" type="text" placeholder="Search images…" disabled title="Client-side filtering not yet wired up for Images">
<button class="podman-btn" id="images-pull-btn">&#11015; Pull Image</button>
</div>
<div class="podman-table-wrap">
<table>
<thead><tr><th>Repository</th><th>Tag</th><th>Image ID</th><th>Size</th><th>Created</th><th>Used By</th><th></th></tr></thead>
<tbody id="images-tbody"></tbody>
</table>
</div>
</div>
</section>
<!-- ============================= VOLUMES ============================= -->
<section class="podman-panel" id="podman-panel-volumes">
<div class="podman-card">
<div class="podman-toolbar">
<div style="flex:1;"></div>
<button class="podman-btn podman-btn-primary" id="volumes-create-btn">+ New Volume</button>
</div>
<div class="podman-table-wrap">
<table>
<thead><tr><th>Name</th><th>Driver</th><th>Mountpoint</th><th>Used By</th><th></th></tr></thead>
<tbody id="volumes-tbody"></tbody>
</table>
</div>
<div class="podman-empty-note" style="border-top:1px solid var(--border); text-align:left; padding:12px 18px;">
App data mapped from Unraid shares (e.g. <span class="mono">/mnt/user/appdata/…</span>) is managed as bind mounts per-container and does not appear here.
</div>
</div>
</section>
<!-- ============================= NETWORKS ============================= -->
<section class="podman-panel" id="podman-panel-networks">
<div class="podman-card">
<div class="podman-toolbar">
<div style="flex:1;"></div>
<button class="podman-btn podman-btn-primary" id="networks-create-btn">+ New Network</button>
</div>
<div class="podman-table-wrap">
<table>
<thead><tr><th>Name</th><th>Driver</th><th>Subnet</th><th>Gateway</th><th>Containers</th><th></th></tr></thead>
<tbody id="networks-tbody"></tbody>
</table>
</div>
</div>
</section>
<!-- ============================= LOGS ============================= -->
<section class="podman-panel" id="podman-panel-logs">
<div class="podman-card">
<div class="podman-logs-layout">
<div class="podman-logs-side" id="logs-sidebar"></div>
<div>
<div class="podman-toolbar">
<input class="podman-search" id="logs-filter" type="text" placeholder="Filter log output…" style="max-width:280px;">
<span id="logs-follow-toggle" style="display:flex; gap:4px; background:var(--surface-2); padding:3px; border-radius:8px;">
<button class="active" data-follow="true">Follow</button>
<button data-follow="false">Paused</button>
</span>
</div>
<div class="podman-log-pane" id="log-pane" role="log"></div>
</div>
</div>
</div>
</section>
<!-- ============================= TERMINAL ============================= -->
<section class="podman-panel" id="podman-panel-terminal">
<div class="podman-card">
<div class="podman-card-pad">
<div style="display:flex; gap:8px; align-items:center; margin-bottom:12px; font-size:12.5px; color:var(--text-dim);">
Exec into: <select id="term-container-select"></select>
</div>
<div class="podman-term" id="term-output"></div>
<input class="podman-term-input" id="term-input" type="text" placeholder="Type a command and press Enter… (one-shot exec — see Compose panel note on API scope)" autocomplete="off">
</div>
</div>
</section>
<!-- ============================= COMPOSE ============================= -->
<section class="podman-panel" id="podman-panel-compose">
<div class="podman-card">
<div class="podman-compose-layout">
<div class="podman-compose-side" id="compose-sidebar"></div>
<div>
<div class="podman-toolbar">
<strong id="compose-title" style="flex:1;">—</strong>
<button class="podman-btn" id="compose-action-pull">&#11015; Pull</button>
<button class="podman-btn" id="compose-action-down">&#9632; Down</button>
<button class="podman-btn podman-btn-primary" id="compose-action-up">&#9654; Up</button>
</div>
<pre class="podman-yaml" id="compose-yaml"></pre>
</div>
</div>
</div>
</section>
<!-- ============================= SETTINGS ============================= -->
<section class="podman-panel" id="podman-panel-settings">
<div class="podman-grid">
<div class="podman-card">
<div class="podman-card-head"><h2>Storage</h2></div>
<div class="podman-field-row">
<label for="settings-storage-path">Storage path</label>
<div>
<input type="text" id="settings-storage-path" class="mono">
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
</div>
</div>
<div class="podman-field-row">
<label for="settings-storage-size">podman.img size</label>
<div><input type="number" id="settings-storage-size" style="max-width:100px;"> <span style="font-size:12px;color:var(--text-dim);">GB</span></div>
</div>
</div>
<div class="podman-card">
<div class="podman-card-head"><h2>Autostart &amp; Lifecycle</h2></div>
<div class="podman-field-row">
<label for="settings-enabled">Start podman on array start</label>
<div><input type="checkbox" id="settings-enabled"></div>
</div>
<div class="podman-field-row">
<label for="settings-stop-timeout">Container stop timeout</label>
<div><input type="number" id="settings-stop-timeout" style="max-width:100px;"> <span style="font-size:12px;color:var(--text-dim);">seconds</span></div>
</div>
<div class="podman-field-row">
<label>Autostart order</label>
<div class="podman-table-wrap">
<table>
<thead><tr><th>#</th><th>Container</th><th></th></tr></thead>
<tbody id="autostart-tbody"></tbody>
</table>
</div>
</div>
<div class="podman-field-row">
<label></label>
<div><button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button></div>
</div>
</div>
<div class="podman-card">
<div class="podman-card-head"><h2>Installed Packages</h2></div>
<div class="podman-field-row">
<label>Versions</label>
<div class="hint mono" id="settings-package-versions" style="max-width:none;">—</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script src="/plugins/podman/javascript/app.js"></script>
<script src="/plugins/podman/javascript/dashboard.js"></script>
<script src="/plugins/podman/javascript/containers.js"></script>
<script src="/plugins/podman/javascript/pods.js"></script>
<script src="/plugins/podman/javascript/images.js"></script>
<script src="/plugins/podman/javascript/volumes.js"></script>
<script src="/plugins/podman/javascript/networks.js"></script>
<script src="/plugins/podman/javascript/logs.js"></script>
<script src="/plugins/podman/javascript/terminal.js"></script>
<script src="/plugins/podman/javascript/compose.js"></script>
<script src="/plugins/podman/javascript/settings.js"></script>
+179
View File
@@ -0,0 +1,179 @@
<?php
/**
* ajax/compose.php
*
* Backs the Compose panel — and is the ONE deliberate exception to
* "talk to podman.sock, never shell out" in this entire WebUI.
*
* Why: libpod's REST API has no endpoint that understands
* docker-compose.yml/compose.yaml at all. The closest thing,
* POST /libpod/kube/play, takes Kubernetes YAML, not Compose YAML, and
* correctly translating arbitrary Compose files into Kube manifests is a
* substantial project of its own (this is exactly what `podman compose` /
* `podman-compose` already do). Reimplementing that translation from
* scratch here — instead of using the `podman compose` CLI, which IS the
* project's own supported way to run Compose files — would be more
* fragile, not more "API-native": there is no API to be native to for
* this one feature. So this file, and only this file, runs the
* `podman compose` CLI via proc_open(), with strict input validation
* (project names are matched against a fixed pattern, never interpolated
* into a shell string) rather than passing user input through a shell.
*
* Compose projects live under $bootDir/compose/<project>/compose.yaml —
* see docs/ARCHITECTURE.md; this mirrors how autostart/networks/backups
* are all rooted under /boot/config/plugins/podman/ for the same
* boot-persistence reasons.
*
* Actions (?action=...):
* list GET -> known projects with up/down status
* get GET (&project=...) -> raw compose.yaml content
* up POST {"project": "..."}
* down POST {"project": "..."}
* pull POST {"project": "..."}
*/
declare(strict_types=1);
require __DIR__ . '/../include/bootstrap.php';
$composeDir = $podmanConfig->bootDir . '/compose';
$action = $_GET['action'] ?? '';
switch ($action) {
case 'list':
podman_json_response(compose_list($composeDir));
break;
case 'get':
$project = (string) ($_GET['project'] ?? '');
podman_json_response(['yaml' => compose_read($composeDir, $project)]);
break;
case 'up':
podman_json_response(compose_run($composeDir, require_project(podman_read_json_body()), ['up', '-d']));
break;
case 'down':
podman_json_response(compose_run($composeDir, require_project(podman_read_json_body()), ['down']));
break;
case 'pull':
podman_json_response(compose_run($composeDir, require_project(podman_read_json_body()), ['pull']));
break;
default:
podman_json_error("Unknown action '{$action}'", 400);
}
/** @param array<string,mixed> $body */
function require_project(array $body): string
{
$project = (string) ($body['project'] ?? '');
if (!is_valid_project_name($project)) {
podman_json_error('Missing or invalid project name', 400);
}
return $project;
}
/**
* Project names come from the user (a form field when creating a new
* compose project, or a value round-tripped from list()). Restricting
* them to a fixed safe character set here — BEFORE they're ever used to
* build a filesystem path or a command argument — is what makes it safe
* to pass them to proc_open() at all.
*/
function is_valid_project_name(string $name): bool
{
return $name !== '' && preg_match('/^[a-zA-Z0-9_-]+$/', $name) === 1;
}
/** @return array<int,array<string,mixed>> */
function compose_list(string $composeDir): array
{
if (!is_dir($composeDir)) {
return [];
}
$projects = [];
foreach (scandir($composeDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..' || !is_valid_project_name($entry)) {
continue;
}
$yamlPath = $composeDir . '/' . $entry . '/compose.yaml';
if (!is_file($yamlPath)) {
continue;
}
$projects[] = [
'name' => $entry,
'path' => $yamlPath,
'status' => compose_status($composeDir, $entry),
];
}
usort($projects, static fn($a, $b) => strcmp($a['name'], $b['name']));
return $projects;
}
/** Best-effort "up"/"down" status via `podman compose ps`; degrades to "unknown" rather than failing the whole list. */
function compose_status(string $composeDir, string $project): string
{
$result = run_compose_command($composeDir, $project, ['ps', '--format', 'json'], 5);
if ($result['exitCode'] !== 0) {
return 'unknown';
}
$decoded = json_decode($result['output'], true);
return (is_array($decoded) && count($decoded) > 0) ? 'up' : 'down';
}
function compose_read(string $composeDir, string $project): string
{
if (!is_valid_project_name($project)) {
podman_json_error('Invalid project name', 400);
}
$path = $composeDir . '/' . $project . '/compose.yaml';
$content = is_file($path) ? file_get_contents($path) : false;
if ($content === false) {
podman_json_error("compose.yaml not found for project '{$project}'", 404);
}
return $content;
}
/** @return array<string,mixed> */
function compose_run(string $composeDir, string $project, array $subcommand): array
{
$result = run_compose_command($composeDir, $project, $subcommand, 300);
if ($result['exitCode'] !== 0) {
podman_json_error("podman compose " . implode(' ', $subcommand) . " failed:\n" . $result['output'], 502);
}
return ['output' => $result['output']];
}
/**
* Runs `podman compose -f <project>/compose.yaml <subcommand...>` via
* proc_open with an argv array (never a shell string — proc_open with an
* array argument bypasses the shell entirely, so there is no injection
* surface even though $project has already been validated above too).
*
* @param array<int,string> $subcommand
* @return array{exitCode:int,output:string}
*/
function run_compose_command(string $composeDir, string $project, array $subcommand, int $timeoutSeconds): array
{
$yamlPath = $composeDir . '/' . $project . '/compose.yaml';
$argv = array_merge(['podman', 'compose', '-f', $yamlPath], $subcommand);
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open($argv, $descriptors, $pipes, $composeDir . '/' . $project);
if (!is_resource($process)) {
return ['exitCode' => 127, 'output' => 'Could not start podman compose process'];
}
stream_set_timeout($pipes[1], $timeoutSeconds);
$stdout = stream_get_contents($pipes[1]) ?: '';
$stderr = stream_get_contents($pipes[2]) ?: '';
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
return ['exitCode' => $exitCode, 'output' => trim($stdout . $stderr)];
}
+132
View File
@@ -0,0 +1,132 @@
<?php
/**
* ajax/containers.php
*
* Backs the Containers panel (and the container rows nested inside the
* Pods panel — see ajax/pods.php). All actions go through PodmanClient,
* i.e. the real libpod REST API over podman.sock; nothing here shells out.
*
* Actions (?action=...):
* list GET -> normalized array of containers for the table view
* inspect GET (&id=...) -> raw inspect JSON, for a detail dialog
* start POST {"id": "..."}
* stop POST {"id": "...", "timeout": 10}
* restart POST {"id": "...", "timeout": 10}
* remove POST {"id": "...", "force": false}
* logs GET (&id=...&tail=200) -> plain text
*/
declare(strict_types=1);
require __DIR__ . '/../include/bootstrap.php';
$action = $_GET['action'] ?? '';
switch ($action) {
case 'list':
podman_json_response(containers_list($client));
break;
case 'inspect':
$id = (string) ($_GET['id'] ?? '');
if ($id === '') {
podman_json_error('Missing id', 400);
}
podman_json_response($client->inspectContainer($id));
break;
case 'logs':
$id = (string) ($_GET['id'] ?? '');
if ($id === '') {
podman_json_error('Missing id', 400);
}
$tail = (int) ($_GET['tail'] ?? 200);
podman_json_response(['text' => $client->containerLogs($id, $tail)]);
break;
case 'start':
$body = podman_read_json_body();
$client->startContainer(require_id($body));
podman_json_response(['status' => 'started']);
break;
case 'stop':
$body = podman_read_json_body();
$client->stopContainer(require_id($body), (int) ($body['timeout'] ?? $podmanConfig->stopTimeoutSeconds));
podman_json_response(['status' => 'stopped']);
break;
case 'restart':
$body = podman_read_json_body();
$client->restartContainer(require_id($body), (int) ($body['timeout'] ?? $podmanConfig->stopTimeoutSeconds));
podman_json_response(['status' => 'restarted']);
break;
case 'remove':
$body = podman_read_json_body();
$client->removeContainer(require_id($body), (bool) ($body['force'] ?? false));
podman_json_response(['status' => 'removed']);
break;
default:
podman_json_error("Unknown action '{$action}'", 400);
}
/** @param array<string,mixed> $body */
function require_id(array $body): string
{
$id = (string) ($body['id'] ?? '');
if ($id === '') {
podman_json_error('Missing id in request body', 400);
}
return $id;
}
/**
* Normalizes libpod's /containers/json entries into exactly what the
* Containers table (javascript/containers.js) renders — keeping this
* shaping logic server-side means the frontend never has to know libpod's
* raw field names/quirks (e.g. Names is an array, State vs Status, etc).
*
* @return array<int,array<string,mixed>>
*/
function containers_list(PodmanClient $client): array
{
$raw = $client->listContainers(true);
$out = [];
foreach ($raw as $c) {
$names = $c['Names'] ?? [];
$name = is_array($names) && count($names) > 0 ? ltrim((string) $names[0], '/') : podman_short_id((string) ($c['Id'] ?? ''));
$ports = [];
foreach (($c['Ports'] ?? []) as $p) {
if (isset($p['host_port'], $p['container_port'])) {
$ports[] = "{$p['host_port']}:{$p['container_port']}/" . ($p['protocol'] ?? 'tcp');
} elseif (isset($p['container_port'])) {
$ports[] = "{$p['container_port']}/" . ($p['protocol'] ?? 'tcp');
}
}
$startedAt = podman_parse_time($c['StartedAt'] ?? null);
$state = strtolower((string) ($c['State'] ?? 'unknown'));
$out[] = [
'id' => (string) ($c['Id'] ?? ''),
'shortId' => podman_short_id((string) ($c['Id'] ?? '')),
'name' => $name,
'image' => (string) ($c['Image'] ?? ''),
'state' => $state,
'status' => (string) ($c['Status'] ?? ''),
'health' => $c['Health']['Status'] ?? null,
'ports' => $ports,
'pod' => $c['Pod'] ?? null,
'podName' => $c['PodName'] ?? null,
'uptimeSeconds' => ($state === 'running' && $startedAt !== null) ? (time() - $startedAt) : null,
'createdAt' => podman_parse_time($c['Created'] ?? null),
];
}
usort($out, static fn($a, $b) => strcmp($a['name'], $b['name']));
return $out;
}

Some files were not shown because too many files have changed in this diff Show More