Skip to content

CLO-188 orchestratord: report Materialize lifecycle transitions as events - #38537

Closed
Alphadelta14 wants to merge 4 commits into
heather/CLO-188-orchestrator-metrics-eventsfrom
heather/CLO-188-orchestrator-metrics-events2
Closed

Alphadelta14 wants to merge 4 commits into
heather/CLO-188-orchestrator-metrics-eventsfrom
heather/CLO-188-orchestrator-metrics-events2

Conversation

@Alphadelta14

Copy link
Copy Markdown
Member

Motivation

While #35829 addresses failure cases, this adds some additional information
about rollouts themselves upon success.

Description

Add Normal events upon rollout stages

Verification

Soon~


Stack created with GitHub Stacks CLI • Give Feedback 💬

@linear-code

linear-code Bot commented Aug 27, 2026

Copy link
Copy Markdown

CLO-188

@Alphadelta14
Alphadelta14 force-pushed the heather/CLO-188-orchestrator-metrics-events2 branch from 5d3c90e to c977770 Compare August 27, 2026 20:56
@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA.
Posted by the CLA Assistant Lite bot.

Copilot AI 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.

Pull request overview

Adds Kubernetes events for Materialize rollout lifecycle transitions.

Changes:

  • Publishes Normal or Warning events after status transitions persist.
  • Shares the reconciliation event publisher with the Materialize controller.
  • Adds orchestratord lifecycle-event coverage.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/orchestratord/src/controller/materialize.rs Publishes lifecycle transition events.
src/orchestratord/src/bin/orchestratord.rs Injects the shared event publisher.
src/orchestratord/src/reconcile.rs Documents lifecycle event reporting.
test/orchestratord/mzcompose.py Verifies successful rollout events.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.


# Two manual promotions have now driven the full lifecycle, so every phase
# of one should have been reported as an event.
check_lifecycle_events(definition)
@Alphadelta14
Alphadelta14 force-pushed the heather/CLO-188-orchestrator-metrics-events2 branch 2 times, most recently from 78ae0b9 to 20c7d0e Compare August 28, 2026 12:22
@Alphadelta14
Alphadelta14 marked this pull request as ready for review August 28, 2026 12:39
@Alphadelta14
Alphadelta14 requested review from a team and jubrad August 28, 2026 12:39
@def-

def- commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- WaitingForApproval is filed as a Kubernetes Warning event

src/orchestratord/src/controller/materialize.rs:227

The condition-to-EventType mapping keys purely on condition.status == "False", and WaitingForApproval is written with status: "False" (materialize.rs:844-850). So a Materialize whose spec changed but whose rollout has not been requested yet -- a normal, user-driven steady state with the default WaitUntilReady strategy -- now files a Warning event on every such spec change, which kubectl get events --field-selector type=Warning and any event-exporter alerting pipeline will treat as a fault.

Details

That is the one reason in the enumerated vocabulary where the rule stated right above it does not hold. The comment says "A condition that went false is the environment failing to reach the state it was asked for" -- but WaitingForApproval is precisely the case where the environment has not been asked to reach a new state. RolloutTimeout and both FailedDeploy sites (materialize.rs:576, 618, 812) are genuine failures and correctly Warning; WaitingForApproval is the odd one out.

The mismatch is not caught by the new test: check_lifecycle_events asserts type == "Normal" only for Applying, ReadyToPromote, Promoting and Applied, and workflow_manually_promote never parks the resource in WaitingForApproval.

Fix -- classify on the reason rather than on the condition status, so the one non-failure False reason stays Normal:

type_: if condition.status == "False" && condition.reason != "WaitingForApproval" {
    EventType::Warning
} else {
    EventType::Normal
},

and adjust the comment to say that a False condition is a failure except for the approval gate, which is a False the user chose.

@jubrad jubrad left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reads correct to me: the one-event-per-transition property genuinely follows from the needs_update guard, and publishing after replace_status means no event claims a transition that failed to persist.

Summary

1 consideration
1 nit

written by claude on behalf of @jubrad


# Two manual promotions have now driven the full lifecycle, so every phase
# of one should have been reported as an event.
check_lifecycle_events(definition)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

consider — Noted by claude:
wait_for_rollout_complete() polls the status, but the Applied event is published after replace_status returns, so the test can observe the final status before the event it asserts on exists. The window is one API round trip and will usually be closed by the 1s poll granularity, but it is a real race.

The sibling check just above uses retry(check, 300) for exactly this reason. Wrapping the body of check_lifecycle_events the same way would cost nothing and remove the flake:

retry(lambda: _check_lifecycle_events(name), 60)

written by claude on behalf of @jubrad

.set(u64::cast_from(needs_update_set.len()));
}

/// Reports a lifecycle transition as a Kubernetes event on the resource.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit — This is ~30 lines of doc and inline prose for ~20 lines of code, and a good part of it re-argues things that already have an owner. The third paragraph ("These do not aggregate the way repeated failures do...") restates Publisher's aggregation contract, which the Publisher doc explains at length; the inline comment at the let condition = ... line restates the guard three lines above it; and the second paragraph explains the relationship to Observed::publish_failure from the wrong end.

