From 7bbfc7fc457a8f3d2d427fcd738b3943626ee632 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Thu, 7 May 2026 21:15:34 +0200 Subject: [PATCH] fix(elasticsearch): skip bootstrap-only checks when joining existing cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `not elasticsearch_cluster_set_up | bool` to the four bootstrap-password tasks in elasticsearch-security.yml plus the master-count check in main.yml, so a joining node skips the checks that only make sense for fresh-bootstrap. Reopens issue #148 after the previous attempt (#149, reverted in #150) regressed fresh-install scenarios. The previous attempt put the gate on the whole "Runtime cluster setup" block. That block contains an inner `set_fact` that flips elasticsearch_cluster_set_up to true after the initial passwords file is written. Ansible re-evaluates block-level whens per task against the live variable, so every inner task after that flip silently skipped, including the fetch that sets elasticstack_password. Downstream tasks then saw it undefined and either fell back to a hardcoded path that doesn't exist on custom-path scenarios, or failed outright. Gating per task avoids that — each task's when is independent and doesn't get re-evaluated against later state. The four gated tasks all already had `not elasticsearch_passwords_file.stat.exists | bool` in their when, so they're a clean place for the additional guard. The master-count check in main.yml was uncontroversial in #149 — it sits before the security tasks run and the inner flip never reaches it — so it returns unchanged. No molecule scenario covers the join-existing-cluster path: it would need a multi-node setup where the joining node sees the passwords file as absent (stat delegated to the CA host), which the existing single-node scenarios can't reproduce. Existing scenarios still validate the fresh-bootstrap path and would catch a regression like the one #149 introduced. --- .../tasks/elasticsearch-security.yml | 16 ++++++++++++++++ roles/elasticsearch/tasks/main.yml | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/roles/elasticsearch/tasks/elasticsearch-security.yml b/roles/elasticsearch/tasks/elasticsearch-security.yml index 9ec3f83..5363751 100644 --- a/roles/elasticsearch/tasks/elasticsearch-security.yml +++ b/roles/elasticsearch/tasks/elasticsearch-security.yml @@ -650,6 +650,18 @@ - name: elasticsearch-security | Runtime cluster setup (requires running Elasticsearch) when: not ansible_check_mode block: + # Bootstrap-password tasks below are gated on + # `elasticsearch_cluster_set_up` so that a node joining an existing + # cluster (where the user signals `elasticsearch_cluster_set_up: true`) + # skips bootstrap-only checks. The bootstrap password is no longer + # accepted on an established cluster — replicated cluster state has + # replaced it — so running these checks on a joining node fails with + # "Bootstrap password authentication failed and this is not the CA + # host". See issue #148. Gated per task rather than at the block + # level because a later task in this block flips + # `elasticsearch_cluster_set_up` to true and Ansible re-evaluates + # block-level whens against the live variable, which would silently + # skip everything after that flip. - name: elasticsearch-security | Check for API with bootstrap password ansible.builtin.uri: url: "{{ elasticsearch_http_protocol }}://{{ elasticsearch_api_host }}:{{ elasticstack_elasticsearch_http_port }}" @@ -662,6 +674,7 @@ no_log: "{{ elasticstack_no_log }}" when: - not elasticsearch_passwords_file.stat.exists | bool + - not elasticsearch_cluster_set_up | bool until: (elasticsearch_api_status_bootstrap.json | default({})).cluster_name is defined retries: 30 delay: 10 @@ -675,6 +688,7 @@ - not elasticsearch_passwords_file.stat.exists | bool - elasticsearch_api_status_bootstrap is failed - inventory_hostname == elasticstack_ca_host + - not elasticsearch_cluster_set_up | bool block: - name: elasticsearch-security | Reset elastic user password via elasticsearch-reset-password ansible.builtin.command: > @@ -727,6 +741,7 @@ - not elasticsearch_passwords_file.stat.exists | bool - elasticsearch_api_status_bootstrap is failed - inventory_hostname != elasticstack_ca_host + - not elasticsearch_cluster_set_up | bool # We need this check twice. One to wait for the API to be actually available. And a second time to # check the actual return code. Should not cause a huge delay. @@ -743,6 +758,7 @@ no_log: "{{ elasticstack_no_log }}" when: - not elasticsearch_passwords_file.stat.exists | bool + - not elasticsearch_cluster_set_up | bool until: (elasticsearch_cluster_status_bootstrap.json | default({})).status | default('') in ["green", "yellow"] retries: 30 delay: 10 diff --git a/roles/elasticsearch/tasks/main.yml b/roles/elasticsearch/tasks/main.yml index a59ba41..8339e91 100644 --- a/roles/elasticsearch/tasks/main.yml +++ b/roles/elasticsearch/tasks/main.yml @@ -125,11 +125,17 @@ elasticsearch_count_of_master_nodes: >- {{ ansible_play_hosts_all | intersect(groups['elasticsearch_role_master'] | default([])) | length }} + # The odd-master-count rule is required for a fresh-bootstrapped cluster + # to have a stable initial quorum. When joining an existing cluster + # (elasticsearch_cluster_set_up: true) the cluster already has its own + # master-eligible quorum independent of how many new nodes are added in + # this play, so the check is irrelevant and would block legitimate joins. - name: Check count of master nodes ansible.builtin.fail: msg: "There must be an odd count of master nodes. You have {{ elasticsearch_count_of_master_nodes }}" when: - elasticsearch_count_of_master_nodes | int % 2 == 0 + - not elasticsearch_cluster_set_up | bool - name: End play in checks ansible.builtin.meta: end_host