Skip to content

[HYPERSHELL-173] Implement GatewayRelease reconciliation - #235

Open
JuanmaBM wants to merge 1 commit into
openshift-online:mainfrom
JuanmaBM:HYPERSHELL-173-gatewayrelease-reconciliation
Open

[HYPERSHELL-173] Implement GatewayRelease reconciliation#235
JuanmaBM wants to merge 1 commit into
openshift-online:mainfrom
JuanmaBM:HYPERSHELL-173-gatewayrelease-reconciliation

Conversation

@JuanmaBM

@JuanmaBM JuanmaBM commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the no-op GatewayReleaseReconciler stub with a real reconciler for the cluster-local control plane (HYPERSHELL-173).

A GatewayRelease has no direct Kubernetes footprint, so "reconciliation" here means:

  • Validate the release image reference via gateway.ValidateImageReference.
  • Write back a deterministic statusAvailable for a valid image, Invalid: <reason> otherwise — idempotently (no redundant write when the persisted status already matches).
  • Fan out to referencing gateways when a release image actually changes, so gateways pick up the new image.

Design notes

  • Shared gateway reconcile queue. Fan-out enqueues referencing gateways through the shared queue via EnqueueForced, which clears the gateway phase so the gateway reconciler's phase gate (which skips Running/Provisioning/Degraded) does not silently drop a release-driven re-reconcile. Using direct Handle calls would both block the release watch goroutine on multi-minute provisioning and hit that phase gate.
  • Per-release serialization + retry. The GatewayRelease watch now runs through a reconcileQueue (previously it only logged handler errors inline).
  • Invalid image retains the last valid baseline. An invalid image is not propagated and does not drop the remembered baseline, so a later correction to a different image is still detected as a genuine change and fans out (a correction back to the same image correctly no-ops).

Scope

In scope: image validation, deterministic status write-back, fan-out on image change, delete = no cluster footprint. Out of scope (sibling tasks): release_id→image resolution (174), rollout/canary (175/176).

Testing

  • Unit tests for the release reconciler (gateway_release_test.go): valid/invalid status, no redundant write, fan-out targeting only referencing gateways, rename no-op, delete no-op, correction-after-invalid regression, status-write and fan-out failure retry.
  • Watcher-level tests (gateway_reconcile_queue_test.go) proving the phase-gate bypass wiring through the real shared queue.
  • go build ./..., go vet ./..., gofmt, go test ./... and go test -race all green.
  • Reviewed via /amber-review loop (APPROVE after the invalid-baseline fix).

Refs HYPERSHELL-173

🤖 Generated with Claude Code

Replace the no-op GatewayReleaseReconciler stub with a real reconciler
that validates the release image reference, writes back a deterministic
status (Available/Invalid), and fans out to referencing gateways when a
release image changes.

Fan-out reuses a shared gateway reconcile queue via EnqueueForced so the
gateway phase gate (which skips Running/Provisioning/Degraded gateways) is
bypassed on release-driven re-reconciles. The GatewayRelease watch now runs
through a reconcile queue for per-release serialization and retry.

An invalid image is not propagated and retains the last valid image
baseline, so a later correction to a different image is still detected as a
genuine change and fans out.

Adds unit tests for the release reconciler and watcher-level tests proving
the phase-gate bypass wiring. Adds the behavior spec.

Refs HYPERSHELL-173

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Team

Run ID: 28dc37c3-0c30-42fd-aed9-b45d7ee54c53

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@jsell-rh

jsell-rh commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Amber review: comment

Amber review

Status: Complete

View the submitted review.

@jsell-rh jsell-rh 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.

Verdict

COMMENT. This is a clean, well-documented, and well-tested implementation of GatewayRelease reconciliation: image validation, idempotent status write-back, and change-driven fan-out are all correct and match the accompanying behavior spec. I am not blocking on this PR's own merit, but there are material cross-PR design decisions (below) that maintainers should settle before/around merge, plus a few minor observations.

What's good

  • Deterministic, idempotent status write-back (no redundant write when persisted status already matches), with errors-style wrapping (fmt.Errorf("...: %w", err)) on every failure path.
  • Fan-out is correctly scoped to gateways referencing the release by release_id, and only fires on a genuine effective-image change.
  • The invalid-image-retains-baseline behavior is subtle and correctly reasoned; the regression test (TestGatewayRelease_CorrectionAfterInvalidFansOut) locks it in.
  • Failures return errors so the new reconcile queue retries them rather than logging-and-dropping; the fan-out-failure-not-advancing-baseline path is tested.
  • No panic(), no secrets logged, image reference validated before it is ever logged (so no log-injection via the %s image log), delete treated as an idempotent no-op. Spec added and indexed.

Findings

All findings are Minor; see inline comments for specifics.

  1. [Minor] The lastImage change-detection baseline is in-memory only and the GatewayRelease watch intentionally has no startup seed, so a release image edited while the controller is down is not re-observed on restart and never fans out. This is harmless today (release_id -> image resolution is out of scope) but becomes a latent gap once that resolution lands. - Reconciliation / restart durability
  2. [Minor] The active create-or-skip map is now redundant with the per-release reconcile queue's serialization, and it returns nil (success) on a skip, which would mask a dropped reconcile if the invariant ever broke. - Reconciliation pattern
  3. [Minor] listGatewaysForRelease pages the entire gateway inventory and filters by release_id client-side on every image change; fine at current scale but worth a note as fleets grow. - Efficiency

