Commit Graph
57 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 e6be88bf97 fix(postgres): size the buffers from the Postgres container, in its own role
shared_buffers, work_mem and maintenance_work_mem were a quarter, half
and a twentieth of the host's RAM, while the Postgres container is
limited to 6g. On a 64 GB host that asked for 15997 MB of shared
buffers inside it. They are now the same shares of the container's
mem_limit through lookup('service_memory_mb'): 1536 MB, 3072 MB divided
by max_connections, and 307 MB.

The three variables move from group_vars/all/17_resource.yml into the
role's vars. They no longer describe the host, and only the role's
compose template reads them. Their *_MB helpers and POSTGRES_TOTAL_RAM_MB
had no other reader and are gone. An inventory can no longer override
the three values; the knob is services.postgres.mem_limit.

Discourse's db_shared_buffers was a literal 4096MB, all of the 4g the
role declares. It becomes a quarter of that limit, 1024MB. It only
applies when Discourse runs its bundled database.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 01:28:43 +02:00
Alejandro Roman 4b9a834546 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-21 03:25:20 +04:00
kevinveenbirkenbachandClaude Opus 5 b036f60039 refactor(container_ports): express the remaining published ports through the lookup
The list form could publish a host:container pair and nothing else, so thirteen blocks stayed hand-written: the ones needing a protocol suffix, a port range, the swarm long syntax with mode host, or a container-port-only publish. Each of those duplicated numbers the role already declares in meta/services.yml, which is the drift the lookup exists to remove.

The lookup keeps the list form unchanged for its 93 existing call sites and gains a dict form: transport emits one item per protocol, container 'same' serves the services that cannot remap (STUN/TURN, media relays), mode 'host' emits the long block an edge role needs in swarm so the client address survives the hop, publish 'ephemeral' emits the container port alone, and key names the block for Discourse's launcher file, whose expose: list is what becomes docker run -p there. A declared {start, end} mapping renders as start-end on both sides, so a relay range needs no term of its own.

svc-prx-openresty had no ports block at all and bound 80 and 443 as literals in its template, while torrc.j2 carried a third literal 80 for the hidden service. None of the three referenced the others. openresty now declares the ports, the compose template reads them, and the onion's port 80 comes from ports.onion.http through TOR_ONION_EXTRA_PORTS.

Three blocks stay raw, each with the reason it cannot route through one call: keycloak's sso_proxy maps a published port owned by the consuming role to a container port owned by web-app-keycloak, and matrix's ansible flavor takes element on 8080 and federation on 8048 from the upstream playbook rather than from this repo's ports.internal.

The enforcing lint scanned only files with compose in the name and only double-quoted list items, so nine of the thirteen blocks were invisible to it. It now reads every role template that can carry a ports block, catches unquoted items, protocol suffixes, ranges, flow style and the long syntax, and tracks the block by the indentation of its key: keying on the next line closed the block on anything shaped like a key, which target, published, protocol and mode all are. Under expose: only a real mapping counts, because there the single-segment form documents a container port and publishes nothing.

Verified by rendering all 110 container_ports call sites through the real templar, the real lookup and the real merged applications view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:55:25 +02:00
kevinveenbirkenbachandClaude Opus 5 84a4b04f05 refactor(discourse): make meta/addons the one place a plugin is pinned
The pinning sat in two places and covered one plugin out of four.
discourse-ldap-auth carried repository and ref both in the services.yml
plugins map and in its addon file, the duplicate silenced by a
"# nocheck: repository-declared". The three official plugins carried neither,
so the template hardcoded https://github.com/discourse/<name>.git and cloned
whatever the default branch held, while only the LDAP one got a --branch
through a special-cased block beside the loop.

The addon layer was already the real source: DISCOURSE_PLUGINS reads
lookup('config', application_id, 'addons'), and the services.yml plugins block
survived only in nocheck comments pointing at it. It is dropped.

Every cloned addon now declares its own repository and ref plus the update
contract, so the template reads the declaration instead of inventing it and
one loop covers all four. Checked against the assembled addon map: the four
with source != bundled all carry repository and ref; discourse-ai carries
neither and is bundled, which the loop condition already excludes.

Pinning these while 4c2e480d5 pins the core is what upstream expects.
docker_manager, discourse-akismet and discourse-activity-pub each ship a
.discourse-compatibility map from core versions to plugin commits, and the
bootstrap falls back to the matching commit for an older core. Their newest
threshold is 2026.4.0-latest, below the pinned core, so the declared branch is
the intended pairing rather than a drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 16:43:17 +02:00
kevinveenbirkenbachandClaude Opus 5 4c2e480d59 feat(discourse): pin the application version where the updater can see it
params.version sat commented out in app.yml, so Discourse silently inherited
tests-passed and every bootstrap could pull a different application revision
with nothing recording which.

