Skip to content

fix(opensearch-operator): gate leader election on replica count - #3040

Merged
IvanHunters merged 1 commit into
mainfrom
ray/fix-opensearch-leader-election
Jun 25, 2026
Merged

fix(opensearch-operator): gate leader election on replica count#3040
IvanHunters merged 1 commit into
mainfrom
ray/fix-opensearch-leader-election

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Problem

opensearch-operator installs intermittently fail with Helm reporting the controller-manager Deployment status: 'Failed'. The real driver is the manager container (operator-controller-manager) crashlooping on leader election lost: controller-runtime calls os.Exit(1) when it misses a lease-renewal deadline under the apiserver/etcd latency that install-time load creates. The Deployment runs a single replica (replicas: 1) yet still passes --leader-elect, so leader election adds no availability — its only effect is to let a transient control-plane blip self-terminate the sole manager. The kube-rbac-proxy BackOff seen alongside is collateral restart-window noise, not the cause: on a long-running cluster the proxy shows restartCount: 0 while the manager has restarted repeatedly with exitCode: 1, reason: Error.

Fix

Gate --leader-elect on the replica count instead of passing it unconditionally. A new manager.replicaCount value (default 1) drives both spec.replicas and the flag: at one replica the manager runs without leader election and rides out control-plane latency instead of crashlooping; setting replicaCount > 1 re-enables --leader-elect automatically so a genuine HA deployment still coordinates. Because both derive from one value, there is no window where multiple managers run without a lease.

The same single-replica path also sets the Deployment strategy to Recreate. With leader election off, a default RollingUpdate would briefly run two managers (maxSurge) during an operator upgrade, both reconciling without lease coordination; Recreate closes that window at zero availability cost on one replica. Installs scaled past one replica keep RollingUpdate.

The arg is hardcoded in the vendored opensearch-operator 2.8.0 chart, so the change ships as packages/system/opensearch-operator/patches/leaderElection.diff, re-applied by the package make update target after helm pull --untar. This mirrors the existing fluxcd-operator patch mechanism so the customization survives a chart refresh. A helm-unittest suite pins both states: the default render drops --leader-elect and sets strategy: Recreate; a replicaCount: 2 render restores --leader-elect and leaves the strategy at the RollingUpdate default.

Screenshots

Not applicable — no UI change.

Release note

fix(opensearch-operator): disable leader election on the single-replica operator Deployment so a transient apiserver/etcd latency spike no longer crashloops the manager at install time; leader election re-enables automatically when scaled past one replica

Summary by CodeRabbit

  • New Features

    • Added configurable replica count for the operator manager, with a default of 1.
    • Single-replica deployments now use a safer rollout strategy and skip leader election.
    • Multi-replica deployments automatically enable leader election.
  • Tests

    • Added coverage for single- and multi-replica deployment behavior.
  • Documentation

    • Updated configuration notes to explain replica count behavior and related deployment changes.

The operator Deployment ships a single replica but always passes
--leader-elect to the manager. On one replica leader election adds no HA
value: a transient apiserver/etcd lease-renewal blip makes
controller-runtime self-terminate (os.Exit on "leader election lost"),
which crashloops the manager and surfaces at install time as a
Deployment stuck in Failed.

Gate --leader-elect on the replica count via a manager.replicaCount
value (default 1). The single-replica install now rides out
control-plane latency instead of crashlooping, and scaling to more than
one replica re-enables leader election automatically for genuine HA.

Also set the Deployment strategy to Recreate on a single replica: with
leader election off, a default RollingUpdate would briefly run two
managers (maxSurge) during an operator upgrade, both reconciling without
lease coordination. Recreate closes that window at zero availability
cost, while installs scaled past one replica keep RollingUpdate.

The change to the vendored opensearch-operator 2.8.0 chart is carried in
patches/leaderElection.diff and re-applied by 'make update' so it
survives a chart refresh.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Jun 24, 2026
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 521d3683-4977-40a3-a46d-64d43306838a

