Commit Graph
29 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 eda6b25329 fix(pihole): pass the admin password through a dotenv-quoted env file and judge the denial before the callback URL
Run 34421944328 pihole#1 (README replay, sso off) never left
/admin/login: the native login was refused. The compose template put
the administrator password inline under environment:, and compose
interpolates every $name in it, so the container held the inventory
password with each $name removed (its sha256 prefix matches that
mangled value, not the real one). The environment moves into
templates/env.j2, which has_env wires as env_file, with the password
through dotenv_quote like every other role. Rendering the real template
with a password containing $name, ${...} and $$ and reading it back from a
compose v5.5.1 container returns it unchanged, while an unquoted RAW=S$x
control line in the same file comes back as "S". TZ now comes from the
shared sys-svc-container include, so PIHOLE_TIMEZONE goes.

pihole#0 (onion) timed out on "still handing off": oauth2-proxy serves
its 403 page for a user outside the allowed group on /oauth2/callback,
and the poll checked the callback URL before the page text. The CI
trace holds "You do not have permission to access this resource." in
that page's collapsed More Info block, which textContent reads. The
poll now judges the denial text first, then the Keycloak auth URL, and
only then treats the callback URL as a handoff in progress. The loose
"403" and "forbidden" matches go, since a reached admin page can
contain either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 13:47:22 +02:00
kevinveenbirkenbachandClaude Opus 5 acc9e25c0f refactor(volumes): one canonical mount surface, context-wired lookups
The mount pipeline was split across two layers: compose_volumes and
container_volumes lived as filter modules that ansible's filter loader
rejected on every start ("has no attribute 'FilterModule'", two warnings
per run), while the same-named lookups wrapped them thinly. The filter
bodies now live inside their lookups (the tor_socks precedent), the
filter files are gone, and the render tests moved next to the lookup
tests.

As lookups they see the templating context, so the application_id term
every call site passed back to its own role is gone too: both lookups
read application_id from context and fail loudly when it is missing.
107 templates and nfs_prep.yml drop the term; the calls collapse to
lookup('compose_volumes') and lookup('container_volumes', service_name).

The container_volumes call is now canonical - four leading spaces,
service_name in the service position, | indent(4) - enforced by a new
lint (test_container_volumes_call_shape). Normalizing the 85 literal
call sites surfaced that erpnext keyed its meta/volumes.yml mounts by
container name instead of compose service key, the only role to do so;
its meta now uses the service keys. wordpress's column-0 call, which
rendered its first line at top level, is indented like every other.

Template logic that only restated meta/volumes.yml is deleted: mailu fed
compose_volumes an extra_volumes dict whose eight names were looked up
from the very meta file compose_volumes reads itself; zammad's init and
railsserver extra mounts were the volumes.yml entries minus the two
services, which the meta now lists (the when-gated OIDC binds included).
Conditionality that meta cannot express stays in code, but in role-local
lookup plugins instead of template loops: matrix_bridge_mounts renders
the per-bridge registration mounts, nextcloud_extra_volumes the
feature-gated whiteboard/recording volumes (type: volume entries are
declared unconditionally by compose_volumes, so gating must happen
before it).

Parity is proven, not assumed: every mechanically rewritten template is
byte-identical to HEAD after the transformation regexes (95/95), and the
semantic roles render identically through the real functions against
HEAD's meta - zammad across six sso/tls polarities, all seven erpnext
services, mailu's top level. make test-lint passes complete.

Also: suitecrm's compose template pins container_extra_hosts with
host_alias (the SOCKS proxy URL host.docker.internal never resolved in
compose+tor), and the five disabled mautrix bridge addons use the
empty-default config lookup so templating the applications dict stops
emitting 125 errors per batch in every CI job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:28:16 +02:00
kevinveenbirkenbachandClaude Opus 5 e3e81e816f refactor(healthcheck): declare probes in services.yml, render them from one lookup
Seven include templates under sys-svc-container decided how a container was
probed, and they read their inputs from template-local `{% set %}` scope.
That scope leaks: a value set for one service silently reached every service
below it, which is how taiga's front and gateway ended up probing a port
declared for a different service, and how bluesky's frontend probed a PDS
endpoint.

Probes now live in utils/docker/healthcheck.py, one class per flavor behind a
PROBES registry, and each service declares its own:

    healthcheck:
      flavor: curl
      path: health/ready
      start_period: 20m

