From b83b361ea94ebde0446c490a646f87e1a14aed1c Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 22 Oct 2026 16:56:40 +0200 Subject: [PATCH 1/4] fix(web-app-bigbluebutton): run BBB v3.0.39 with the shared-notes server The version-source update in #1146 moved the bigbluebutton/docker ref from v3.0.23 to v3.0.39. That tag changes three things the role did not handle, and it exposed a fourth defect in compose_file_args. 1. generate-compose renders etherpad and bbb-pads only behind USE_INSECURE_ETHERPAD and otherwise emits bbb-shared-notes-server with a `build:` block and no `image:` (upstream adds the image only after the tag, in 9a6a9a9a4c). compose_mods drops `build` only where an image is set, so the guard "Fail if any service still builds locally" stopped every BigBlueButton deploy (main run 39749899667, Nextcloud#11: `rc: 0`, `stdout: ' build:'`, bootstrap_compose_project.yml:35). meta/services.yml now pins alangecker/bbb-shared-notes-server:v3.0.39, compose_mods takes `pinned` (BBB_PINNED_SERVICES, every service the role config pins an image for) and drops their `build`, and the etherpad leftovers go (ep_redis_publisher patch, etherpad postgres entrypoint and initdb hook, etherpad database tasks, ETHERPAD_* env, etherpad_api_key secret). 2. The notes server connects to postgres once at start and exits with 2 when it is not up yet; upstream gives it no restart policy. The override sets the role's restart policy, so it retries until postgres accepts connections. 3. Greenlight moves from v3.8.0 to v3.9.0.1, which sets `config.hosts = URL_HOST`, and the template passes `URL_HOST: https://${DOMAIN}`. Rails compares hostnames, so every request was blocked (`Blocked hosts: bbb.talk.infinito.test`, 403). Greenlight documents URL_HOST as the bare hostname; the override sets it to the role domain. 4. compose_file_args decided whether a role ships a compose override by templating "{{ application_id | abs_role_path_by_application_id }}" through the templar. ansible-core 2.21 does not render an untrusted string, so the check saw the literal and always returned False: the CA override generation and the pull handler never read BBB's compose.override.yml. That was harmless while compose.yml alone was valid; with the notes-server image living in the override, `compose config` failed ("has neither an image nor a build context"). The check now resolves the role path for the application id it is given. BBB is the only role that ships an override, so no other role's file list changes. Existing installations drop the etherpad and bbb-pads containers on the next `compose up --remove-orphans`; the old `etherpad` database stays in PostgreSQL untouched. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/lookup/compose_file_args.py | 25 +-- roles/web-app-bigbluebutton/README.md | 5 +- .../javascript/ep_redis_publisher_index.js | 148 ------------------ .../files/shell/etherpad_pg_entrypoint.sh | 9 -- .../files/shell/postgres_initdb_etherpad.sh | 11 -- .../filter_plugins/compose_mods.py | 11 +- roles/web-app-bigbluebutton/meta/secrets.yml | 4 - roles/web-app-bigbluebutton/meta/services.yml | 21 +-- roles/web-app-bigbluebutton/meta/volumes.yml | 22 --- .../tasks/00_core/manager_ops.yml | 21 --- .../01_compose/bootstrap_compose_project.yml | 3 +- .../01_compose/postgres_password_sync.yml | 23 --- .../templates/compose.override.yml.j2 | 17 +- roles/web-app-bigbluebutton/templates/env.j2 | 6 - roles/web-app-bigbluebutton/vars/main.yml | 2 +- .../plugins/lookup/test_compose_file_args.py | 12 ++ .../filter_plugins/test_compose_mods.py | 20 +++ 17 files changed, 61 insertions(+), 299 deletions(-) delete mode 100644 roles/web-app-bigbluebutton/files/javascript/ep_redis_publisher_index.js delete mode 100644 roles/web-app-bigbluebutton/files/shell/etherpad_pg_entrypoint.sh delete mode 100644 roles/web-app-bigbluebutton/files/shell/postgres_initdb_etherpad.sh diff --git a/plugins/lookup/compose_file_args.py b/plugins/lookup/compose_file_args.py index e935d8f6c6..1f05a45e29 100644 --- a/plugins/lookup/compose_file_args.py +++ b/plugins/lookup/compose_file_args.py @@ -30,6 +30,7 @@ from ansible.errors import AnsibleError from ansible.plugins.loader import lookup_loader from ansible.plugins.lookup import LookupBase +from plugins.filter.role_path_by_app_id import abs_role_path_by_application_id from utils.docker.paths_utils import get_docker_paths from utils.templating.jinja import render_strict @@ -80,7 +81,7 @@ def _has_domain(domains: Any, application_id: str) -> bool: return _value_has_domain(domains) -def _role_provides_override(*, application_id: str, templar: Any) -> bool: +def _role_provides_override(*, application_id: str) -> bool: """ Mirror tasks/04_files.yml "with_first_found" logic: @@ -89,22 +90,10 @@ def _role_provides_override(*, application_id: str, templar: Any) -> bool: Only if one of these exists, we append "-f ". """ - tpl = getattr(templar, "template", None) - if not callable(tpl): - raise AnsibleError( - "compose_file_args: templar is required to resolve abs_role_path_by_application_id" - ) - - role_base = _as_str(tpl("{{ application_id | abs_role_path_by_application_id }}")) - if not role_base: - raise AnsibleError( - "compose_file_args: abs_role_path_by_application_id resolved to empty" - ) - - c1 = str(Path(role_base) / "templates" / "compose.override.yml.j2") - c2 = str(Path(role_base) / "files" / "compose.override.yml") - - return Path(c1).is_file() or Path(c2).is_file() + role_base = Path(abs_role_path_by_application_id(application_id)) + return (role_base / "templates" / "compose.override.yml.j2").is_file() or ( + role_base / "files" / "compose.override.yml" + ).is_file() class LookupModule(LookupBase): @@ -174,7 +163,7 @@ class LookupModule(LookupBase): parts = [f"{flag} {base}"] - if _role_provides_override(application_id=application_id, templar=templar): + if _role_provides_override(application_id=application_id): if not _as_str(override): raise AnsibleError( "compose_file_args: compose.files.compose_override is required " diff --git a/roles/web-app-bigbluebutton/README.md b/roles/web-app-bigbluebutton/README.md index b51b67a0ff..9a4911a4c2 100644 --- a/roles/web-app-bigbluebutton/README.md +++ b/roles/web-app-bigbluebutton/README.md @@ -48,8 +48,7 @@ flowchart LR svc_bbb_web["bbb-web"] svc_freeswitch["freeswitch"] svc_nginx["nginx"] - svc_etherpad["etherpad"] - svc_bbb_pads["bbb-pads"] + svc_bbb_shared_notes_server["bbb-shared-notes-server"] svc_bbb_export_annotations["bbb-export-annotations"] svc_webrtc_sfu["webrtc-sfu"] svc_fsesl_akka["fsesl-akka"] @@ -91,7 +90,7 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( - ๐Ÿงฑ Automatic `.env` templating and domain/NGINX integration - ๐Ÿ›  Volume patching and Docker Compose customization - ๐Ÿ“ฌ SMTP integration and Greenlight admin creation -- ๐Ÿงช Workarounds for known Docker Compose or Etherpad issues +- ๐Ÿงช Workarounds for known Docker Compose issues ## Quick Setup diff --git a/roles/web-app-bigbluebutton/files/javascript/ep_redis_publisher_index.js b/roles/web-app-bigbluebutton/files/javascript/ep_redis_publisher_index.js deleted file mode 100644 index 47fcdd9c15..0000000000 --- a/roles/web-app-bigbluebutton/files/javascript/ep_redis_publisher_index.js +++ /dev/null @@ -1,148 +0,0 @@ -// nocheck: mirrored-unit-test - requires ep_etherpad-lite's Settings module at load -// time and reads settings.ep_redis_publisher, so it cannot be imported outside an -// Etherpad install -const redis = require('redis'); -const settings = require('ep_etherpad-lite/node/utils/Settings'); -const logger = { error: (...args) => console.error('[ep_redis_publisher]', ...args) }; - -const CHANNEL = settings.ep_redis_publisher.channel || 'from-etherpad-redis-channel'; - -const sanitizeMessageBody = (body) => { - let response = { ...body }; - - if (response.pad && response.pad._db) { - response = { - ...response, - pad: { - ...response.pad, - _db: undefined, - }, - }; - } - - if (body?.pad?.id) { body.pad.padId = body.pad.id; } - - return response; -}; - -const getCircularReplacer = () => { - const seen = new WeakSet(); - - return (key, value) => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) return; - - seen.add(value); - } - - return value; - }; -}; - -const retry = (options) => { - if (options.error && options.error.code === 'ECONNREFUSED') { - logger.error('redis', 'connection refused'); - - return new Error('refused'); - } - - if (options.total_retry_time > 1000 * 60 * 60) { - logger.error('redis', 'retry exhausted'); - - return new Error('exhausted'); - } - - if (options.attempt > 10) { - logger.error('redis', 'attempt limit'); - - return undefined; - } - - return Math.min(options.attempt * 100, 3000); -}; - -const options = { - host: settings.ep_redis_publisher.host || '127.0.0.1', - port: settings.ep_redis_publisher.port || 6379, - retry_strategy: retry, -}; - -if (settings.ep_redis_publisher.password) { - options.password = settings.ep_redis_publisher.password; -} - -const publisher = redis.createClient(options); - -const buildEnvelope = (name) => { - - return { - name, - routing: { sender: 'etherpad' }, - timestamp: (new Date()).getTime(), - }; -}; - -const buildCore = (name, body) => { - - return { - header: { name }, - body, - }; -}; - -const build = (name, body) => { - - return JSON.stringify({ - envelope: buildEnvelope(name), - core: buildCore(name, sanitizeMessageBody(body)), - }, getCircularReplacer()); -}; - -exports.publish = (event, body) => { - let message; - switch (event) { - case 'loadSettings': - message = build('PadLoadSettingsSysMsg', body); - break; - case 'shutdown': - message = build('PadShutdownSysMsg', body); - break; - case 'expressConfigure': - message = build('PadConfigureSysMsg', body); - break; - case 'expressCreateServer': - message = build('PadCreateServerSysMsg', body); - break; - case 'expressCloseServer': - message = build('PadCloseServerSysMsg', body); - break; - case 'onAccessCheck': - message = build('PadAccessCheckSysMsg', body); - break; - case 'padCreate': - message = build('PadCreateSysMsg', body); - break; - case 'padLoad': - message = build('PadLoadSysMsg', body); - break; - case 'padUpdate': - message = build('PadUpdateSysMsg', body); - break; - case 'padCopy': - message = build('PadCopySysMsg', body); - break; - case 'padRemove': - message = build('PadRemoveSysMsg', body); - break; - case 'userLeave': - message = build('PadUserLeaveSysMsg', body); - break; - - default: - return; - } - - if (message) { - publisher.publish(CHANNEL, message); - } -}; diff --git a/roles/web-app-bigbluebutton/files/shell/etherpad_pg_entrypoint.sh b/roles/web-app-bigbluebutton/files/shell/etherpad_pg_entrypoint.sh deleted file mode 100644 index 64ede8aef2..0000000000 --- a/roles/web-app-bigbluebutton/files/shell/etherpad_pg_entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -e - -sed -i \ - -e 's#"dbType": "redis"#"dbType": "postgres"#' \ - -e "s#\"url\": \"redis://redis:6379\"#\"host\": \"${ETHERPAD_DB_HOST}\", \"port\": ${ETHERPAD_DB_PORT}, \"database\": \"${ETHERPAD_DB_NAME}\", \"user\": \"${ETHERPAD_DB_USER}\", \"password\": \"${ETHERPAD_DB_PASSWORD}\"#" \ - /opt/etherpad-lite/settings.json - -exec /entrypoint.sh diff --git a/roles/web-app-bigbluebutton/files/shell/postgres_initdb_etherpad.sh b/roles/web-app-bigbluebutton/files/shell/postgres_initdb_etherpad.sh deleted file mode 100644 index 11364c0015..0000000000 --- a/roles/web-app-bigbluebutton/files/shell/postgres_initdb_etherpad.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -eu - -: "${POSTGRES_USER:?set by the upstream compose file}" -: "${ETHERPAD_DB_NAME:?set in env.j2 and broadcast to every service}" - -if ! psql --username "${POSTGRES_USER}" --dbname postgres -tAc \ - "SELECT 1 FROM pg_database WHERE datname = '${ETHERPAD_DB_NAME}'" | grep -q 1; then - psql --username "${POSTGRES_USER}" --dbname postgres \ - -c "CREATE DATABASE \"${ETHERPAD_DB_NAME}\"" -fi diff --git a/roles/web-app-bigbluebutton/filter_plugins/compose_mods.py b/roles/web-app-bigbluebutton/filter_plugins/compose_mods.py index 24d0413510..fb7e5a8514 100644 --- a/roles/web-app-bigbluebutton/filter_plugins/compose_mods.py +++ b/roles/web-app-bigbluebutton/filter_plugins/compose_mods.py @@ -3,7 +3,9 @@ import re from utils.cache.yaml import dump_yaml_str, load_yaml_str -def compose_mods(yml_text, compose_repository_path, env_file, extra_hosts=None): +def compose_mods( + yml_text, compose_repository_path, env_file, extra_hosts=None, pinned=None +): """Normalize the upstream-generated bigbluebutton compose file. Args: @@ -14,6 +16,9 @@ def compose_mods(yml_text, compose_repository_path, env_file, extra_hosts=None): env_file: env file broadcast to every service via ``env_file``. extra_hosts: optional host mappings added to every service that is not on the host network. + pinned: names of services whose image the role pins in + compose.override.yml; their upstream ``build`` is dropped even + where the upstream file names no image. Returns: Transformed compose YAML text. Structural deltas (named data @@ -32,7 +37,7 @@ def compose_mods(yml_text, compose_repository_path, env_file, extra_hosts=None): data = load_yaml_str(yml_text) or {} services = data.get("services", {}) or {} - for svc in services.values(): + for name, svc in services.items(): if not isinstance(svc, dict): continue @@ -49,7 +54,7 @@ def compose_mods(yml_text, compose_repository_path, env_file, extra_hosts=None): hosts.append(entry) svc["extra_hosts"] = hosts - if "build" in svc and svc.get("image"): + if "build" in svc and (svc.get("image") or name in (pinned or [])): svc.pop("build") data["services"] = services diff --git a/roles/web-app-bigbluebutton/meta/secrets.yml b/roles/web-app-bigbluebutton/meta/secrets.yml index 8a14f2b6f1..e3683a6e83 100644 --- a/roles/web-app-bigbluebutton/meta/secrets.yml +++ b/roles/web-app-bigbluebutton/meta/secrets.yml @@ -4,10 +4,6 @@ credentials: description: Shared secret for BigBlueButton API authentication algorithm: sha256 validation: ^[a-f0-9]{64}$ - etherpad_api_key: - description: API key for Etherpad integration - algorithm: random_hex_32 - validation: ^[a-zA-Z0-9]{32}$ rails_secret: description: Secret key for Rails backend algorithm: random_hex diff --git a/roles/web-app-bigbluebutton/meta/services.yml b/roles/web-app-bigbluebutton/meta/services.yml index e93411f153..b9a026c7b9 100644 --- a/roles/web-app-bigbluebutton/meta/services.yml +++ b/roles/web-app-bigbluebutton/meta/services.yml @@ -137,26 +137,13 @@ nginx: enabled: false # nocheck: playwright-service-flag: prebuilt upstream-compose image, mirror pre-pull only # nocheck: dynamic-flag: fixed upstream-compose sub-service, not group-gated -etherpad: - image: alangecker/bbb-docker-etherpad - version: "2.4.2-s8328b77-p88f3f6b" - update: - type: http_regex - url: - - https://raw.githubusercontent.com/bigbluebutton/docker/develop/repos/tags - - https://raw.githubusercontent.com/bigbluebutton/docker/develop/docker-compose.tmpl.yml - pattern: "repos/bbb-etherpad-plugin (?P[0-9a-f]+)\\nrepos/bbb-etherpad-skin (?P[0-9a-f]+)\\n(?:.*\\n)*?.*bbb-docker-etherpad:(?P[0-9.]+)-s" - template: "{base}-s{skin}-p{plugin}" - enabled: false -# nocheck: playwright-service-flag: prebuilt upstream-compose image, mirror pre-pull only -# nocheck: dynamic-flag: fixed upstream-compose sub-service, not group-gated -bbb-pads: - image: alangecker/bbb-docker-pads - version: "v1.5.8" +bbb-shared-notes-server: + image: alangecker/bbb-shared-notes-server + version: "v3.0.39" update: type: http_regex url: https://raw.githubusercontent.com/bigbluebutton/docker/develop/repos/tags - pattern: "repos/bbb-pads (v[0-9.]+)" + pattern: "repos/bigbluebutton (v[0-9.]+)" enabled: false # nocheck: playwright-service-flag: prebuilt upstream-compose image, mirror pre-pull only # nocheck: dynamic-flag: fixed upstream-compose sub-service, not group-gated diff --git a/roles/web-app-bigbluebutton/meta/volumes.yml b/roles/web-app-bigbluebutton/meta/volumes.yml index fc48d4e670..478bbded2f 100644 --- a/roles/web-app-bigbluebutton/meta/volumes.yml +++ b/roles/web-app-bigbluebutton/meta/volumes.yml @@ -62,25 +62,3 @@ coturn: mounts: - service: coturn target: /var/lib/coturn -ep_redis_publisher_patch: - type: bind - source: "{{ [lookup('container', 'web-app-bigbluebutton', 'directories.instance'), 'ep_redis_publisher_index.js'] | path_join }}" - read_only: true - mounts: - - service: etherpad - target: /opt/etherpad-lite/src/plugin_packages/.versions/ep_redis_publisher@0.0.3/index.js -etherpad_pg_entrypoint: - type: config - source: "{{ [lookup('container', 'web-app-bigbluebutton', 'directories.instance'), 'etherpad_pg_entrypoint.sh'] | path_join }}" - mode: "0444" - mounts: - - service: etherpad - target: /etherpad_pg_entrypoint.sh - -postgres_initdb_etherpad: - type: config - source: "{{ [lookup('container', 'web-app-bigbluebutton', 'directories.instance'), 'postgres_initdb_etherpad.sh'] | path_join }}" - mode: "0555" - mounts: - - service: postgres - target: /docker-entrypoint-initdb.d/zz-etherpad.sh diff --git a/roles/web-app-bigbluebutton/tasks/00_core/manager_ops.yml b/roles/web-app-bigbluebutton/tasks/00_core/manager_ops.yml index 24c5e4b44c..afb9259865 100644 --- a/roles/web-app-bigbluebutton/tasks/00_core/manager_ops.yml +++ b/roles/web-app-bigbluebutton/tasks/00_core/manager_ops.yml @@ -31,27 +31,6 @@ dest: "{{ [lookup('container', application_id, 'directories.instance'), 'compose.override.yml'] | path_join }}" notify: compose-up -- name: "๐Ÿฉน Vendor the ep_redis_publisher fix mounted over the etherpad plugin" - stack_host_copy: - src: javascript/ep_redis_publisher_index.js - dest: "{{ [lookup('container', application_id, 'directories.instance'), 'ep_redis_publisher_index.js'] | path_join }}" - mode: "0444" - notify: compose-up - -- name: "๐Ÿฉน Vendor the etherpad postgres-DB entrypoint wrapper" - stack_host_copy: - src: shell/etherpad_pg_entrypoint.sh - dest: "{{ [lookup('container', application_id, 'directories.instance'), 'etherpad_pg_entrypoint.sh'] | path_join }}" - mode: "0444" - notify: compose-up - -- name: "๐Ÿฉน Vendor the initdb hook that creates the etherpad database" - stack_host_copy: - src: shell/postgres_initdb_etherpad.sh - dest: "{{ [lookup('container', application_id, 'directories.instance'), 'postgres_initdb_etherpad.sh'] | path_join }}" - mode: "0555" - notify: compose-up - - name: ๐Ÿ“ Create symbolic link from .env file to target location file: src: "{{ BBB_ENV_FILE_ORIGINE }}" diff --git a/roles/web-app-bigbluebutton/tasks/01_compose/bootstrap_compose_project.yml b/roles/web-app-bigbluebutton/tasks/01_compose/bootstrap_compose_project.yml index 31542c38c4..58e6f60662 100644 --- a/roles/web-app-bigbluebutton/tasks/01_compose/bootstrap_compose_project.yml +++ b/roles/web-app-bigbluebutton/tasks/01_compose/bootstrap_compose_project.yml @@ -27,7 +27,8 @@ extra_hosts=( [lookup('domain', 'web-app-keycloak') ~ ':host-gateway'] if 'web-app-keycloak' in group_names else [] - ) + ), + pinned=BBB_PINNED_SERVICES ) }} dest: "{{ BBB_COMPOSE_FILE_FINAL }}" notify: diff --git a/roles/web-app-bigbluebutton/tasks/01_compose/postgres_password_sync.yml b/roles/web-app-bigbluebutton/tasks/01_compose/postgres_password_sync.yml index d36a6d7a0a..960638dd8f 100644 --- a/roles/web-app-bigbluebutton/tasks/01_compose/postgres_password_sync.yml +++ b/roles/web-app-bigbluebutton/tasks/01_compose/postgres_password_sync.yml @@ -30,26 +30,3 @@ delay: 3 until: bbb_pg_alter.rc == 0 changed_when: false - -- name: "๐Ÿ”Ž Check whether the etherpad database exists" - ansible.builtin.shell: > # nocheck: container-address-quote compose_mode_force role - container exec -i {{ BBB_POSTGRES_CONTAINER_ADDRESS | quote }} - psql -U postgres -d postgres -tAc - "SELECT 1 FROM pg_database WHERE datname = 'etherpad'" - register: bbb_etherpad_db - retries: 20 - delay: 3 - until: bbb_etherpad_db.rc == 0 - changed_when: false - -- name: "๐Ÿ—„๏ธ Create the etherpad database" - ansible.builtin.shell: > # nocheck: container-address-quote compose_mode_force role - container exec -i {{ BBB_POSTGRES_CONTAINER_ADDRESS | quote }} - psql -U postgres -d postgres -c "CREATE DATABASE etherpad" - register: bbb_etherpad_create - retries: 20 - delay: 3 - until: bbb_etherpad_create.rc == 0 or 'already exists' in bbb_etherpad_create.stderr - failed_when: bbb_etherpad_create.rc != 0 and 'already exists' not in bbb_etherpad_create.stderr - when: (bbb_etherpad_db.stdout | trim) != '1' - changed_when: bbb_etherpad_create.rc == 0 diff --git a/roles/web-app-bigbluebutton/templates/compose.override.yml.j2 b/roles/web-app-bigbluebutton/templates/compose.override.yml.j2 index 7d17058e94..fbcaaa9711 100644 --- a/roles/web-app-bigbluebutton/templates/compose.override.yml.j2 +++ b/roles/web-app-bigbluebutton/templates/compose.override.yml.j2 @@ -5,7 +5,7 @@ services: {{ lookup('container_volumes', service_name) | indent(4) }} {{ lookup('container_extra_hosts') | indent(4) }} {% if BBB_COLLABORA_ENABLED | bool %} - {{ lookup('depends_on', ['redis', 'etherpad', 'bbb-pads']) }} + {{ lookup('depends_on', ['redis']) }} {% endif %} {% set service_name = 'freeswitch' %} {{ service_name }}: @@ -15,19 +15,10 @@ services: {{ service_name }}: {{ lookup('container_image', application_id, service_name) }} {{ lookup('container_volumes', service_name) | indent(4) }} -{% set service_name = 'etherpad' %} - {{ service_name }}: - {{ lookup('container_image', application_id, service_name) }} - {{ lookup('container_volumes', service_name) | indent(4) }} - entrypoint: ["sh", "{{ lookup('volume', 'web-app-bigbluebutton', 'etherpad_pg_entrypoint').target }}"] -{% if BBB_COLLABORA_ENABLED | bool %} - {{ lookup('depends_on', ['redis', 'postgres']) }} -{% else %} - {{ lookup('depends_on', ['postgres']) }} -{% endif %} -{% set service_name = 'bbb-pads' %} +{% set service_name = 'bbb-shared-notes-server' %} {{ service_name }}: {{ lookup('container_image', application_id, service_name) }} + {{ lookup('compose_restart') }} {% set service_name = 'bbb-export-annotations' %} {{ service_name }}: {{ lookup('container_image', application_id, service_name) }} @@ -68,6 +59,8 @@ services: {% set service_name = 'greenlight' %} {{ service_name }}: {{ lookup('container_volumes', service_name) | indent(4) }} + environment: + URL_HOST: "{{ domain }}" {% endif %} {% if BBB_RECORDING_ENABLED | bool %} {% set service_name = 'recordings' %} diff --git a/roles/web-app-bigbluebutton/templates/env.j2 b/roles/web-app-bigbluebutton/templates/env.j2 index 73cbdfb7c6..9b99d0331b 100644 --- a/roles/web-app-bigbluebutton/templates/env.j2 +++ b/roles/web-app-bigbluebutton/templates/env.j2 @@ -23,12 +23,6 @@ REMOVE_OLD_RECORDING={{ BBB_RECORDING_CLEANUP_ENABLED | lower }} RECORDING_MAX_AGE_DAYS={{ BBB_RECORDING_MAX_AGE_DAYS }} SHARED_SECRET={{ BBB_SHARED_SECRET }} -ETHERPAD_API_KEY={{ BBB_ETHERPAD_API_KEY }} -ETHERPAD_DB_HOST=postgres -ETHERPAD_DB_PORT=5432 -ETHERPAD_DB_NAME=etherpad -ETHERPAD_DB_USER=postgres -ETHERPAD_DB_PASSWORD={{ BBB_POSTGRESQL_SECRET | dotenv_quote }} RAILS_SECRET={{ BBB_RAILS_SECRET }} POSTGRESQL_SECRET={{ BBB_POSTGRESQL_SECRET }} FSESL_PASSWORD={{ BBB_FSESL_PASSWORD | dotenv_quote }} diff --git a/roles/web-app-bigbluebutton/vars/main.yml b/roles/web-app-bigbluebutton/vars/main.yml index b7eafd9df5..3356e3faa2 100644 --- a/roles/web-app-bigbluebutton/vars/main.yml +++ b/roles/web-app-bigbluebutton/vars/main.yml @@ -9,11 +9,11 @@ http_port: "{{ lookup('config', application_id, 'services.bigbluebutton.ports.lo BBB_REPOSITORY: "{{ lookup('config', 'web-app-bigbluebutton', 'services.' ~ ('web-app-bigbluebutton' | get_entity_name) ~ '.repository') }}" BBB_REF: "{{ lookup('config', 'web-app-bigbluebutton', 'services.' ~ ('web-app-bigbluebutton' | get_entity_name) ~ '.ref') }}" +BBB_PINNED_SERVICES: "{{ lookup('config', application_id, 'services') | dict2items | selectattr('value.image', 'defined') | map(attribute='key') | list }}" _BBB_COTURN_ROLE: "web-svc-coturn" BBB_SHARED_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.shared_secret') }}" -BBB_ETHERPAD_API_KEY: "{{ lookup('config', application_id, 'secrets.credentials.etherpad_api_key') }}" BBB_RAILS_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.rails_secret') }}" BBB_POSTGRESQL_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.postgresql_secret') }}" BBB_FSESL_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.fsesl_password') }}" diff --git a/tests/unit/python/plugins/lookup/test_compose_file_args.py b/tests/unit/python/plugins/lookup/test_compose_file_args.py index 8ad1a27a86..6d37f73d6b 100644 --- a/tests/unit/python/plugins/lookup/test_compose_file_args.py +++ b/tests/unit/python/plugins/lookup/test_compose_file_args.py @@ -1,5 +1,7 @@ import importlib.util +import tempfile import unittest +from pathlib import Path from unittest.mock import patch from ansible.errors import AnsibleError @@ -168,6 +170,16 @@ class TestComposeFArgs(unittest.TestCase): self.assertEqual(out, "-f /x/compose.yml") + def test_role_provides_override_reads_the_role_of_the_given_application(self): + with ( + tempfile.TemporaryDirectory() as role, + patch.object(self.m, "abs_role_path_by_application_id", return_value=role), + ): + self.assertFalse(self.m._role_provides_override(application_id="x")) + (Path(role) / "templates").mkdir() + (Path(role) / "templates" / "compose.override.yml.j2").write_text("") + self.assertTrue(self.m._role_provides_override(application_id="x")) + def test_requires_one_term(self): with self.assertRaises(AnsibleError): self.lookup.run([], variables=self.vars) diff --git a/tests/unit/python/roles/web-app-bigbluebutton/filter_plugins/test_compose_mods.py b/tests/unit/python/roles/web-app-bigbluebutton/filter_plugins/test_compose_mods.py index d5822c4e59..28084185c2 100644 --- a/tests/unit/python/roles/web-app-bigbluebutton/filter_plugins/test_compose_mods.py +++ b/tests/unit/python/roles/web-app-bigbluebutton/filter_plugins/test_compose_mods.py @@ -197,6 +197,26 @@ services: self.compose_repository_path + "/mod/html5-dev", ) + def test_build_stripped_for_a_pinned_service_without_upstream_image(self): + original = """services: + bbb-shared-notes-server: + build: + context: mod/bbb-shared-notes-server + html5-dev: + build: + context: mod/html5-dev +""" + services = load_yaml_str( + compose_mods( + original, + self.compose_repository_path, + self.env_file, + pinned=["bbb-shared-notes-server"], + ) + )["services"] + self.assertNotIn("build", services["bbb-shared-notes-server"]) + self.assertIn("build", services["html5-dev"]) + if __name__ == "__main__": unittest.main() From f1198cd9faccb7833e7528992b96ce96c69a6600 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:07:37 +0000 Subject: [PATCH 2/4] update(images): update Docker image versions (web-app-mattermost) --- roles/web-app-mattermost/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-mattermost/meta/services.yml b/roles/web-app-mattermost/meta/services.yml index eb42143a3e..32d58f5b2c 100644 --- a/roles/web-app-mattermost/meta/services.yml +++ b/roles/web-app-mattermost/meta/services.yml @@ -46,7 +46,7 @@ mattermost: shared: true user_access_token_enabled: "{{ 'web-app-nextcloud' in group_names }}" image: mattermost/mattermost-team-edition - version: 12.0.0 + version: 12.0.1 min_storage: 659MB backup: no_stop_required: true From 9d80486cb42f7d47e7386b3da8b529ef1079b28c Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:07:42 +0000 Subject: [PATCH 3/4] update(images): update Docker image versions (web-app-socialhome) --- roles/web-app-socialhome/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-socialhome/meta/services.yml b/roles/web-app-socialhome/meta/services.yml index 536d8dce2e..70dc854023 100644 --- a/roles/web-app-socialhome/meta/services.yml +++ b/roles/web-app-socialhome/meta/services.yml @@ -13,7 +13,7 @@ socialhome: # nocheck: default-placement-manager embedded sqlite in a node-local volume; no clustered storage placement: manager image: ghcr.io/social-home-io/socialhome - version: "2026.10.20" + version: "2026.10.22" ports: local: http: 8089 From 93cbd2045e5b35c55b2c8247645817d0a5be45ff Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 22 Oct 2026 17:41:37 +0200 Subject: [PATCH 4/4] fix(web-app-penpot): pass the administrator password on stdin manage.py create-profile and update-profile parse their options with argparse, and `-p ` reads a value that starts with `-` as the next option. A generated password with a leading dash therefore fails both commands with exit 2 ("argument -p/--password: expected one argument") before they reach the backend, and the bootstrap task retries until it gives up. On main this broke the guide run of Penpot#2 in run 39668792538: the Docker events of the rescue snapshot show eight create-profile execs ending with exit 2 while the backend was healthy and its prepl received no call. Without -p, manage.py reads the password with getpass, which falls back to stdin when the exec has no TTY. Both tasks now feed the password to `container exec -i` on stdin. This also keeps the password out of the exec command line, which Docker writes into its event stream and therefore into the rescue diagnostics artifacts. The password-quote lint masked only block `stdin: |` / `stdin: >` values and flagged the inline `stdin: "{{ PENPOT_ADMIN_PASSWORD }}"` as an unquoted password in a shell command. stdin never reaches a shell, so the lint now masks every `stdin:` form; an unquoted password in `cmd` is still reported. Fixes #1139 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../web-app-penpot/tasks/01_administrator.yml | 24 ++++++++++--------- .../iam/passwords/test_quote_in_shell.py | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/roles/web-app-penpot/tasks/01_administrator.yml b/roles/web-app-penpot/tasks/01_administrator.yml index f55317c8b5..d1fc727a80 100644 --- a/roles/web-app-penpot/tasks/01_administrator.yml +++ b/roles/web-app-penpot/tasks/01_administrator.yml @@ -7,12 +7,13 @@ resolve_compose_cid: "{{ PENPOT_BACKEND_CONTAINER_ADDRESS }}" - name: "๐Ÿฅพ Bootstrap the Penpot administrator's local account (idempotent)" - ansible.builtin.shell: > # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node - container exec -i {{ PENPOT_BACKEND_LOCAL_CID | quote }} - python3 manage.py create-profile - -n {{ PENPOT_ADMIN_FULLNAME | quote }} - -e {{ PENPOT_ADMIN_EMAIL | quote }} - -p {{ PENPOT_ADMIN_PASSWORD | quote }} + ansible.builtin.shell: + cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec -i {{ PENPOT_BACKEND_LOCAL_CID | quote }} + python3 manage.py create-profile + -n {{ PENPOT_ADMIN_FULLNAME | quote }} + -e {{ PENPOT_ADMIN_EMAIL | quote }} + stdin: "{{ PENPOT_ADMIN_PASSWORD }}" register: penpot_admin_create retries: 30 delay: 5 @@ -27,11 +28,12 @@ delegate_to: "{{ PENPOT_BACKEND_HOST_NODE }}" - name: "๐Ÿ”ง Set the Penpot administrator's local password when the profile already exists" - ansible.builtin.shell: > # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node - container exec -i {{ PENPOT_BACKEND_LOCAL_CID | quote }} - python3 manage.py update-profile - -e {{ PENPOT_ADMIN_EMAIL | quote }} - -p {{ PENPOT_ADMIN_PASSWORD | quote }} + ansible.builtin.shell: + cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec -i {{ PENPOT_BACKEND_LOCAL_CID | quote }} + python3 manage.py update-profile + -e {{ PENPOT_ADMIN_EMAIL | quote }} + stdin: "{{ PENPOT_ADMIN_PASSWORD }}" when: "'already exists' in (penpot_admin_create.stdout | default('') ~ penpot_admin_create.stderr | default(''))" register: penpot_admin_update changed_when: "'Updated' in penpot_admin_update.stdout" diff --git a/tests/integration/iam/passwords/test_quote_in_shell.py b/tests/integration/iam/passwords/test_quote_in_shell.py index 280998ecaa..be3801d6d0 100644 --- a/tests/integration/iam/passwords/test_quote_in_shell.py +++ b/tests/integration/iam/passwords/test_quote_in_shell.py @@ -19,7 +19,7 @@ PASSWORD_TOKEN_RE = re.compile(r"(?i)\b[a-z0-9_]*password[a-z0-9_]*\b") QUOTE_FILTER_RE = re.compile(r"\|\s*quote\b", re.IGNORECASE) SHELL_KEY_RE = re.compile(r"^\s*(?:ansible\.builtin\.)?shell\s*:\s*(.*)$") -STDIN_KEY_RE = re.compile(r"^\s*stdin\s*:\s*[|>]") +STDIN_KEY_RE = re.compile(r"^\s*stdin\s*:") QUOTE_CHARS = {"'", '"'}