Declaring it as services.discourse.version would have been invisible to both
updaters: utils/update/docker.py reaches version: only through
iter_role_images, which skips any service without an image: (discovery.py,
"if not image or not version: continue"), and Discourse is built by the
launcher and has none. A nested application entry carrying repository: and
ref: lands in utils/update/repository.py instead, whose walk_repo_ref_pairs
descends the whole tree; the collector now reports

  discourse/application | discourse.git | v2026.8.0

so git ls-remote --tags keeps the pin current rather than freezing it, and the
external URL test sees the repository through iter_non_ignored_files. This is
the shape 7b15c61dfc established when it migrated version/branch keys to ref.

v2026.8.0 is the newest stable tag: v2026.9.0 answers 404, only
v2026.9.0-latest exists. The auto-bump will not drift onto those pointers,
version_flavor("v2026.8.0") is empty and latest_semver with that flavor picks
the plain tag back.

Pinning the core while the plugins clone from their default branch is safe
because upstream solves it: docker_manager, discourse-akismet and
discourse-activity-pub each ship a .discourse-compatibility map, and the
bootstrap checks out the plugin commit matching an older core. The newest
threshold in those maps is 2026.4.0-latest, below this pin, so the default
branch is the intended pairing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 16:41:35 +02:00
kevinveenbirkenbachandClaude Opus 5 e11c9517b2 fix(discourse): set runtime state at runtime, not during bootstrap
All 22 run: hooks in app.yml were rails and rake calls against the database,
yet the launcher executes them only while building the image. Two of them
carried rotating values, the OIDC client secret and the administrator
password, so every rotation forced a full rebuild with bundle install and
asset precompile: 961.82s in one pass and 13m16s in the next, on a job that
then missed its wall-clock budget by 201s.

They now run against the running instance through container exec, following
the pattern web-app-openproject already uses: real .rb files under files/ruby,
values through ENV.fetch, a readback that prints booleans rather than values,
and an assert that fails the deploy when a rotated credential did not arrive.
ensure_admin_user.rb mirrors the Y/Y path of admin:create using upstream's own
grant_admin!, change_trust_level! and activate; setting the flags by hand had
skipped auto_approve_user, the staff welcome message, the notification levels,
trust level 1 and the email confirmation.

What stays in run: is the CA trust call, which writes into the image and would
not survive a recreate. With the rest gone, the bootstrap digest f47ea0e5bb
records covers templates, expose, params, volumes, after_code and docker_args,
and nothing in that set rotates.

00_core.yml carried the same IS_STACK_HOST condition three times; it now
includes 04_stack_host.yml once, which holds the three with their own
conditions, and their implementations moved under tasks/stack_host/.

The OIDC values are read only when OIDC is enabled. Ansible resolves vars
lazily, so referencing DISCOURSE_OIDC_LOGOUT_REDIRECT_URL unconditionally
would have run its lookup against web-svc-logout on a deployment that has no
SSO, failing where the template did not.

test_oidc_setting_order.py follows the settings into apply_settings.rb. The
invariant 88218fc740 introduced is unchanged: openid_connect_enabled comes
after the discovery document, the client id and the client secret.

Four explanatory "# Exception:" comments are removed, two of them from
f47ea0e5bb. They described mechanisms rather than naming a trip-wire, which
belongs in a commit body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 16:07:15 +02:00
kevinveenbirkenbachandClaude Opus 5 88218fc740 fix(discourse): enable OpenID Connect only after its prerequisites are set
Discourse latest now validates openid_connect_enabled and refuses it
until the discovery document, client ID and client secret are set. The
bootstrap enabled OIDC first, so a fresh database aborted the launcher
rebuild at `rails r "SiteSetting.openid_connect_enabled = true"` with
Discourse::InvalidHTMLParameters (run 34615516009, WordPress#0, job
103327034654). Existing databases hid it because the values were already
stored.

The enable step now follows the three settings it depends on. A test
reads the real template and requires every prerequisite line to precede
the enable line; against the previous template it fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 20:54:37 +02:00
Alejandro Roman e6596d51f2 fix(stalwart): add the fix PR into main PR 2026-09-09 00:21:58 +04:00
kevinveenbirkenbachandClaude Opus 5 8d1d45d246 fix(discourse): pin the OIDC issuer for the launcher-built container
Nextcloud#7 of run 33445831564 fails on three discourse specs, all on one
assertion, all for one reason. The received body is Discourse's own page:
"Sorry, there was an error while trying to authorize your account with oidc."

The browser leg works. What dies is Discourse's server-side back-channel:

    "GET /auth/oidc/callback?...code=..."  302  2.125 2.125
    "GET /auth/failure?message=openid_connect_request_failed&...&strategy=oidc"  200

