perf(web-app-openproject): apply settings and the administrator in one rails runner

Three `bundle exec rails runner -` calls each booted OpenProject's Rails
app (35.2 s, 33.3 s and 33.1 s on echoserver): the settings in
02_settings.yml, the administrator and the LDAP link in 04_admin.yml.
They now run as one runner in 04_admin.yml with apply_settings.rb,
ensure_admin_user.rb and, under header SSO, link_admin_to_ldap.rb in that
order. It runs after 03_ldap.yml, which creates the ldap_auth_sources row
the link needs; nothing between 02 and 04 reads the settings.
02_settings.yml keeps only the migration and becomes 02_migrate.yml.

The administrator password and OPENPROJECT_RAILS_SETTINGS (which carries
smtp_password) leave argv: they are the first two stdin lines, exported
by `sh -c 'IFS= read -r …; export …; exec "$@"'` before rails reads the
script from the rest of stdin. stdin is a `|-` block, one expression per
line.

On merge: feature/store-i18n 463053e061 moved these secrets to
`environment:`; keep this version. feature/iam-profile-rbac 89e291e738
adds a fourth runner (reconcile_admins.rb) to 04_admin.yml; fold it into
this single runner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-24 20:17:56 +02:00
1 parent 2949d46bff
commit c78d5f906c
4 files changed
+24 -57

No files matched your search