📥 Commits

Reviewing files that changed from the base of the PR and between 29bf56f and db797b1.

📒 Files selected for processing (5)
  • packages/system/opensearch-operator/Makefile
  • packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml
  • packages/system/opensearch-operator/patches/leaderElection.diff
  • packages/system/opensearch-operator/tests/leader_election_test.yaml
  • packages/system/opensearch-operator/values.yaml

📝 Walkthrough

Walkthrough

The operator chart now uses manager.replicaCount to set Deployment replicas, switches single-replica rollouts to Recreate, and omits --leader-elect unless more than one replica is configured. The update workflow applies a local patch, and new tests cover both replica modes.

Changes

OpenSearch operator leader-election update

Layer / File(s) Summary
Replica count and rollout strategy
packages/system/opensearch-operator/values.yaml, packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml, packages/system/opensearch-operator/patches/leaderElection.diff, packages/system/opensearch-operator/Makefile
manager.replicaCount is added with a default of 1, the Deployment uses that value for spec.replicas, Recreate is selected when replicas are <= 1, and the update target applies the matching patch.
Leader election args
packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml, packages/system/opensearch-operator/patches/leaderElection.diff
--leader-elect is rendered only when manager.replicaCount is greater than 1.
Leader election tests
packages/system/opensearch-operator/tests/leader_election_test.yaml
New YAML tests cover single-replica omission of --leader-elect and two-replica inclusion with the expected Deployment strategy.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through values in the YAML glade,
Saw one little replica quietly laid.
With two, leader-elect twinkles bright and true,
With one, Recreate keeps the burrow through.
Thump! The operator dances just as told.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: leader election is conditionally enabled based on the operator replica count.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ray/fix-opensearch-leader-election

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses intermittent installation failures in the opensearch-operator caused by the manager container crashlooping due to leader election timeouts. By gating the leader election flag and deployment strategy on the replica count, the operator now runs more reliably in single-replica configurations while maintaining high-availability features when scaled up. The changes are implemented via a patch to the vendored chart, supported by new unit tests to ensure consistent behavior across different scaling scenarios.

