commit 1dba304085fb324a916c28d2314cabb1eedc794f Author: Lars Rossen Date: Sat Aug 22 18:46:24 2026 +0200 Seed the MakerFLOSS devops repo with the podman module README, a module-catalog.json validated against TAPPaaS's module-catalog-fields.json, and the podman module copied verbatim from Community/src/larsrossen/containers/podman into a flat src/containers/ layout (the catalog carries the explicit moduleJson path, so layout is free). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4d7148 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# makerfloss + +The MakerFLOSS **DevOps repository** for experimental TAPPaaS modules. + +Modules are developed and tried out here, on the TAPPaaS system at +[makerfloss.eu](https://makerfloss.eu), before they are proposed upstream to +[TAPPaaS](https://codeberg.org/TAPPaaS/TAPPaaS) or +[Community](https://codeberg.org/TAPPaaS/Community). Expect things to be +half-built, renamed, or removed. + +## Layout + +```text +src/module-catalog.json # the registry a TAPPaaS instance reads +src/containers/podman/ # one directory per module +``` + +## Using it from a TAPPaaS instance + +Register the repository on the `tappaas-cicd` mothership, then install a module +from it: + +```bash +site-manager repository add makerfloss \ + --url https://forgejo.makerfloss.eu/TAPPaaS/makerfloss.git \ + --branch main + +module-manager module add podman --environment +``` + +## Modules + +| Module | What it is | Status | +| --- | --- | --- | +| [podman](src/containers/podman) | Debian 13 VM with rootless Podman + the Cockpit web console | incomplete | diff --git a/src/containers/podman/INSTALL.md b/src/containers/podman/INSTALL.md new file mode 100644 index 0000000..2d855dd --- /dev/null +++ b/src/containers/podman/INSTALL.md @@ -0,0 +1,110 @@ +# Podman container host — Installation + +Primary audience: TAPPaaS admin. Steps the scripts cannot automate. + +## Prerequisites + +- `tappaas@tappaas-cicd` with the usual cluster SSH/sudo access. +- The dependencies are pulled in automatically (`cluster:vm`, `templates:debian`, + `backup:vm`, `network:proxy`). +- For the friendly HTTPS name to present a valid certificate, the TAPPaaS wildcard must be in + OPNsense Trust (run `acme-setup.sh` once — see the platform INSTALL). Until then the internal + endpoint still works, with a cert warning (Cockpit also ships a self-signed cert). + +## Install + +```bash +cd /home/tappaas/Community/src/larsrossen/containers/podman +install-module.sh podman +``` + +This automatically: +- creates a Debian 13 VM (vmid 812, 2 vCPU / 2 GB / 20 GB) and OS-preps it; +- installs `podman`, `podman-compose`, `cockpit` and `cockpit-podman` from apt; +- enables the rootless podman socket (with linger) and the Cockpit console on `:9090`; +- publishes the internal reverse-proxy name `podman.` (console reachable from the + `mgmt` zone only, no internet). + +### Placement / zone + +The module JSON sets **no `zone0`**, so the VM lands in the **default environment's zone** +(ADR-007 P5: with `zone0` unset, install resolves the zone from +`config/environments/.json → network.zone`). To place it in a specific environment — and +therefore its associated zone — pass `--environment` at install time: + +```bash +install-module.sh podman --environment # VM joins 's network.zone +``` + +An explicit `zone0` in the JSON would override this, so it is intentionally omitted. + +## Post-install (manual — required for the web login) + +The Cockpit console authenticates against **Linux (PAM) accounts on the VM**. The cloud-init +`tappaas` user has SSH-key auth and **no password**, so it cannot log in to the web console +until you give it one (or create a dedicated admin user). + +**Option A — give the `tappaas` user a password:** + +```bash +ssh tappaas@ 'sudo passwd tappaas' +``` + +**Option B — create a dedicated admin user (recommended for shared access):** + +```bash +ssh tappaas@ +sudo useradd -m -s /bin/bash -G sudo podadmin && sudo passwd podadmin +sudo loginctl enable-linger podadmin # keep its rootless podman socket alive +``` + +Then open **`https://podman.`** (or `https://:9090`), log in with that account, +and choose **Podman containers** in the left menu (Cockpit starts the user's podman service on +first use). + +## Using it + +- CLI: `ssh tappaas@` then `podman run …`, `podman ps`, `podman images`. +- Compose files: `podman-compose up -d` in a directory with a `compose.yaml`. +- Web: manage/start/stop containers, pull images and inspect logs from the Cockpit + **Podman containers** page. + +## Verification + +```bash +test-module.sh podman +``` + +Manual checks: + +| Check | Expected | +|-------|----------| +| `ssh tappaas@ podman --version` | prints the Podman version | +| `https://podman.` from a `mgmt` browser | Cockpit login page loads | +| Same URL from an out-of-policy zone or the internet | 403 (blocked) | +| Cockpit → **Podman containers** after login | the Podman page renders | + +## Troubleshooting + +**Web login rejected for `tappaas`** +The account has no password by default — set one (Option A above) or use a dedicated admin +user (Option B). Cockpit cannot log in a key-only account. + +**Podman page in Cockpit says the service is not running** +Cockpit starts the *per-user* podman service on first open; if it does not, on the VM run +`systemctl --user start podman.socket` as that user, and ensure `loginctl enable-linger ` +was set so the socket survives logout. + +**Friendly name unreachable** +Confirm you are in the `mgmt` zone (others get 403 by design). Check the Caddy route: +`network:proxy test-service.sh podman`. Re-apply with `install-module.sh podman --force`. + +**Friendly name loads a blank page (valid cert, no UI)** +Cockpit rides a WebSocket; behind a TLS reverse proxy Caddy must talk HTTP/1.1 to the upstream. +This module sets `proxyUpstreamHttp1: true` (os-caddy `HttpVersion=http1`). If you see a blank +page, verify the handler has HTTP Version = HTTP/1.1 and re-apply: +`install-module.sh podman --force`. The direct console `https://:9090` always works. + +**Re-run / upgrade** +`install-module.sh podman --force` is idempotent: apt re-runs (a no-op when already current) +and the version marker `/etc/tappaas-podman.version` is refreshed. diff --git a/src/containers/podman/README.md b/src/containers/podman/README.md new file mode 100644 index 0000000..497ddea --- /dev/null +++ b/src/containers/podman/README.md @@ -0,0 +1,60 @@ +# Podman — container host with a web console + +Primary audience: TAPPaaS operator who wants a plain place to run OCI containers. + +A **Debian 13 (trixie) VM with [Podman](https://podman.io) installed** — the daemonless, +rootless-capable container engine (a drop-in for `docker`). On top of the engine it adds the +**Cockpit web console** with the **`cockpit-podman`** plugin, so containers, images and pods +can be managed from a browser instead of only the CLI. Cockpit's console has a **login +screen**, so per the module brief it is published internally as `https://podman.` via +`network:proxy`. + +## What you get + +| Capability | Access from | How | +|------------|-------------|-----| +| Podman container engine (rootless + CLI) | on the VM | `ssh tappaas@` then `podman …` | +| Cockpit web console (login + Podman page) | `mgmt` zone | `https://podman.` (internal) | +| Direct console | LAN reachable from `mgmt` | `https://:9090` | +| `podman-compose` for compose files | on the VM | `podman-compose up -d` | + +## What is not included + +- **No pre-loaded containers.** This is a clean host — you bring the images/compose files. +- **No internet exposure.** The friendly name is reachable only from the `mgmt` zone; it is + not published to the internet (no inbound NAT/port-forward). +- **No Docker daemon / Kubernetes.** Podman is daemonless; there is no `dockerd` and no k8s + control plane here (Podman can generate/play Kubernetes YAML, but that is out of scope). +- **No default web login.** The console authenticates against Linux (PAM) accounts on the VM; + the cloud-init `tappaas` user has no password until you set one — see [INSTALL.md](./INSTALL.md). + +## Access (internal only) + +`network:proxy` publishes **`https://podman.`** (split-horizon DNS → Caddy → the VM's +`:9090`, HTTPS upstream), with an access-list permitting only the **`mgmt`** zone — **not +reachable from the internet**. Cockpit rides WebSockets, so the route is created with +`proxyUpstreamHttp1: true` (Caddy talks HTTP/1.1 to the upstream); the direct +`https://:9090` always works too. + +## Dependencies + +| Depends on | Purpose | +|------------|---------| +| `cluster:vm` | Creates the Debian 13 VM from the cloud image | +| `templates:debian` | OS prep (apt update/upgrade + qemu-guest-agent) | +| `backup:vm` | Scheduled VM backup to PBS | +| `network:proxy` | Internal-only reverse proxy for `podman.` | + +## Placement + +The module sets no explicit `zone0`, so the VM is placed in the **default environment's zone**; +install it with `--environment ` to put it in a specific environment and its associated +zone instead (see [INSTALL.md](./INSTALL.md#placement--zone)). + +## Sizing + +2 vCPU / **2 GB** RAM / 20 GB disk — enough for the engine, Cockpit and a handful of light +containers. Bump memory/disk to match whatever you actually run on it (container images and +volumes land on the VM disk, thin-provisioned on ZFS). + +For installation steps see [INSTALL.md](./INSTALL.md). diff --git a/src/containers/podman/install.sh b/src/containers/podman/install.sh new file mode 100755 index 0000000..db8bfd0 --- /dev/null +++ b/src/containers/podman/install.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# +# podman module install — thin wrapper. +# +# The VM itself is created by the cluster:vm provider (Debian 13 cloud image), +# then OS-prepped by the templates:debian provider (apt update + qemu-guest-agent +# via update-os.sh). This script applies the module-specific step: installing +# Podman + the Cockpit web console onto the Debian VM. All real work lives in +# update.sh (install == update for this module). +# +# Usage: install.sh +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/update.sh" "$@" diff --git a/src/containers/podman/podman.json b/src/containers/podman/podman.json new file mode 100644 index 0000000..81deb1c --- /dev/null +++ b/src/containers/podman/podman.json @@ -0,0 +1,37 @@ +{ + "description": "Podman container host — a Debian 13 VM with rootless Podman + the Cockpit web console (cockpit-podman) for managing containers, images and pods from a browser", + "version": "0.1.0", + "appVersion": "5.4", + "releaseDate": "2026-08-18", + "maintainer": "@larsrossen", + "status": "Development", + "vmname": "podman", + "vmid": 812, + "vmtag": "TAPPaaS,Community,Containers", + "ports": [ + { "port": 9090, "protocol": "TCP", "description": "Cockpit web console UI + login (HTTPS)" } + ], + "dependsOn": ["cluster:vm", "templates:debian", "backup:vm", "network:proxy"], + "provides": [], + "config": { + "cluster:vm": { + "bios": "seabios", + "ostype": "l26", + "cores": 2, + "memory": "2048", + "diskSize": "20G", + "storage": "tanka1", + "imageType": "img", + "image": "debian-13-generic-amd64.qcow2", + "imageLocation": "https://cdimage.debian.org/cdimage/cloud/trixie/latest", + "cloudInit": "true", + "bridge0": "lan" + }, + "network:proxy": { + "proxyPort": 9090, + "proxyUpstreamTls": true, + "proxyUpstreamHttp1": true, + "proxyAllowedZones": ["mgmt"] + } + } +} diff --git a/src/containers/podman/test.sh b/src/containers/podman/test.sh new file mode 100755 index 0000000..a2ac1fe --- /dev/null +++ b/src/containers/podman/test.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# +# podman module test — health checks for the Podman container-host VM. +# +# Verifies (from tappaas-cicd, over the Proxmox guest agent + SSH): +# - the VM is reachable +# - podman is installed (version marker + working binary) +# - the cockpit-podman plugin is present +# - the Cockpit web console answers (HTTPS, :9090) +# +# Usage: test.sh +# + +set -uo pipefail + +. /home/tappaas/bin/common-install-routines.sh + +MODULE="${1:-podman}" +readonly MGMT="mgmt" +VMID="$(get_config_value 'vmid')" +VMNAME="$(get_config_value 'vmname' "${MODULE}")" + +PASS=0; FAIL=0 +ok() { info " ${GN}✓${CL} $1"; PASS=$((PASS+1)); } +no() { error " ✗ $1"; FAIL=$((FAIL+1)); } + +[[ -n "${VMID}" && "${VMID}" != "null" ]] || die "no vmid for ${MODULE}" + +PRIMARY="$(get_primary_node_fqdn 2>/dev/null || echo "tappaas1.${MGMT}.internal")" +NODE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "root@${PRIMARY}" \ + "pvesh get /cluster/resources --type vm --output-format json 2>/dev/null" \ + | jq -r --arg v "${VMID}" '.[] | select(.vmid==($v|tonumber)) | .node' 2>/dev/null | head -1)" +[[ -n "${NODE}" ]] || NODE="$(get_config_value 'node' "$(get_node_hostname 0)")" + +IP="$(ssh -o BatchMode=yes "root@${NODE}.${MGMT}.internal" \ + "qm guest cmd ${VMID} network-get-interfaces" 2>/dev/null \ + | jq -r '.[] | select(.name | test("^lo$") | not) | ."ip-addresses"[]? | select(."ip-address-type"=="ipv4") | ."ip-address"' 2>/dev/null \ + | grep -v '^127\.' | head -1)" + +info "${BOLD}podman test${CL} (${VMNAME}, VM ${VMID} on ${NODE})" +if [[ -z "${IP}" ]]; then + no "could not resolve VM IP via guest agent" + info "Result: ${PASS} passed, ${FAIL} failed" + exit 1 +fi +ok "VM IP resolved (${IP})" + +vm() { ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "tappaas@${IP}" "$@"; } + +# Version marker + working podman binary +GOT_VER="$(vm "cat /etc/tappaas-podman.version 2>/dev/null" || true)" +if [[ -n "${GOT_VER}" ]]; then + ok "podman version marker present (${GOT_VER})" +else + no "podman version marker missing (not installed?)" +fi +if vm "command -v podman >/dev/null 2>&1 && podman --version >/dev/null 2>&1"; then + ok "podman binary works ($(vm "podman --version 2>/dev/null" || echo '?'))" +else + no "podman binary not working" +fi + +# cockpit-podman plugin installed +if vm "dpkg -s cockpit-podman >/dev/null 2>&1"; then + ok "cockpit-podman plugin installed" +else + no "cockpit-podman plugin not installed" +fi + +# Cockpit web console answers on :9090 (login page pre-auth is a pass). +# Plain -s (no --fail): --fail exits 22 on 4xx and would corrupt the -w code. +code="$(vm "curl -sk -o /dev/null -w '%{http_code}' https://localhost:9090/ 2>/dev/null" || echo 000)" +if [[ "${code}" =~ ^(200|302|401|403)$ ]]; then + ok "Cockpit console answers on :9090 (HTTP ${code})" +else + no "Cockpit console did not answer on :9090 (HTTP ${code})" +fi + +echo "" +info "Result: ${PASS} passed, ${FAIL} failed" +[[ "${FAIL}" -eq 0 ]] diff --git a/src/containers/podman/update.sh b/src/containers/podman/update.sh new file mode 100755 index 0000000..274efd7 --- /dev/null +++ b/src/containers/podman/update.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# +# podman module update — install/upgrade Podman + the Cockpit web console on the +# Debian 13 VM. +# +# Podman is the daemonless, rootless-capable container engine; it ships in the +# Debian 13 (trixie) main repo, so no external download is needed. To give it the +# browser interface the module promises, we also install Cockpit and its +# cockpit-podman plugin — Cockpit serves an HTTPS admin console with a PAM LOGIN +# SCREEN on :9090, and cockpit-podman adds the "Podman containers" page for +# managing containers, images and pods. network:proxy publishes that console as +# https://podman. (internal, mgmt zone only). +# +# This runs on tappaas-cicd: it resolves the VM's IP via the Proxmox guest agent, +# then over SSH installs the packages from apt. Idempotent: it re-runs apt every +# time (apt is a no-op when already current) and records a version marker. +# +# Usage: update.sh +# + +set -euo pipefail + +. /home/tappaas/bin/common-install-routines.sh + +MODULE="${1:-podman}" +readonly MGMT="mgmt" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +readonly MARKER="/etc/tappaas-podman.version" # on the VM: records installed podman version + +VMNAME="$(get_config_value 'vmname' "${MODULE}")" +VMID="$(get_config_value 'vmid')" +[[ -n "${VMID}" && "${VMID}" != "null" ]] || die "no vmid for ${MODULE}" + +# ── Locate the node hosting the VM (HA-safe) and resolve its IP ─────── +PRIMARY="$(get_primary_node_fqdn 2>/dev/null || echo "tappaas1.${MGMT}.internal")" +NODE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "root@${PRIMARY}" \ + "pvesh get /cluster/resources --type vm --output-format json 2>/dev/null" \ + | jq -r --arg v "${VMID}" '.[] | select(.vmid==($v|tonumber)) | .node' 2>/dev/null | head -1)" +[[ -n "${NODE}" ]] || NODE="$(get_config_value 'node' "$(get_node_hostname 0)")" +[[ -n "${NODE}" ]] || die "could not locate the node hosting VM ${VMID}" + +get_vm_ip() { + ssh -o BatchMode=yes "root@${NODE}.${MGMT}.internal" \ + "qm guest cmd ${VMID} network-get-interfaces" 2>/dev/null \ + | jq -r '.[] | select(.name | test("^lo$") | not) | ."ip-addresses"[]? | select(."ip-address-type"=="ipv4") | ."ip-address"' 2>/dev/null \ + | grep -v '^127\.' | head -1 +} + +info "${BOLD}Installing Podman + Cockpit${CL} on ${VMNAME} (VM ${VMID}, node ${NODE})" + +IP="" +for _ in $(seq 1 18); do IP="$(get_vm_ip)"; [[ -n "${IP}" ]] && break; sleep 10; done +[[ -n "${IP}" ]] || die "could not resolve ${VMNAME} IP via guest agent (is qemu-guest-agent up? templates:debian installs it)" +info " VM IP: ${IP}" + +# SSH helper: run a command on the VM as the tappaas user. +vm() { ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "tappaas@${IP}" "$@"; } + +# Wait for SSH (cloud-init may still be finishing). +for _ in $(seq 1 40); do vm "exit 0" 2>/dev/null && break; sleep 3; done +vm "exit 0" 2>/dev/null || die "SSH to tappaas@${IP} not available" + +# ── Friendly URL for the operator ──────────────────────────────────── +DOMAIN="$(get_variant_config "" 2>/dev/null | jq -r '.domain // empty' || true)" +PROXY_DOMAIN="$(get_config_value 'proxyDomain' "${VMNAME}${DOMAIN:+.${DOMAIN}}")" +COCKPIT_DIRECT_URL="https://${IP}:9090" +if [[ -n "${PROXY_DOMAIN}" ]] && getent hosts "${PROXY_DOMAIN}" >/dev/null 2>&1; then + COCKPIT_URL="https://${PROXY_DOMAIN}" +else + [[ -n "${PROXY_DOMAIN}" ]] && info " ${PROXY_DOMAIN} does not resolve yet — using the direct URL" + COCKPIT_URL="${COCKPIT_DIRECT_URL}" +fi + +# ── 1. Install podman + cockpit + cockpit-podman (apt) ─────────────── +# podman-compose/slirp4netns give rootless networking + compose-file support; +# cockpit + cockpit-podman provide the web console with a login screen on :9090. +info " Installing podman, podman-compose, cockpit, cockpit-podman (apt)..." +vm "sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq" || die "apt-get update failed" +vm "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ + podman podman-compose slirp4netns uidmap \ + cockpit cockpit-podman \ + curl ca-certificates" \ + || die "failed to install podman/cockpit packages" + +PODMAN_VER="$(vm "podman --version 2>/dev/null | awk '{print \$3}'" || true)" +[[ -n "${PODMAN_VER}" ]] || die "podman did not install correctly (no version reported)" +info " podman version: ${PODMAN_VER}" + +# ── 2. Enable podman socket + Cockpit web console ──────────────────── +# Rootless podman API socket for the tappaas user (lets cockpit-podman talk to it +# without root), and the Cockpit HTTPS console on :9090. +info " Enabling the rootless podman socket + Cockpit web console..." +vm "systemctl --user enable --now podman.socket 2>/dev/null || true" +vm "sudo loginctl enable-linger tappaas 2>/dev/null || true" # keep the user socket alive after logout +vm "sudo systemctl enable --now cockpit.socket" || die "failed to enable cockpit.socket" + +# Record version marker. +vm "echo '${PODMAN_VER}' | sudo tee ${MARKER} >/dev/null" || warn "could not write version marker" + +# ── 3. Wait for the Cockpit console to answer (port 9090) ──────────── +info " Waiting for the Cockpit console to come up (https://${IP}:9090)..." +UP=0 +for _ in $(seq 1 18); do + code="$(vm "curl -fsk -o /dev/null -w '%{http_code}' https://localhost:9090/ 2>/dev/null" || echo 000)" + [[ "${code}" =~ ^(200|302|401|403)$ ]] && { UP=1; break; } + sleep 5 +done + +echo "" +if [[ "${UP}" -eq 1 ]]; then + info "${GN}✓ Podman ${PODMAN_VER} + Cockpit console installed${CL}" +else + warn "Packages installed but the Cockpit console did not answer yet — it may still be starting." +fi + +echo "" +info "${BOLD}═══ Next steps ═══${CL}" +info " ${BOLD}Console:${CL} ${COCKPIT_URL} (direct: https://${IP}:9090)" +info " Log in with a Linux account on the VM. The cloud-init ${BOLD}tappaas${CL} user has" +info " SSH-key auth and no password, so set one first for the web login:" +info " ssh tappaas@${IP} 'sudo passwd tappaas'" +info " Then open the console and pick ${BOLD}Podman containers${CL} in the left menu." +info " See INSTALL.md for the details (and how to add a dedicated admin user instead)." diff --git a/src/module-catalog.json b/src/module-catalog.json new file mode 100644 index 0000000..1e1b2dd --- /dev/null +++ b/src/module-catalog.json @@ -0,0 +1,17 @@ +{ + "description": "MakerFLOSS DevOps Module Registry — experimental TAPPaaS modules", + "foundationModules": [], + "applicationModules": [ + { + "moduleName": "podman", + "repo": "community", + "moduleJson": "src/containers/podman/podman.json", + "vmid": 812, + "stack": "infrastructure", + "category": "containers", + "status": "incomplete" + } + ], + "proxmoxTemplates": [], + "testModules": [] +}