Fix StepMutator.mutate() not seeing late-attached platform decorators#3026
Fix StepMutator.mutate() not seeing late-attached platform decorators#3026chirag-bajaj wants to merge 8 commits intoNetflix:masterfrom
Conversation
In 2.19.14, _init_step_decorators (which runs StepMutator.mutate()) was removed from the Argo CLI's make_flow() and replaced with _process_late_attached_decorator. This changed the execution order: mutators now run before the platform attaches @kubernetes, so StepMutators that iterate decorator_specs looking for "kubernetes" find nothing. This adds a re-run of step mutators inside _process_late_attached_decorator, after late-attached decorators have been added but before step_init is called. The graph is also rebuilt so that node.decorators reflects any changes made by the mutators (e.g. add_decorator with OVERRIDE).
Verify that _process_late_attached_decorator re-runs step mutators so they can see and modify late-attached platform decorators like @kubernetes. Tests cover: - Mutator overrides apply after late attachment - Steps without mutators retain defaults - Mutator is a no-op when target decorator is never attached
Greptile SummaryThis PR fixes a regression introduced in PR #2719 where
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as cli.py start()
participant ISD as _init_step_decorators()
participant AD as _attach_decorators()
participant PLAD as _process_late_attached_decorator()
participant SM as StepMutator.mutate()
participant IG as cls._init_graph()
CLI->>ISD: run flow/step mutators
note over ISD: @kubernetes NOT yet attached
ISD->>SM: mutate() — sees no @kubernetes ❌ (pre-fix)
ISD->>IG: rebuild graph
CLI->>AD: late-attach @kubernetes to steps
CLI->>PLAD: process late-attached decorators
PLAD->>PLAD: external_init() on @kubernetes<br/>collect late_attached_step_names
note over PLAD: NEW (this PR)
PLAD->>SM: re-run mutate() for affected steps ✅
PLAD->>IG: rebuild graph (only if mutators_ran)
PLAD->>PLAD: external_init() on any new<br/>decorator instances (_ran_init=False)
PLAD->>PLAD: step_init() on all @kubernetes<br/>using refreshed graph
|
- Refresh local graph variable after _init_graph() so step_init receives the rebuilt graph instead of the stale one passed as a parameter (mirrors _init_step_decorators line 874). - Restrict mutator re-run to only steps that received a late-attached decorator, avoiding issues with non-idempotent mutators. - Update test_process_late_attached_preserves_defaults_without_mutator to call _process_late_attached_decorator so the new code path is actually exercised.
- Only call _init_graph() and refresh graph when late-attached decorators were actually found, preserving the caller-supplied graph parameter when no mutations occur. - Use pytest.importorskip for KubernetesDecorator import so tests skip gracefully if the plugin is unavailable.
- Remove dead __iter__ and _steps_list from _MockFlow — after __class__ reassignment, FlowSpec.__iter__ is used via type(obj). - Update test_mutator_is_noop_without_late_attachment to call _process_late_attached_decorator, exercising the empty late_attached_step_names guard path.
- Only rebuild graph when a mutator actually executed, avoiding unnecessary _init_graph() when late-attached decorators exist but no step has a StepMutator. - Add assertion that start step's @kubernetes was overridden in test_process_late_attached_preserves_defaults_without_mutator to confirm the mutator loop ran successfully.
Add assertion on end_step in test_process_late_attached_reruns_mutators to confirm the step without a StepMutator keeps default cpu=1, memory=4096 after the mutator re-run loop.
When add_decorator(OVERRIDE) creates a new decorator instance, it skips external_init(). Ensure replacement decorators are initialized before step_init() is called on them.
67854ba to
ac1b6c9
Compare
PR Type
Summary
_process_late_attached_decoratornow re-runsStepMutator.mutate()after late-attaching platform decorators, so mutators can see and modify decorators like@kubernetesthat are attached after_init_step_decoratorshas already executed.Issue
Fixes #3025
Reproduction
Runtime: Argo Workflows
test_flow.py
Commands to run:
Where evidence shows up: Generated Argo workflow JSON — resource requests on steps
Before (error / log snippet)
After (evidence that fix works)
Root Cause
PR #2719 moved
_init_step_decorators()from Argo'smake_flow()intocli.py start(). This means mutators now run before Argo attaches@kubernetesto steps. The new_process_late_attached_decorator()function callsexternal_init()andstep_init()on the late-attached decorators but never re-runsStepMutator.mutate(), so mutators that iteratedecorator_specslooking for"kubernetes"find nothing.2.19.13 order (working):
_attach_decoratorsadds@kubernetes_init_step_decoratorsrunsmutate()— mutators see@kubernetes✅2.19.14 order (broken):
_init_step_decoratorsrunsmutate()— no@kubernetesyet ❌_attach_decoratorsadds@kubernetes_process_late_attached_decorator— does not re-runmutate()Why This Fix Is Correct
The fix adds a mutator re-run loop inside
_process_late_attached_decorator, betweenexternal_init()and thestep_init()loop. This restores the invariant thatmutate()always sees the full set of decorators on a step. After mutators run,cls._init_graph()is called to rebuild the graph sonode.decoratorsreflects any changes made byadd_decorator(OVERRIDE).Failure Modes Considered
StepMutatorinconfig_decoratorsare unaffected — the inner loop simply doesn't execute. Verified bytest_process_late_attached_preserves_defaults_without_mutator.@kubernetesis never late-attached, the mutator's iteration overdecorator_specsfinds nothing and is a no-op. Verified bytest_mutator_is_noop_without_late_attachment.cls._init_graph()is called after mutator re-run to ensurenode.decoratorsmatchesstep.decorators. Without this, the Argo compiler reads stale decorator references from graph nodes.external_init()is only called once (in the existing code, before the mutator re-run). The mutator re-run only callsmutate(), notinit()again.Tests
Three regression tests added in
test/unit/test_late_attached_mutator.py:test_process_late_attached_reruns_mutators— verifies mutator overrides apply after late attachment (fails without fix, passes with fix)test_process_late_attached_preserves_defaults_without_mutator— verifies steps without mutators retain defaultstest_mutator_is_noop_without_late_attachment— verifies mutator is a no-op when target decorator is never attachedNon-Goals
StepMutatorAPI orMutableStepinterface._init_step_decoratorsworks — only_process_late_attached_decoratoris modified.AI Tool Usage
AI tooling was used to verify the solution against the test suite and to help write the PR description. All code changes were reviewed, understood, and tested manually.