Skip to content

fix(podman): keep the rootless network alive across a job teardown on Podman 4.9 (#458) #113

fix(podman): keep the rootless network alive across a job teardown on Podman 4.9 (#458)

fix(podman): keep the rootless network alive across a job teardown on Podman 4.9 (#458) #113

Workflow file for this run

# Syntax lint for the deploy/ daemon artifacts.
#
# These files are hand-written examples an operator adapts; nothing exercises them in the normal test
# suite (deploy/** is not in any other workflow's path filter), so a typo'd directive or a malformed
# plist ships silently and only fails on the operator's box. This workflow is the cheap gate: it checks
# STRUCTURE, not runtime behaviour.
#
# - worker.service / receiver.service : `systemd-analyze verify` parses the units. The PLACEHOLDER
# paths (/opt/pi-dispatch, /usr/bin/node) do not exist on the runner, so path-existence warnings are
# EXPECTED and ignored; only genuine syntax errors (unknown directives, parse failures) fail.
# - com.pi-dispatch.worker.plist : `xmllint --noout` proves the plist is well-formed XML. plutil is
# macOS-only, so xmllint (libxml2-utils) is the Linux stand-in for well-formedness.
# - worker-env-wrapper.sh : `sh -n` is a POSIX shell parse check (no execution).
# - the Quadlet templates (pi-dispatch-*.container / .network, issue #430) : rendered by the real planner, then
# Podman 4.9's own generator in `-dryrun` mode (the `quadlet` job below). The planner is in the path filter for
# that reason: it is half of what the generator is fed.
#
# The `.cmd` wrappers (worker-env-wrapper.cmd, nssm-install.cmd) have no practical Linux linter and are
# not checked here -- deliberately, rather than with a flaky approximation.
name: deploy lint
on:
# MAIN ONLY, for the reason `pi-upgrade-check.yml` records at greater length: without a `branches:` key
# this fired on every branch, so a PR branch touching `deploy/` ran twice and produced two check runs
# under one context name. This check is deliberately NOT required, so the duplicate could not deadlock a
# merge -- but it could still show green twice while one of them was red.
push:
branches: [main]
paths:
- "deploy/**"
- "worker/src/podman-stack.mjs"
- ".github/workflows/deploy-lint.yml"
pull_request:
paths:
- "deploy/**"
- "worker/src/podman-stack.mjs"
- ".github/workflows/deploy-lint.yml"
workflow_dispatch:
# Superseded runs are cancelled rather than queued. Same reasoning as `pi-upgrade-check.yml`: a literal
# group would serialise every PR against every other, and nothing here is a publish that must finish.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: deploy/ artifacts are syntactically valid
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install linters
run: sudo apt-get update && sudo apt-get install -y libxml2-utils
- name: systemd units parse (syntax only; PLACEHOLDER path warnings ignored)
run: |
# verify exits non-zero for the missing PLACEHOLDER exec paths, which is expected -- so gate on
# the CONTENT of the diagnostics, not the exit code. A real defect is an unknown directive or a
# parse failure; a missing /opt/pi-dispatch or /usr/bin/node is not.
systemd-analyze verify --no-pager deploy/worker.service deploy/receiver.service > verify.log 2>&1 || true
cat verify.log
if grep -Eiq 'Unknown (key name|lvalue|section)|Failed to parse|Invalid setting|not a valid|expected ' verify.log; then
echo "::error::systemd-analyze found a syntax error in a unit file. See the log above."
exit 1
fi
echo "OK: unit files are syntactically valid (path-existence warnings ignored)"
- name: plist is well-formed XML
run: |
xmllint --noout deploy/com.pi-dispatch.worker.plist \
|| { echo "::error::deploy/com.pi-dispatch.worker.plist is not well-formed XML."; exit 1; }
echo "OK: plist well-formed"
- name: POSIX wrapper parses
run: |
sh -n deploy/worker-env-wrapper.sh \
|| { echo "::error::deploy/worker-env-wrapper.sh has a shell syntax error."; exit 1; }
echo "OK: worker-env-wrapper.sh parses"
quadlet:
# The podman venue's Quadlet templates (issue #430), put through Podman's own generator in dry-run mode, rendered
# first by the real planner (worker/src/podman-stack.mjs) so what is checked is what `service install` writes.
# ubuntu-24.04 PINNED, not -latest: it ships Podman 4.9.3, the oldest Podman the templates claim to support, with
# the generator at /usr/libexec/podman/quadlet. A key 4.9 does not know is exactly the defect this exists to catch.
name: deploy/ Quadlet templates generate on Podman 4.9
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install podman (for its quadlet generator; nothing is run)
run: sudo apt-get update && sudo apt-get install -y podman && podman --version
- name: Render the templates and dry-run the generator
run: |
set -eu
units="$RUNNER_TEMP/quadlet"
mkdir -p "$units"
# A deployment folder the planner accepts, with the allowlist the proxy mounts.
deploy="$RUNNER_TEMP/deploy"
mkdir -p "$deploy" && : > "$deploy/egress-allowlist.conf"
node --input-type=module -e "
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { planStack } from './worker/src/podman-stack.mjs';
const plan = planStack({ components: { valkey: true, proxy: true, keeper: true }, templatesDir: resolve('worker/deploy'), deployDir: process.argv[1], home: '/nonexistent', fs: { readFileSync, existsSync } });
if (plan.error) { console.error(plan.error); process.exit(1); }
for (const f of plan.files) writeFileSync(join(process.argv[2], basename(f.path)), f.text);
" "$deploy" "$units"
ls -l "$units"
QUADLET_UNIT_DIRS="$units" /usr/libexec/podman/quadlet -dryrun -user > generated.txt 2> quadlet.log || { cat quadlet.log; echo "::error::quadlet refused a template"; exit 1; }
cat quadlet.log generated.txt
if grep -Eiq 'unsupported|error|invalid' quadlet.log; then
echo "::error::quadlet reported a problem with a template. See the log above."
exit 1
fi
# What the worker relies on by name, read off the generated units rather than the templates. Extended
# regexes, because the two generators spell an option differently: Podman 4.9.3 writes `--name=X` and
# `--network=X` (run 36275999554), 5.8.1 writes `--name X` (the M0-b measurement). Either is one argv word
# pair to podman, so both are accepted and nothing else is.
for needle in \
'ExecStart=/usr/bin/podman network create --ignore pi-dispatch-valkey$' \
'ExecStart=/usr/bin/podman network create --ignore pi-dispatch-egress-out$' \
'ExecStart=/usr/bin/podman network create --ignore --disable-dns --internal pi-dispatch-netns-keeper$' \
'--name[= ]pi-dispatch-valkey ' \
'--name[= ]pi-dispatch-egress-proxy ' \
'--network[= ]pi-dispatch-valkey ' \
'--network[= ]pi-dispatch-egress-out ' \
'--publish[= ]127\.0\.0\.1:6379:6379 ' \
'--health-cmd[= ]"\[\\"bash\\",'; do
grep -qE -- "$needle" generated.txt || { echo "::error::generated units lack: $needle"; exit 1; }
done
# The keeper's own flags (issue #458), read off ITS ExecStart line alone: `--read-only` or `--cap-drop` anywhere
# else in the generated units would otherwise satisfy a needle the keeper itself had lost.
keeper="$(grep -E '^ExecStart=/usr/bin/podman run .*--name[= ]pi-dispatch-netns-keeper ' generated.txt)"
test "$(printf '%s\n' "$keeper" | wc -l)" -eq 1 || { echo "::error::expected exactly one keeper ExecStart"; exit 1; }
for needle in \
' --network[= ]pi-dispatch-netns-keeper ' \
' --init ' \
' --cap-drop[= ]all ' \
' --read-only ' \
' --user[= ]65534:65534 ' \
' --security-opt[= ]no-new-privileges ' \
' --entrypoint[= ]sleep --image-volume[= ]ignore docker\.io/ubuntu/squid@sha256:[0-9a-f]{64} infinity$'; do
printf '%s\n' "$keeper" | grep -qE -- "$needle" || { echo "::error::the keeper's ExecStart lacks: $needle"; exit 1; }
done
# And its unit-level restart policy, passed through by the generator: no start limit, a one-second retry.
for needle in '^StartLimitIntervalSec=0$' '^RestartSec=1s$' '^Restart=always$'; do
grep -qE -- "$needle" generated.txt || { echo "::error::the keeper's unit lacks: $needle"; exit 1; }
done
for banned in ' --publish' ' -p ' ' --volume' ' -v ' ' --cap-add' ' --privileged' ' --mount'; do
if printf '%s\n' "$keeper" | grep -qF -- "$banned"; then echo "::error::the keeper's ExecStart carries $banned"; exit 1; fi
done
echo "OK: every Quadlet template generates on $(podman --version)"