Skip to content

feat(common): support rollout strategy in options - #3813

Open
l-qing wants to merge 1 commit into
tektoncd:mainfrom
l-qing:feat/options-deployment-strategy
Open

feat(common): support rollout strategy in options#3813
l-qing wants to merge 1 commit into
tektoncd:mainfrom
l-qing:feat/options-deployment-strategy

Conversation

@l-qing

@l-qing l-qing commented Jul 27, 2026

Copy link
Copy Markdown
Member

Changes

Fixes #3812

Adds spec.strategy (Deployment) and spec.updateStrategy (StatefulSet) to the
set of fields that options copies onto the generated manifests.

Why

options does not merge the embedded object — the transformer copies an
explicit list of fields, documented under
Deployments.
replicas, affinity and topologySpreadConstraints are on that list; the
rollout strategy is not, so a strategy block under options is stored by the
API server (the field is x-kubernetes-preserve-unknown-fields, so it is not
validated) and then dropped by the reconciler without any error, warning or
condition.

That combination has a concrete failure mode, because the fields already
supported are exactly the ones needed to construct it:

options:
  deployments:
    tekton-pipelines-controller:
      spec:
        replicas: 3
        template:
          spec:
            affinity:
              podAntiAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                  - topologyKey: kubernetes.io/hostname
                    labelSelector:
                      matchLabels:
                        app.kubernetes.io/name: controller

Three HA replicas, hard anti-affinity so no two share a node. None of the
shipped Deployments declare spec.strategy, so Kubernetes applies the default
RollingUpdate with maxSurge: 25% / maxUnavailable: 25%, computed by
rounding the surge up and the unavailable count down. At replicas: 3
that is maxSurge: 1, maxUnavailable: 0.

On a cluster with exactly three eligible nodes, all three already hold a
replica. The surge pod has no node satisfying the anti-affinity rule and stays
Pending, and maxUnavailable: 0 forbids terminating any old pod to make room.
The rollout cannot complete — not as a scheduling race, but arithmetically, and
maxUnavailable rounds to 0 for any replicas ≤ 3, which is the common
on-premises cluster size. topologySpreadConstraints with
whenUnsatisfiable: DoNotSchedule deadlocks identically.

The remedy is maxSurge: 0 with maxUnavailable: 1 — replace the replicas one
at a time, never needing a spare node — or type: Recreate. That is exactly the
field options will not carry over, and there is no way to set it from outside
options either: resourceReconcileFields() returns spec for Deployment and
StatefulSet and copyResourceFields() applies
unstructured.SetNestedField(dst.Object, fieldValue, "spec"), so the whole
spec is overwritten from the expected manifest and a hand-edited strategy is
reverted on the next reconcile.

I do not think the operator should infer this. Whether a spare eligible node
exists is not knowable from the Deployment alone, clusters with headroom
genuinely want the surge behaviour, and inference would not cover the adjacent
cases (a full namespace ResourceQuota leaving no room for the surge pod —
which the operator already detects via ReplicaSetReplicaFailure /
FailedCreate — or single-node clusters). It is an environment-specific
decision, the same reasoning behind priorityClassName, runtimeClassName and
topologySpreadConstraints being added to options.

Implementation

Two symmetric additions, in updateDeployments() and updateStatefulSets(),
guarded on a non-empty strategy type to match the existing
PriorityClassName != "" style in the same functions:

if deploymentOptions.Spec.Strategy.Type != "" {
    targetDeployment.Spec.Strategy = deploymentOptions.Spec.Strategy
}

Three semantics worth calling out, each covered by a test:

  • The whole struct is replaced, not merged field by field. rollingUpdate
    may not be set when the type is Recreate (OnDelete for StatefulSets); a
    field-wise merge would leave the base manifest's rollingUpdate block behind
    and produce an object the API server rejects. That is the failure mode the
    Recreate test case exists to catch.
  • An empty type is a no-op, keeping the base manifest's strategy. This
    matches the "non-empty wins" semantics of every other field in the
    transformer.
  • A self-contradictory strategy is passed through as given rather than
    partially dropped, so the user gets a real API server error instead of a
    silent half-application.

updateDeploymentHashValue() already zeroes Spec.Strategy before computing
the pod-template hash, so changing only the strategy leaves
operator.tekton.dev/deployment-spec-applied-hash unchanged and does not
trigger a rollout through that mechanism — which is the behaviour you want, and
is now asserted by a test.

Open to feedback: requiring type means the common case above has to
spell out type: RollingUpdate alongside rollingUpdate, even though it is
not changing the type. I chose that over honouring a bare rollingUpdate:
block because the latter has to decide whether an unset type inherits the base
strategy's type or falls back to the Kubernetes default, and either answer is
surprising in some case. Happy to change it if reviewers prefer the looser
form.

Tests

Added to the existing table-driven golden-file test in
transformer_additional_options_test.go:

