fix(garage): make bootstrap & provision idempotent across reruns

* bootstrap: `garage layout show` truncates node IDs to 16 chars, but
  the membership check compared against the full hex. After the first
  successful join, subsequent runs no longer found the short ID in
  `layout show` and re-issued `layout assign`, marking the task
  changed every time. Compare against both the truncated and the full
  form so a configured node stays detected. Also tag the read-only
  `garage node id` / `layout show` probes with `changed_when: false`.

* provision keys: the old parser sliced `stdout_lines[1:]` to drop the
  header but missed that INFO log lines and ANSI escapes can interleave
  with table rows. Replace with an explicit `^GK[0-9a-fA-F]+` filter
  after stripping ANSI, so probe-output noise no longer corrupts the
  existing-keys set and triggers spurious `key new` calls.

* provision buckets: same class of fix — match `^[0-9a-f]{16}\s` data
  rows instead of slicing `[2:]`, which broke when the table header
  wasn't exactly two lines.

* provision permissions: pre-read `bucket info` for each (key, bucket)
  pair and only run `bucket allow` when the current `RWO` flag set for
  that key ID doesn't already match the desired permissions. Previously
  `bucket allow` ran unconditionally and reported changed every play.

* `changed_when: false` on all read-only probes (`key list`, `key info`,
  `bucket list`).
This commit is contained in:
Simon Bärlocher 2026-05-26 14:03:58 +02:00
parent c27584cd9c
commit 1157448d59
No known key found for this signature in database
GPG key ID: 63DE20495932047A
2 changed files with 55 additions and 6 deletions

View file

@ -7,21 +7,27 @@
container: "{{ garage_service_name }}"
command: /garage node id -q
register: _garage_node_id
changed_when: false
- name: Extract short node ID
ansible.builtin.set_fact:
_garage_node_id_short: "{{ _garage_node_id.stdout.split('@')[0] }}"
- name: Extract truncated node ID (first 16 chars, matches `garage layout show` output)
ansible.builtin.set_fact:
_garage_node_id_truncated: "{{ _garage_node_id_short[:16] }}"
- name: Check if node layout is configured
community.docker.docker_container_exec:
container: "{{ garage_service_name }}"
command: /garage layout show
register: _garage_layout_show
failed_when: false
changed_when: false
- name: Check if node is in layout
ansible.builtin.set_fact:
_node_in_layout: "{{ _garage_node_id_short in _garage_layout_show.stdout }}"
_node_in_layout: "{{ (_garage_node_id_truncated in _garage_layout_show.stdout) or (_garage_node_id_short in _garage_layout_show.stdout) }}"
- name: Configure garage node layout
community.docker.docker_container_exec:

View file

@ -4,11 +4,17 @@
container: "{{ garage_service_name }}"
command: /garage key list
register: _existing_keys_output
changed_when: false
when: garage_s3_keys | length > 0
- name: Parse existing key names
ansible.builtin.set_fact:
_existing_keys: "{{ _existing_keys_output.stdout_lines[1:] | select('match', '^GK') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+)\\s+.*$', '\\1') | list }}"
# `garage key list` columns: ID Created Name Expiration.
# Data rows begin with a GK<hex> key ID; header is "ID Created ..."
# and INFO log lines may interleave on stderr (kept separate by
# docker_container_exec). Strip ANSI escapes defensively, filter to
# GK-prefixed rows, then take the 3rd whitespace-separated field.
_existing_keys: "{{ _existing_keys_output.stdout_lines | map('regex_replace', '\\x1b\\[[0-9;]*m', '') | select('match', '^GK[0-9a-fA-F]+') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1') | list }}"
when: garage_s3_keys | length > 0
- name: Create S3 keys
@ -27,6 +33,7 @@
command: /garage key info {{ item.name }}
loop: "{{ garage_s3_keys }}"
register: _key_info_results
changed_when: false
when: garage_s3_keys | length > 0
- name: Extract key IDs from info
@ -42,11 +49,21 @@
container: "{{ garage_service_name }}"
command: /garage bucket list
register: _existing_buckets_output
changed_when: false
when: garage_s3_keys | length > 0
- name: Parse existing bucket names
ansible.builtin.set_fact:
_existing_buckets: "{{ _existing_buckets_output.stdout_lines[2:] | map('split') | map('first') | list }}"
# `garage bucket list` columns: ID Created Global aliases Local aliases
# Data rows start with a hex bucket ID; filter to those and take the
# third whitespace-separated field (the global alias = bucket name).
_existing_buckets: >-
{{
_existing_buckets_output.stdout_lines
| select('match', '^[0-9a-f]{16}\\s')
| map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1')
| list
}}
when: garage_s3_keys | length > 0
- name: Get unique bucket names
@ -64,12 +81,37 @@
- item not in _existing_buckets
failed_when: false
- name: Get current bucket permissions
community.docker.docker_container_exec:
container: "{{ garage_service_name }}"
command: /garage bucket info {{ item.1.name }}
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
loop_control:
label: "{{ item.1.name }}"
register: _bucket_info_results
changed_when: false
when: garage_s3_keys | length > 0
- name: Set bucket permissions using key IDs
community.docker.docker_container_exec:
container: "{{ garage_service_name }}"
command: /garage bucket allow {{ item.1.name }} {% for perm in item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.0.name] }}
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
when: garage_s3_keys | length > 0
command: /garage bucket allow {{ item.item.1.name }} {% for perm in item.item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.item.0.name] }}
loop: "{{ _bucket_info_results.results }}"
loop_control:
label: "{{ item.item.1.name }} -> {{ item.item.0.name }}"
when:
- garage_s3_keys | length > 0
- >-
(item.stdout | regex_search(
'(?m)^\s*' ~ _wanted_flags ~ '\s+' ~ _key_id_map[item.item.0.name]
)) is none
vars:
_wanted_flags: >-
{{
('R' if 'read' in item.item.1.permissions else '-')
~ ('W' if 'write' in item.item.1.permissions else '-')
~ ('O' if 'owner' in item.item.1.permissions else '-')
}}
# Export key credentials for use by other roles
- name: Get detailed key information for all keys
@ -78,6 +120,7 @@
command: /garage key info {{ item.name }} --show-secret
loop: "{{ garage_s3_keys }}"
register: _key_details_results
changed_when: false
when: garage_s3_keys | length > 0
- name: Build garage S3 credentials map