Files
computer-playbook/roles/web-app-nextcloud/tasks/addons/integration_openproject_provision.yml
kevinveenbirkenbachandClaude Opus 5 bbf4ca09cc fix(nextcloud): let occ read a credential that starts with a dash
Symfony ArgvInput reads a token beginning with a dash as an option, so passing the secret as a separate token after --value handed occ two tokens and occ reported the option as missing its value. Doorkeeper mints base64url secrets, so roughly one run in 43 produced one starting with a dash and the OpenProject provisioning died with: The "--value" option requires a value.

The equals form is unambiguous: Symfony splits at the first equals sign and takes everything after it as the value. The quote filter replaces the hand-written single quotes and stays shell-safe. Note that shlex leaves a leading-dash value unquoted, so the equals form is what carries the fix, not the quoting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 11:36:58 +02:00

111 lines
5.7 KiB
YAML

---
- name: "🔍 Resolve the OpenProject web hosting node and container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: web
resolve_service_name: "{{ lookup('container_service', 'web-app-openproject', 'web') }}"
resolve_output_prefix: NEXTCLOUD_OPENPROJECT
resolve_compose_cid: "{{ NEXTCLOUD_OPENPROJECT_CONTAINER_ADDRESS }}"
resolve_converge_retries: 120
resolve_converge_delay: 5
- name: ⏳ Wait for the OpenProject web endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', 'web-app-openproject', 'services.openproject.ports.local.http') }}"
delay: 5
timeout: "{{ lookup('timeout', 600) | int }}"
- name: 📁 Create the Nextcloud storage + OpenProject OAuth application
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver NEXTCLOUD_OPENPROJECT_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
container exec -i
-e NEXTCLOUD_HOST={{ lookup('tls', 'web-app-nextcloud', 'url.base') | quote }}
-e STORAGE_NAME={{ nextcloud_op_storage_name | quote }}
-e NC_HAS_OP_SECRET={{ nextcloud_op_client_secret_current.stdout | trim | quote }}
{{ NEXTCLOUD_OPENPROJECT_LOCAL_CID | quote }}
bash -lc 'cd /app && bundle exec rails runner -'
stdin: "{{ lookup('file', 'ruby/utils/doorkeeper_allow_onion_redirect.rb') ~ '\n' ~ lookup('file', 'ruby/addons/openproject/ensure_nextcloud_storage.rb') }}"
delegate_to: "{{ NEXTCLOUD_OPENPROJECT_HOST_NODE }}"
register: nextcloud_op_storage_create
changed_when: "'\"oauth_application_created\":true' in nextcloud_op_storage_create.stdout"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🔎 Parse the OpenProject OAuth application credentials
set_fact:
nextcloud_op_oauth: "{{ nextcloud_op_storage_create.stdout | regex_search('{.*}') | from_json }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🔍 Resolve the effective OpenProject OAuth client secret (fresh or persisted)
set_fact:
nextcloud_op_client_secret: >-
{{ nextcloud_op_oauth.openproject_client_secret
| default((nextcloud_op_client_secret_current.stdout | trim), true) }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🛑 Fail when no usable OpenProject OAuth application secret is available
assert:
that:
- nextcloud_op_oauth.openproject_client_id | default('') | length > 0
- (nextcloud_op_client_secret | default('') | trim) | length > 0
fail_msg: >-
OpenProject did not return a usable OAuth application and no client secret is persisted in
Nextcloud. A freshly generated client secret is only available the first time the application
is created; reset the integration_openproject app config (openproject_client_id,
openproject_client_secret, nc_oauth_client_id) and the OpenProject storage to re-provision.
- name: ⬆️ Push the OpenProject OAuth application credentials into Nextcloud
shell: >
{{ NEXTCLOUD_DOCKER_EXEC_OCC }} config:app:set integration_openproject {{ item.key }}
--value={{ item.value | quote }}
loop:
- { key: "authorization_method", value: "oauth2" }
- { key: "openproject_client_id", value: "{{ nextcloud_op_oauth.openproject_client_id }}" }
- { key: "openproject_client_secret", value: "{{ nextcloud_op_client_secret }}" }
loop_control:
label: "{{ item.key }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 📝 Register a Nextcloud OAuth2 client for OpenProject
shell: >
{{ NEXTCLOUD_DOCKER_EXEC_OCC }} oauth2:add-client
"OpenProject client"
"{{ nextcloud_op_expected_url }}/oauth_clients/%s/callback"
--output=json
register: nextcloud_op_ncclient_create
changed_when: nextcloud_op_ncclient_create.rc == 0
failed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🔌 Wire the Nextcloud OAuth2 client into both sides when it was created
when: (nextcloud_op_ncclient_create.rc | default(1)) == 0
block:
- name: 🔎 Parse the Nextcloud OAuth2 client credentials
set_fact:
nextcloud_op_ncclient: "{{ nextcloud_op_ncclient_create.stdout | trim | from_json }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: ⬆️ Push the Nextcloud OAuth2 client id into integration_openproject
shell: >
{{ NEXTCLOUD_DOCKER_EXEC_OCC }} config:app:set integration_openproject nc_oauth_client_id
--value={{ nextcloud_op_ncclient.clientId | quote }}
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 📝 Register the Nextcloud OAuth2 client back on the OpenProject storage
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver NEXTCLOUD_OPENPROJECT_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
container exec -i
-e NEXTCLOUD_HOST={{ lookup('tls', 'web-app-nextcloud', 'url.base') | quote }}
-e STORAGE_NAME={{ nextcloud_op_storage_name | quote }}
-e NC_OAUTH_CLIENT_ID={{ nextcloud_op_ncclient.clientId | quote }}
-e NC_OAUTH_CLIENT_SECRET={{ nextcloud_op_ncclient.clientSecret | quote }}
{{ NEXTCLOUD_OPENPROJECT_LOCAL_CID | quote }}
bash -lc 'cd /app && bundle exec rails runner -'
stdin: "{{ lookup('file', 'ruby/utils/doorkeeper_allow_onion_redirect.rb') ~ '\n' ~ lookup('file', 'ruby/addons/openproject/ensure_nextcloud_storage.rb')
}}"
delegate_to: "{{ NEXTCLOUD_OPENPROJECT_HOST_NODE }}"
register: nextcloud_op_client_store
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"