Repository navigation
fix(podman): keep the rootless network alive across a job teardown on Podman 4.9 (#458) #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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)" |