Cross-PR coordination

  • #151 redefines the gateway reconciler's provisioning gate from a phase gate (skip Running/Provisioning/Degraded) to a generation/observed_generation convergence gate, deleting the exact gw.Phase check this PR's fan-out is built to defeat. This PR propagates release image changes by force-clearing a gateway's phase (EnqueueForced -> clearGatewayPhaseForRetry) so the phase gate does not skip a Running gateway. If #151 lands, clearing the phase no longer bypasses the gate, and release-driven fan-out to an already-converged gateway would be silently skipped. Maintainers need to decide the merge order and how release fan-out signals "re-provision" under a convergence gate (e.g. bump desired generation) rather than by clearing phase.

  • #185 specifies periodic control-plane world synchronization that explicitly includes GatewayRelease inventory polling, requires the reconcile queue to replace any "create-or-skip or active-map pattern," and constrains the shared Gateway queue so periodic sync SHALL NOT force-clear a gateway phase. This PR's design makes the opposite assumptions: release change detection lives in an ephemeral in-memory lastImage baseline with no release resync, and it introduces an out-of-band EnqueueForced (phase-clearing) enqueuer onto the shared Gateway queue. Maintainers should decide whether release change detection becomes durable/world-sync-driven and how force-clear ownership on the shared queue is bounded.

  • #200 defines a control-plane reconciliation contract requiring status/finalizer writes to be conditional on resource generation ("a stale pass SHALL NOT publish success for a newer generation") and change detection anchored to generation. This PR writes release status and detects change without any generation conditioning (in-memory lastImage). A decision is needed on whether this reconciler must conform to that contract (and therefore whether it should land before or be reworked after the contract is adopted).

// The first observation records a baseline without fanning out: a brand-new
// release has no referencing gateways yet, and on controller restart every
// release would otherwise force-reconcile every running gateway.
prev, seen := r.lastImageFor(event.ResourceID)

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.

The lastImage baseline is in-memory and WatchGatewayReleases deliberately has no startup seed, so change detection resets on every controller restart. A release whose image is edited while the controller is down is not re-observed on reconnect (the stream only carries future events), so its referencing gateways are never fanned out; and the first post-restart event for any release is classified as a baseline (seen == false) and skips fan-out.

This is harmless while release_id -> image resolution is out of scope, but it becomes a real convergence gap once gateways consume the release image. Consider deriving the change signal from persisted state (e.g. the release generation / previously-reconciled image on the resource) rather than process-local memory, or from the world-sync inventory pass, so a change across a restart is still detected. See the Cross-PR coordination note.


_, endSpan := cpotel.StartReconcileSpan(ctx, "GatewayRelease", event.Type.String())
defer func() { endSpan(nil) }()
var reconcileErr error

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.

(Refers to the active create-or-skip guard at the top of Handle.) That guard is now redundant: WatchGatewayReleases drives this handler through a per-release reconcileQueue, which already serializes work per resource, so a concurrent second Handle for the same ID should never occur. If the invariant ever did break, the guard returns nil (success), which would cause the queue to treat a genuinely dropped reconcile as done and clear any retry/backoff - masking the drop rather than requeuing it. Consider removing the map now that serialization is owned by the queue (also called out as a pattern to retire in the world-sync design).


// listGatewaysForRelease returns every gateway whose release_id references the
// given release, paginating through the API server.
func (r *GatewayReleaseReconciler) listGatewaysForRelease(ctx context.Context, releaseID string) ([]*pb.Gateway, error) {

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.

listGatewaysForRelease pages the full gateway inventory and filters by release_id in the client on every image change. That is fine at current scale (page size matches the other reconcilers), but it is O(all gateways) per release edit with no server-side filter. Worth a follow-up to add a server-side release_id filter to ListGateways if fleet sizes grow, to avoid scanning unrelated gateways.

@JuanmaBM

JuanmaBM commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-PR coordination — merge order with #151

Following up on the Amber review's cross-PR findings. Short version: this PR (HYPERSHELL-173) is still required and correct as-is — no other open PR implements GatewayRelease reconciliation (#185 and #200 are spec-only; #151 reworks the Gateway provisioning gate, not release reconciliation). Under main's current phase gate the release fan-out works correctly and is well covered by tests.

The one thing maintainers must not miss

This PR's release fan-out re-provisions a referencing gateway by routing it through the shared queue with EnqueueForcedclearGatewayPhaseForRetry, i.e. it clears gw.Phase so the gateway reconciler's phase gate (Running/Provisioning/Degraded → skip) does not drop a Running gateway.

#151 replaces that phase gate with a convergence gate:

- if gw.Phase != nil && (*gw.Phase == "Running" || "Provisioning" || "Degraded") { skip }
+ if gw.ObservedGeneration != nil && *gw.ObservedGeneration == gw.Generation { skip }

Once #151 lands, clearing gw.Phase no longer influences the gate. A release image change fanned out to an already-converged (observed_generation == generation) gateway would be silently skipped — the gateway never picks up the new image.

Requested decision

Whichever merge order maintainers pick, the release fan-out must migrate from phase-clear to bumping the gateway's desired generation (the exact mechanism #151 introduces to force a re-apply). Concretely:

Follow-up ticket to track the migration is being filed (linked to #151). The reconciler's core logic — image validation, idempotent status write-back, change detection, and which gateways to fan out to — is unaffected either way; only the signal used to request re-provision changes.

cc @jsell-rh

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.

2 participants