Four of those, at 2.125 / 2.015 / 2.063 / 2.014 seconds. That is not the plugin's
10s HTTP timeout; it is Discourse's DNS budget, hard-coded at 2s in
lib/final_destination/resolver.rb (connect_timeout arrives nil, so the default
applies). openid_connect_request_failed has exactly one emitter --
omniauth_open_id_connect.rb:147-155, rescue Faraday::Error -- and the alternative,
SocketError -> LookupFailedError, is not a Faraday::Error and would have produced a
500 rather than the observed 302.

The correlation leaves no room: across 18 back-channel requests, all from
192.168.9.3 with UA Faraday v2.14.3, there are exactly two complete token 200 +
userinfo 200 pairs -- and exactly two specs passed. Thirteen truncated flows,
thirteen failures. No test that got a complete exchange failed; none that failed got
one.

Why discourse alone: discourse.inspect.json:72 reads "ExtraHosts": null. Grepping the
whole rescue bundle for the pin finds nextcloud, nextcloud-proxy, mailu-admin,
prometheus and prometheus-sso-proxy. Discourse could not have it: config.yml.j2's
docker_args: carried only --network= and --name=. It is built by discourse_docker's
launcher, not by a compose file, so the lookup's rendered block has nowhere to go --
hence entries_only, which returns the strings and lets the template write
--add-host= lines. The pin itself is NOT re-derived: _sso_pins() and
_gateway_address() stay the single place that knows compose from swarm.

Its existing allowed_internal_hosts cannot help, because ssrf_detector's
lookup_and_filter_ips resolves BEFORE host_bypasses_checks?; the setting only makes a
private answer acceptable once it arrives.

Rendered through a real Templar both ways: with SSO on, the two --add-host lines land
between the networks and --name; with SSO off the output is byte-identical to today,
so clearnet and variant-1 deployments render an unchanged app.yml and trigger no
launcher rebuild. 24 lookup tests green.

Not closed: the biber persona has never passed, here or in run 33317483326. Its
failure text and code path match administrator, which did pass, so it is explained --
but a persona-specific second defect is not positively excluded. Deploying discourse
variant 0 locally, which already has sso and tor enabled, settles it.

Separately worth queueing: discourse's own meta/variants.yml variant 1 sets
sso.enabled: false and only variant 1 is scheduled, so the role that owns these specs
never runs them. Only partner rounds like Nextcloud#7 catch this class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 12:00:56 +02:00
kevinveenbirkenbachandClaude Opus 5 2c5ad70d24 feat(discourse): register the platform gateway as the Discourse AI model
Discourse AI is configured through an LlmModel row rather than settings, so the
addon upserts one via rails: provider open_ai, the gateway's chat-completions
URL, the model the gateway serves, and this role's virtual key. It then binds
that row to ai_default_llm_model and to the bot, because a model that exists
but is bound to nothing serves no surface.

llm_models.url is POSTed verbatim, so DISCOURSE_LITELLM_CHAT_ENDPOINT carries
the full /v1/chat/completions path rather than a base URL. Every other consumer
in this requirement takes a base and appends; getting this one wrong produces a
404 at prompt time, long after a green deploy.

Three things had to be cleared before any of that could work.

Discourse refuses to call a private address unless the host is in
allowed_internal_hosts, and it fails as an opaque provider error rather than as
a blocked request. The script appends the gateway host to that list and leaves
existing entries alone, which matters because the OIDC bootstrap in
config.yml.j2 appends the discovery host to the same pipe-separated setting.
Both sides check membership before adding, so they compose instead of
clobbering each other.

The launcher-managed container sits outside the compose graph and does not join
the gateway network on its own. handlers/stack_host.yml adds
DISCOURSE_LITELLM_NETWORK to its network list and config.yml.j2 passes the
matching --network flag, both only while litellm is enabled.

The bootstrap aborted before the plugin could install at all. after_code cloned
every enabled plugin unconditionally, but a bundled plugin already sits in
$home/plugins, git exits non-zero on the clone, and the whole bootstrap stops.
discourse-ai is bundled in current images, so it now skips plugins whose source
is bundled.

mem_limit goes from 2g to 4g. `rails r` boots a second Rails inside the
container already running unicorn and sidekiq, and at 2g the kernel kills it
mid-run: measured in deploy-036-11 and again in deploy-ai-v2-7, where the task
died with "Killed" and the deploy ended at exit 137. Upstream documents 4 GB as
Discourse's own minimum, so this corrects an under-provisioned limit rather than
working around the task. The variant resource budget still passes.

