Skip to content

Commit 92d2eed

Browse files
authored
test(opensearch): separate the mount-plan rewrite from the roll onto it (#4279)
## What this is `Refs #4259`. A diagnosis of the opensearch suite timeout plus the one in-repo change it warrants: making the failing step say which of its two candidates actually happened. It does **not** claim to fix the root cause — see "What this does not do". ## What the run actually showed The `verify-statefulset-rolled-onto-the-new-mount-plan` assert erred with exactly two field failures, both under `status`: `(currentRevision == updateRevision): false` and `updatedReplicas: field not found`. No volume field appears in the error list, and `readyReplicas: 1` shows as an unchanged line, so the pod was Ready at the deadline and nothing had replaced it. Per e2e-testing.md ("Reading a failure diff: the actual side is a projection"), the real cause is the error line, not the diff — and a Chainsaw diff projects the live object onto the expected fields, so the three `http-ca` / `http-cert` / `http-key` lines showing in the diff are the expected tree, not proof the volumes are absent. Read that way, the volume assertions matched: the three volumes were present on the live template. The "volumes missing" reading is the projection trap the doc warns against. ## Which candidate Candidate (b): the template was rewritten onto the subPath mount plan, and the operator has not restarted the pod onto it. This is consistent with the source, traced against opensearch-k8s-operator v2.8.0 as vendored: - The chart renders the CR correctly — `generate` dropped, `secret` and `caSecret` named — proven by `packages/apps/opensearch/tests/tls_upgrade_test.yaml`. - Given that CR, `pkg/reconcilers/tls.go` `handleHttp` appends exactly the three volumes the step asserts (`http-ca` from `opensearch-test.http-ca`, `http-key` and `http-cert` from `opensearch-test-http-server`), and `pkg/reconcilers/cluster.go` `reconcileNodeStatefulSet` writes that template onto the StatefulSet. Three branches return ahead of that write — no node-pool config hash, the `opensearch.role` recreate, and parallel recovery — but the parallel-recovery gate is `ReadyReplicas < replicas-1`, unreachable at one replica, so the write happens. The reconcilers that run ahead of the cluster reconciler do not stop it either: the TLS reconciler runs first and returns the admin-certificate path's result, empty in the ordinary case, and `securityconfig.Reconcile` short-circuits to nil when the security-config checksum is unchanged, which the move does not change. So the operator does write the volumes. - The nodePool StatefulSet is `OnDelete` (`pkg/builders/cluster.go` sets `UpdateStrategy.Type: OnDelete` alongside `PodManagementPolicy: OrderedReady`), so the StatefulSet controller never deletes the pod to apply a template change: `podManagementPolicy` orders creation and deletion during scaling, `updateStrategy` decides the update. The roll is the operator's rolling-restart reconciler's to drive (`pkg/reconcilers/rollingRestart.go`) — it deletes the pod itself. It needs `status.Initialized`, it needs `ReadyReplicas == replicas` (a Ready pod is a precondition, not the blocker), then it opens a client to the OpenSearch API and calls `CheckClusterStatusForRestart`, which holds the restart while the cluster is red; on yellow the gate now enables shard allocation, waits for every data node to join, waits for shard activity to settle, then proceeds (#4317 deleted the old `.opensearch-observability` carve-out, and this suite builds the operator with that patch). Any of those gates can sit indefinitely. `status.currentRevision` never advances by itself under `OnDelete`; the operator patches it once `updatedReplicas == replicas`, so the second assert's own condition cannot go true unless that reconciler ran to the end. - The pod's startup probe is a TCP check on 9200 (`builders/cluster.go`), so "connection refused" is OpenSearch not listening, not a probe-scheme mismatch. The plaintext `GET / HTTP/1.1` from `curl/8.5.0` on the TLS listener is neither the startup/liveness probe (TCP) nor the readiness probe (`curl -k https://localhost:9200`, localhost, HTTPS) — it is external traffic to the LoadBalancer address, a red herring for the stall. ## What this changes The step now runs two asserts instead of one. The first waits for the operator to rewrite the template onto the three volumes, on a reconcile-scale budget (6m) — a failure here is the operator not writing them (candidate a). The second waits for the roll to complete onto that template, on the pod-lifetime budget (12m) — a failure here with the first green is the roll not completing (candidate b): the operator never restarting the pod, which the rolling-restart reconciler holds while `CheckClusterStatusForRestart` reports the cluster red — on yellow the gate enables shard allocation, waits for every data node to join, waits for shard activity to settle, then proceeds — or a replacement that was deleted but never came Ready. The second assert carries the three volume projections and `observedGeneration == metadata.generation` as well, so the roll is pinned to the template the first assert checked rather than to whatever revision is current when the status converges. The suite `catch` also captures the StatefulSet as YAML and as a describe, and the operator's own log, because `updateRevision`, `currentRevision`, `generation` and `updatedReplicas` are what separate the two halves — describe alone omits them — and the restart decision is logged in the operator, not in the cluster pods. The reconciler logs `Starting rolling restart of the StatefulSet` (`rollingRestart.go:155`) once its early gates (`Initialized`, ready-replica, update-pending) pass; it builds its OpenSearch client just before, at `:138`, and that client pings the cluster — so a run whose log lacks `Starting rolling restart` either had an earlier gate hold it silently or had the client ping fail loudly, not only the former, while that line present followed by a `Couldn't proceed with rolling restart ... because <reason>` line names a health-stage hold (the health gate is `CheckClusterStatusForRestart`, and it is not the last: `PreparePodForDelete` follows it). The operator-log `tail` was raised so it spans the whole assert window rather than a third of it. ## What this does not do It does not fix the stall; #4317 does, and is already merged. The run that captured this (`Run E2E tests`, job 104576486900) shows the rolling-restart reconciler holding on health: the manager logged `Starting rolling restart of the StatefulSet opensearch-test-nodes` (`rollingRestart.go:155`, printed only after the `Initialized` and ready-replica gates passed) and then, every ten seconds to the deadline, `Couldn't proceed with rolling restart ... waiting for health to be green` (`os_data_service.go:180`), which is reached only after `GetFlatClusterSettings` returned 2xx (`os_client.go:224` makes anything else an error). So the operator reached the cluster and the cluster answered; health held the restart. #4259 established why, and the CA move plays no part: on a single data node the bootstrap node leaving is audited into a fresh `security-auditlog-*` index with one replica, that replica has nowhere to go, the cluster stays yellow for good, and the v2.8.0 restart gate opens only on green (or on a yellow `.opensearch-observability`) so it never opens. #4317 backports opensearch-project/opensearch-k8s-operator#1569 to fix that gate; this PR is only the diagnostic split that names which half of the roll a failure is. The split content itself is not the failure: on `49f3f059f` both asserts passed, the second after 1m47s, and this change touches only the step's comments and the operator-log tail, not the assertions. With this change in place, a future occurrence names the half, and the catch carries the roll's own fields and the operator log, so the crust-gather can be read against a specific claim rather than an ambiguous one. ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved OpenSearch StatefulSet failure diagnostics by capturing YAML, detailed resource descriptions, and the latest operator pod logs. * Expanded rolling-restart verification to check volume projections, template updates, rollout readiness, observed and metadata generations, and revision consistency. * Separated template validation from rollout completion checks with dedicated six- and twelve-minute time limits. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents e32353e + 36bbc62 commit 92d2eed

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

‎hack/e2e-chainsaw/opensearch/chainsaw-test.yaml‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ spec:
3232
apiVersion: opensearch.opster.io/v1
3333
kind: OpenSearchCluster
3434
name: opensearch-test
35+
# The roll's own fields — updateRevision, currentRevision, generation,
36+
# updatedReplicas — are what separate a template that was never rewritten from
37+
# one the operator has not restarted the pod onto (#4259). The nodePool
38+
# StatefulSet is OnDelete (opensearch-k8s-operator v2.8.0, builders/cluster.go
39+
# sets UpdateStrategy.Type OnDelete), so the roll is the operator's
40+
# rolling-restart reconciler's to drive — it deletes the pod itself — not the
41+
# StatefulSet controller's. describe omits those fields, so the StatefulSet is
42+
# captured both ways: get -o yaml carries the revisions and the rendered volume
43+
# list, describe carries the pod template and the controller's events.
44+
- get:
45+
apiVersion: apps/v1
46+
kind: StatefulSet
47+
format: yaml
48+
- describe:
49+
apiVersion: apps/v1
50+
kind: StatefulSet
51+
name: opensearch-test-nodes
3552
# cert-manager records a refusal to issue in the Certificate's conditions.
3653
# Without this an unissued chain shows up only as an absent Secret, and the
3754
# CR then names a Secret that is never going to appear.
@@ -46,6 +63,37 @@ spec:
4663
- podLogs:
4764
selector: opster.io/opensearch-cluster=opensearch-test
4865
tail: 100
66+
# The restart is the operator's rolling-restart reconciler's to start (see the
67+
# StatefulSet catch note above). It logs "Starting rolling restart of the
68+
# StatefulSet" at rollingRestart.go:155 (opensearch-k8s-operator v2.8.0) once
69+
# the early gates pass — cluster Initialized (:123), every replica ready (:94),
70+
# an update actually pending (:150-154). Just before that line, at :138, it
71+
# builds its OpenSearch client, which pings the cluster
72+
# (NewOsClusterClientFromConfig, os_client.go:104-112) and returns the error
73+
# from the reconcile at :139-141 when the cluster is unreachable. So the line's
74+
# absence reads two ways, not one: an early gate held silently, or that client
75+
# ping failed — loudly, as a reconcile error — before the line was reached. The
76+
# line present and followed by "Couldn't proceed with rolling restart ...
77+
# because <reason>" (:182) means the roll reached the health/pod-delete stage,
78+
# where CheckClusterStatusForRestart (:177) holds it until cluster health is
79+
# green: a yellow cluster no longer holds under an exception — it enables shard
80+
# allocation, waits for every data node to join, waits for shard activity to
81+
# settle, then proceeds — so only red reaches the health hold, and <reason>
82+
# names it "waiting for health to be green" for the mount-plan incident.
83+
# That "Couldn't proceed ... because" line is the health hold's signal, one per
84+
# ~10s requeue and what the tail below is sized for, and
85+
# CheckClusterStatusForRestart is not the last gate: PreparePodForDelete (:195)
86+
# follows it. The verdict is in the operator, not the cluster pods above, and it
87+
# runs in its own namespace.
88+
- podLogs:
89+
namespace: cozy-opensearch-operator
90+
selector: control-plane=controller-manager
91+
# tail spans the whole assert window. The manager logs ~4-5 lines per ~10s
92+
# requeue, not one, so 100 lines reach back only about a third of a 12m roll
93+
# and can lose a one-shot cause. 2000 is effectively everything the container
94+
# has logged, not a sized window — on run 105251129282 it returned all 457
95+
# lines since start.
96+
tail: 2000
4997
# The external Service is a LoadBalancer, so an address pool with nothing left
5098
# in it holds the HelmRelease short of Ready with nothing in the HelmRelease
5199
# itself naming the cause.
@@ -291,13 +339,54 @@ spec:
291339
# same reason — the replacement pod pulls and starts OpenSearch from scratch.
292340
- name: verify-statefulset-rolled-onto-the-new-mount-plan
293341
try:
342+
# Two asserts, not one, so a timeout names which half did not happen: the
343+
# template rewrite and the roll onto it are separate outcomes with separate
344+
# owners (#4259).
345+
#
346+
# First half: the operator rewrote the template onto the subPath mount plan.
347+
# That is the operator's to do the moment the CR names the cert-manager
348+
# Secrets, which verify-cr-dropped-generate and the certificate steps already
349+
# waited for, so it lands within a reconcile or two rather than a pod
350+
# lifetime. A shorter budget than the roll below, and a failure here is the
351+
# operator not writing the volumes — not a pod that will not start.
352+
- assert:
353+
timeout: 6m
354+
resource:
355+
apiVersion: apps/v1
356+
kind: StatefulSet
357+
metadata:
358+
name: opensearch-test-nodes
359+
(spec.template.spec.volumes[?name == 'http-cert'].secret.secretName):
360+
- opensearch-test-http-server
361+
(spec.template.spec.volumes[?name == 'http-key'].secret.secretName):
362+
- opensearch-test-http-server
363+
(spec.template.spec.volumes[?name == 'http-ca'].secret.secretName):
364+
- opensearch-test.http-ca
365+
# Second half: the operator restarted the pod onto that template and the
366+
# StatefulSet finished rolling. currentRevision only advances here once the
367+
# operator's rolling-restart reconciler patches it (see the StatefulSet catch
368+
# note above), so equal revisions plus a ready updated replica is that
369+
# reconciler having run to the end. This half also asserts readyReplicas, so a
370+
# failure with the assert above green is the roll not completing — either the
371+
# operator never restarted the pod (the reconciler holds on the health gate,
372+
# per the podLogs note) or a replacement was deleted but never came Ready — not
373+
# a template that was never written.
374+
#
375+
# The three volume projections are carried in here too, so the roll is pinned
376+
# to the template the first half checked and not to whatever revision happens
377+
# to be current when the status converges. The CR moves only
378+
# once in this suite, but the split otherwise lets a rewrite landing after the
379+
# first assert satisfy this half on its own. observedGeneration == generation
380+
# is compared at the resource root because a status-scoped expression cannot
381+
# reach metadata from its own node.
294382
- assert:
295383
timeout: 12m
296384
resource:
297385
apiVersion: apps/v1
298386
kind: StatefulSet
299387
metadata:
300388
name: opensearch-test-nodes
389+
(status.observedGeneration == metadata.generation): true
301390
(spec.template.spec.volumes[?name == 'http-cert'].secret.secretName):
302391
- opensearch-test-http-server
303392
(spec.template.spec.volumes[?name == 'http-key'].secret.secretName):

0 commit comments

Comments
 (0)