Case Covers
test-strategy-recreate-for-deployments base has RollingUpdate + a rollingUpdate block, options set Recreate → result is Recreate with no rollingUpdate key
test-strategy-rollingupdate-tuning-for-deployments maxSurge / maxUnavailable tuning is applied (the case from the description)
test-strategy-recreate-to-rollingupdate-for-deployments the reverse direction, base Recreate → options RollingUpdate
test-strategy-not-set-for-deployments options present without a strategy → base strategy preserved (regression guard)
test-strategy-rollingupdate-without-type-is-ignored-for-deployments rollingUpdate without type is a no-op
test-strategy-recreate-with-rollingupdate-is-passed-through-for-deployments contradictory options are passed through, not partially dropped
test-updatestrategy-ondelete-for-statefulsets StatefulSet OnDelete drops the base rollingUpdate block
test-updatestrategy-partition-tuning-for-statefulsets partition is applied
test-updatestrategy-not-set-for-statefulsets regression guard

Plus TestDeploymentStrategyDoesNotAffectSpecHash, which the golden-file cases
cannot cover because the shared test helper strips the hash label before
comparing. It pairs the assertion with a replicas change as a vacuity guard,
so it cannot pass by hashing nothing.

Falsified against the source change: with
pkg/reconciler/common/transformer_additional_options.go reverted, the four
feature cases fail and the three regression guards still pass.

Docs

docs/TektonConfig.md:

  • strategy and updateStrategy added to the supported-field lists under
    #### Deployments and #### StatefulSets.
  • Both options examples extended with the maxSurge: 0 / maxUnavailable: 1
    form, since that is the shape people will actually need.
  • A note in the Additional fields as options section stating that the embedded
    objects are not merged as a whole and that the per-kind lists are exhaustive —
    fields outside them are ignored silently. The lists were already there and
    correct, but nothing said they were the complete story, and the silent drop is
    easy to mistake for a bug in your own YAML.

Submitter Checklist

These are the criteria that every PR should meet, please check them off as you
review them:

See the contribution guide for more details.

This change was AI-assisted (Claude Opus 5 via Claude Code) and is disclosed
with an Assisted-by: trailer on the commit, per the
AI contribution policy.
I have reviewed and tested it, I understand it, and I take responsibility for
it.

Release Notes

`options` now applies `deployments[].spec.strategy` and
`statefulSets[].spec.updateStrategy` to the generated workloads instead of
ignoring them. This makes it possible to set, for example,
`rollingUpdate.maxSurge: 0` on a component whose replicas are pinned
one-per-node by anti-affinity, where the default surge-based rollout cannot
schedule the extra pod.

The additional options transformer copies a fixed list of fields from
the Deployment / StatefulSet given under `options` onto the manifest.
The rollout strategy was not part of that list, so `spec.strategy` and
its StatefulSet counterpart `spec.updateStrategy` were dropped without
any error or warning.

This blocks a configuration the operator otherwise supports:
`options` already carries `replicas` and `affinity`, so a component
can be pinned to one replica per node, but the default rollout then
has nowhere to schedule its surge pod, and `maxUnavailable` rounds
down to zero at three replicas, so the rollout can never complete.

There is no supported way to set it outside of `options` either: the
installer set treats `spec` as a reconcile field and copies it
wholesale from the expected manifest, so editing the strategy on the
live object is reverted the next time the installer set reconciles a
differing spec.

Copy both fields when a strategy type is set. The whole struct is
replaced rather than merged field by field, because `rollingUpdate`
may not be set when the type is `Recreate`, or `OnDelete` for
StatefulSets, and a field-wise merge would leave the base manifest's
rollingUpdate block behind, producing an object the API server
rejects. An empty type keeps the strategy from the base manifest. A
self-contradictory strategy is passed through as given rather than
partially dropped, leaving validation to the API server.

updateDeploymentHashValue() zeroes the strategy before hashing, so
changing only the strategy leaves the operator-generated pod-template
hash unchanged and does not trigger a rollout through that mechanism.

Fixes tektoncd#3812

Signed-off-by: qingliu <qingliu@alauda.io>
Assisted-by: Claude Opus 5 (via Claude Code)
@tekton-robot tekton-robot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Jul 27, 2026
@tekton-robot
tekton-robot requested review from divyansh42 and khrm July 27, 2026 14:32
@l-qing

l-qing commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

/kind feature

@tekton-robot tekton-robot added kind/feature Categorizes issue or PR as related to a new feature. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 27, 2026
@l-qing

l-qing commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

/assign @l-qing

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 25.41%. Comparing base (5a94d1b) to head (b115281).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3813      +/-   ##
==========================================
+ Coverage   25.40%   25.41%   +0.01%     
==========================================
  Files         449      449              
  Lines       23477    23481       +4     
==========================================
+ Hits         5964     5968       +4     
  Misses      16822    16822              
  Partials      691      691              
Flag Coverage Δ
unit-tests 25.41% <ø> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jkhelil

jkhelil commented Jul 30, 2026

Copy link
Copy Markdown
Member

/approve

@tekton-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: jkhelil

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@tekton-robot tekton-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. kind/feature Categorizes issue or PR as related to a new feature. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support strategy and updateStrategy under options for deployments and statefulSets

4 participants