`port_key` picks another entry under ports.internal for the services that
expose their probe elsewhere (keycloak on management, nextcloud talk on
signaling). Without a flavor the entry must carry an explicit test argv, so a
missing declaration fails loudly instead of rendering a probe against nothing.

Rendering HEAD's templates against the new output confirms 49 of 56 call
sites are byte-identical; the remaining seven are covered by an Ansible run.
Three probes change on purpose: erpnext's backend gains a check it never had,
bluesky's frontend drops the leaked PDS path, and msmtp_curl moves from a
hardcoded localhost:80 to the service's own port on 127.0.0.1 like every
other flavor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 04:05:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f5412682b0 refactor(container-port): derive healthcheck port, drop redundant defs
The sys-svc-container healthcheck templates (curl/wget/nc/tcp/http) now
derive container_port from services.<service_name>.ports.internal.http,
so a role no longer needs to set container_port only to feed the shared
healthcheck.

Add the container-port-set-unused lint that fails a role setting
container_port without using it elsewhere, then resolve every flagged
role: drop the redundant assignment where the derivation reproduces the
value, or mark it with nocheck where the port is non-http, a constant, or
keyed on a service the derivation cannot reach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 05:05:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f2af680a2f test(lint): require container_port to read the internal-port config lookup
Add a lint that rejects hardcoded container_port literals and the
host-published ports.local port; container_port is the port the container
listens on, so it must resolve from
lookup('config', application_id, 'services.<svc>.ports.internal.<name>')
or a constant built that way. Bring 42 roles into compliance: move each
literal into services.<svc>.ports.internal.<name> and reference it via the
lookup, using entity_name/service_name where they equal the service key.
Values are preserved (mattermost/prometheus pointed at ports.local with the
same value as internal). Drop the dead container_port from fider and
prometheus, and the narrative comments the edits pulled into the comment
lint's changed-file scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:20:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d4ee9a85ba refactor(compose): align templates to the service_name skeleton
Every compose.yml.j2 now declares services as '{{ service_name }}:' with
its '{% set %}' knobs on two-space lines above the key, includes at four
spaces and if/for tags aligned to the content they wrap. The layout
renders correctly because stack_host_template enables lstrip_blocks for
compose.yml.j2; indent(4, True) in the container base and '{%+' guards
in whitespace-sensitive scalars keep the shared fragments identical
under both render modes. A new lint test enforces the skeleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:36:58 +02:00
kevinveenbirkenbachandClaude Opus 4.8 594109018a refactor(compose): publish every container port through the container_ports lookup
Take the host IP as an optional third element of each term
([service, protocol, host_ip]) instead of an ip= kwarg, and build the whole
ports: block from declared ports.local/public (host) + ports.internal
(container). Convert every compose template across 84 roles, folding the
remaining raw var/numeric/specific-ip mappings (including mailu's per-interface
mail ports bound to MAILU_IP4_PUBLIC). Multi-term calls render one term per line.

Add the compose-ports-must-use-container-ports lint: it forbids any raw
"<host>:<published>:<container>" publish item under a ports: block and requires
every term to declare ports.internal. Remove
tests/lint/roles/test_swarm_internal_ports.py, now subsumed.

Fix a container_depends_on rendering regression. The lookup self-indented
(default indent=4), so the added 4-space placement + | indent(4) triple-indented
the block. Flip the lookup default to column 0 (consistent with
container_networks), make every call use the 4-space + | indent(4) form, and add
a lint enforcing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c8a1ca59d refactor(compose): source published container ports from ports.internal
Rewrite every hardcoded literal container port in a compose.yml.j2 publish
mapping (- "<host>:<...ports.local|public.kind...>:<LITERAL>") to read the
declared ports.internal.<kind> via lookup, across 22 roles, so the published
(local/public) port and the container (internal) port are a single declared
source of truth. Mappings whose container already references the lookup or a
variable are left as-is.

