feat(common): support rollout strategy in options - #3813
Open
l-qing wants to merge 1 commit into
Open
Conversation
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)
Member
Author
|
/kind feature |
Member
Author
|
/assign @l-qing |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Member
|
/approve |
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 DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
Fixes #3812
Adds
spec.strategy(Deployment) andspec.updateStrategy(StatefulSet) to theset of fields that
optionscopies onto the generated manifests.Why
optionsdoes not merge the embedded object — the transformer copies anexplicit list of fields, documented under
Deployments.
replicas,affinityandtopologySpreadConstraintsare on that list; therollout strategy is not, so a
strategyblock underoptionsis stored by theAPI server (the field is
x-kubernetes-preserve-unknown-fields, so it is notvalidated) 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:
Three HA replicas, hard anti-affinity so no two share a node. None of the
shipped Deployments declare
spec.strategy, so Kubernetes applies the defaultRollingUpdatewithmaxSurge: 25%/maxUnavailable: 25%, computed byrounding the surge up and the unavailable count down. At
replicas: 3that 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, andmaxUnavailable: 0forbids terminating any old pod to make room.The rollout cannot complete — not as a scheduling race, but arithmetically, and
maxUnavailablerounds to 0 for anyreplicas≤ 3, which is the commonon-premises cluster size.
topologySpreadConstraintswithwhenUnsatisfiable: DoNotScheduledeadlocks identically.The remedy is
maxSurge: 0withmaxUnavailable: 1— replace the replicas oneat a time, never needing a spare node — or
type: Recreate. That is exactly thefield
optionswill not carry over, and there is no way to set it from outsideoptionseither:resourceReconcileFields()returnsspecfor Deployment andStatefulSet and
copyResourceFields()appliesunstructured.SetNestedField(dst.Object, fieldValue, "spec"), so the wholespecis overwritten from the expected manifest and a hand-edited strategy isreverted 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
ResourceQuotaleaving no room for the surge pod —which the operator already detects via
ReplicaSetReplicaFailure/FailedCreate— or single-node clusters). It is an environment-specificdecision, the same reasoning behind
priorityClassName,runtimeClassNameandtopologySpreadConstraintsbeing added tooptions.Implementation
Two symmetric additions, in
updateDeployments()andupdateStatefulSets(),guarded on a non-empty strategy type to match the existing
PriorityClassName != ""style in the same functions:Three semantics worth calling out, each covered by a test:
rollingUpdatemay not be set when the type is
Recreate(OnDeletefor StatefulSets); afield-wise merge would leave the base manifest's
rollingUpdateblock behindand produce an object the API server rejects. That is the failure mode the
Recreatetest case exists to catch.matches the "non-empty wins" semantics of every other field in the
transformer.
partially dropped, so the user gets a real API server error instead of a
silent half-application.
updateDeploymentHashValue()already zeroesSpec.Strategybefore computingthe pod-template hash, so changing only the strategy leaves
operator.tekton.dev/deployment-spec-applied-hashunchanged and does nottrigger a rollout through that mechanism — which is the behaviour you want, and
is now asserted by a test.
Tests
Added to the existing table-driven golden-file test in
transformer_additional_options_test.go:test-strategy-recreate-for-deploymentsRollingUpdate+ arollingUpdateblock, options setRecreate→ result isRecreatewith norollingUpdatekeytest-strategy-rollingupdate-tuning-for-deploymentsmaxSurge/maxUnavailabletuning is applied (the case from the description)test-strategy-recreate-to-rollingupdate-for-deploymentsRecreate→ optionsRollingUpdatetest-strategy-not-set-for-deploymentstest-strategy-rollingupdate-without-type-is-ignored-for-deploymentsrollingUpdatewithouttypeis a no-optest-strategy-recreate-with-rollingupdate-is-passed-through-for-deploymentstest-updatestrategy-ondelete-for-statefulsetsOnDeletedrops the baserollingUpdateblocktest-updatestrategy-partition-tuning-for-statefulsetspartitionis appliedtest-updatestrategy-not-set-for-statefulsetsPlus
TestDeploymentStrategyDoesNotAffectSpecHash, which the golden-file casescannot cover because the shared test helper strips the hash label before
comparing. It pairs the assertion with a
replicaschange as a vacuity guard,so it cannot pass by hashing nothing.
Falsified against the source change: with
pkg/reconciler/common/transformer_additional_options.goreverted, the fourfeature cases fail and the three regression guards still pass.
Docs
docs/TektonConfig.md:strategyandupdateStrategyadded to the supported-field lists under#### Deploymentsand#### StatefulSets.optionsexamples extended with themaxSurge: 0/maxUnavailable: 1form, since that is the shape people will actually need.
Additional fields as optionssection stating that the embeddedobjects 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:
make test lintbefore submitting a PRSee 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 theAI contribution policy.
I have reviewed and tested it, I understand it, and I take responsibility for
it.
Release Notes