Highlights

  • Leader Election Gating: Implemented conditional leader election for the opensearch-operator, disabling it for single-replica deployments to prevent unnecessary crashloops during transient control-plane latency.
  • Deployment Strategy Adjustment: Configured the deployment to use the 'Recreate' strategy when running as a single replica to ensure safe upgrades without lease coordination.
  • Automated Patching: Added a patch mechanism in the Makefile to inject these changes into the vendored Helm chart, ensuring customizations persist across chart updates.
  • Validation: Introduced a new helm-unittest suite to verify that leader election and deployment strategies correctly toggle based on the replica count.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/*.diff (1)
    • packages/system/opensearch-operator/patches/leaderElection.diff
  • Ignored by pattern: **/charts/** (1)
    • packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request gates the leader election of the opensearch-operator on the replica count, disabling it for a single replica to prevent transient crashloops and enabling it automatically when scaled. It adds a patch step to the Makefile, introduces a new test suite to verify this behavior, and defines replicaCount: 1 in values.yaml. The feedback points out that values.schema.json should be updated by running make generate to reflect the new replicaCount configuration option.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

# value but lets a transient apiserver/etcd lease-renewal blip self-terminate
# the manager (controller-runtime exits on "leader election lost"), which
# surfaces at install time as a crashlooping, Failed Deployment.
replicaCount: 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

When adding a new configuration option like replicaCount to values.yaml, the corresponding values.schema.json should be updated to maintain schema correctness for the dashboard UI and input validation. Please run make generate to regenerate the schema file.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff. --leader-elect gated on manager.replicaCount <= 1, paired with strategy: Recreate to close the overlap window. Patch lives in patches/leaderElection.diff so it survives chart bumps. helm-unittest covers both code paths. Scale-up to N>=2 path is safe (controller-runtime handles fresh lease acquisition). One harmless side effect worth noting: scale-down from N>=2 back to N=1 will leave an orphan lease object in the namespace (the new single pod ignores it). Cosmetic, not functional. LGTM.

@IvanHunters
IvanHunters merged commit 84345da into main Jun 25, 2026
12 checks passed
@IvanHunters
IvanHunters deleted the ray/fix-opensearch-leader-election branch June 25, 2026 16:21
myasnikovdaniil added a commit that referenced this pull request Jul 16, 2026
…reate

#3040 set the single-replica operator Deployment to strategy.type:
Recreate to avoid two uncoordinated managers overlapping during a
rollout while leader election is gated off. That breaks Helm upgrades of
existing releases: the upgrade fails validation immediately,
helm-controller retries it (x19 over 11m in Actions run 29480724982),
and the HelmRelease never becomes Ready:

  Deployment.apps "opensearch-operator-controller-manager" is invalid:
  spec.strategy.rollingUpdate: Forbidden: may not be specified when
  strategy type is Recreate

The trigger is server-side apply. A v1.5.x release shipped no strategy
at all, so the apiserver defaulted spec.strategy.rollingUpdate to
25%/25%, and the helm-controller field manager never owned that block.
helm-controller applies server-side by default (Install.ServerSideApply
defaults to true, Upgrade.ServerSideApply to "auto"; the operator's
package_reconciler.go sets neither), and SSA does not remove a field the
applier never owned merely because the new intent omits it -- so sending
type: Recreate leaves the defaulted rollingUpdate in place and the
apiserver rejects the merged object.

The client-side path is unaffected: DeploymentSpec.Strategy carries
patchStrategy:"retainKeys", so a 3-way merge emits
{"$retainKeys":["type"],"type":"Recreate"} and does clear rollingUpdate.
That is why this surfaces through helm-controller's server-side apply
but not a local 'helm upgrade'.

Keep type: RollingUpdate and set rollingUpdate.maxSurge: 0 /
maxUnavailable: 1 on the single-replica path instead. maxSurge: 0 caps
total pods at replicas, so the old manager is scaled down before the new
one is scaled up, rather than starting a surge pod while the old manager
is still fully live as the default 25% would. It is not Recreate's
exclusivity -- the deployment controller orders the ReplicaSet desired
counts, not the pod lifecycles, so a draining old manager can still
overlap the new pod -- but it keeps the type transition legal on every
apply path, and recovers releases already on an rc build with Recreate.
This matches the existing kube-ovn idiom (central-deploy.yaml).

Also add a 'test' target to the package Makefile:
hack/helm-unit-tests.sh runs a package's suite only when its Makefile
defines one, so tests/leader_election_test.yaml had never executed.

Multi-replica installs keep the default RollingUpdate (leader election
on), unchanged. The change is carried in patches/leaderElection.diff so
it survives 'make update'.

Verified on kind v1.33.1: SSA of the v1.5.x manifest followed by SSA of
the Recreate manifest reproduces the error, while the same pair applied
client-side succeeds; SSA of the maxSurge:0 manifest succeeds both from
v1.5.x and from a live Recreate Deployment.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
myasnikovdaniil added a commit that referenced this pull request Jul 20, 2026
…reate

#3040 set the single-replica operator Deployment to strategy.type:
Recreate to avoid two uncoordinated managers overlapping during a
rollout while leader election is gated off. That breaks Helm upgrades of
existing releases: the upgrade fails validation immediately,
helm-controller retries it (x19 over 11m in Actions run 29480724982),
and the HelmRelease never becomes Ready:

  Deployment.apps "opensearch-operator-controller-manager" is invalid:
  spec.strategy.rollingUpdate: Forbidden: may not be specified when
  strategy type is Recreate

The trigger is server-side apply. A v1.5.x release shipped no strategy
at all, so the apiserver defaulted spec.strategy.rollingUpdate to
25%/25%, and the helm-controller field manager never owned that block.
helm-controller applies server-side by default (Install.ServerSideApply
defaults to true, Upgrade.ServerSideApply to "auto"; the operator's
package_reconciler.go sets neither), and SSA does not remove a field the
applier never owned merely because the new intent omits it -- so sending
type: Recreate leaves the defaulted rollingUpdate in place and the
apiserver rejects the merged object.

The client-side path is unaffected: DeploymentSpec.Strategy carries
patchStrategy:"retainKeys", so a 3-way merge emits
{"$retainKeys":["type"],"type":"Recreate"} and does clear rollingUpdate.
That is why this surfaces through helm-controller's server-side apply
but not a local 'helm upgrade'.

Keep type: RollingUpdate and set rollingUpdate.maxSurge: 0 /
maxUnavailable: 1 on the single-replica path instead. maxSurge: 0 caps
total pods at replicas, so the old manager is scaled down before the new
one is scaled up, rather than starting a surge pod while the old manager
is still fully live as the default 25% would. It is not Recreate's
exclusivity -- the deployment controller orders the ReplicaSet desired
counts, not the pod lifecycles, so a draining old manager can still
overlap the new pod -- but it keeps the type transition legal on every
apply path, and recovers releases already on an rc build with Recreate.
This matches the existing kube-ovn idiom (central-deploy.yaml).

Also add a 'test' target to the package Makefile:
hack/helm-unit-tests.sh runs a package's suite only when its Makefile
defines one, so tests/leader_election_test.yaml had never executed.

Multi-replica installs keep the default RollingUpdate (leader election
on), unchanged. The change is carried in patches/leaderElection.diff so
it survives 'make update'.

Verified on kind v1.33.1: SSA of the v1.5.x manifest followed by SSA of
the Recreate manifest reproduces the error, while the same pair applied
client-side succeeds; SSA of the maxSurge:0 manifest succeeds both from
v1.5.x and from a live Recreate Deployment.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
myasnikovdaniil added a commit that referenced this pull request Jul 21, 2026
…reate (#3319)

## What this PR does

Fixes a Helm-upgrade failure in the `opensearch-operator` chart that
blocks the new release-upgrade E2E lane (added in #3276).

Upgrading from a chart that shipped no explicit strategy (v1.5.x) to a
v1.6.0-rc build fails validation immediately, helm-controller retries
it, and the HelmRelease never becomes Ready:

```
Helm upgrade failed for release cozy-opensearch-operator/opensearch-operator:
Deployment.apps "opensearch-operator-controller-manager" is invalid:
spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is Recreate
```

Observed in the ["Upgrade E2E Test" job of run
29480724982](https://github.com/cozystack/cozystack/actions/runs/29480724982)
— `UpgradeFailed ... (x19 over 11m)`, with the HelmRelease never
reaching Ready.

### Root cause

#3040 (commit db797b1) set the single-replica operator Deployment to
`strategy.type: Recreate` — a reasonable goal: with leader election
gated off on one replica, a default `RollingUpdate` briefly runs two
uncoordinated managers (`maxSurge`) during a rollout, and `Recreate`
closes that window.

The problem surfaces only on **upgrade from an older chart, under
server-side apply**:

- A v1.5.x release shipped no strategy at all, so the apiserver
**defaulted** `spec.strategy.rollingUpdate` to `25%/25%`, and the
`helm-controller` field manager never owned that block. Its
`managedFields` entry covers `f:replicas`, `f:selector` and
`f:template`, but not `f:strategy`.
- helm-controller applies **server-side** by default
(`Install.ServerSideApply` defaults to true, `Upgrade.ServerSideApply`
to `auto`; `internal/operator/package_reconciler.go` sets neither), and
SSA does not remove a field the applier never owned merely because the
new intent omits it.
- So the v1.6.0-rc manifest's `type: Recreate` merges onto a live object
that still carries the defaulted `rollingUpdate`, and the apiserver
rejects the result with `FieldValueForbidden`.

The **client-side** path is unaffected, which is why this surfaces
through helm-controller's SSA but not a local `helm upgrade`:
`DeploymentSpec.Strategy` carries `patchStrategy:"retainKeys"`
(`k8s.io/api@v0.34.1/apps/v1/types.go:395`), so a 3-way merge emits
`{"spec":{"strategy":{"$retainKeys":["type"],"type":"Recreate"}}}`,
which merges to a valid `{"strategy":{"type":"Recreate"}}` and does
clear the block.

This is a genuine product upgrade bug, not a test-harness issue — #3276
only adds the upgrade lane that exposes it; it does not touch
`opensearch-operator`.

> An earlier revision of this PR attributed the failure to Helm's
client-side 3-way merge. That was wrong, and the correction is
@lexfrei's — see [his
review](#3319 (review)).
The mechanism above is now reproduced end to end (below).

### The fix

Keep `type: RollingUpdate` on the single-replica path and set
`rollingUpdate.maxSurge: 0 / maxUnavailable: 1` instead of switching to
`Recreate`:

- `maxSurge: 0` caps total pods at `replicas`, so the deployment
controller must scale the old manager down before it can scale the new
one up — unlike the default 25% surge, which starts a second manager
while the first is still fully live.
- This is **not** `Recreate`'s exclusivity, and the PR no longer claims
it is. `rolloutRecreate` gates scale-up on `oldPodsRunning()`, which
counts non-terminal `Status.Phase`, so `Recreate` waits for the old pod
to be gone. `rolloutRolling` has no such gate: `NewRSNewReplicas`
derives `currentPodCount` from `GetReplicaCountForReplicaSets`, which
sums `rs.Spec.Replicas` (desired, not live). The ordering that is
guaranteed is over ReplicaSet desired counts, not pod lifecycles, so a
draining old manager can still overlap the new pod within
`terminationGracePeriodSeconds` (10s here).
- Because `strategy.type` never becomes `Recreate`, the forbidden merge
cannot occur on any apply path. It also recovers anyone already on an rc
build with `Recreate`, since `Recreate -> RollingUpdate` is permitted.
- This matches the idiom already vendored in this repo:
`packages/system/kubeovn/charts/kube-ovn/templates/central-deploy.yaml:11-15`
uses `RollingUpdate` with `maxSurge: 0 / maxUnavailable: 1`.

Multi-replica installs (`manager.replicaCount > 1`) are unchanged: they
keep the default `RollingUpdate` with leader election enabled.

The change is carried in `patches/leaderElection.diff` so it survives a
chart re-vendor via `make update`; the patch applies to pristine
upstream 2.8.0 with no fuzz and reproduces the committed strategy block
exactly.

### Alternatives considered

| Option | Why not |
| --- | --- |
| Keep `Recreate`, but make the chart own `rollingUpdate` first (ship an
explicit block, then switch `type` in a later release) | This is the
only option that preserves real exclusivity, but it needs two releases
and only works once every install has reconciled the intermediate one.
Disproportionate for a single-replica operator whose residual overlap is
a draining manager. |
| Pre-upgrade hook / Job that patches or deletes the Deployment strategy
before the upgrade | Heavy: needs a kubectl image, a ServiceAccount and
RBAC to `patch`/`delete` Deployments, and hook ordering; runs on every
upgrade even when unneeded; adds standing attack surface —
disproportionate for a scalar field transition. |
| `helm.sh/resource-policy` / force-replace | `resource-policy: keep`
only governs deletion on uninstall (irrelevant). Helm has no
per-resource "force recreate" annotation; `--force` / HelmRelease
`spec.upgrade.force` is global and disruptive and not controllable from
within the chart. |
| Plain revert to default `RollingUpdate` (25%/25%) | Fixes the
rejection but starts a surge pod while the old manager is still fully
live — the window #3040 set out to close. `maxSurge: 0` narrows it to a
draining old pod without leaving `RollingUpdate`. |

### Verification

The mechanism and the fix were reproduced on kind v1.33.1 (a bare
apiserver is enough — the failure is defaulting + SSA + validation):

| Path | Result |
| --- | --- |
| SSA install of the v1.5.x manifest (no strategy) | apiserver defaults
`rollingUpdate: 25%/25%`; `managedFields` shows `helm-controller` does
not own `f:strategy` |
| SSA upgrade to `type: Recreate` | fails with the exact production
error above |
| Client-side apply of the same pair | succeeds; `rollingUpdate` is
cleared, live strategy becomes `{"type":"Recreate"}` |
| SSA upgrade to `maxSurge: 0` (this PR) | succeeds |
| SSA of `maxSurge: 0` onto a live `Recreate` Deployment (rc recovery) |
succeeds |
| Rollout under `maxSurge: 0` vs `Recreate` | `maxSurge: 0`: two pods
coexisted within 1s (old Terminating, new ContainerCreating).
`Recreate`: one pod, waited the full drain. This is the evidence for the
"not exclusivity" wording above. |

### Note on regression coverage

`tests/leader_election_test.yaml` was never executed:
`hack/helm-unit-tests.sh` runs a package's suite only when the package
Makefile defines a `test:` target, and this one did not — so the
coverage the previous revision of this PR claimed did not exist. This PR
adds the target (matching `packages/system/etcd-operator/Makefile`),
which revives the suite; the repo-wide runner now picks the package up
and a revert to `Recreate` fails it. Thanks to @lexfrei for catching
that.

The apiserver-side merge that produces the invalid object still needs a
live cluster, so the behavioural regression test remains the
release-upgrade E2E lane in #3276 (which this fix unblocks).

### Follow-ups filed

- #3324 — `packages/system/ouroboros` has the same latent shape
(`Recreate` gated on `controller.mode == "external-dns"`, while
`controller.mode` defaults to `coredns`). Not fixed here, and it likely
needs a different fix, since that mode genuinely depends on exclusivity.
- #3325 — `make update` does not reproduce this package's vendored
template: three hand-edits are not captured in `patches/`, and one of
them breaks rendering when `manager.extraEnv` is set. Pre-existing on
`main`; an earlier revision of this body claimed the regeneration was
exact, which was wrong.

### Downstream repositories

- [x] No downstream repository is affected by this change

<!-- Walked the trigger map in docs/agents/contributing.md against the
diff: the change touches only a vendored packages/system operator chart
template + its patch + its helm-unittest suite + the package Makefile
test target. No packages/apps or packages/extra add/rename/remove, no
values.schema.json / enum / default change, no node contract, no
annotations/labels, no hack/ or ApplicationDefinition change.
opensearch-k8s-operator is a third-party upstream (not a cozystack
downstream), and the fix lives in patches/leaderElection.diff so it
survives make update. -->

### Release note

```release-note
fix(opensearch-operator): fix a Helm upgrade failure on single-replica installs. Upgrading a release that predates an explicit Deployment strategy failed under server-side apply, because switching strategy.type to Recreate does not clear the apiserver-defaulted spec.strategy.rollingUpdate block that the chart never owned, and the merged object is rejected. The operator now uses RollingUpdate with maxSurge=0/maxUnavailable=1, which keeps the transition legal on every apply path and recovers releases already stuck on Recreate.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Updated OpenSearch Operator controller rollout behavior for
single-replica installs to use a safer rolling update configuration.
* Leader election is now applied only when multiple replicas are
configured, reducing the chance of conflicting controller activity.
* **Tests**
* Expanded Helm unittest assertions for the controller deployment
rollout strategy and leader-election behavior for single- and
multi-replica scenarios.
* **Chores**
  * Added a `make test` target to run Helm unit tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants