You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
opensearch: a single-replica cluster loses quorum for good when the operator removes the bootstrap pod #4230
With replicas: 1 a managed OpenSearch cluster has exactly two cluster-manager-eligible nodes: the operator's bootstrap pod and the single node pool member. OpenSearch keeps the voting configuration at an odd size, so with two eligible nodes it holds exactly one, and that one is the node that bootstrapped the cluster: the bootstrap pod.
The operator deletes the bootstrap pod as soon as status.initialized flips, which happens the moment every cluster-manager StatefulSet reports its replicas ready. Nothing checks the voting configuration first. That deletion removes the only voter, and the surviving node can no longer hold an election. There is no path back: changing the voting configuration itself needs a quorum.
The cluster is dead about a minute after it finishes installing, and nothing says so.
Environment
Cozystack: main
opensearch-k8s-operator 2.8.0 (the same code path is unchanged in 3.0.2)
OpenSearch 2.11.1
Reproduced both in the in-tree e2e lane and standalone in Docker
To Reproduce
In a cluster:
Create an OpenSearch release with replicas: 1.
Wait for the node pod to go ready and for the operator to delete <release>-bootstrap-0.
Delete the node pod, or change anything that rolls it. It never comes back ready.
Standalone, no Kubernetes needed, which isolates it from everything Cozystack adds:
docker network create vc-net
fornin osA osB;do
docker run -d --name $n --network vc-net \
-e node.name=$n -e DISABLE_SECURITY_PLUGIN=true \
-e OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m" \
-e cluster.name=vc -e discovery.seed_hosts=osA,osB \
-e cluster.initial_master_nodes=osA -e network.bind_host=0.0.0.0 \
opensearchproject/opensearch:2.11.1
done# wait for both to join, then:
docker exec osA curl -s "localhost:9200/_cluster/state/metadata?filter_path=metadata.cluster_coordination"
docker rm -f osA # what the operator does to the bootstrap pod
docker logs osB | tail # osB can no longer elect a cluster-manager
osA stands in for the bootstrap pod, osB for the single node pool member, and the env vars are the ones the operator sets on both (pkg/builders/cluster.go, the node StatefulSet at the cluster.initial_master_nodes entry and NewBootstrapPod).
Expected behaviour
Removing the bootstrap pod leaves a cluster that can still elect a cluster-manager.
Actual behaviour
The voting configuration names only the bootstrap node, so removing it strands the survivor:
A-0DkgYIR-WxgFmv4oQtrg is osA, the bootstrapper. osB joined the cluster and is cluster-manager-eligible, and it is still not a voter.
Logs
From the e2e run, the node pod after the bootstrap pod was deleted:
[WARN ][o.o.c.c.ClusterFormationFailureHelper] [opensearch-test-nodes-0] cluster-manager not discovered or elected yet,
an election requires a node with id [XJOEIRauRceKqYSbEdvGoQ],
have discovered [{opensearch-test-nodes-0}{MqEXb6CkQW-sPtQYWayKiA}...] which is not a quorum
The standalone reproduction prints the same sentence, with its own ids:
[WARN ][o.o.c.c.ClusterFormationFailureHelper] [osB] cluster-manager not discovered or elected yet,
an election requires a node with id [A-0DkgYIR-WxgFmv4oQtrg],
have discovered [{osB}{govFgV6FTGSntR5qsd_Mdg}...]
Timeline from the e2e run, which is where the ordering is visible:
Time
Event
19:47:30
bootstrap-0 and nodes-0 start
19:50:01
the securityconfig job completes
19:50:03
nodes-0 is ready, status.initialized flips
19:50:04
the operator deletes bootstrap-0
19:50:36
nodes-0 is recreated for an unrelated reason
19:51:16
and from here on it cannot elect a cluster-manager
Additional context
It is invisible until something restarts the node. A node with no cluster-manager still answers GET / with 200, which is what the operator's readiness probe asks it. The security plugin already has its configuration in memory, so the probe keeps passing and the pod stays Ready over a cluster that can no longer serve a single cluster-state request. Only after a restart does the node fail to load its security configuration, start answering the probe with an error, and reveal the state it has been in since the bootstrap pod went away.
Why this is only surfacing now. Until #4228 the securityconfig job was OOMKilled, status.initialized never flipped, and the bootstrap pod therefore lived forever. The suite failed earlier for that reason and never reached this. Fixing the OOM lets initialization complete, which is what triggers the deletion.
Why replicas: 1 specifically. The count of cluster-manager-eligible nodes is the bootstrap pod plus the node pool members carrying the role. At replicas: 1 that is 2, so the voting configuration holds 1, and it is the bootstrap pod. At replicas: 2 it is 3, the voting configuration holds 3, and losing the bootstrap pod leaves 2 of 3, which is still a quorum. So replicas: 1 is the broken case and the chart's own default of 3 is not affected.
Upstream already knows.opensearch-k8s-operator#1448 is open, labelled bug and release-blocker, and its third point describes this exact case: with a 1-master pool the bootstrap pod is the sole voter, and deleting it strands the survivor. Nothing there needs filing from us, and the analysis above was reached independently of it, which is some comfort that both are right.
What is in flight there, and why I would not wait for it:
#1483 adds a voting-config exclusion before the bootstrap pod is deleted, with the comment "so small master pools do not lose quorum" and a test for the POST-delete-DELETE sequence. It is the fix for this. Last touched 2026-09-11, and currently carries changes-requested from a maintainer, a merge conflict, and a red golangci lint.
#1150 approaches it from the other side: for a single node it creates no bootstrap pod at all, sets discovery.type: single-node with an empty cluster.initial_master_nodes, and drops --fail from the node probe. Open since 2025-10-29, untouched since 2026-07-07.
#1430 removes the bootstrap pod entirely. Untouched since 2026-07-07.
#1504 is merged, but it keeps the bootstrap pod alive longer rather than making its removal safe, so it does not cover this.
What we could do here. The choice looks like a product decision rather than a mechanical one, so I am listing rather than recommending:
Refuse replicas: 1 for OpenSearch in values.schema.json. It fixes the product by removing the configuration that cannot work, and it is a breaking change for anyone running one replica today, who is in any case running a cluster that died shortly after it was installed. The restriction can come back off once the operator carries the upstream fix.
Move the e2e suite to a replica count that survives the teardown. On its own this only hides the defect, so it is worth doing only alongside the point above, and with a comment naming this issue and Feat/webhook workload monitors #1448.
Wait for upstream and bump the operator. Given the state of the three PRs above, that is not a date anyone can put in a plan.
CI evidence for this, from run 34776442115, which carries #4228.
With the securityconfig job no longer dying, the suite gets past the first-boot wait and fails at the last step instead. The job pod is exitCode: 0, PodCompleted. There is no opensearch-test-bootstrap-0 in the cluster snapshot at all, and the recreated opensearch-test-nodes-0 logs:
cluster-manager not discovered or elected yet, an election requires a node with id [...] which is not a quorum
That also puts #4227 in front of this one rather than beside it. Status.Initialized comes from AllMastersReady in controllers/opensearchController.go:244, and pkg/reconcilers/cluster.go:100-103 moves the bootstrap pod to StateAbsent once that flips, so the node going ready is the event that removes the only voter. While the job was being OOMKilled the node never went ready, so this could not fire.
The chainsaw fixture is replicas: 1, so the opensearch suite stays red after #4228 merges. This is the issue whose fix turns it green.
Describe the bug
With
replicas: 1a managed OpenSearch cluster has exactly two cluster-manager-eligible nodes: the operator's bootstrap pod and the single node pool member. OpenSearch keeps the voting configuration at an odd size, so with two eligible nodes it holds exactly one, and that one is the node that bootstrapped the cluster: the bootstrap pod.The operator deletes the bootstrap pod as soon as
status.initializedflips, which happens the moment every cluster-manager StatefulSet reports its replicas ready. Nothing checks the voting configuration first. That deletion removes the only voter, and the surviving node can no longer hold an election. There is no path back: changing the voting configuration itself needs a quorum.The cluster is dead about a minute after it finishes installing, and nothing says so.
Environment
mainTo Reproduce
In a cluster:
replicas: 1.<release>-bootstrap-0.kubectl -n <ns> exec <release>-nodes-0 -- curl -sk https://localhost:9200/_cluster/healthhangs.Standalone, no Kubernetes needed, which isolates it from everything Cozystack adds:
osAstands in for the bootstrap pod,osBfor the single node pool member, and the env vars are the ones the operator sets on both (pkg/builders/cluster.go, the node StatefulSet at thecluster.initial_master_nodesentry andNewBootstrapPod).Expected behaviour
Removing the bootstrap pod leaves a cluster that can still elect a cluster-manager.
Actual behaviour
The voting configuration names only the bootstrap node, so removing it strands the survivor:
{"metadata": {"cluster_coordination": { "term": 1, "last_committed_config": ["A-0DkgYIR-WxgFmv4oQtrg"], "last_accepted_config": ["A-0DkgYIR-WxgFmv4oQtrg"], "voting_config_exclusions": [] }}}A-0DkgYIR-WxgFmv4oQtrgisosA, the bootstrapper.osBjoined the cluster and is cluster-manager-eligible, and it is still not a voter.Logs
From the e2e run, the node pod after the bootstrap pod was deleted:
The standalone reproduction prints the same sentence, with its own ids:
Timeline from the e2e run, which is where the ordering is visible:
bootstrap-0andnodes-0startnodes-0is ready,status.initializedflipsbootstrap-0nodes-0is recreated for an unrelated reasonAdditional context
It is invisible until something restarts the node. A node with no cluster-manager still answers
GET /with 200, which is what the operator's readiness probe asks it. The security plugin already has its configuration in memory, so the probe keeps passing and the pod stays Ready over a cluster that can no longer serve a single cluster-state request. Only after a restart does the node fail to load its security configuration, start answering the probe with an error, and reveal the state it has been in since the bootstrap pod went away.Why this is only surfacing now. Until #4228 the securityconfig job was OOMKilled,
status.initializednever flipped, and the bootstrap pod therefore lived forever. The suite failed earlier for that reason and never reached this. Fixing the OOM lets initialization complete, which is what triggers the deletion.Why
replicas: 1specifically. The count of cluster-manager-eligible nodes is the bootstrap pod plus the node pool members carrying the role. Atreplicas: 1that is 2, so the voting configuration holds 1, and it is the bootstrap pod. Atreplicas: 2it is 3, the voting configuration holds 3, and losing the bootstrap pod leaves 2 of 3, which is still a quorum. Soreplicas: 1is the broken case and the chart's own default of 3 is not affected.Upstream already knows. opensearch-k8s-operator#1448 is open, labelled
bugandrelease-blocker, and its third point describes this exact case: with a 1-master pool the bootstrap pod is the sole voter, and deleting it strands the survivor. Nothing there needs filing from us, and the analysis above was reached independently of it, which is some comfort that both are right.What is in flight there, and why I would not wait for it:
golangci lint.discovery.type: single-nodewith an emptycluster.initial_master_nodes, and drops--failfrom the node probe. Open since 2025-10-29, untouched since 2026-07-07.What we could do here. The choice looks like a product decision rather than a mechanical one, so I am listing rather than recommending:
replicas: 1for OpenSearch invalues.schema.json. It fixes the product by removing the configuration that cannot work, and it is a breaking change for anyone running one replica today, who is in any case running a cluster that died shortly after it was installed. The restriction can come back off once the operator carries the upstream fix.Checklist