This surfaced and fixed a real swarm bug: web-app-openproject declared
ports.internal.http: 8080 while the container actually listens on 80, so the
swarm resolve_upstream pointed openproject at the wrong port. Reconciled to 80
(the mapping's real container port).

Extend tests/lint/roles/test_swarm_internal_ports.py with a second check: a
published mapping's container segment must come from ports.internal via a lookup
(or a variable referencing it), never a hardcoded literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6df91fb0c2 fix(web-app-pihole): conform compose template to lookups + disable unused email
Route image/volumes/networks through the container_image, container_volumes, compose_volumes and *_networks lookups (was a raw image ref, an inline service volumes block, and the deprecated compose_volumes pipe form); add meta/volumes.yml as the volume source and drop the four now-unused PIHOLE_IMAGE/VERSION/VOLUME vars. Pi-hole sends no mail, so the email service is pinned enabled=false/shared=false (dynamic-flag nocheck) and dropped from the variant matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:46:12 +02:00
Prageeth Panicker 71a565b9f0 fix(pihole): variant-1 test failures and Pi-hole v6 compatibility
Three fixes:

1. guest.spec.js: skip instead of fail when OIDC_ISSUER_URL is unset
   (variant-1 has SSO disabled so no Keycloak redirect is expected;
   the old expect().toBeTruthy() caused a hard failure instead of skip)

2. playwright.env.j2: only emit OIDC_ISSUER_URL when SSO is proxy-gated
   (was unconditionally set, causing native.spec.js to wrongly skip in
   variant-1 and oauth2.spec.js/guest.spec.js to fail on missing redirect)

3. compose.yml.j2: Pi-hole v6 compatibility fixes
   - Replace WEBPASSWORD with FTLCONF_webserver_api_password (v6 dropped
     legacy env vars; WEBPASSWORD is silently ignored in v6)
   - Add FTLCONF_webserver_domain={{ domain }} to whitelist the nginx
     proxy Host header; Pi-hole v6 FTL returns 403 for unrecognised Host
     headers (DNS rebinding protection) — this was causing the baseline
     playwright.spec.js to fail with HTTP 403 in variant-1
2026-06-24 03:46:11 +02:00
Prageeth Panicker 4342f2b2d7 fix(pihole): remove duplicate sso-proxy block from compose.yml.j2
This block was added in an earlier (reverted-in-intent but never
actually removed) attempt and caused a duplicate 'sso-proxy' YAML key
(also defined via sys-svc-compose/base.yml.j2's sso_proxy include),
breaking compose_ca.py's compose config parsing entirely.

The CA trust fix now lives solely in 01_core.yml's lineinfile task,
which patches the rendered oauth2-proxy config file directly -- no
compose.yml changes needed.
2026-06-24 03:46:11 +02:00
Prageeth Panicker 08a79490fd fix(pihole): sort playwright.env.j2 keys alphabetically 2026-06-24 03:46:11 +02:00
Prageeth Panicker bd44a3036c fix(pihole): declare CANONICAL_DOMAIN in playwright.env.j2
Required by playwright.spec.js baseline checks (canonical domain
assertion), added in the previous commit.
2026-06-24 03:46:11 +02:00
Prageeth Panicker e5058549a2 fix(pihole): inject CA cert for sso-proxy on self-signed TLS
oauth2-proxy is a distroless Go binary — the platform CA wrapper
(with-ca-trust.sh) is skipped because there is no /bin/sh, and Go
ignores SSL_CERT_FILE. Pass the CA cert directly via --provider-ca-file
so oauth2-proxy trusts the internal Keycloak OIDC endpoint.

Follows the penpot pattern of inline command override in compose.yml.j2
conditioned on TLS_MODE == self_signed. No-op on production (letsencrypt).
2026-06-24 03:46:11 +02:00
Prageeth Panicker cea4905572 fix(pihole): trust self-signed CA for sso-proxy OIDC connection
oauth2-proxy is distroless (no /bin/sh), so the platform CA wrapper
entrypoint is skipped. Go binaries ignore SSL_CERT_FILE and use the
system cert pool only, causing x509 errors on self-signed TLS setups.

Override sso-proxy command in compose.override.yml.j2 to append
--provider-ca-file pointing to the platform CA cert
(/tmp/infinito/ca/root-ca.crt, always bind-mounted by compose_ca.py)
when TLS_MODE is self_signed. No-op on production (letsencrypt).
2026-06-24 03:46:11 +02:00
Prageeth Panicker 2746654d36 fix(web-app-pihole): migrate to v9.x SSO pattern and fix compose_volumes 2026-06-24 03:46:10 +02:00
Prageeth Panicker 23d8374bcf fix(web-app-pihole): fix dynamic flags, port, personas, block size, docker exec, README 2026-06-24 03:46:10 +02:00
Prageeth Panicker 02e6c36aba fix(web-app-pihole): resolve CI lint failures and test compliance 2026-06-24 03:46:10 +02:00
Prageeth Panicker 97529ef784 fix(web-app-pihole): fix oauth2-proxy deployment and javascript injection
- Fix oauth2.enabled: replace Jinja2 group_names expression with literal true
- Set oauth2.shared: false to deploy role-local oauth2-proxy instance
- Add allowed_groups for RBAC administrator access control
- Add javascript service for logout button injection
- Remove variant 1 (standalone mode) - pihole always requires Keycloak
- Delete compose.override.yml.j2 (--ssl-insecure-skip-verify no longer needed)
- Fix javascript.js.j2: wrap redirect in DOMContentLoaded to fix DOM timing
- Add TEST_E2E_PLAYWRIGHT_WAIT_ENABLED: false for oauth2-protected app
2026-06-24 03:46:10 +02:00
Prageeth Panicker 64faee4ac6 fix(web-app-pihole): skip TLS verification for oauth2-proxy OIDC discovery
oauth2-proxy cannot verify Keycloak's self-signed certificate during OIDC
discovery because the platform CA cert is injected via env vars but Go's
TLS stack doesn't pick it up reliably in the DinD environment.

Adding --ssl-insecure-skip-verify allows oauth2-proxy to complete OIDC
discovery and serve requests instead of crash-looping with certificate errors.
This is acceptable for a self-signed lab environment.
2026-06-24 03:46:10 +02:00
Prageeth Panicker 2ec86ee841 fix(web-app-pihole): add compose.override.yml.j2 to fix oauth2-proxy DNS resolution
oauth2-proxy cannot resolve auth.infinito.example from inside Docker in CI
environments where CoreDNS does not have this entry. Adding extra_hosts via
compose.override.yml.j2 (auto-included by compose_file_args lookup) maps
the Keycloak domain to host-gateway so OIDC discovery succeeds.
2026-06-24 03:46:10 +02:00
Prageeth Panicker e11223e389 fix(web-app-pihole): remove host port 53 binding to fix CI on Fedora
Port 53 is already in use by systemd-resolved on Fedora CI runners.
Pi-hole DNS still works within the Docker network without host binding.
Playwright tests don't require DNS functionality on the host.
2026-06-24 03:46:09 +02:00
Prageeth Panicker b6ff073136 fix(web-app-pihole): fix playwright logout test, remove duplicate oauth2-proxy block, add playwright wait flag
- Update logout test to navigate directly to oauth2/sign_out endpoint with Keycloak logout redirect
- Remove duplicate oauth2-proxy service definition from compose.yml.j2 (was causing duplicate key error)
- Add TEST_E2E_PLAYWRIGHT_WAIT_ENABLED: false to vars/main.yml (oauth2-proxy returns 403 before redirect)
2026-06-24 03:46:09 +02:00
Prageeth Panicker b9cf770578 fix(web-app-pihole): wrap javascript.js.j2 in raw block to prevent Jinja2 parsing 2026-06-24 03:46:09 +02:00
Prageeth Panicker 0c91412727 fix(web-app-pihole): add variants, javascript injection, fix oauth2 DNS, update playwright tests 2026-06-24 03:46:09 +02:00
Prageeth Panicker 470118494a fix(web-app-pihole): migrate to meta/services.yml, fix domain resolution, disable native password 2026-06-24 03:46:09 +02:00
Prageeth Panicker 786bda0bfb fix(web-app-pihole): fix compose template, RBAC group, playwright env and wait flag 2026-06-24 03:46:09 +02:00
Prageeth Panicker 49d039acfb fix(web-app-pihole): add sys-svc-compose networks include to compose template 2026-06-24 03:46:09 +02:00
Prageeth Panicker 561db31693 feat(web-app-pihole): add Pi-hole role with OAuth2/Keycloak RBAC
- Deploy Pi-hole as web app with NGINX proxy and OAuth2 protection
- Only web-app-pihole-administrators group can access the dashboard
- DNS port 53 (TCP/UDP) exposed for network-wide ad blocking
- Upstream DNS configurable per inventory (default: Cloudflare 1.1.1.1)
- Redis session store for OAuth2 proxy
- Timezone from HOST_TIMEZONE platform variable
- Port bindings use DOCKER_BIND_HOST platform variable
- Register subnet 192.168.102.240/28 in networks
- Register ports 8070 (http) and 16493 (oauth2) in ports
- Playwright tests: oauth2 protection, admin login, biber denied (3/3 passing)

Known limitations:
- Pi-hole v6 gravity blocklist requires manual setup via admin UI
- Internal *.infinito.example DNS forwarding to CoreDNS planned as follow-up
2026-06-24 03:46:08 +02:00