docs: define control plane reconciliation contract - #200
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Central YAML (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| deletion intent, and fields required to locate owned state. | ||
|
|
||
| #### Scenario: Soft-deleted record no longer exists | ||
| #### Scenario: Gateway delete event carries identity |
There was a problem hiding this comment.
I don't fully get what does "carries identity" mean
The description says that includes Gateway ID (identity), UID, namespace and ownership data...
What I understood by "event carries identity" is that the event is a light event, or anemic/skinny event which only carries an ID...
So, do you refer that it:
- Carries identities of objects to be deleted
- Or that is contains data?
- A fat event, or sometimes a more pompous name of Event-Carried State Transfer
- Which will be enough for a consumer to do their work
There was a problem hiding this comment.
Yeah it should be more accurately titled Gateway delete event carries resource snapshot or something.
Is this bad practice?
| - THEN the API SHALL reject or supersede that stale write | ||
| - AND a later pass SHALL reconcile generation 5 | ||
|
|
||
| ### Requirement: Deletion Uses Durable Finalization |
There was a problem hiding this comment.
When I reviewed @jjaggars #197 PR, there the tombstone concept for deleting managedDatabases is introduced
I was curious how aligned is to your specs here. In LLMs wordy format...
The alignment is strong and direct. Here's how they connect:
Jesse's tombstone fix (effdbce): When a ManagedDatabase delete event arrives with a resource snapshot, the reconciler now calls rememberManagedDatabase after the event arrives (not before, as in the test setup). This means a failed cleanup retains the tombstone, so a subsequent retry event carrying only the resource ID can still find the data it needs to finish cleanup.
This PR formalizes exactly that behavior as a platform-wide contract. The new reconciliation-contract.spec.md is the key document — specifically:
- "Deletion Uses Durable Finalization" — states that partial cleanup or process restart SHALL leave deletion pending and retryable. Jesse's in-memory tombstone retention is a direct implementation of this: it keeps finalization data alive across failed attempts.
- "Destructive Actions Fail Closed" — if observation fails, defer and error. Jesse's fix: cleanup failure doesn't remove the tombstone, so the next pass isn't blind.
- "Missing Event Payload Does Not Complete Cleanup" (in the updated watch-delete-events.spec.md) — if the snapshot is absent, the reconciler SHALL fetch durable state and SHALL NOT treat the missing payload as success. Jesse's tombstone is the in-memory analog of this: it's the fallback when the retry event carries no Resource.
The key tension: Jesse's tombstone is an in-memory cache — it doesn't survive a process restart. This PR's contract goes further: it specifies that the API resource itself (with a finalizer) must remain readable and listable as the durable source of truth, not a process-local cache. The updated watch-delete-events.spec.md makes this explicit: "the durable deleting resource and its finalizer are authoritative", while the snapshot in the event is just a latency optimization.
So Jesse's fix is correctly aligned in spirit — it implements the "fail closed, retain cleanup data, retry until confirmed absent" principle — but it's an intermediate step. The reconciliation contract this PR defines points toward a more robust, restart-safe finalization model where the API server (with finalizers) holds the tombstone durably, not process memory.
| #### Scenario: Actual state drifts | ||
|
|
||
| - GIVEN a resource reports healthy at its current generation | ||
| - AND an owned actual resource is later removed or changed |
There was a problem hiding this comment.
How is ownership defined?
Imagine someone creates one resource (say a namespace) and then the reconcile process wants to create/update that same resource... does it take ownership of it?
In the previous scenario we also mention the reconciliation SHALL "adopt" that resource
There was a problem hiding this comment.
Should we define ownership at this spec level, or should we leave it to individual resource reconcilers how they'd like to define ownership?
|
|
||
| The control plane SHALL run at most one reconcile pass for one resource UID at a time. It MAY reconcile different resources concurrently with a bounded worker count. A slow or failed resource SHALL NOT block watch consumption or unrelated resources. | ||
|
|
||
| A failed required operation SHALL return an error and SHALL be retried with bounded exponential backoff. A newer event SHALL cause another pass against current state. A delete request SHALL take precedence over older queued updates for the same UID. |
There was a problem hiding this comment.
A delete request SHALL take precedence over older queued updates for the same UID
The word "older" makes me wonder... is it possible to receive an update event after a delete request?
I guess the API will reject updates to a resource marked for deletion, so my case may be not possible.... or maybe request come out of order and it is still possible
There was a problem hiding this comment.
Yeah I'm not sure how it works today, but agree that if something is either marked for deletion, or actually deleted, update events for that object should 404 or something
|
|
||
| A failed required operation SHALL return an error and SHALL be retried with bounded exponential backoff. A newer event SHALL cause another pass against current state. A delete request SHALL take precedence over older queued updates for the same UID. | ||
|
|
||
| If more than one control-plane replica can mutate state, the deployment SHALL provide one active leader or equivalent cross-process exclusion. |
There was a problem hiding this comment.
Are there control-plane instances that do not mutate state?
There was a problem hiding this comment.
no, I think that's just saying that in the event we have more than 1 replica of a control plane, there must be some leader/follower mechanism
| - GIVEN a pass observes generation 4 | ||
| - AND desired state changes to generation 5 before the pass records success | ||
| - WHEN the pass attempts to write status for generation 4 | ||
| - THEN the API SHALL reject or supersede that stale write |
There was a problem hiding this comment.
What does it mean "or supersede" can the API behave differently than rejecting?
| - GIVEN deletion requires several cleanup operations | ||
| - WHEN one required operation fails | ||
| - THEN the finalizer SHALL remain | ||
| - AND reconciliation SHALL retry until all required state is confirmed absent |
There was a problem hiding this comment.
This makes me wonder if that retry means there is a gorutine/thread alive retrying the deletion... or if it returns and retries on a later "resync" event
|
|
||
| Every reconciled API resource SHALL have an immutable UID, a resource version that changes on every write, and a generation that changes only when desired state changes. Controller status SHALL identify the generation it observed. | ||
|
|
||
| Status and finalizer writes SHALL be conditional on the resource identity and relevant version or generation. A stale pass SHALL NOT publish success or complete deletion for a newer generation. Controllers SHALL update only fields they own and SHALL NOT use whole-resource replacement for independent status fields. |
There was a problem hiding this comment.
I don't know if "finalizers" here refer to:
- Kubernetes finalizers in created resources
- Another concept of finalizer tied to HyperShell API objects
There was a problem hiding this comment.
Controllers SHALL update only fields they own
Does this mean different reconcilers can modify the same resource?
Amber reviewStatus: Complete VerdictCOMMENT (do not merge without cross-PR coordination). This is a clean, well-structured, spec-only change that establishes a genuinely useful canonical reconciliation contract; the specification text itself is sound. However, it directly overlaps three other open control-plane spec PRs and, in a few places, rewrites specs out from under already-shipped behavior (the Hi, Amber here. I loaded SummaryThe new Findings1. [Major] The prior spec described soft-delete + an unscoped DAO read ( 2. [Major] Contract mandates data-model primitives (immutable UID, per-write resource version, generation, finalizers) that "Resource Versions Prevent Stale Commits" and "Deletion Uses Durable Finalization" require every reconciled API resource to carry an immutable UID, a resource version that changes on every write, a generation that changes only on desired-state change, deletion intent, and a control-plane finalizer. None of these exist in 3. [Minor] New canonical spec is not registered in the Spec Registry — Spec Completeness (
4. [Minor] The existing e2e expectation (" Cross-PR coordinationI compared PR #200 against the other 23 open PRs (listed below). Three are in the same control-plane-reconciliation design cluster and have material conflicts that need a maintainer decision; the rest do not conflict. Other open PRs reviewed: #216 fix(console) OpenShift Route; #214 Hypershell-112 UI; #212 e2e performance harness; #211 fix(kind) metrics/DNS; #210 gateway-matched CLI install; #209 Dashboard UI; #208 sandbox-connect UI; #207 reconcile-to-request trace correlation; #206 hsctl login; #201 Red Hat openshell images; #194 adopt upstream OpenShell Helm chart; #189/#188/#135 PatternFly/react-intl dep bumps; #185 periodic world synchronization; #182 JWT audience; #179 reconcile Keycloak clients on gated gateways; #151 gate re-provisioning on desired-state convergence; #150 kind LOCAL_IMAGES; #148 OpenShell branch build spec; #109 security tools; #75/#73 dep bumps. Material conflict — PR #151 ( Material conflict — PR #185 ( Related, softer — PR #179 ( Data-model coordination (spans #151 + #185 + #200): Three PRs concurrently introduce resource-versioning vocabulary — No material conflicts were found with any other open PR. Convention Checklist
Findings Summary (ordered by severity, highest first):
Confidence: High on the cross-PR conflicts (#151/#185 edit the same requirements/files, verified from their diffs) and on the |
jsell-rh
left a comment
There was a problem hiding this comment.
Verdict
COMMENT (do not merge without cross-PR coordination). This is a clean, well-structured, spec-only change that establishes a genuinely useful canonical reconciliation contract; the specification text itself is sound. However, it directly overlaps three other open control-plane spec PRs and, in a few places, rewrites specs out from under already-shipped behavior (the GetUnscoped delete-enrichment path) and mandates data-model primitives (finalizers, UID/resourceVersion/generation) that no spec currently defines. Those coordination items should be resolved by maintainers before this merges.
Hi, Amber here. I loaded CLAUDE.md, the security and control-plane conventions specs, and the review guidance, then reviewed all seven changed spec files at a5dbb5c. Because this PR is spec-only, most code-level conventions (panics, error wrapping, SecurityContext, secret handling) are N/A; the review focuses on spec consistency, plan/change-order, and cross-PR conflicts as requested.
Summary
The new reconciliation-contract.spec.md is a strong, coherent, level-based contract and the cross-references added to conventions.spec.md and control-plane.spec.md are appropriate. The risk in this PR is not its prose but its coordination surface: it re-specifies the phase gate, periodic resync, resource versioning, and delete/finalization semantics that are simultaneously being defined by PRs #151 and #185, and it deprecates a delete-enrichment design (GetUnscoped) that is already merged and running.
Findings
1. [Major] watch-delete-events.spec.md rewrite supersedes a shipped design without a migration/transition note — Spec Consistency / Change Order (specs/platform/watch-delete-events.spec.md L12-L44)
The prior spec described soft-delete + an unscoped DAO read (GetUnscoped) to enrich delete events, plus removal of the in-memory namespace cache. That design is already implemented: components/api-server/plugins/gateways/{dao.go,service.go,grpc_handler.go} and components/api-server/plugins/roleBindings/grpc_handler.go all use GetUnscoped to load soft-deleted rows for delete events. The rewrite replaces this with a finalizer-based durable-deletion model and deletes the Design Decisions table that recorded the shipped rationale. No finalizer support exists anywhere in the codebase today (grep finds finalizers only in these specs). The spec should either (a) keep/annotate the GetUnscoped approach as the current transitional mechanism, or (b) add an explicit note that finalizer-based deletion supersedes it and that GetUnscoped is now legacy — otherwise the spec silently contradicts running behavior.
2. [Major] Contract mandates data-model primitives (immutable UID, per-write resource version, generation, finalizers) that data-model.spec.md does not define — Spec Completeness (specs/standards/control-plane/reconciliation-contract.spec.md L58, L70-L76)
"Resource Versions Prevent Stale Commits" and "Deletion Uses Durable Finalization" require every reconciled API resource to carry an immutable UID, a resource version that changes on every write, a generation that changes only on desired-state change, deletion intent, and a control-plane finalizer. None of these exist in data-model.spec.md, and the API server is PostgreSQL/rh-trex-backed (today it orders on updated_at). As written, the canonical contract is currently unsatisfiable and floats above the data model. It should reference the data-model additions that provide these primitives (see cross-PR note on #151 and #185, which each introduce part of this vocabulary under different names).
3. [Minor] New canonical spec is not registered in the Spec Registry — Spec Completeness (specs/index.spec.md, unchanged)
specs/index.spec.md is the machine-readable registry the /reconcile skill consumes, and it already lists standards/control-plane/conventions.spec.md. The new standards/control-plane/reconciliation-contract.spec.md is not added there, so autonomous reconciliation and the dependency graph won't see the contract. PR #185 registered its new spec in the same index; this PR should add a row (Domain standards, Components CP, Depends On control-plane, data-model).
4. [Minor] e2e-testing.spec.md scenario now asserts finalization-based 404 that the running system does not yet produce — Spec Consistency / Change Order (specs/platform/e2e-testing.spec.md L336-L383)
The existing e2e expectation ("404 once the control plane has processed the delete event") is changed to "404 once the control plane completes cleanup and removes its finalizer." This is consistent with the new deletion model but describes behavior that depends on the unimplemented finalizer capability. Fine as a forward-looking target, but it should land in step with the finalizer implementation so the e2e suite's desired state isn't ahead of what any component can satisfy. This is not a test-assertion regression (it's a spec scenario, not a _test.go change), but it is a guarantee change worth calling out explicitly.
Cross-PR coordination
I compared PR #200 against the other 23 open PRs (listed below). Three are in the same control-plane-reconciliation design cluster and have material conflicts that need a maintainer decision; the rest do not conflict.
Other open PRs reviewed: #216 fix(console) OpenShift Route; #214 Hypershell-112 UI; #212 e2e performance harness; #211 fix(kind) metrics/DNS; #210 gateway-matched CLI install; #209 Dashboard UI; #208 sandbox-connect UI; #207 reconcile-to-request trace correlation; #206 hsctl login; #201 Red Hat openshell images; #194 adopt upstream OpenShell Helm chart; #189/#188/#135 PatternFly/react-intl dep bumps; #185 periodic world synchronization; #182 JWT audience; #179 reconcile Keycloak clients on gated gateways; #151 gate re-provisioning on desired-state convergence; #150 kind LOCAL_IMAGES; #148 OpenShell branch build spec; #109 security tools; #75/#73 dep bumps.
Material conflict — PR #151 (spec(control-plane): gate gateway re-provisioning on desired-state convergence): Both PRs edit the same two files and the same two requirements. In openshell-gateway-health.spec.md, #200 replaces the requirement Health Reconciliation Not Suppressed By Phase with Health Status Does Not Suppress Reconciliation, while #151 replaces the same requirement with Provisioning Gate Keyed On Desired State. In control-plane.spec.md both rewrite the Status Synchronization requirement. Beyond the textual collision this is a design conflict: #151 gates re-apply strictly on observed_generation == generation and explicitly defers out-of-band drift repair to a later decision; #200's contract requires drift repair even for out-of-band changes ("If owned actual state changes after initial convergence, a later pass SHALL repair it" / "an owned actual resource is later removed or changed … it SHALL restore the desired state"). Maintainers need to decide: is out-of-band drift repair in-scope now (#200) or deferred (#151), and which requirement heading/data model is canonical. They also each add versioning fields (#151 persists generation/observed_generation; #200 requires UID + resourceVersion + generation) that must be reconciled into one data model.
Material conflict — PR #185 (docs(control-plane): specify periodic world synchronization): #200's "Desired State Is Authoritative" requirement (initial list + periodic resync as recovery) and #185's entire control-plane-world-sync.spec.md specify the same mechanism at different levels of detail, and both edit control-plane.spec.md and data-model.spec.md. #185 introduces a resource_revision primitive (explicitly "transport/concurrency metadata rather than persisted domain fields") plus an inventory watermark; #200 requires "a resource version that changes on every write" as a property of the resource. These are the same concept with divergent names and layering. Maintainers should decide ownership: make reconciliation-contract.spec.md the umbrella contract and have control-plane-world-sync.spec.md implement it, and pick a single name/semantic for the resource-version primitive so the API contract isn't defined twice.
Related, softer — PR #179 (fix(control-plane): reconcile existing Keycloak clients on gated gateways): #179 is an implementation hotfix that depends on the current phase-gate early-return and already documents its interaction with #151. It doesn't touch the files #200 changes, but its correctness assumption (phase-gate return exists) is exactly what #151/#200 are redesigning. No direct conflict, but it belongs to the same decision cluster; whatever gate model maintainers pick should confirm where #179's lightweight Keycloak pass runs.
Data-model coordination (spans #151 + #185 + #200): Three PRs concurrently introduce resource-versioning vocabulary — generation/observed_generation (persisted, #151), resource_revision/watermark (transport metadata, #185), and UID + resourceVersion + generation + finalizers (contract-level, #200). These must converge into one coherent data model and one set of field names; merging any two without alignment will leave the data-model spec internally inconsistent.
No material conflicts were found with any other open PR.
Convention Checklist
| Convention | Result |
|---|---|
| Conventional commit message | Pass |
| Spec internal links resolve | Pass |
git diff --check (whitespace) clean |
Pass |
| New canonical spec registered in Spec Registry | Fail |
| Spec consistent with shipped behavior / data model | Fail |
Findings Summary (ordered by severity, highest first):
- [Major]
watch-delete-events.spec.mdrewrite supersedes shippedGetUnscopeddelete-enrichment without a migration note - Spec Consistency / Change Order (L12-L44) - [Major] Contract requires UID/resourceVersion/generation/finalizer primitives absent from the data model - Spec Completeness (reconciliation-contract L58, L70-L76)
- [Minor] New canonical spec not registered in
specs/index.spec.mdSpec Registry - Spec Completeness - [Minor]
e2e-testing.spec.mdnow asserts finalization-based404not yet producible - Spec Consistency / Change Order (L336-L383)
Confidence: High on the cross-PR conflicts (#151/#185 edit the same requirements/files, verified from their diffs) and on the GetUnscoped spec/code divergence (verified in the tree). Medium on severity of the data-model gap, since this is an intentionally forward-looking contract.
| - THEN its delete event SHALL include the Gateway ID, UID, namespace, and ownership data | ||
|
|
||
| ### Requirement: Control Plane SHALL Prefer Event Resource Over In-Memory Cache | ||
| ### Requirement: Deleting Resources Remain Readable |
There was a problem hiding this comment.
[Major] Spec supersedes shipped behavior without a transition note. The prior spec's soft-delete + unscoped-read enrichment is already implemented and running: components/api-server/plugins/gateways/{dao.go,service.go,grpc_handler.go} and components/api-server/plugins/roleBindings/grpc_handler.go all call GetUnscoped to load soft-deleted rows for delete events. This rewrite replaces that with a finalizer/durable-deleting model and drops the Design Decisions table that recorded the shipped rationale, but no finalizer support exists anywhere in the codebase (grep finds finalizers only in these spec files). Please either keep/annotate GetUnscoped as the current transitional mechanism or add an explicit note that finalizer-based deletion supersedes it, so the spec doesn't silently contradict running behavior.
|
|
||
| ### Requirement: Resource Versions Prevent Stale Commits | ||
|
|
||
| Every reconciled API resource SHALL have an immutable UID, a resource version that changes on every write, and a generation that changes only when desired state changes. Controller status SHALL identify the generation it observed. |
There was a problem hiding this comment.
[Major] Contract mandates data-model primitives that no spec defines. This requires every reconciled API resource to carry an immutable UID, a resource version that changes on every write, and a generation that changes only on desired-state change; the deletion requirement below (L70+) adds deletion intent and a control-plane finalizer. None of these exist in data-model.spec.md, and the API server orders on updated_at today. As written the canonical contract is currently unsatisfiable and floats above the data model. Please reference the data-model additions that provide these primitives. Note: PR #151 introduces generation/observed_generation and PR #185 introduces resource_revision/watermark under different names — these need to converge into one data model.
| - THEN the control plane SHALL skip re-applying the gateway manifests | ||
| - BUT it SHALL still update the `phase` to `Degraded` if the workload is | ||
| observed unhealthy | ||
| ### Requirement: Health Status Does Not Suppress Reconciliation |
There was a problem hiding this comment.
[Major cross-PR] Competing redesign of this exact requirement with PR #151. #151 replaces the previous Health Reconciliation Not Suppressed By Phase requirement with Provisioning Gate Keyed On Desired State (gates re-apply strictly on observed_generation == generation and explicitly defers out-of-band drift repair). This PR replaces the same requirement with a drift-repair mandate that requires repairing out-of-band changes. These can't both be canonical as-is — maintainers must decide whether out-of-band drift repair is in-scope now or deferred, and pick one requirement/heading. See the Cross-PR coordination section.
| ### Requirement: Status Synchronization | ||
|
|
||
| The control plane SHALL continuously reconcile the `phase` and `status` fields of Gateway resources in the API server to reflect actual cluster state, even after a Gateway has reached `Running`. The phase gate that prevents redundant re-provisioning SHALL NOT suppress these health updates. Full lifecycle semantics are defined in the [health spec](./openshell-gateway-health.spec.md). | ||
| The control plane SHALL continuously reconcile the `phase` and `status` fields of Gateway resources in the API server to reflect actual cluster state, even after a Gateway has reached `Running`. It MAY avoid unconditional manifest writes, but Gateway phase SHALL NOT suppress health observation or periodic drift repair. Full lifecycle semantics are defined in the [health spec](./openshell-gateway-health.spec.md). |
There was a problem hiding this comment.
[Cross-PR] This Status Synchronization requirement is also rewritten by PR #151, which gates re-application on observed_generation == generation convergence rather than on "periodic drift repair." Both edits target the same requirement; they need to be reconciled into a single wording/model before either merges.
| - AND its absence SHALL be reported as a failure, because the GC check cannot then be validated | ||
|
|
||
| #### Scenario: Gateway record removed after delete | ||
| #### Scenario: Gateway record removed after finalization |
There was a problem hiding this comment.
[Minor] Forward-looking guarantee change. The e2e expectation now asserts 404 follows finalizer removal, which depends on the unimplemented finalizer capability; the running system produces 404 on soft-delete processing. Fine as a target, but land it in step with the finalizer implementation so the e2e desired state isn't ahead of any component's behavior.
Executive Summary
Specification Summary
specs/standards/control-plane/reconciliation-contract.spec.mdas the canonical behavior contract.404follows successful finalization, not only delete-event processing.Validation
git diff --check origin/main...HEADpassed.Jira
No Jira ticket is associated with this specification change.
Scope
This pull request defines desired reconciliation behavior. Control-plane and API implementation changes are separate work.
Checklist