Per CLAUDE.md the doc comment should be the caller's contract, with reasoning at the decision point and each fact owned once. Something like a one-line summary plus "published only once the status is durable" would carry everything a caller needs, with the severity policy staying on transition_event_type where it already lives.

written by claude on behalf of @jubrad

Alphadelta14 and others added 3 commits September 8, 2026 10:30
The status conditions record only the phase an environment is in now, so
a rollout that has moved on leaves no account of the phases it passed
through. Report each transition as a Kubernetes event, which the
monitoring stack persists as a structured log.

`update_status` is the single place a condition is written, and it
already returns early unless the status substantively moved, comparing
everything but the timestamp. That makes it an exact edge detector, so a
reconciler that re-runs on every watch event still files one event per
transition without tracking any state of its own. The event carries the
condition's own reason and message, so `Applying`, `ReadyToPromote`,
`Promoting`, `Applied`, `WaitingForApproval`, `RolloutTimeout` and
`FailedDeploy` are one vocabulary across the status, `kubectl describe`,
and a dashboard.

Publishing happens after the status write lands, so no event claims a
transition that failed to persist. Failures now report twice, once here
with the phase and once from the reconciliation wrapper with the cause;
the reasons tell them apart.

`workflow_manually_promote` drives a promotion through every phase, so it
asserts the vocabulary rather than that any event at all was published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The severity of a lifecycle event was taken from the condition's status,
so every reason carrying `UpToDate=False` reported as a warning. Three
reasons carry it: `FailedDeploy` and `RolloutTimeout`, which are
failures, and `WaitingForApproval`, which is not. That last one is the
operator doing exactly what it was configured to do, holding a detected
change until someone requests the rollout. Flagging it teaches people to
ignore warnings on Materialize resources, which costs them the two that
mean something.

The rule moves into `transition_event_type`, which special-cases that
reason and otherwise falls back to the status, so a failure reason added
later still reports as a warning without having to be named there. A unit
test pins the severity of every condition the controller writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Alphadelta14
Alphadelta14 force-pushed the heather/CLO-188-orchestrator-metrics-events2 branch from a0503df to 8698e04 Compare September 8, 2026 14:31
@def-

def- commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Failure events now bypass Publisher, restoring the pinned-note bug it was added to fix

src/orchestratord/src/reconcile.rs:324

Observed::publish_failure was rewritten to publish through self.recorder, a field Observed does not have (it holds publisher: Arc<Publisher>), so mz-orchestratord does not build. More than a slip: taken at its evident intent, sending reconciliation failures through one long-lived Recorder instead of Publisher puts a failing resource's event note back on whichever error came first, which is exactly what the parent commit added Publisher to prevent.

Details

cargo check -p mz-orchestratord at this commit: error[E0609]: no field 'recorder' on type '&Observed<Ctx>', reconcile.rs:324. The new free function reconcile::publish (reconcile.rs:426), which takes the &Recorder this call site wants, has no callers, so the refactor is half-applied in both directions.

Why the endpoint is wrong: Recorder::publish keys its aggregation cache on (type, action, reason, reportingController, reportingInstance, regarding, related) and, on a hit, clones the cached event and only bumps its series count, so a repeat keeps the first note (kube-runtime 3.1.0 events.rs:305-330). Every reconciliation failure of a resource shares that key. The entry expires six minutes after last use but each republish refreshes it, and the controller backoff tops out well inside that window, so one recorder pins the note for as long as the resource keeps failing: kubectl describe goes on reporting a missing secret after the real cause has become an invalid license key, under a count and timestamp that both look current. Publisher::recorder_for (reconcile.rs:532) exists to key on the note as well and hand out a fresh recorder when it changes.

Two more consequences of the same edit. Observed::record still calls self.publisher.forget(resource) on a clean pass (reconcile.rs:355), which would then have nothing to forget on the failure path it was written for. And the base branch's workflow_failure_events (test/orchestratord/mzcompose.py:3343) drives an environment through two different failures in a row and requires the note to follow, so resolving the build error by giving Observed a plain Recorder field fails that test rather than passing silently.

Fix: publish through self.publisher again and delete reconcile::publish, keeping Publisher::publish as the single entry point for both failures and transitions.

This reverts commit 8698e04, which does
not build: it points `Observed::publish_failure` back at a `recorder`
field that no longer exists, since the aggregation fix replaced it with
`publisher`.

The commit reapplies the free `publish` function verbatim, stale doc
comment included, and leaves it beside the `Publisher` that superseded it
with no callers. That is the rebase conflict resolved toward the wrong
side rather than an intended change, so reverting it restores the file to
its parent exactly.

Reinstating that path would also undo the fix for the aggregation bug QA
reported on the parent branch: publishing every failure through one
shared recorder pins a failing resource's event note to whichever error
came first, for as long as the failure lasts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Alphadelta14

Copy link
Copy Markdown
Member Author

Closed in favor of #39030

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants