mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Ansible templates delegate_to before it evaluates the task's when, so a task delegating to N8N_HOST_NODE kills the play with 'N8N_HOST_NODE' is undefined whenever the resolver that sets it was skipped, even though the delegating task would have been skipped too. 43e09ebf0d put the resolver inside the same conditional block. Run 34653754175 shows the pair directly above the failure: the include skipped on the false condition while the setup call had returned status 200, and the play ended at 01_bootstrap.yml:92.
The resolver now runs unconditionally, before the block. It costs one lookup per deploy and changes no state.
The new lint reads the class rather than this instance: a conditional resolve_host_cid include whose PREFIX_HOST_NODE a delegate_to in the same role reads, unless an unconditional resolver or set_fact defines that prefix role-wide. The walker carries the condition through block, rescue and always, so an include nested in a block does not read as unconditional. Against the pre-fix file it reports would flag: ['N8N'].
It found one more: web-app-yourls/tasks/install.yml wrapped its tasks in a block whose when repeated the IS_STACK_HOST condition that main.yml already carries on the include. The wrapper is gone and the tasks are dedented; nothing had broken there because the condition always held.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
4.4 KiB
YAML
107 lines
4.4 KiB
YAML
---
|
|
# n8n owner setup: probe whether the owner account exists by attempting a
|
|
# single login with the alphanumeric break-glass owner_password. This
|
|
# replaces the deprecated /api/v1/owner/pre-setup probe (removed in n8n 1.x,
|
|
# always returns 404) and avoids two n8n class-validator false-positives:
|
|
# 1. @NoXss() on password: escapeHtml turns special chars (e.g. single quotes)
|
|
# to HTML entities, so the system admin password fails the check. Fix: use a
|
|
# dedicated alphanumeric owner_password (meta/secrets.yml).
|
|
# 2. @NoUrl() on firstName/lastName: regex /(\.[\p{L}\d-]+)/iu flags ANY dot
|
|
# followed by letters — including org names like "Infinito.Nexus". Fix:
|
|
# replace dots with spaces before sending (see firstName/lastName below).
|
|
# Normal users authenticate via SSO/Keycloak; this password is break-glass only.
|
|
#
|
|
# A single probe avoids triggering n8n's rate limiter. If a retry is still
|
|
# needed (e.g. n8n not fully up yet), keep well under n8n's default login
|
|
# rate limit of 5 attempts per 5-minute window: delay=30s/retries=4 caps this
|
|
# task at 5 attempts total, spanning 120s, so it never crosses the window.
|
|
- name: "🔎 Probe n8n owner account via single login attempt"
|
|
uri_retry:
|
|
url: "{{ N8N_INTERNAL_API_URL }}/rest/login"
|
|
method: POST
|
|
body_format: json
|
|
body:
|
|
emailOrLdapLoginId: "{{ lookup('users', 'administrator').email }}"
|
|
password: "{{ N8N_OWNER_PASSWORD }}"
|
|
status_code: [200, 401, 403]
|
|
return_content: false
|
|
register: n8n_owner_probe
|
|
until: "'json' in (n8n_owner_probe.content_type | default(''))"
|
|
retries: 4
|
|
delay: 30
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: "🔧 Initialise n8n owner account (first-time only)"
|
|
uri_retry:
|
|
url: "{{ N8N_INTERNAL_API_URL }}/rest/owner/setup"
|
|
method: POST
|
|
body_format: json
|
|
body:
|
|
email: "{{ lookup('users', 'administrator').email }}"
|
|
firstName: "{{ lookup('users', 'administrator').firstname | replace('.', ' ') | trim }}"
|
|
lastName: "{{ lookup('users', 'administrator').lastname | replace('.', ' ') | trim }}"
|
|
password: "{{ N8N_OWNER_PASSWORD }}"
|
|
status_code: [200, 201, 400]
|
|
return_content: true
|
|
register: n8n_owner_setup
|
|
when: n8n_owner_probe.status != 200
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: "✅ Assert n8n owner setup succeeded or was already done"
|
|
ansible.builtin.assert:
|
|
that:
|
|
- >-
|
|
(n8n_owner_setup is skipped)
|
|
or (n8n_owner_setup.status in [200, 201])
|
|
or ('already setup' in (n8n_owner_setup.content | default('')))
|
|
fail_msg: "n8n owner setup returned {{ n8n_owner_setup.status | default('n/a') }}: {{ n8n_owner_setup.content | default('') }}"
|
|
|
|
- name: "🔍 Resolve {{ application_id }} hosting node + container id"
|
|
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
|
|
vars:
|
|
resolve_service_key: "{{ entity_name }}"
|
|
resolve_output_prefix: N8N
|
|
resolve_compose_cid: "{{ N8N_CONTAINER }}"
|
|
|
|
- name: "🔐 Converge a rotated owner password n8n still rejects"
|
|
when:
|
|
- n8n_owner_probe.status in [401, 403]
|
|
- "'already setup' in (n8n_owner_setup.content | default(''))"
|
|
block:
|
|
- name: "🔐 Write the deployed owner password into n8n's user row"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- container
|
|
- exec
|
|
- -i
|
|
- -e
|
|
- "NODE_PATH={{ N8N_NODE_MODULES }}"
|
|
- -e
|
|
- "N8N_OWNER_EMAIL={{ N8N_OWNER_EMAIL }}"
|
|
- -e
|
|
- "N8N_OWNER_PASSWORD={{ N8N_OWNER_PASSWORD }}"
|
|
- "{{ N8N_LOCAL_CID }}" # nocheck: container-exec-resolver
|
|
- node
|
|
- "-"
|
|
stdin: "{{ lookup('file', 'javascript/reconcile/owner_password.js') }}"
|
|
register: n8n_owner_password
|
|
retries: 30
|
|
delay: 10
|
|
until: n8n_owner_password.rc == 0
|
|
changed_when: n8n_owner_password.stdout.startswith('CHANGED')
|
|
delegate_to: "{{ N8N_HOST_NODE }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: "🔎 Confirm the owner signs in with the deployed password"
|
|
uri_retry:
|
|
url: "{{ N8N_INTERNAL_API_URL }}/rest/login"
|
|
method: POST
|
|
body_format: json
|
|
body:
|
|
emailOrLdapLoginId: "{{ lookup('users', 'administrator').email }}"
|
|
password: "{{ N8N_OWNER_PASSWORD }}"
|
|
status_code: [200]
|
|
return_content: false
|
|
retries: 0
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|