mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
fix(roles): keep the provisioning secrets out of the exec command line
Five roles handed a credential to `container exec` as `-e NAME=<value>`, or as a positional argument. An argv element reaches the docker client's command line, the process list, and the task result's `cmd` field, which ansible prints from -v upward. Local deploys run with MASK_CREDENTIALS_IN_LOGS disabled, so the retained `no_log` does not mask them there. Where the container already carries the value, the flag added exposure and nothing else, so it is gone rather than rerouted. `container exec` runs in the container's own environment, and sys-svc-container's compose base wires every role's rendered env file as `env_file`: web-app-akaunting ADMIN_EMAIL, ADMIN_PASSWORD templates/env.j2:27-28 web-app-baserow SECRET_KEY, BASEROW_SECRET_KEY templates/env.j2:8-9 The rest are values no container holds, so they travel through the task's `environment` and a valueless `-e NAME`, which the docker client resolves from its own environment: baserow's MCP_ENDPOINT_KEY and BOOTSTRAP_ADMIN_PASSWORD, pretix's MCP_TOKEN, and home assistant's HA_PASSWORD, HA_SERVICE_PASSWORD and HA_TOKEN. Every script already read these names through os.environ, so none of them changed. web-app-homeassistant loses tasks/utils/mcp/realign_owner.yml rather than having its secret rerouted. It held the owner password as a positional of `hass --script auth`, and it is unreachable: 00_core.yml:32 includes utils/owner.yml before utils/mcp.yml at :35, owner.yml ends in a hard assert on `homeassistant_owner.rc == 0`, and that register comes from the same provisioning script with the same HA_USERNAME and HA_PASSWORD. A hub that refused the owner password therefore fails the play before the MCP tasks run, so the LOGIN_REFUSED branch that was this file's only caller cannot be taken. tasks/utils/owner/realign.yml is the remaining realign path and passes its arguments as JSON on standard input. The two LOGIN_REFUSED clauses in mcp/provision.yml go with it, because `rc != 0` is what they now mean. files/python/provision_mcp.py becomes provision.py: owner/provision.yml runs it with HA_OWNER_ONLY=1, so it has provisioned the owner since that task existed and the name covered half its job. Two co-located defects in the same tasks, which cannot be split from the transport change at file granularity: web-app-akaunting matched `'CHANGED' in stdout`, which is also true of the `UNCHANGED` the snippet prints when the password already matches, so the task reported a change on every deploy. It now matches the whole output line. `expand_argument_vars: false` guards the argv because one element carries the PHP snippet: ansible's expandvars runs over every element, and a `$name` the environment happens to hold would be substituted into the code. Measured as a no-op on today's content, since expandvars leaves an unknown name alone. web-app-pretix read its result from `stdout_lines[-1]`, which is IPython's exit prompt. `pretix shell_scoped` runs the script through IPython, which executes an indented block only once a dedented line follows, so the block that created the token was left unrun when it ended the input. The script's last statement is now a top-level print of a marker, which forces the block before it to execute, and the task matches `(?m)^PRETIX_RESULT=CHANGED`. The end anchor is left off on purpose: a trailing carriage return defeats `$`, and a literal backslash would have to survive both YAML and Jinja. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
845a654957
commit
177255329f
13 files changed
+78
-119
No files matched your search
@@ -41,7 +41,7 @@ flowchart LR
|
||||
|
||||
| Guarantee | Enforced by |
|
||||
| --- | --- |
|
||||
| Only Assist-exposed entities are reachable | `ensure_mcp_entry` binds the MCP config entry to `llm_hass_api: ["assist"]` in [provision_mcp.py](../../roles/web-app-homeassistant/files/python/provision_mcp.py) |
|
||||
| Only Assist-exposed entities are reachable | `ensure_mcp_entry` binds the MCP config entry to `llm_hass_api: ["assist"]` in [provision.py](../../roles/web-app-homeassistant/files/python/provision.py) |
|
||||
| Mutating tools stay off | the MCP long-lived token belongs to a dedicated account pinned to Home Assistant's `system-read-only` group; `ensure_service_account` re-asserts `group_ids` through `config/auth/update` on every run, so an account promoted by hand is demoted again |
|
||||
| The endpoint refuses an anonymous caller | [test-mcp-guest.js](../../roles/web-app-homeassistant/files/playwright/test-mcp-guest.js) |
|
||||
| The stored bearer still authenticates | `tasks/utils/mcp/probe.yml`, which hard-fails the deploy when the hub rejects it |
|
||||
|
||||
@@ -33,14 +33,18 @@
|
||||
resolve_compose_cid: "{{ AKAUNTING_CONTAINER }}"
|
||||
|
||||
- name: "🔑 Akaunting | Realign the administrator password with the declared one"
|
||||
ansible.builtin.shell: > # nocheck: container-exec-resolver AKAUNTING_LOCAL_CID resolved at delegate_to runtime
|
||||
container exec
|
||||
-e ADMIN_EMAIL={{ AKAUNTING_ADMIN_EMAIL | quote }}
|
||||
-e ADMIN_PASSWORD={{ AKAUNTING_ADMIN_PASSWORD | quote }}
|
||||
{{ AKAUNTING_LOCAL_CID | quote }}
|
||||
php artisan tinker --execute={{ lookup('file', 'php/realign_admin_password.php') | quote }}
|
||||
ansible.builtin.command:
|
||||
expand_argument_vars: false
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- "{{ AKAUNTING_LOCAL_CID }}" # nocheck: container-exec-resolver AKAUNTING_LOCAL_CID resolved at delegate_to runtime
|
||||
- php
|
||||
- artisan
|
||||
- tinker
|
||||
- "--execute={{ lookup('file', 'php/realign_admin_password.php') }}"
|
||||
register: akaunting_admin_password
|
||||
changed_when: "'CHANGED' in akaunting_admin_password.stdout"
|
||||
changed_when: "'CHANGED' in akaunting_admin_password.stdout_lines"
|
||||
failed_when: >-
|
||||
akaunting_admin_password.rc != 0
|
||||
or 'FAILED' in akaunting_admin_password.stdout
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
resolve_compose_cid: "{{ BASEROW_CONTAINER }}"
|
||||
|
||||
- name: "▶️ Run Baserow Django migrations before bootstrapping users"
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
container exec -i
|
||||
-e DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE | quote }}
|
||||
-e SECRET_KEY={{ BASEROW_SECRET_KEY | quote }}
|
||||
-e BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY | quote }}
|
||||
{{ BASEROW_LOCAL_CID | quote }}
|
||||
{{ BASEROW_PYTHON }} {{ BASEROW_MANAGE_PY }} locked_migrate --noinput
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- -i
|
||||
- -e
|
||||
- "DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}"
|
||||
- "{{ BASEROW_LOCAL_CID }}" # nocheck: container-exec-resolver BASEROW_LOCAL_CID resolved at delegate_to runtime
|
||||
- "{{ BASEROW_PYTHON }}"
|
||||
- "{{ BASEROW_MANAGE_PY }}"
|
||||
- locked_migrate
|
||||
- --noinput
|
||||
register: baserow_migrate_result
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: >
|
||||
@@ -52,17 +56,27 @@
|
||||
resolve_compose_cid: "{{ BASEROW_CONTAINER }}"
|
||||
|
||||
- name: "🎬 Bootstrap Baserow superuser '{{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}'"
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
container exec -i
|
||||
-e DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE | quote }}
|
||||
-e SECRET_KEY={{ BASEROW_SECRET_KEY | quote }}
|
||||
-e BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY | quote }}
|
||||
-e BOOTSTRAP_ADMIN_USERNAME={{ BASEROW_BOOTSTRAP_ADMIN_USERNAME | quote }}
|
||||
-e BOOTSTRAP_ADMIN_EMAIL={{ BASEROW_BOOTSTRAP_ADMIN_EMAIL | quote }}
|
||||
-e BOOTSTRAP_ADMIN_PASSWORD={{ BASEROW_BOOTSTRAP_ADMIN_PASSWORD | quote }}
|
||||
{{ BASEROW_LOCAL_CID | quote }}
|
||||
{{ BASEROW_PYTHON }} {{ BASEROW_MANAGE_PY }} shell -c {{ lookup('file', role_path ~ '/files/python/bootstrap_admin.py') | quote }}
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- -i
|
||||
- -e
|
||||
- "DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}"
|
||||
- -e
|
||||
- "BOOTSTRAP_ADMIN_USERNAME={{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}"
|
||||
- -e
|
||||
- "BOOTSTRAP_ADMIN_EMAIL={{ BASEROW_BOOTSTRAP_ADMIN_EMAIL }}"
|
||||
- -e
|
||||
- BOOTSTRAP_ADMIN_PASSWORD
|
||||
- "{{ BASEROW_LOCAL_CID }}" # nocheck: container-exec-resolver BASEROW_LOCAL_CID resolved at delegate_to runtime
|
||||
- "{{ BASEROW_PYTHON }}"
|
||||
- "{{ BASEROW_MANAGE_PY }}"
|
||||
- shell
|
||||
- -c
|
||||
- "{{ lookup('file', role_path ~ '/files/python/bootstrap_admin.py') }}"
|
||||
environment:
|
||||
BOOTSTRAP_ADMIN_PASSWORD: "{{ BASEROW_BOOTSTRAP_ADMIN_PASSWORD }}"
|
||||
register: baserow_bootstrap_admin_result
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: "'created' in baserow_bootstrap_admin_result.stdout"
|
||||
|
||||
@@ -15,23 +15,21 @@
|
||||
- -e
|
||||
- "DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}"
|
||||
- -e
|
||||
- "SECRET_KEY={{ BASEROW_SECRET_KEY }}"
|
||||
- -e
|
||||
- "BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY }}"
|
||||
- -e
|
||||
- "MCP_OWNER_EMAIL={{ BASEROW_MCP_OWNER_EMAIL }}"
|
||||
- -e
|
||||
- "MCP_WORKSPACE_NAME={{ BASEROW_MCP_WORKSPACE_NAME }}"
|
||||
- -e
|
||||
- "MCP_ENDPOINT_NAME={{ BASEROW_MCP_ENDPOINT_NAME }}"
|
||||
- -e
|
||||
- "MCP_ENDPOINT_KEY={{ BASEROW_MCP_ENDPOINT_KEY }}"
|
||||
- MCP_ENDPOINT_KEY
|
||||
- "{{ BASEROW_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- "{{ BASEROW_PYTHON }}"
|
||||
- "{{ BASEROW_MANAGE_PY }}"
|
||||
- shell
|
||||
- -c
|
||||
- "{{ lookup('file', 'python/provision_mcp.py') }}"
|
||||
environment:
|
||||
MCP_ENDPOINT_KEY: "{{ BASEROW_MCP_ENDPOINT_KEY }}"
|
||||
register: baserow_mcp_provision
|
||||
retries: 30
|
||||
delay: 10
|
||||
@@ -52,11 +50,13 @@
|
||||
- -e
|
||||
- "MCP_ENDPOINT_PATH={{ BASEROW_MCP_ENDPOINT_PATH }}"
|
||||
- -e
|
||||
- "MCP_ENDPOINT_KEY={{ BASEROW_MCP_ENDPOINT_KEY }}"
|
||||
- MCP_ENDPOINT_KEY
|
||||
- "{{ BASEROW_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- "{{ BASEROW_PYTHON }}"
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/probe_mcp_endpoint.py') }}"
|
||||
environment:
|
||||
MCP_ENDPOINT_KEY: "{{ BASEROW_MCP_ENDPOINT_KEY }}"
|
||||
register: baserow_mcp_probe
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -13,7 +13,7 @@ This role deploys Home Assistant as a container on its own canonical domain behi
|
||||
- **Containerized hub:** Runs the official Home Assistant image on both the Docker Compose and Swarm stacks behind the reverse proxy.
|
||||
- **Reverse-proxy trust:** Renders a `configuration.yaml` with `default_config`, `use_x_forwarded_for`, and the project-wide trusted-proxy CIDRs listed in `trusted_proxies`. In swarm the hub joins the proxy's own overlay, so the forwarded request arrives from that network rather than from the hub's, and trusting only the hub's subnet makes Home Assistant answer every proxied request with HTTP 400.
|
||||
- **Persistent configuration:** Mounts `/config` as a named volume and hands it to the container backup service when volume backups are part of the deployment.
|
||||
- **Owner provisioned on every deploy:** Once the hub answers, the deploy runs `files/python/provision_mcp.py` inside the container with `HA_OWNER_ONLY=1`. On a fresh hub the script creates the owner from the platform administrator's username and `credentials.administrator_password`, names the hub after the corporate design title when the design service is on, and closes the remaining onboarding steps. On every later deploy it signs in with the same credentials and reports no change. When the hub refuses them, the deploy writes the password into the hub's auth store with Home Assistant's `auth` script, restarts the hub and signs in again. Both calls receive their values on standard input.
|
||||
- **Owner provisioned on every deploy:** Once the hub answers, the deploy runs `files/python/provision.py` inside the container with `HA_OWNER_ONLY=1`. On a fresh hub the script creates the owner from the platform administrator's username and `credentials.administrator_password`, names the hub after the corporate design title when the design service is on, and closes the remaining onboarding steps. On every later deploy it signs in with the same credentials and reports no change. When the hub refuses them, the deploy writes the password into the hub's auth store with Home Assistant's `auth` script, restarts the hub and signs in again. Both calls receive their values on standard input.
|
||||
- **Closed onboarding wizard:** A Playwright spec asserts that every onboarding step is done and that `/onboarding.html` hands a visitor over to the sign-in page.
|
||||
- **Corporate design:** With the design service on, `configuration.yaml` carries a theme whose variables point at the design tokens, and a start-up automation selects it as the default theme for light and dark mode. The sign-in page takes the same variables from the role stylesheet; logo, favicon and page title come from the design assets through the stylesheet and `templates/design.js.j2`. A theme or mode picked in the profile keeps its own palette, and the script mirrors the picked mode onto the design tokens.
|
||||
- **MCP server contract:** Declares the built-in MCP server at `/api/mcp` over streamable HTTP as an internal, bearer-token-authenticated endpoint, and generates the token that MCP clients present. Adding the MCP Server integration itself stays a Home Assistant onboarding step.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# TODO
|
||||
|
||||
- MCP task secrets: `tasks/utils/mcp/provision.yml` and `tasks/utils/mcp/realign_owner.yml` hand `HA_PASSWORD` and `HA_SERVICE_PASSWORD` to `container exec` with `-e`, `realign_owner.yml` passes the owner password as an argument of `hass --script auth`, and `tasks/utils/mcp/probe.yml` passes `HA_TOKEN` with `-e`, so each value is part of the task command. Feed them on standard input the way `tasks/utils/owner/provision.yml` and `tasks/utils/owner/realign.yml` do.
|
||||
- Script name: `files/python/provision_mcp.py` also provisions the owner since `tasks/utils/owner/provision.yml` runs it with `HA_OWNER_ONLY=1`. Rename it to a name that covers both uses.
|
||||
- Product name: the sidebar title, the `alt` text of the sign-in logo, the web app manifest (`name`, icons) and the launch screen logo still show Home Assistant; the frontend offers no setting for them, and the theme and the sign-in stylesheet do not reach them.
|
||||
- Sign-in stylesheet: `templates/style.css.j2` repeats the `--wa-color-*` assignments Home Assistant derives on `html`, because the sign-in page takes no theme. Compare the list with the frontend when the image version moves.
|
||||
File renamed without changes.
@@ -8,11 +8,13 @@
|
||||
- -e
|
||||
- "HA_PORT={{ HOMEASSISTANT_PORT_INTERNAL }}"
|
||||
- -e
|
||||
- "HA_TOKEN={{ homeassistant_mcp_token }}"
|
||||
- HA_TOKEN
|
||||
- "{{ HOMEASSISTANT_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/probe_mcp_token.py') }}"
|
||||
environment:
|
||||
HA_TOKEN: "{{ homeassistant_mcp_token }}"
|
||||
register: homeassistant_token_probe
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
- -e
|
||||
- "HA_USERNAME={{ HOMEASSISTANT_ADMIN_USERNAME }}"
|
||||
- -e
|
||||
- "HA_PASSWORD={{ HOMEASSISTANT_ADMIN_PASSWORD }}"
|
||||
- HA_PASSWORD
|
||||
- -e
|
||||
- "HA_TOKEN_CLIENT_NAME={{ HOMEASSISTANT_TOKEN_CLIENT_NAME }}"
|
||||
- -e
|
||||
- "HA_SERVICE_USERNAME={{ HOMEASSISTANT_MCP_ACCOUNT_USERNAME }}"
|
||||
- -e
|
||||
- "HA_SERVICE_PASSWORD={{ HOMEASSISTANT_MCP_ACCOUNT_PASSWORD }}"
|
||||
- HA_SERVICE_PASSWORD
|
||||
- -e
|
||||
- "HA_SERVICE_NAME={{ HOMEASSISTANT_MCP_ACCOUNT_NAME }}"
|
||||
- -e
|
||||
@@ -24,27 +24,19 @@
|
||||
- "{{ HOMEASSISTANT_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp.py') }}"
|
||||
stdin: "{{ lookup('file', 'python/provision.py') }}"
|
||||
environment:
|
||||
HA_PASSWORD: "{{ HOMEASSISTANT_ADMIN_PASSWORD }}"
|
||||
HA_SERVICE_PASSWORD: "{{ HOMEASSISTANT_MCP_ACCOUNT_PASSWORD }}"
|
||||
register: homeassistant_provision
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: homeassistant_provision.rc == 0 or (HOMEASSISTANT_LOGIN_REFUSED in homeassistant_provision.stderr)
|
||||
failed_when:
|
||||
- homeassistant_provision.rc != 0
|
||||
- HOMEASSISTANT_LOGIN_REFUSED not in homeassistant_provision.stderr
|
||||
until: homeassistant_provision.rc == 0
|
||||
changed_when: "'CHANGED' in homeassistant_provision.stdout"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
delegate_to: "{{ HOMEASSISTANT_HOST_NODE }}"
|
||||
run_once: true
|
||||
|
||||
- name: "🔑 Realign the owner password the hub still holds from its onboarding"
|
||||
ansible.builtin.include_tasks:
|
||||
file: realign_owner.yml
|
||||
apply:
|
||||
delegate_to: "{{ HOMEASSISTANT_HOST_NODE }}"
|
||||
when: HOMEASSISTANT_LOGIN_REFUSED in homeassistant_provision.stderr
|
||||
run_once: true
|
||||
|
||||
- name: "🔧 Take the minted token from the provisioning output"
|
||||
ansible.builtin.set_fact:
|
||||
homeassistant_mcp_token: "{{ homeassistant_provision.stdout_lines | last | trim }}"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
- name: "🔑 Write the rotated owner password into the hub's auth store"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- -i
|
||||
- "{{ HOMEASSISTANT_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- hass
|
||||
- --script
|
||||
- auth
|
||||
- --config
|
||||
- "{{ HOMEASSISTANT_CONFIG_DIR }}"
|
||||
- change_password
|
||||
- "{{ HOMEASSISTANT_ADMIN_USERNAME }}"
|
||||
- "{{ HOMEASSISTANT_ADMIN_PASSWORD }}"
|
||||
changed_when: true
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔄 Restart the hub so it reads the store it no longer has in memory"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- restart
|
||||
- "{{ HOMEASSISTANT_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
changed_when: true
|
||||
|
||||
- name: "🔑 Mint the long-lived token now that the hub accepts the rotated password"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- exec
|
||||
- -i
|
||||
- -e
|
||||
- "HA_PORT={{ HOMEASSISTANT_PORT_INTERNAL }}"
|
||||
- -e
|
||||
- "HA_USERNAME={{ HOMEASSISTANT_ADMIN_USERNAME }}"
|
||||
- -e
|
||||
- "HA_PASSWORD={{ HOMEASSISTANT_ADMIN_PASSWORD }}"
|
||||
- -e
|
||||
- "HA_TOKEN_CLIENT_NAME={{ HOMEASSISTANT_TOKEN_CLIENT_NAME }}"
|
||||
- -e
|
||||
- "HA_SERVICE_USERNAME={{ HOMEASSISTANT_MCP_ACCOUNT_USERNAME }}"
|
||||
- -e
|
||||
- "HA_SERVICE_PASSWORD={{ HOMEASSISTANT_MCP_ACCOUNT_PASSWORD }}"
|
||||
- -e
|
||||
- "HA_SERVICE_NAME={{ HOMEASSISTANT_MCP_ACCOUNT_NAME }}"
|
||||
- -e
|
||||
- "HA_SERVICE_GROUP={{ HOMEASSISTANT_MCP_ACCOUNT_GROUP }}"
|
||||
- "{{ HOMEASSISTANT_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp.py') }}"
|
||||
register: homeassistant_provision
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: homeassistant_provision.rc == 0
|
||||
changed_when: "'CHANGED' in homeassistant_provision.stdout"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
@@ -15,7 +15,7 @@
|
||||
os.environ.update(
|
||||
{name: str(value) for name, value in json.loads({{ HOMEASSISTANT_OWNER_ENVIRONMENT | to_json | to_json }}).items()}
|
||||
)
|
||||
{{ lookup('file', 'python/provision_mcp.py') }}
|
||||
{{ lookup('file', 'python/provision.py') }}
|
||||
register: homeassistant_owner
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -18,9 +18,17 @@ a single organizer chosen deterministically by slug. With no organizer there is
|
||||
nothing to attach it to, and no tool call could name one either, so that case
|
||||
converges to a no-op instead of failing.
|
||||
|
||||
``shell_scoped`` feeds this to IPython, which executes an indented block only
|
||||
once a dedented line follows it. A block that ends the input is left unrun, and
|
||||
the last line of the output is IPython's exit prompt rather than a result, so
|
||||
the final statement is a top-level print of a marker the task matches on.
|
||||
|
||||
Environment:
|
||||
MCP_TEAM: team name to converge.
|
||||
MCP_TOKEN: token value to pin on it.
|
||||
|
||||
Output:
|
||||
PRETIX_RESULT=CHANGED or PRETIX_RESULT=UNCHANGED on its own line.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -35,9 +43,7 @@ TOKEN_NAME = "mcp-upstream" # noqa: S105 a label, not a secret
|
||||
changed = False
|
||||
|
||||
organizer = Organizer.objects.order_by("slug").first()
|
||||
if organizer is None:
|
||||
print("UNCHANGED")
|
||||
else:
|
||||
if organizer is not None:
|
||||
team, created = Team.objects.get_or_create(
|
||||
organizer=organizer,
|
||||
name=TEAM,
|
||||
@@ -58,4 +64,4 @@ else:
|
||||
)
|
||||
changed = changed or created
|
||||
|
||||
print("CHANGED" if changed else "UNCHANGED")
|
||||
print("PRETIX_RESULT=CHANGED" if changed else "PRETIX_RESULT=UNCHANGED")
|
||||
@@ -21,17 +21,19 @@
|
||||
- -e
|
||||
- "MCP_TEAM={{ PRETIX_MCP_UPSTREAM_TEAM }}"
|
||||
- -e
|
||||
- "MCP_TOKEN={{ PRETIX_MCP_UPSTREAM_KEY }}"
|
||||
- MCP_TOKEN
|
||||
- "{{ PRETIX_IDENTITY_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- pretix
|
||||
- shell_scoped
|
||||
- --override
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp_identity.py') }}"
|
||||
environment:
|
||||
MCP_TOKEN: "{{ PRETIX_MCP_UPSTREAM_KEY }}"
|
||||
register: pretix_mcp_identity
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: pretix_mcp_identity.rc == 0
|
||||
changed_when: pretix_mcp_identity.stdout_lines[-1] | default('') == 'CHANGED'
|
||||
until: "pretix_mcp_identity.rc == 0 and (pretix_mcp_identity.stdout is search('(?m)^PRETIX_RESULT=(UN)?CHANGED'))"
|
||||
changed_when: "pretix_mcp_identity.stdout is search('(?m)^PRETIX_RESULT=CHANGED')"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
delegate_to: "{{ PRETIX_IDENTITY_HOST_NODE }}"
|
||||
run_once: true
|
||||
Reference in new issue
Block a user