Verified: the OOM with the exact task and exit code across two independent runs,
the bundled-plugin abort, and that the budget test holds at 4g.
Not verified: the registration has never completed, because every attempt so far
died at the OOM first. The eight-fact read-back assertion and
addons/discourse-ai.spec.js are both entirely unexercised, the spec because the
addon bridge gate had it skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:23:58 +02:00
Alejandro Roman ac101d2efa Merge kevinveenbirkenbach/feature/swarm-nfs into stalwart integration branch 2026-07-21 10:00:17 +02:00
Alejandro Roman df71fb9d40 fixes in CI 2026-07-13 18:20:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1d70a71259 fix(web-app-discourse): dedent Jinja block tags in config.yml.j2
With trim_blocks on, the two-space indented block tags leaked their leading
whitespace into the rendered output, prepending spaces to the next line and
producing a mis-indented key inside the env: mapping that broke Psych YAML
parsing on a fresh wordpress+discourse deploy. Move the block tags to column 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:26:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f2ebcb7430 fix(web-app-discourse): accept already-attached network and fix logo gate
network connect failed_when only matched one idempotent stderr phrasing; also accept 'already attached to network'. Fix the platform-logo truthiness gate (a URL string is never a bool) and drop a now-unused error var.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:28:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8390cc4ce4 test(comments): lint .j2 templates and add file-level nocheck opt-out
Drop trap/warning from the exception markers; lint .j2 by stripping the suffix to the underlying language plus Jinja {# #} comments; add a head-level '# nocheck: comments-valid' opt-out via is_suppressed_in_head, documented in suppression.md; strip narration comments from friendica/nextcloud templates and opt the vendored discourse app.yml out with the new marker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:24:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a4e897e882 fix(web-app-discourse): attach redis/pg overlays at launcher bootstrap
The redis/pg `--network` flags were gated behind `not SWARM_CLUSTER_AVAILABLE`,
so in swarm the standalone launcher container was not on the redis overlay when
`rake db:migrate` runs during `./launcher rebuild` -> "Connection refused" to
redis-central. The post-rebuild network-connect handler runs too late. Emit the
flags in both modes (overlays are attachable); compose output is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 22:12:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 36ebf34b64 Merge branch 'main' into feature/swarm-nfs
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 76935d1cbe fix(web-app-discourse): give the swarm launcher a node-local bridge network
The discourse launcher is a standalone container (docker run via ./launcher), not a swarm service, so it cannot attach to swarm-scoped overlay networks. In swarm it was left without a reachable network for its shared pg/redis/openresty peers.

The shared network util now creates a node-local bridge instead of an overlay when the role declares networks.local.force_bridge in meta/server.yml, read via the config lookup with a false default so every other caller stays byte-identical. Discourse sets force_bridge: true and connects the rebuilt launcher to the shared overlays (web/pg/redis) so swarm services stay reachable; the launcher config drops the explicit --network flags in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 00:50:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c1f7839cd7 fix(web-app-discourse): attach the launcher to the shared redis and db overlays
In swarm the standalone ./launcher container was created with only --network=DISCOURSE_NETWORK. With a shared redis/db the engine resolves DISCOURSE_REDIS_HOST/DISCOURSE_DB_HOST to the central aliases (redis-central, the postgres name) which live on the svc-db-redis / svc-db-postgres overlays, not the discourse network. So 'rake db:migrate' during './launcher rebuild' aborted with Redis::CannotConnectError 'redis://redis-central:6379' (resolving to 127.0.0.1:6379, refused). The compose-only db-bridge handler is skipped in swarm and 05_network's connect runs after the rebuild, so neither can help a migrate that runs during the rebuild. Attach the launcher to DISCOURSE_REDIS_NETWORK (when redis is shared) and DISCOURSE_PG_NETWORK (when the db is shared) at creation time, gated on the engine local lookup so embedded backends keep their sidecar on the discourse network. Multiple --network at docker run is honored on the targeted Docker 28/29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:10:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f4774ec145 refactor(apps): remap consumers to central engines
About 38 web-app/web-svc roles now consume the central svc-db-* and svc-dns engines via lookup('engine', ...) instead of embedded sidecars: meta/services flags, variant matrices, and env/conf hosts point at the shared services. Lets these apps run unpinned and NFS-shareable in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 42f272240b feat(discourse): migrate plugins to meta/addons (req 026)
docker_manager, discourse-activity-pub, discourse-akismet, discourse-ldap-auth (bridges ldap) declared as mechanism: plugin; config.yml.j2 + vars/main.yml read the new addons path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dbbe0c7aea chore(web-app-discourse): drop the stale commented-out OIDC account-link inspiration snippet from config.yml.j2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d91a09743b chore(web-app-discourse): drop the stale commented-out OIDC account-link inspiration snippet from config.yml.j2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d2dd66a30d feat(lookup): add 'local' flag (enabled AND NOT shared) on database/service
Fixes Cluster 3a ca-inject YAML parse: when a role variant disables its
DB (enabled=false), the depends_on template emitted `      :` because
the existing `not shared` guard fired for a non-existing DB host.

The new derived flag mirrors `service.required` (= enabled AND shared)
and is consumed in:
 - depends_on/dbms_base.yml.j2, dmbs_excl.yml.j2 (the actual fix)
 - sys-svc-rdbms postgres/mariadb service templates
 - web-app-discourse, web-app-snipe-it (latent same-pattern defensives)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:52:42 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fd22d34559 refactor(roles): declare every git URL in meta/services.yml + per-role REPOSITORY/REF vars
Co-locates every git-clone URL with its owning entity in the role's
`meta/services.yml` so the unified `image`/`version` ↔ `repository`/`ref`
naming convention covers secondary repos (plugins, build helpers,
bootstrap sources) just as cleanly as the primary entity source.

Migrations:
- sys-aur, pkgmgr (non-invokable bootstrap roles): `meta/services.yml`
  gains `repository:`/`ref:` on the role's own entity; `vars/main.yml`
  renders them via `lookup('config', role_name, …)` because these
  roles never set `application_id`.
- web-app-{akaunting,bigbluebutton,bluesky,discourse,roulette-wheel,
  taiga}: declare the secondary git URL on the most fitting entity
  (`akaunting` itself for the docker-helper repo; `view` for bluesky's
  social-app web client; `plugins.discourse-ldap-auth` for that
  Discourse plugin; `oidc` for taiga's OIDC plugin). Each role gains
  named `<ROLE>_REPOSITORY` / `<ROLE>_REF` constants in `vars/main.yml`
  so tasks and templates stay readable instead of inlining a
  `lookup('config', application_id, …)` string at every call site.
- svc-db-postgres `templates/Dockerfile.j2`: keep the `pgvector`
  extension clone but mark the line with `{# nocheck: repository-declared #}`
  since pgvector is an extension source, not the role's primary repo.

New integration test
`tests/integration/roles/meta/services/test_repository_declared_in_services.py`
walks every role's text files and fails if a literal `https://…/<…>.git`
URL is not declared as a `repository:` value somewhere in that role's
`meta/services.yml` (recursive — top-level entity, sub-entity, plugin
map). Legitimate exceptions opt out with `# nocheck: repository-declared`
on the same line or the preceding line; Jinja templates may use the
`{# … #}` form so a `\`-continued Dockerfile shell command stays valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0adfa10c5d fix(web-app-discourse): rename disable_local_logins → local_login_enabled
Two bugs in one config option:

1. `SiteSetting.enable_local_logins = {{ ... | bool | string }}` emitted
   Python's capitalized `True`/`False`, which Ruby parses as undefined
   constants → `uninitialized constant True` aborts `./launcher rebuild`
   (CI run 25634428082 deploy job for web-app-discourse).
2. `enable_local_logins` was set to the *raw* `disable_local_logins`
   value, so when the config truly meant "disable", the live setting
   was stuck on "enable", and vice versa.

Renames the field across `meta/services.yml`, `vars/main.yml`, and
`templates/config.yml.j2` to its positive form and applies De Morgan to
keep the original semantics:

    disable_local_logins = oidc.enabled AND NOT ldap.enabled
    local_login_enabled  = NOT oidc.enabled OR ldap.enabled

Truth table is preserved across all four (oidc, ldap) combinations; the
LDAP-needs-the-local-mask path stays open.

The template now uses `| string | lower` so the rendered Ruby literal
is `true`/`false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7dd01398e3 feat(discourse): gate enable_local_logins=false on a config flag
Move the enable_local_logins=false setting out of the OIDC-only
conditional and gate it on a new services.discourse.disable_local_logins
flag instead. The flag evaluates to true when OIDC is the sole auth
path (Keycloak in group_names, OpenLDAP not in group_names); with
LDAP running in parallel the local-login form stays active so the
LDAP plugin's username/password hook keeps working.

- services.yml: services.discourse.disable_local_logins jinja predicate
- vars/main.yml: DISCOURSE_DISABLE_LOCAL_LOGINS via lookup('config', ...)
- config.yml.j2: standalone {% if DISCOURSE_DISABLE_LOCAL_LOGINS %} block;
  the OIDC-only block keeps enable_passkeys=false / username_change_period
  / openid_connect_* unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:35:54 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ebd24268f8 fix(discourse): set DISCOURSE_FORCE_HTTPS env so assets compile with https URLs
The existing `rails r SiteSetting.force_https = true` exec runs in the
`run:` block, AFTER Discourse precompiles its assets. As a result the
generated stylesheets contain `@font-face { src: url(http://forum.../...) }`
references, which the CSP `font-src 'self' https://forum.../` blocks at
runtime, failing the Playwright `discourse enforces Content-Security-Policy`
test (CI runs 25387069690 / 25402722355).

Setting `DISCOURSE_FORCE_HTTPS` in the env section lets Discourse pick up
the value before asset precompilation, so font and asset URLs are baked
as https from the start.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 09:52:14 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ed04dad5d5 refactor(meta): land req-008/009/010 role-meta layout/networks/ports/run_after migration
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).

req-008 — role meta layout
  * Move every role's metadata under meta/<topic>.yml with file-root
    convention (no wrapping key matching the basename):
      - schema/main.yml + config/main.yml.credentials → meta/schema.yml
      - users/main.yml                                → meta/users.yml
      - config/main.yml.server                        → meta/server.yml
      - config/main.yml.rbac                          → meta/rbac.yml
      - config/main.yml.compose.services + every other top-level key
        of config/main.yml inlined under <entity>.<key>
                                                      → meta/services.yml
      - config/main.yml.compose.volumes               → meta/volumes.yml
  * Drop the legacy schema/, users/, config/ directories from every role.
  * meta/schema.yml: support nested credential keys, optional `default:`
    field (Jinja literal, not rendered/validated/regenerated), implicit
    `algorithm: plain`.
  * Materialised paths drop the `compose.` prefix:
      compose.services.<e>.<…> → services.<e>.<…>
      compose.volumes.<k>      → volumes.<k>
      <former-top-level>.<…>   → services.<entity_name>.<former-top-level>.<…>
  * Update every Python consumer (utils/cache, utils/manager, utils/applications,
    plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
    and task file to the new paths.
  * No legacy fallback. Repository-wide grep for `compose.services.` /
    `compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
    is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
  * Add tests/lint/repository/test_role_meta_layout.py guarding against the
    old paths returning, and an explicit lint guard that meta/variants.yml
    files do not carry a `compose:` wrapper.

req-009 — per-role networks/ports
  * Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
    dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
    dns_resolver}.
  * Delete group_vars/all/09_ports.yml entirely; ports now live on the
    service entity that exposes them at
    roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
  * Rename + int-cast every former top-level `services.<entity>.port`
    field to `services.<entity>.ports.inter` (single int).
  * Always-category-keyed maps for ports.local / ports.public; relay
    spans use {start, end} integers.
  * Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
    of truth for the per-<scope>.<category> band ranges, consumed by both
    the new CLI helpers and the lint test.
  * New CLI helpers (cli meta ports suggest, cli meta networks suggest)
    walk the live role tree gap-first then increment-fallback, with
    deterministic output and non-zero exit on capacity overflow.
  * cli create role consumes both helpers when scaffolding a new role.
  * Lint extensions: bare-int port shape rejection, host-bound port
    collision check (single ints + relay span integers, inter excluded),
    band-membership check with documented allow-list for the legacy
    web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.

req-010 — run_after / lifecycle migration
  * Move galaxy_info.run_after and galaxy_info.lifecycle out of
    meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
    lifecycle}; <primary_entity> is the value returned by
    get_entity_name(role_name).
  * For multi-entity roles whose primary entity has no compose service
    (web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
    gen roles), create a dedicated metadata-only top-level entry under
    <primary_entity>.
  * Introduce utils/roles/meta_lookup.py with get_role_run_after /
    get_role_lifecycle that resolve <primary_entity> once and read from
    meta/services.yml; degrade gracefully when meta/services.yml is
    absent OR the field is absent; raise MetaServicesShapeError when the
    file exists but is malformed.
  * Update every consumer (utils/service_registry, utils/invokable,
    cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
    lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
    cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
    init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
    use the helper.
  * Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
    values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
    run_after/lifecycle never on a non-primary entity; empty run_after
    lists rejected.

Also folded into this change set
  * services_disabler now writes `services.<svc>.{enabled,shared}` into
    host_vars instead of the old `compose.services.<svc>.{...}` path; the
    discover/find pair walks meta/services.yml and only treats entries
    carrying enabled/shared/provides as provider declarations (so
    metadata-only entries from req-010 do not create spurious provider
    rows).
  * service_registry skips `run_after` deps whose target was filtered
    out via SERVICES_DISABLED instead of failing the whole load.
  * cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
    consume the new applications.<role>.services.<svc>.image shape
    (mirror.md updated to match).
  * utils/domains/list.build_applications_from_roles now reads
    meta/server.yml directly (it had been silently returning {} since
    config/main.yml stopped existing).
  * utils/roles/resource_validation.filter_roles_by_min_storage reads
    meta/services.yml.<entity>.min_storage instead of the legacy
    compose.services path.
  * web-app-bluesky pre-clones the social-app build context BEFORE
    sys-svc-compose so the build context is on disk by the time
    sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
  * 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
    web-app-wordpress, web-app-keycloak) lose their `compose: services:`
    wrapper so deep-merge over meta/services.yml lands on the new
    file-root shape.

Validation
  * Unit tests:        1571 passed.
  * Lint tests:        50 passed.
  * Integration tests: 161 passed (1 skipped).
  * Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
    set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
    discourse,mattermost}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:58:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2dbb2aaa1a feat(email): introduce lookup('email') plugin, migrate all consumers
Moves the SYSTEM_EMAIL_* resolution logic out of group_vars/all/03_email.yml
into a dedicated Ansible lookup plugin, so every role template reads outgoing
mail settings via lookup('email') (global) or lookup('email', application_id)
(per-app). Per-app compose.services.email is merged on top of the resolved
defaults; SYSTEM_EMAIL_* variables remain usable as an inventory escape hatch.

Also splits the email docs: administrator-facing configuration stays under
docs/administration/configuration/, plugin contract and extension rules move
to docs/contributing/design/services/email.md. services.md is relocated to
services/base.md so the services/ subtree can host per-service design pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:47:40 +02:00
kevinveenbirkenbachandClaude Opus 4.6 b70a783423 refactor(lookups): migrate to runtime applications/users lookups
Replaces inline `applications`/`users` merging with dedicated lookup
plugins and runtime hydration. Introduces `add_csp_hash` filter,
integrates sort into `find_dock_val_by_bkp_entr`, splits the Keycloak
one-shot realm import into `05a_realm_import_run.yml`, and fixes the
Mastodon administrator task YAML. Removes obsolete `csp_hashes` filter,
`sys-token-store` Jinja filter, and empty CLI setup modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 23:44:19 +02:00
kevinveenbirkenbach e3a16cbe8a refactor: switch docker CLI usage to container wrapper
- Replace direct docker calls with container wrapper across scripts, roles, and templates
- Add support for cp, start, stop, and restart passthrough commands
- Align handlers and notifications to container manager restart semantics
- Fix systemd ExecStartPost to use container wrapper without hardcoded paths
- Silence ShellCheck SC2016 false positive for container exec password injection
- Update tests and documentation strings to reflect container-based workflow

https://chatgpt.com/share/69866958-c88c-800f-9b4f-3a972615bf42
2026-02-06 23:21:02 +01:00
kevinveenbirkenbach 2d344fb129 refactor(domains): switch to domain() lookup across config and templates
Replace `domains | get_domain(...)` usage with `lookup('domain', ...)` throughout
group_vars, roles, and templates. Fix load_app domain resolution to use the
current application_id.

https://chatgpt.com/share/6985ef1e-11b8-800f-b231-52523055060b
2026-02-06 14:46:29 +01:00
kevinveenbirkenbach 5e33e70892 refactor(database lookup usage): switch conditionals to enabled flag
Replace indirect type!='' checks with explicit database.enabled lookups
to improve semantics, readability, and alignment with strict lookup API.

https://chatgpt.com/share/6985d642-b40c-800f-9cdf-ed54fc484a92
2026-02-06 13:25:37 +01:00
kevinveenbirkenbach e1e1f3eb11 chore(vars): rename PATH_* to DIR_/BIN_/FILE_ conventions
https://chatgpt.com/share/6985cf49-3c74-800f-af97-f68b1969ab01
2026-02-06 12:23:46 +01:00
kevinveenbirkenbach f6f96a11ac Refactor: use database lookup helper everywhere
https://chatgpt.com/share/6983bcf6-85bc-800f-a0fc-5684b9078469
2026-02-04 22:52:08 +01:00
kevinveenbirkenbach bce21a2c39 fix(discourse): persist OIDC FinalDestination allowlist and clear discovery cache
Ensure OIDC discovery works behind internal/private networking by
automatically adding the discovery host to Discourse's
allowed_internal_hosts during bootstrap and clearing the cached
OIDC discovery document. Also enable optional verbose OIDC logging
when MODE_DEBUG is active.

https://chatgpt.com/share/6980af14-9218-800f-a597-9851b3c47421
2026-02-02 15:05:01 +01:00
kevinveenbirkenbach 63d12da368 web-app-discourse: fix admin creation, pin repo version, add self-signed CA support
- Pin Discourse repository to configurable version instead of implicit HEAD
- Add TLS/self-signed CA env + volume injection to container config
- Make force_https depend on resolved TLS state
- Replace fragile printf-based admin creation with safe heredoc
- Ensure admin user creation works with arbitrary password characters
- Expose DISCOURSE_VERSION via role vars for reproducible deploys

https://chatgpt.com/share/6980a49e-3000-800f-b6a4-cb6a3e15a3d8
2026-02-02 14:20:18 +01:00
kevinveenbirkenbach a8191463a5 fix(discourse,wordpress,pgadmin): correct https handling, CSP font-src, and harden install logic
- Discourse:
  - Allow explicit http font-src to diagnose mixed-content font loading
  - Fix OIDC RP-initiated logout redirect to use resolved base URL
- WordPress:
  - Make core install idempotent by seeding wp core if volume is empty
  - Refactor install task to use explicit role variables
  - Normalize docker service variable paths
- pgAdmin:
  - Remove implicit defaults in get_app_conf lookups for clarity
- General:
  - Align variable naming and formatting across roles

https://chatgpt.com/share/6980820e-6670-800f-9812-9b87debf1dee
2026-02-02 11:52:51 +01:00
kevinveenbirkenbach 5084b17279 refactor(iam): centralize OIDC enablement via role variables
Replace inline get_app_conf(...) checks with dedicated *_OIDC_ENABLED variables
across roles (Discourse, Gitea, Listmonk, Matrix, Mobilizon, Moodle, Pixelfed).
Remove implicit defaults to rely on explicit config and consistent | bool usage.

https://chatgpt.com/share/69763133-1778-800f-959b-e14685fdc6b4
2026-01-25 16:04:57 +01:00
kevinveenbirkenbach 76b4e4e08f fix(discourse): force C.UTF-8 locale to prevent Ruby US-ASCII crashes
https://chatgpt.com/share/6975155f-76e8-800f-999e-a8ee3b24236c
2026-01-24 19:54:14 +01:00
kevinveenbirkenbach 2b4e8473e4 Refactored LDAP for Discourse configuration 2026-01-24 18:13:13 +01:00
kevinveenbirkenbach 04c9490745 Solved unit and integration tests 2026-01-10 13:07:12 +01:00
kevinveenbirkenbach 8d3e8d0b73 feat(email): unify SYSTEM_EMAIL_* vars and improve msmtp/postfix local delivery
- Replace DEFAULT_SYSTEM_EMAIL merge with flat SYSTEM_EMAIL_* variables
- Update templates across apps to new variable names
- Fix msmtp health check to use -t and local username fallback
- Improve postfix localhost config (localdomain, mydestination, timeouts)
- Extend sys-hostname to ensure /etc/hosts mapping once

https://chatgpt.com/share/695e8f39-0694-800f-b91c-a74f4b78d7c8
2026-01-07 17:52:04 +01:00
kevinveenbirkenbach 6e5516c3d7 Replaced 127.0.0.1 by DOCKER_REACH_HOST 2026-01-02 04:56:39 +01:00
kevinveenbirkenbach 0f7add0f1a Unify Mailu token handling via users[*].tokens and clean up inventory validation
- Migrate Mailu credentials from legacy mailu_token to tokens['web-app-mailu']
- Update all affected application templates to use the new token structure
- Adjust inventory validation to no longer special-case mailu_token
- Minor typing and formatting cleanups in inventory creation and tests

https://chatgpt.com/share/69554915-87c8-800f-afd5-12118dbb1107
2025-12-31 17:02:20 +01:00
kevinveenbirkenbach 26fa760847 Added system email enabled switch 2025-12-30 12:39:28 +01:00
kevinveenbirkenbach 5f66c1a622 feat(postgres): add split_postgres_connections filter and average pool fact
Compute POSTGRES_ALLOWED_AVG_CONNECTIONS once and propagate to app roles (gitlab, mastodon, listmonk, matrix, pretix, mobilizon, openproject, discourse). Fix docker-compose postgres command (-c flags split). Add unit tests. Minor env/locale tweaks and includes.

Conversation: https://chatgpt.com/share/68b48e72-cc28-800f-9c21-270cbc17d82a
2025-08-31 20:04:14 +02:00
kevinveenbirkenbach cb66fb2978 Refactor LDAP variable schema to use top-level constant LDAP and nested ALL-CAPS keys.
- Converted group_vars/all/13_ldap.yml from lower-case to ALL-CAPS nested keys.
- Updated all roles, tasks, templates, and filter_plugins to reference LDAP.* instead of ldap.*.
- Fixed Keycloak JSON templates to properly quote Jinja variables.
- Adjusted svc-db-openldap filter plugins and unit tests to handle new LDAP structure.
- Updated integration test to only check uniqueness of TOP-LEVEL ALL-CAPS constants, ignoring nested keys.

See: https://chatgpt.com/share/68b01017-efe0-800f-a508-7d7e2f1c8c8d
2025-08-28 10:15:48 +02:00
kevinveenbirkenbach 79517b2fe9 Optimized spacing 2025-08-20 01:01:32 +02:00