@@ -33,12 +33,12 @@
- name: 🚿 flush docker service
meta: flush_handlers
- name: 🚀 Deploy Settings
include_tasks: 02_settings.yml
- name: ▶️ Migrate the OpenProject database
include_tasks: 02_migrate.yml
- name: 🔧 Setup LDAP
include_tasks: 03_ldap.yml
when: OPENPROJECT_LDAP_ENABLED | bool
- name: 📁 Create OpenProject Administrator
- name: 🔧 Apply settings and the OpenProject administrator
include_tasks: 04_admin.yml
@@ -50,18 +50,3 @@
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/wait_converged.yml') }}"
vars:
wait_service_key: "{{ OPENPROJECT_WEB_SERVICE }}"
- name: 🔧 Set settings in OpenProject (rails runner via STDIN from an external script)
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver OPENPROJECT_WEB_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
container exec -i
-e OPENPROJECT_RAILS_SETTINGS={{ OPENPROJECT_RAILS_SETTINGS | to_json | quote }}
{{ OPENPROJECT_WEB_LOCAL_CID | quote }}
bash -lc "cd /app && bundle exec rails runner -"
stdin: "{{ lookup('file', 'ruby/apply_settings.rb') }}"
delegate_to: "{{ OPENPROJECT_WEB_HOST_NODE }}"
register: openproject_apply_settings
retries: 30
delay: 5
until: openproject_apply_settings.rc == 0
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
+19 -37
View File
@@ -6,42 +6,24 @@
resolve_output_prefix: OPENPROJECT_WEB
resolve_compose_cid: "{{ OPENPROJECT_WEB_CONTAINER_ADDRESS }}"
- name: ✅ Ensure administrator user exists and is admin
ansible.builtin.command:
argv:
- container
- exec
- -i
- -e
- "OPENPROJECT_ADMIN_PASSWORD={{ OPENPROJECT_ADMINISTRATOR_PASSWORD }}"
- -e
- "OPENPROJECT_ADMIN_LOGIN={{ OPENPROJECT_ADMINISTRATOR_USERNAME }}"
- -e
- "OPENPROJECT_ADMIN_EMAIL={{ OPENPROJECT_ADMINISTRATOR_EMAIL }}"
- "{{ OPENPROJECT_WEB_LOCAL_CID }}" # nocheck: container-exec-resolver CID resolved at delegate_to runtime; container_address targets a manager-only subshell
- bash
- -lc
- "cd /app && bundle exec rails runner -"
stdin: "{{ lookup('file', 'ruby/ensure_admin_user.rb') }}"
- name: 🔧 Apply settings, ensure the administrator and link it to LDAP
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver CID resolved at delegate_to runtime; container_address targets a manager-only subshell
container exec -i
-e {{ ('OPENPROJECT_ADMIN_LOGIN=' ~ OPENPROJECT_ADMINISTRATOR_USERNAME) | quote }}
-e {{ ('OPENPROJECT_ADMIN_EMAIL=' ~ OPENPROJECT_ADMINISTRATOR_EMAIL) | quote }}
{% if OPENPROJECT_HEADER_SSO_ENABLED | bool %}-e {{ ('OPENPROJECT_LDAP_NAME=' ~ OPENPROJECT_LDAP.name) | quote }}{% endif %}
{{ OPENPROJECT_WEB_LOCAL_CID | quote }}
sh -c 'IFS= read -r OPENPROJECT_ADMIN_PASSWORD; IFS= read -r OPENPROJECT_RAILS_SETTINGS; export OPENPROJECT_ADMIN_PASSWORD OPENPROJECT_RAILS_SETTINGS; exec "$@"' sh bash -lc 'cd /app && bundle exec rails runner -'
stdin: |-
{{ OPENPROJECT_ADMINISTRATOR_PASSWORD }}
{{ OPENPROJECT_RAILS_SETTINGS | to_json }}
{{ lookup('file', 'ruby/apply_settings.rb') }}
{{ lookup('file', 'ruby/ensure_admin_user.rb') }}
{% if OPENPROJECT_HEADER_SSO_ENABLED | bool %}{{ lookup('file', 'ruby/link_admin_to_ldap.rb') }}{% endif %}
delegate_to: "{{ OPENPROJECT_WEB_HOST_NODE }}"
register: openproject_rails_runner
retries: 30
delay: 5
until: openproject_rails_runner.rc == 0
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🔧 Link admin to LDAP auth source so trusted-header bridge can resolve it
ansible.builtin.command:
argv:
- container
- exec
- -i
- -e
- "OPENPROJECT_LDAP_NAME={{ OPENPROJECT_LDAP.name }}"
- -e
- "OPENPROJECT_ADMIN_LOGIN={{ OPENPROJECT_ADMINISTRATOR_USERNAME }}"
- "{{ OPENPROJECT_WEB_LOCAL_CID }}" # nocheck: container-exec-resolver CID resolved at delegate_to runtime; container_address targets a manager-only subshell
- bash
- -lc
- "cd /app && bundle exec rails runner -"
stdin: "{{ lookup('file', 'ruby/link_admin_to_ldap.rb') }}"
delegate_to: "{{ OPENPROJECT_WEB_HOST_NODE }}"
when: OPENPROJECT_HEADER_SSO_ENABLED | bool
register: openproject_ldap_link
changed_when: "'Linked' in openproject_ldap_link.stdout"
@@ -25,10 +25,10 @@ OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL | dotenv_quote }}
OPENPROJECT_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }}
# nocheck: persona-block-rationale — administrator is deliberately NOT blocked: tasks/04_admin.yml:9-27 seeds a native OpenProject account whose password is the Keycloak administrator secret (files/ruby/ensure_admin_user.rb:5-18), so the journey runs in every variant; the flag stays rendered because the contract wants the opt-out state declared, not implied.
# nocheck: persona-block-rationale — administrator is deliberately NOT blocked: tasks/04_admin.yml:9-29 seeds a native OpenProject account whose password is the Keycloak administrator secret (files/ruby/ensure_admin_user.rb:5-18), so the journey runs in every variant; the flag stays rendered because the contract wants the opt-out state declared, not implied.
PERSONA_ADMINISTRATOR_BLOCKED=false
# nocheck: persona-block-rationale — OpenProject is seeded with the administrator only (files/ruby/ensure_admin_user.rb:5-18); biber owns no OpenProject account and is auto-provisioned solely through the oauth2-proxy trusted-header bridge onto the LDAP auth source (meta/services.yml:96-109, vars/main.yml:45, tasks/04_admin.yml:29-45), which the sso: false variants remove (see README "Persona contract opt-outs").
# nocheck: persona-block-rationale — OpenProject is seeded with the administrator only (files/ruby/ensure_admin_user.rb:5-18); biber owns no OpenProject account and is auto-provisioned solely through the oauth2-proxy trusted-header bridge onto the LDAP auth source (meta/services.yml:96-109, vars/main.yml:69, tasks/04_admin.yml:9-29), which the sso: false variants remove (see README "Persona contract opt-outs").
PERSONA_BIBER_BLOCKED={{ 'false' if (lookup('config', application_id, 'services.sso.enabled') | bool) else 'true' }}
SSO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.sso.enabled') | string | lower }}