Files
computer-playbook/roles/web-app-minio/tasks/01_oidc.yml
kevinveenbirkenbachandClaude Opus 5 cd1225d2d9 fix(async): collect fire-and-forget results
The repo's async convention is fire and forget: ASYNC_POLL is 0 whenever
ASYNC_ENABLED is true, and the swarm test lane forces it true through
extra vars. A task dispatched that way returns a stub, so a changed_when
or failed_when reading its own register evaluates against nothing and can
never report changed and never fail. Several tasks carried a
"not ASYNC_ENABLED and" prefix in front of those conditions, added to
avoid touching undefined rc and stdout, which turned the guard off
entirely rather than adapting it.

That is how the nextcloud plugin configuration stayed broken unseen: the
batch reported ok after 0.8s while its delegated commands kept running,
the next task read the half written configuration, and no failure in any
of them could surface.

Every async in the repo was decided individually rather than swept. Where
nothing consumes the result, the async stays and a one line rationale now
records what does not read it. Where a later task, a handler or a browser
does consume it, either the async goes or the result is collected.

Collecting is what the new reaper does. It waits for a whole batch, then
reports every broken job at once instead of aborting on the first, so one
run shows whether a single item or the whole set is broken. Deciding what
counts as broken lives in a filter plugin, because the distinction is
subtle enough to deserve tests: a module reports failure in msg, a command
in rc and stdout, an idempotent API answers "already exists" and means
success, and a job that cannot be read at all comes back marked finished
with no outcome, which would otherwise read as green.

Timing cost is roughly one to two minutes on a deploy measured in hours.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 04:05:38 +02:00

161 lines
6.7 KiB
YAML

---
# mc URL-embedded credentials (`MC_HOST_<alias>=https://USER:PASS@host`)
# are NOT URL-decoded by mc when read from the environment, while the
# MinIO server stores the URL-decoded password. With administrator
# credentials containing $ / € / ! / ' the two sides disagree and every
# admin call fails with "request signature we calculated does not
# match". Authenticate via `mc alias set` with explicit args instead;
# mc applies the credentials verbatim, matching the server.
# MinIO finishes booting its IAM subsystem (where policies and the
# OIDC IdP live) AFTER `mc admin info` already reports the server
# as up. Probing IAM directly via `mc admin policy ls` is the only
# readiness signal that matches what the policy admin API needs;
# `mc admin info` returns rc=0 in a window where the very next
# `mc admin policy info` still fails with `Server not initialized
# yet, please try again.`.
- name: "⏳ Wait for MinIO IAM subsystem to finish initializing"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
shell: |
set -euo pipefail
container run --rm \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc admin policy ls minio'
args: { executable: /bin/bash }
register: mc_admin_policy_ls
retries: 60
delay: 5
until: mc_admin_policy_ls.rc == 0
changed_when: false
- name: "📝 Register Keycloak as named OIDC IdP on MinIO"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
MINIO_OIDC_CONFIG_URL: "{{ OIDC.CLIENT.DISCOVERY_DOCUMENT }}"
MINIO_OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
MINIO_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
MINIO_OIDC_DISPLAY_NAME: "{{ OIDC.BUTTON_TEXT }}"
MINIO_OIDC_CLAIM_NAME: "{{ RBAC.GROUP.CLAIM }}"
shell: |
set -euo pipefail
container run --rm \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
-e MINIO_OIDC_CONFIG_URL \
-e MINIO_OIDC_CLIENT_ID \
-e MINIO_OIDC_CLIENT_SECRET \
-e MINIO_OIDC_DISPLAY_NAME \
-e MINIO_OIDC_CLAIM_NAME \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc idp openid add minio keycloak \
"config_url=$MINIO_OIDC_CONFIG_URL" \
"client_id=$MINIO_OIDC_CLIENT_ID" \
"client_secret=$MINIO_OIDC_CLIENT_SECRET" \
"display_name=$MINIO_OIDC_DISPLAY_NAME" \
"claim_name=$MINIO_OIDC_CLAIM_NAME" \
scopes=openid,profile,email,groups \
redirect_uri_dynamic=on'
args: { executable: /bin/bash }
register: mc_idp_add
changed_when: '"Successfully" in mc_idp_add.stdout or "Successfully" in mc_idp_add.stderr'
failed_when:
- mc_idp_add.rc != 0
- '"already exists" not in (mc_idp_add.stderr | default(""))'
- name: "🔄 Restart MinIO so the named OIDC IdP is picked up by the Console"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
shell: |
set -euo pipefail
container run --rm \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc admin service restart minio --json'
args: { executable: /bin/bash }
register: mc_admin_service_restart
changed_when: mc_admin_service_restart.rc == 0
when: mc_idp_add.changed | bool
- name: "⏳ Wait for MinIO IAM subsystem to come back after restart"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
shell: |
set -euo pipefail
container run --rm \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc admin policy ls minio'
args: { executable: /bin/bash }
register: mc_admin_policy_ls_after_restart
retries: 60
delay: 5
until: mc_admin_policy_ls_after_restart.rc == 0
changed_when: false
when: mc_idp_add.changed | bool
- name: "🔎 Check policy (RAW with slash) exists"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
shell: |
set -euo pipefail
container run --rm \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc admin policy info minio {{ MINIO_OIDC_POLICY_NAME | quote }}'
args: { executable: /bin/bash }
register: mc_policy_info_raw
failed_when: false
changed_when: false
- name: "📁 Create policy (RAW with slash) if missing"
environment:
MINIO_ROOT_USER: "{{ lookup('users', 'administrator').username }}"
MINIO_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}"
shell: |
set -euo pipefail
printf '%s' '{{ (MINIO_OIDC_POLICY_CONTENT | from_yaml | to_json) | b64encode }}' \
| base64 -d \
| container run --rm -i \
-e MINIO_ROOT_USER \
-e MINIO_ROOT_PASSWORD \
--entrypoint /bin/sh \
{{ MINIO_MC_IMAGE }} \
-c 'mc alias set minio {{ MINIO_API_URL | quote }} "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null \
&& mc admin policy create minio {{ MINIO_OIDC_POLICY_NAME | quote }} /dev/stdin'
args: { executable: /bin/bash }
register: minio_policy_create
changed_when: false
# rationale: async; consumed only by the STS playwright spec, reaped at the end of this file
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
when:
- mc_policy_info_raw.rc != 0
- name: "⏳ Reap the MinIO policy job"
ansible.builtin.include_tasks: utils/async/reap.yml
vars:
async_reap_jobs: "{{ minio_policy_create.results | default([minio_policy_create]) }}"
async_reap_label: "MinIO OIDC policy"
async_reap_tolerate: []