slides(tappaas): the session builds the portainer module
All checks were successful
Build slides / build (push) Successful in 1m22s
Build docs site / build (push) Successful in 1m26s

Retargets the deck from podman to portainer: the contract, the three script
slides, both demo slides and the repo-location slide. Demo 3 is now an actual
single sign-on — 'no login form, you are already you' — because Portainer is an
OIDC client and Cockpit never could be.

The test.sh slide keeps the check worth stealing: /api/settings/public reporting
method 3, which catches identity silently falling back to local accounts.
This commit is contained in:
Lars Rossen 2026-08-23 09:46:59 +02:00
parent 33e783efb5
commit 87d6d83ff3

View file

@ -4,7 +4,7 @@ theme: gaia
class: invert
paginate: true
title: How to implement a TAPPaaS module
description: Live build of a Podman module — OrangeMaker, 24 August
description: Live build of a Portainer module — OrangeMaker, 24 August
---
<style>
@ -27,7 +27,7 @@ section.diagram h2 { margin-bottom: 0.1em; }
# Getting a module onto TAPPaaS
Live build of a **Podman** container host
Live build of a **Portainer** container console
OrangeMaker · 24 August
@ -42,7 +42,7 @@ counterpart — keep the deck moving and spend the time in the shell.
1. **What TAPPaaS is** — a few diagrams, ten minutes, no deeper
2. **What a module actually is** — a json contract and three scripts
3. **Build one live** — the `lab1` environment, then `podman` into it
3. **Build one live** — the `lab1` environment, then `portainer` into it
4. **Prove it works** — tests, then the web console
5. **You get a login** — your own identity on this system
@ -69,7 +69,7 @@ flowchart LR
ha["home-assistant"]
end
subgraph z2["lab1 · zone lab1"]
pod["podman — today"]
pod["portainer — today"]
end
sat --> net
cicd --> nc
@ -156,8 +156,8 @@ We track **`main`**, not `stable` — this is a lab, we want the new things.
## A module is a contract and three scripts
```text
podman/
├── podman.json # the contract — what it is, needs, provides
portainer/
├── portainer.json # the contract — what it is, needs, provides
├── install.sh # put the software in the VM (once)
├── update.sh # keep it patched (on schedule)
├── test.sh # prove it still works (gates updates)
@ -173,15 +173,16 @@ That is the whole surface. Everything else is the platform's job.
```json
{
"description": "Podman container host — Debian 13 VM with rootless Podman + Cockpit",
"vmname": "podman", "vmid": 812,
"description": "Portainer CE — container console with TAPPaaS login",
"vmname": "portainer", "vmid": 813,
"dependsOn": ["cluster:vm", "templates:debian", "backup:vm",
"network:proxy", "identity:accessControl"],
"network:proxy", "identity:identity"],
"identity": { "oidcRedirectPaths": ["/"],
"secretsEnv": "/etc/secrets/portainer.env" },
"config": {
"cluster:vm": { "cores": 2, "memory": "2048", "diskSize": "20G",
"cluster:vm": { "cores": 2, "memory": "2048", "diskSize": "32G",
"image": "debian-13-generic-amd64.qcow2" },
"network:proxy": { "proxyPort": 9090, "proxyUpstreamTls": true,
"proxyUpstreamHttp1": true, "proxyAllowedZones": ["mgmt"] }
"network:proxy": { "proxyPort": 9443, "proxyUpstreamTls": true }
}
}
```
@ -199,11 +200,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${SCRIPT_DIR}/update.sh" "$@"
```
By the time it runs, `cluster:vm` has built the VM and `templates:debian` has
apt-upgraded it and installed the guest agent.
By the time it runs: `cluster:vm` built the VM, `templates:debian` apt-upgraded it,
`network:proxy` published the name — and `identity:identity` already created the
OIDC application and dropped its client credentials on the VM.
For podman there is nothing install-only — **install *is* update**. Five lines is a
perfectly good `install.sh`.
Nothing here is install-only, so **install *is* update**. Five lines is a perfectly
good `install.sh`.
---
@ -211,15 +213,16 @@ perfectly good `install.sh`.
Runs on the **mothership**, not on the VM. It:
1. finds the node hosting the VM via `pvesh` — HA-safe, the VM may have moved
2. resolves the VM's IP through the **Proxmox guest agent**, then waits for SSH
3. `apt-get install podman podman-compose slirp4netns uidmap cockpit cockpit-podman`
4. enables the **rootless** `podman.socket`, `loginctl enable-linger`, `cockpit.socket`
5. records `/etc/tappaas-podman.version`
6. polls `https://localhost:9090` until the console answers, then prints the URL
1. finds the node hosting the VM via `pvesh`, resolves its IP through the **guest agent**
2. `apt install podman`, then enables the **rootful** `podman.socket` — that socket
*is* the Docker-compatible API Portainer speaks
3. pulls `portainer-ce:lts` and runs it against the socket + a named volume
4. creates a **break-glass admin**, password into `/etc/secrets/portainer-admin`
5. reads `/etc/secrets/portainer.env`, fetches the OIDC **discovery document**,
and `PUT`s the three endpoints into `/api/settings` → login becomes OAuth
Every step is re-runnable: apt is a no-op when current, the marker is overwritten,
the sockets are `enable --now`. That is what "idempotent" has to mean in practice.
Re-runnable throughout: the container is recreated only when the image digest
actually changed, and an existing admin password is never regenerated.
---
@ -228,14 +231,14 @@ the sockets are `enable --now`. That is what "idempotent" has to mean in practic
| Check | How |
| --- | --- |
| VM reachable | IP resolves via the guest agent |
| podman installed | version marker + `podman --version` actually runs |
| plugin present | `dpkg -s cockpit-podman` |
| console alive | `:9090` answers `200`, `302`, `401` or `403` |
| container API | rootful `podman.socket` is active |
| app running | `podman container inspect` says `running` |
| API alive | `/api/status` returns a version |
| **identity intact** | `/api/settings/public` reports method `3` = OAuth |
A login page **is** a pass — pre-auth, `401` is the healthy answer.
Prints `N passed, M failed` and exits non-zero on any failure. Note `curl -s`, not
`--fail`: `--fail` exits 22 on 4xx and would corrupt the `-w '%{http_code}'` we read.
That last one is the check worth stealing. It catches the failure nobody notices:
identity quietly falling back to local accounts, so the app still *works* while no
longer being the login you thought it was.
Run on demand, and again before every update is merged. If `test.sh` is honest,
unattended updates are safe — that is the entire deal.
@ -247,8 +250,8 @@ unattended updates are safe — that is the entire deal.
- A VM — `cluster:vm` built it from the Debian 13 cloud image
- OS prep — `templates:debian` did apt + guest agent
- A VLAN, an interface, DHCP, firewall rules — the zone came with the environment
- `https://podman.lab1.makerfloss.eu` with a real certificate — `network:proxy`
- An access gate — `identity:accessControl`: only the bound groups reach the URL
- `https://portainer.lab1.makerfloss.eu` with a real certificate — `network:proxy`
- An OIDC application, a group binding and client credentials — `identity:identity`
- Nightly backup to PBS, scheduled updates, health reporting
A few lines of json bought all of it.
@ -279,10 +282,10 @@ A tenant with its own VLAN, firewall posture and DNS names —
## Demo 2 — install the module
```bash
module-manager module add podman --environment lab1
module-manager module add portainer --environment lab1
module-manager module list # what is deployed
module-manager module show podman # the resolved config, cascade applied
module-manager module show portainer # the resolved config, cascade applied
```
Watch the order: dependencies resolve first, then the VM, then the network, then
@ -296,31 +299,35 @@ maintains a list.
## Demo 3 — prove it, then look at it
```bash
module-manager module test podman
module-manager module test portainer
```
Then open **`https://podman.lab1.makerfloss.eu`** — Cockpit, with the Podman page.
Then open **`https://portainer.lab1.makerfloss.eu`** and press **Sign in**.
- Reachable from the `mgmt` zone only. Not from the internet, by declaration.
- Authentik gates the URL: no `devops` group, no console. Then Cockpit asks *again* —
it is not an OIDC client, and a local Unix account is not optional for it.
- Two logins, honestly. `DESIGN.md` in the repo says what it would take to fix.
- No login form. You are already you — Authentik, via OIDC.
- Not in `devops`? No console. The group binding **is** the access gate.
- Other machines in `lab1` join as Environments: one agent container on `:9001`.
Create a container. Start it. Read its logs. From a browser, as yourself.
---
## Where this module lives
```text
Community/src/larsrossen/containers/podman/
makerfloss/src/containers/portainer/
```
Two siblings next to it: `podman/` (one host, local login) and `komodo/` (GPL,
scripts still to write). Same job, three answers.
Three homes for a module, all first-class:
| Home | For |
| --- | --- |
| **TAPPaaS** repo, by pull request | modules the whole project should carry |
| **Community** repo | yours, shared, no gatekeeping — today's podman |
| **Private** repo — e.g. `forgejo.makerfloss.eu` | yours, not shared |
| **Community** repo | yours, shared, no gatekeeping |
| **Private** repo — `forgejo.makerfloss.eu/TAPPaaS/makerfloss` | ours, today's portainer |
Adding a repository is one `site-manager repository add`.
@ -339,7 +346,7 @@ authentik-manager user-recovery-link <you> # one-time URL: set your own passwo
One login per person, roles via groups. Nobody types a password into a chat window.
<!-- Do these live, one per participant, while the podman VM builds. -->
<!-- Do these live, one per participant, while the portainer VM builds. -->
---