From 72a1271f29bd36cce025fc421610e6ff115cd36e Mon Sep 17 00:00:00 2001 From: Rafael Benevides Date: Tue, 5 May 2026 20:12:55 -0300 Subject: [PATCH] HYPERFLEET-1017 - refactor: rename aggregated Available condition to LastKnownReconciled Rename the API-computed aggregated condition from Available to LastKnownReconciled. The adapter-level Available condition remains unchanged. Includes backward compatibility for legacy DB records, differentiated reason/message for false conditions, minItems: 3 enforcement in OpenAPI schemas, and updated documentation. --- docs/api-operator-guide.md | 48 +++++++------- docs/api-resources.md | 60 ++++++++++++++---- docs/search.md | 4 +- openapi/openapi.yaml | 38 +++++------ pkg/api/status_types.go | 13 ++-- pkg/services/aggregation.go | 100 ++++++++++++++++++++--------- pkg/services/aggregation_test.go | 105 +++++++++++++++++++++---------- pkg/services/cluster.go | 4 +- pkg/services/cluster_test.go | 12 ++-- pkg/services/node_pool_test.go | 10 +-- pkg/services/status_helpers.go | 4 +- 11 files changed, 261 insertions(+), 137 deletions(-) diff --git a/docs/api-operator-guide.md b/docs/api-operator-guide.md index 12c4b07c..8bf5e92d 100644 --- a/docs/api-operator-guide.md +++ b/docs/api-operator-guide.md @@ -17,8 +17,8 @@ A practical guide for deploying, configuring, and operating the HyperFleet API c - [Rules to accept/discard adapter reports](#rules-to-acceptdiscard-adapter-reports) - [Computing `observed_generation`](#computing-observed_generation) - [Computing `status.conditions[type==Ready].last_updated_time`](#computing-statusconditionstypereadylast_updated_time) - - [Computing `status.conditions[type==Available].last_updated_time`](#computing-statusconditionstypeavailablelast_updated_time) - - [Computing `last_transition_time` for both `Ready` and `Available`](#computing-last_transition_time-for-both-ready-and-available) + - [Computing `status.conditions[type==LastKnownReconciled].last_updated_time`](#computing-statusconditionstypelastknownreconciledlast_updated_time) + - [Computing `last_transition_time` for both `Ready` and `LastKnownReconciled`](#computing-last_transition_time-for-both-ready-and-lastknownreconciled) 3. [Configuration Reference](#3-configuration-reference) - [Adapter Requirements (REQUIRED)](#31-adapter-requirements-required) - [Database Configuration](#32-database-configuration) @@ -108,7 +108,7 @@ Resource (e.g., Cluster) 2. **Automatic Version Tracking (generation)**: Every time you update the `spec`, the API automatically increments the `generation` counter. This allows distributed adapters to detect when they need to reconcile infrastructure changes. -3. **Observed State (status)**: Adapters report their progress and results back to the API via status endpoints. The API aggregates these reports into unified resource-level conditions (e.g., `Ready`, `Available`). +3. **Observed State (status)**: Adapters report their progress and results back to the API via status endpoints. The API aggregates these reports into unified resource-level conditions (e.g., `Ready`, `LastKnownReconciled`). 4. **Filtering (labels)**: Labels are key-value pairs you can attach to resources for organization and filtering (e.g., `environment: production`, `region: us-east-1`). E.g., Sentinel instances can define resource selectors based on labels to watch specific subsets of resources, enabling horizontal scaling across multiple Sentinel deployments. @@ -147,7 +147,13 @@ GET /api/hyperfleet/v1/clusters/{id} "status": { "conditions": [ { - "type": "Available", + "type": "Reconciled", + "status": "True", + "observed_generation": 1, + "last_transition_time": "2026-03-10T07:56:35Z" + }, + { + "type": "LastKnownReconciled", "status": "True", "observed_generation": 1, "last_transition_time": "2026-03-10T07:56:35Z" @@ -164,7 +170,7 @@ GET /api/hyperfleet/v1/clusters/{id} "updated_time": "2026-03-10T07:56:35Z" } -→ API returns aggregated status with Available and Ready conditions +→ API returns aggregated status with Reconciled, LastKnownReconciled, and Ready conditions # 3. View adapter statuses GET /api/hyperfleet/v1/clusters/{id}/statuses @@ -323,7 +329,7 @@ HyperFleet API aggregates the condition values reported by adapters associated w | Condition | Meaning | When True | |-----------|---------|-----------| | **Ready** | Resource is fully reconciled at current spec | All registered adapters report `Available=True` at the **current** `resource.spec.generation` | -| **Available** | Resource is operational at any known good configuration | All registered adapters report `Available=True` (at any generation) | +| **LastKnownReconciled** | Resource is operational at any known good configuration | All registered adapters report `Available=True` for a common `observed_generation`, or sticky-true is preserved when adapters are transitioning to a new generation | **Note**: The meaning of the field `last_updated_time` for the aggregated conditions has special meaning. It doesn't reflect the last time it was updated from adapters but the OLDEST time it can be considered to be valid. @@ -337,16 +343,16 @@ The resource `status.conditions` array contains: - `True`: All required adapters `conditions[type=Available].status==True` at current spec generation - `False`: Any other combination of conditions -- **Available** - The resource is reconciled at a generation of the spec, current or past +- **LastKnownReconciled** - The resource is reconciled at a generation of the spec, current or past - This condition is stateful meaning that is computed taking into account its previous values of `status` and `observed_generation` - This condition is "best effort", since there are cases that can not be covered correctly. - `True`: - All required adapters `conditions[type=Available].status==True` for the same `observed_generation` - Current value `status==True` and required adapters `conditions[type=Available]` at mixed `observed_generation` - `False`: Any other combination of conditions - - e.g. `Available=True` for `observed_generation==1` - - One adapter reports `Available=False` for `observed_generation=1` `Available` transitions to `False` - - One adapter reports `Available=False` for `observed_generation=2` `Available` keeps its `True` status + - e.g. `LastKnownReconciled=True` for `observed_generation==1` + - One adapter reports `Available=False` for `observed_generation=1` `LastKnownReconciled` transitions to `False` + - One adapter reports `Available=False` for `observed_generation=2` `LastKnownReconciled` keeps its `True` status - One **per-adapter** condition for each required adapter that has reported, mirroring the adapter's `conditions[type=Available]`: - `type`: Derived from the adapter name — PascalCase with `Successful` suffix (e.g., `adapter1` → `Adapter1Successful`, `my-adapter` → `MyAdapterSuccessful`) @@ -392,10 +398,10 @@ These are API examples for a resource and resource statuses: "last_transition_time": "2021-01-01T10:00:00Z" }, { - "type": "Available", + "type": "LastKnownReconciled", "status": "True", - "reason": "All adapters reported Available True for the same generation", - "message": "All adapters reported Available True for the same generation", + "reason": "AllAdaptersReconciled", + "message": "All required adapters report Available=True for the tracked generation", "observed_generation": 1, "created_time": "2021-01-01T10:00:00Z", "last_updated_time": "2021-01-01T10:00:00Z", @@ -497,23 +503,23 @@ These are API examples for a resource and resource statuses: When a resource is created: - Initial `generation` is 1 and aggregated conditions are evaluated -- `observed_generation` for `Ready` and `Available` aggregated conditions is 1 -- `last_updated_time` and `last_transition_time` for `Ready` and `Available` aggregated conditions is `resource.last_updated_time` +- `observed_generation` for `Ready` and `LastKnownReconciled` aggregated conditions is 1 +- `last_updated_time` and `last_transition_time` for `Ready` and `LastKnownReconciled` aggregated conditions is `resource.last_updated_time` When a resource is changed: - `resource.generation` gets incremented and aggregated conditions are re-evaluated - `status.conditions[type==Ready].observed_generation` always follows `resource.generation` -- `status.conditions[type==Available].observed_generation` changes when all required adapters `condition[type==Available].observed_generation==resource.generation` otherwise remains unchanged. +- `status.conditions[type==LastKnownReconciled].observed_generation` changes when all required adapters `condition[type==Available].observed_generation==resource.generation` otherwise remains unchanged. ##### Computing `observed_generation` - For `Ready` it always matches `resource.generation` -- For `Available`: +- For `LastKnownReconciled`: - If all required adapters have a common `observed_generation` it will match the common value - If required adapters have mixed `observed_generation` - - If `Available` is `True`, `observed_generation` remains at its current value - - If `Available` is `False`, `observed_generation` will get the value of the `max(condition[type==Available].observed_generation)` + - If `LastKnownReconciled` is `True`, `observed_generation` remains at its current value + - If `LastKnownReconciled` is `False`, `observed_generation` will get the value of the `max(condition[type==Available].observed_generation)` ##### Computing `status.conditions[type==Ready].last_updated_time` @@ -526,14 +532,14 @@ The meaning of `last_updated_time` in the aggregated conditions refers to the ne - Why do we want to keep the "oldest" value? because if it is too old, we need to trigger a reconciliation - When some required adapter conditions `condition[type==Available].observed_generation==resource.generation` then `last_updated_time=min(statuses[].conditions[type==Available && observed_generation==resource.generation].observed_time)` -##### Computing `status.conditions[type==Available].last_updated_time` +##### Computing `status.conditions[type==LastKnownReconciled].last_updated_time` - If all required adapters have `condition[type==Available].observed_generation` at the same value then `last_updated_time=min(statuses[].conditions[type==Available].observed_time)` - If not all required adapters have `condition[type==Available].observed_generation` at the same value: - If any adapter at current `observed_generation==X` has `conditions[type==Available].status==False` then `last_updated_time=min(adapters[type==Available && observed_generation==X].observed_time` - In any other case `last_updated_time` is kept unchanged -##### Computing `last_transition_time` for both `Ready` and `Available` +##### Computing `last_transition_time` for both `Ready` and `LastKnownReconciled` - Meaning is last time this condition’s status (True / False) changed, regardless of the existing and new `observed_generation` - This property is stateful since it relies on the existing value to determine if there has been a transition diff --git a/docs/api-resources.md b/docs/api-resources.md index b69dd7f6..a78af764 100644 --- a/docs/api-resources.md +++ b/docs/api-resources.md @@ -53,7 +53,17 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses "status": { "conditions": [ { - "type": "Available", + "type": "Reconciled", + "status": "False", + "reason": "AwaitingAdapters", + "message": "Waiting for adapters to report status", + "observed_generation": 1, + "created_time": "2025-01-01T00:00:00Z", + "last_updated_time": "2025-01-01T00:00:00Z", + "last_transition_time": "2025-01-01T00:00:00Z" + }, + { + "type": "LastKnownReconciled", "status": "False", "reason": "AwaitingAdapters", "message": "Waiting for adapters to report status", @@ -79,7 +89,7 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses -**Note**: Status initially has `Available=False` and `Ready=False` conditions until adapters report status. +**Note**: Status initially has `Reconciled=False`, `LastKnownReconciled=False`, and `Ready=False` conditions until adapters report status. ### Get Cluster @@ -108,10 +118,20 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses "status": { "conditions": [ { - "type": "Available", + "type": "Reconciled", "status": "True", - "reason": "ResourceAvailable", - "message": "Cluster is accessible", + "reason": "AllAdaptersReconciled", + "message": "All adapters report ready at current generation", + "observed_generation": 1, + "created_time": "2025-01-01T00:00:00Z", + "last_updated_time": "2025-01-01T00:00:00Z", + "last_transition_time": "2025-01-01T00:00:00Z" + }, + { + "type": "LastKnownReconciled", + "status": "True", + "reason": "AllAdaptersReconciled", + "message": "All required adapters report Available=True for the tracked generation", "observed_generation": 1, "created_time": "2025-01-01T00:00:00Z", "last_updated_time": "2025-01-01T00:00:00Z", @@ -304,7 +324,17 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses "status": { "conditions": [ { - "type": "Available", + "type": "Reconciled", + "status": "False", + "reason": "AwaitingAdapters", + "message": "Waiting for adapters to report status", + "observed_generation": 1, + "created_time": "2025-01-01T00:00:00Z", + "last_updated_time": "2025-01-01T00:00:00Z", + "last_transition_time": "2025-01-01T00:00:00Z" + }, + { + "type": "LastKnownReconciled", "status": "False", "reason": "AwaitingAdapters", "message": "Waiting for adapters to report status", @@ -361,10 +391,17 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses "status": { "conditions": [ { - "type": "Available", + "type": "Reconciled", + "status": "True", + "reason": "AllAdaptersReconciled", + "message": "All adapters report ready at current generation", + "observed_generation": 1 + }, + { + "type": "LastKnownReconciled", "status": "True", - "reason": "ResourceAvailable", - "message": "NodePool is accessible", + "reason": "AllAdaptersReconciled", + "message": "All required adapters report Available=True for the tracked generation", "observed_generation": 1 }, { @@ -446,8 +483,9 @@ See **[search.md](search.md)** for complete documentation. The status object contains synthesized conditions computed from adapter reports: - `conditions` - Array of resource conditions, including: - - **Available** - Whether resource is running at any known good configuration - - **Ready** - Whether all adapters have processed current spec generation + - **Reconciled** - Whether all adapters have reconciled at the current spec generation + - **LastKnownReconciled** - Whether resource is running at any known good configuration + - **Ready** *(deprecated — use Reconciled)* - Whether all adapters have processed current spec generation - Additional conditions from adapters (with `observed_generation`, timestamps) ### Condition Fields diff --git a/docs/search.md b/docs/search.md index 5f96460d..ac54c784 100644 --- a/docs/search.md +++ b/docs/search.md @@ -98,14 +98,14 @@ Label keys must contain only lowercase letters (a-z), digits (0-9), and undersco Query resources by status conditions: `status.conditions.=''` -Condition types must be PascalCase (`Ready`, `Available`) and status must be `True` or `False` for resource conditions. +Condition types must be PascalCase (`Ready`, `LastKnownReconciled`) and status must be `True` or `False` for resource conditions. **Note:** Only the `=` operator is supported for condition queries. Other operators (`!=`, `<`, `>`, `in`, etc.) will return an error. The `NOT` operator is not supported with condition queries (`status.conditions.` or `status.conditions..`) and will return a `400 Bad Request` error. Use the inverse condition value instead (e.g., `status.conditions.Ready='False'` rather than `NOT status.conditions.Ready='True'`). ```bash # Find available clusters curl -G "http://localhost:8000/api/hyperfleet/v1/clusters" \ - --data-urlencode "search=status.conditions.Available='True'" + --data-urlencode "search=status.conditions.LastKnownReconciled='True'" # Find clusters that are not ready curl -G "http://localhost:8000/api/hyperfleet/v1/clusters" \ diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2fd61202..5ce61e35 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -50,7 +50,7 @@ paths: **Note**: The `status` object in the response is read-only and computed by the service. It is NOT part of the request body. Initially, - status.conditions will include mandatory "Available", "Ready" and "Reconciled" conditions. + status.conditions will include mandatory "LastKnownReconciled", "Ready" and "Reconciled" conditions. parameters: [] responses: '201': @@ -189,10 +189,10 @@ paths: created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' last_transition_time: '2021-01-01T10:00:00Z' - - type: Available + - type: LastKnownReconciled status: 'True' - reason: All adapters reported Available True for the same generation - message: All adapters reported Available True for the same generation + reason: AllAdaptersReconciled + message: All required adapters report Available=True for the tracked generation observed_generation: 2 created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' @@ -394,10 +394,10 @@ paths: created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' last_transition_time: '2021-01-01T10:00:00Z' - - type: Available + - type: LastKnownReconciled status: 'True' - reason: All adapters reported Available True for the same generation - message: All adapters reported Available True for the same generation + reason: AllAdaptersReconciled + message: All required adapters report Available=True for the tracked generation observed_generation: 2 created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' @@ -1075,10 +1075,10 @@ components: created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' last_transition_time: '2021-01-01T10:00:00Z' - - type: Available + - type: LastKnownReconciled status: 'True' - reason: All adapters reported Available True for the same generation - message: All adapters reported Available True for the same generation + reason: AllAdaptersReconciled + message: All required adapters report Available=True for the tracked generation observed_generation: 1 created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' @@ -1192,14 +1192,14 @@ components: type: array items: $ref: '#/components/schemas/ResourceCondition' - minItems: 2 + minItems: 3 description: |- List of status conditions for the cluster. - **Mandatory conditions**: + **Mandatory conditions**: - `type: "Ready"` *(deprecated — use Reconciled)*: Whether all adapters report successfully at the current generation. - `type: "Reconciled"`: Whether the resource's desired state has been fully reconciled by all adapters at the current generation. - - `type: "Available"`: Aggregated adapter result for a common observed_generation. + - `type: "LastKnownReconciled"`: Aggregated adapter result for a common observed_generation. Sticky — stays True as long as all required adapters were reconciled at a common observed generation, even if a new generation is being processed. These conditions are present immediately upon resource creation. description: |- @@ -1374,10 +1374,10 @@ components: created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' last_transition_time: '2021-01-01T10:00:00Z' - - type: Available + - type: LastKnownReconciled status: 'True' - reason: All adapters reported Available True for the same generation - message: All adapters reported Available True for the same generation + reason: AllAdaptersReconciled + message: All required adapters report Available=True for the tracked generation observed_generation: 1 created_time: '2021-01-01T10:00:00Z' last_updated_time: '2021-01-01T10:00:00Z' @@ -1558,14 +1558,14 @@ components: type: array items: $ref: '#/components/schemas/ResourceCondition' - minItems: 2 + minItems: 3 description: |- List of status conditions for the nodepool. - **Mandatory conditions**: + **Mandatory conditions**: - `type: "Ready"` *(deprecated — use Reconciled)*: Whether all adapters report successfully at the current generation. - `type: "Reconciled"`: Whether the resource's desired state has been fully reconciled by all adapters at the current generation. - - `type: "Available"`: Aggregated adapter result for a common observed_generation. + - `type: "LastKnownReconciled"`: Aggregated adapter result for a common observed_generation. Sticky — stays True as long as all required adapters were reconciled at a common observed generation, even if a new generation is being processed. These conditions are present immediately upon resource creation. description: |- diff --git a/pkg/api/status_types.go b/pkg/api/status_types.go index a65f102e..c1050ee3 100644 --- a/pkg/api/status_types.go +++ b/pkg/api/status_types.go @@ -28,12 +28,13 @@ func (s AdapterConditionStatus) IsValid() bool { // Condition type constants const ( - ConditionTypeAvailable = "Available" - ConditionTypeApplied = "Applied" - ConditionTypeHealth = "Health" - ConditionTypeReady = "Ready" - ConditionTypeReconciled = "Reconciled" - ConditionTypeFinalized = "Finalized" + ConditionTypeAvailable = "Available" + ConditionTypeLastKnownReconciled = "LastKnownReconciled" + ConditionTypeApplied = "Applied" + ConditionTypeHealth = "Health" + ConditionTypeReady = "Ready" + ConditionTypeReconciled = "Reconciled" + ConditionTypeFinalized = "Finalized" ) // ResourceCondition represents a condition of a resource diff --git a/pkg/services/aggregation.go b/pkg/services/aggregation.go index b67e9004..fd7fbb38 100644 --- a/pkg/services/aggregation.go +++ b/pkg/services/aggregation.go @@ -25,15 +25,21 @@ const ( ) // fixedConditionCount is the number of top-level conditions always present in resource status: -// Ready (deprecated), Reconciled, and Available. +// Ready (deprecated), Reconciled, and LastKnownReconciled. const fixedConditionCount = 3 -// reasonMissingRequiredAdapters is the reason code for the Ready condition when one or more -// required adapters have not yet reported Available=True at the current resource generation. -const reasonMissingRequiredAdapters = "MissingRequiredAdapters" -const reasonAllAdaptersReconciled = "All required adapters reported Available=True or Finalized=True " + - "at the current generation" -const reasonWaitingForChildResources = "WaitingForChildResources" +const ( + reasonReadyAllReconciled = "ReadyAllReconciled" + reasonReadyMissingAdapters = "ReadyMissingAdapters" + reasonReadyWaitingForChildren = "ReadyWaitingForChildren" +) + +const ( + reasonLKRAllReconciled = "AllAdaptersReconciled" + reasonLKRMissingReports = "AdaptersMissingReports" + reasonLKRNotAvailable = "AdapterReportedNotAvailable" + reasonLKRNotAtSameGeneration = "AdaptersNotAtSameGeneration" +) // ValidateMandatoryConditions checks if all mandatory conditions are present. // Format validation (empty type, duplicates, invalid status) is done in the Handler layer. @@ -52,7 +58,7 @@ func ValidateMandatoryConditions(conditions []api.AdapterCondition) (errorType, return "", "" } -// --- Aggregated Ready / Available ------------------------------------------------- +// --- Aggregated Reconciled / LastKnownReconciled ---------------------------------- // adapterConditionSuffixMap allows overriding the default suffix for specific adapters (reserved). var adapterConditionSuffixMap = map[string]string{} @@ -107,13 +113,13 @@ func AdapterObservedTime(as *api.AdapterStatus) time.Time { return as.LastReportTime } -// AggregateResourceStatus computes Reconciled, Available, and per-adapter conditions from stored adapter +// AggregateResourceStatus computes Reconciled, LastKnownReconciled, and per-adapter conditions from stored adapter // rows and previous conditions. It does not use wall clock. // // The returned adapterConditions slice contains one entry per required adapter that has reported, // with a type derived from the adapter name (e.g. "adapter1" → "Adapter1Successful"). func AggregateResourceStatus(ctx context.Context, in AggregateResourceStatusInput) ( - reconciled, available api.ResourceCondition, adapterConditions []api.ResourceCondition, + reconciled, lastKnownReconciled api.ResourceCondition, adapterConditions []api.ResourceCondition, ) { prevReconciled, prevAvail, prevAdapterByType := parsePrevConditions(ctx, in.PrevConditionsJSON) @@ -128,14 +134,14 @@ func AggregateResourceStatus(ctx context.Context, in AggregateResourceStatusInpu reports, in.HasChildResources, ) - available = computeAvailable( + lastKnownReconciled = computeLastKnownReconciled( in.RefTime, prevAvail, in.RequiredAdapters, reports, ) adapterConditions = computeAdapterConditions(in.RequiredAdapters, reports, prevAdapterByType, in.RefTime) - return reconciled, available, adapterConditions + return reconciled, lastKnownReconciled, adapterConditions } func parsePrevConditions(ctx context.Context, raw []byte) ( @@ -163,6 +169,11 @@ func parsePrevConditions(ctx context.Context, raw []byte) ( case api.ConditionTypeReconciled: prevReconciled = &c case api.ConditionTypeAvailable: + // Migration: legacy records stored this as "Available" + if prevAvail == nil { + prevAvail = &c + } + case api.ConditionTypeLastKnownReconciled: prevAvail = &c default: prevAdapterByType[c.Type] = &c @@ -340,16 +351,16 @@ func computeReconciled( var reason, message string switch { case status == api.ConditionTrue: - reason = reasonAllAdaptersReconciled - message = reason + reason = reasonReadyAllReconciled + message = "All required adapters reported Available=True or Finalized=True at the current generation" case isDeleting && allAdaptersConditionMet: // adapters finalized but children still exist - reason = reasonWaitingForChildResources + reason = reasonReadyWaitingForChildren message = "Deletion in progress. All required adapters reported Finalized=True but child resources still exist" case isDeleting: - reason = reasonMissingRequiredAdapters + reason = reasonReadyMissingAdapters message = buildFinalizedFalseMessage(requiredAdapters, snapshotsByAdapter, resourceGen) default: - reason = reasonMissingRequiredAdapters + reason = reasonReadyMissingAdapters message = buildReadyFalseMessage(requiredAdapters, snapshotsByAdapter, resourceGen) } @@ -423,14 +434,14 @@ func computeReadyLastTransitionTime( return newLastUpdated } -// computeAvailableStatus decides the Available condition status from normalized adapter snapshots. +// computeLastKnownReconciledStatus decides the LastKnownReconciled condition status from normalized adapter snapshots. // // Rules (in order): // 1. No required adapters, or any required adapter has not yet reported → False. // 2. All adapters True at a uniform generation, or mixed-gen but aggregate was already True → True. // 3. Some adapter is False, but aggregate was True and no False is at the tracked generation → True (sticky). // 4. Otherwise → False. -func computeAvailableStatus( +func computeLastKnownReconciledStatus( prev *api.ResourceCondition, required []string, byAdapter map[string]adapterAvailableSnapshot, @@ -469,18 +480,18 @@ func computeAvailableStatus( return api.ConditionTrue } -func computeAvailable( +func computeLastKnownReconciled( refTime time.Time, prev *api.ResourceCondition, required []string, byAdapter map[string]adapterAvailableSnapshot, ) api.ResourceCondition { allTrue, commonGen, mixed := sameGenerationAllTrue(required, byAdapter) - status := computeAvailableStatus(prev, required, byAdapter, allTrue, mixed) + status := computeLastKnownReconciledStatus(prev, required, byAdapter, allTrue, mixed) - obsGen := computeAvailableObservedGeneration(status, prev, required, byAdapter, allTrue, commonGen, mixed) + obsGen := computeLastKnownReconciledObservedGeneration(status, prev, required, byAdapter, allTrue, commonGen, mixed) - lastUpdated := computeAvailableLastUpdatedTime( + lastUpdated := computeLastKnownReconciledLastUpdatedTime( status, prev, refTime, required, byAdapter, obsGen, allTrue, mixed, ) lastTransition := computeGenericLastTransitionTime(prev, status, lastUpdated) @@ -490,15 +501,24 @@ func computeAvailable( created = prev.CreatedTime } - reason := "AdaptersNotAtSameGeneration" - message := "Required adapters do not report a consistent Available state" - if status == api.ConditionTrue { - reason = "All required adapters report Available=True for the tracked generation" - message = reason + var reason, message string + switch { + case status == api.ConditionTrue: + reason = reasonLKRAllReconciled + message = "All required adapters report Available=True for the tracked generation" + case len(required) == 0 || hasMissingAdapter(required, byAdapter): + reason = reasonLKRMissingReports + message = "Required adapters have not yet reported status" + case hasAdapterNotAvailable(required, byAdapter): + reason = reasonLKRNotAvailable + message = "One or more required adapters report Available=False" + default: + reason = reasonLKRNotAtSameGeneration + message = "Required adapters do not report a consistent Available state" } return api.ResourceCondition{ - Type: api.ConditionTypeAvailable, + Type: api.ConditionTypeLastKnownReconciled, Status: status, ObservedGeneration: obsGen, Reason: strPtr(reason), @@ -509,6 +529,24 @@ func computeAvailable( } } +func hasMissingAdapter(required []string, byAdapter map[string]adapterAvailableSnapshot) bool { + for _, name := range required { + if _, ok := byAdapter[name]; !ok { + return true + } + } + return false +} + +func hasAdapterNotAvailable(required []string, byAdapter map[string]adapterAvailableSnapshot) bool { + for _, name := range required { + if s, ok := byAdapter[name]; ok && !s.availableTrue { + return true + } + } + return false +} + func sameGenerationAllTrue( required []string, byAdapter map[string]adapterAvailableSnapshot, @@ -533,7 +571,7 @@ func sameGenerationAllTrue( return true, *g, mixed } -func computeAvailableObservedGeneration( +func computeLastKnownReconciledObservedGeneration( status api.ResourceConditionStatus, prev *api.ResourceCondition, required []string, @@ -576,7 +614,7 @@ func computeAvailableObservedGeneration( return maxG } -func computeAvailableLastUpdatedTime( +func computeLastKnownReconciledLastUpdatedTime( status api.ResourceConditionStatus, prev *api.ResourceCondition, refTime time.Time, diff --git a/pkg/services/aggregation_test.go b/pkg/services/aggregation_test.go index 64b9ece9..ca1905e1 100644 --- a/pkg/services/aggregation_test.go +++ b/pkg/services/aggregation_test.go @@ -86,7 +86,7 @@ func mkPrevReconciled( } } -// mkPrevAvail builds an Available ResourceCondition for use as a prev fixture. +// mkPrevAvail builds a previous LastKnownReconciled (formerly Available) ResourceCondition for backward-compat testing. func mkPrevAvail( status api.ResourceConditionStatus, obsGen int32, lastTransition, lastUpdated time.Time, ) *api.ResourceCondition { @@ -213,6 +213,24 @@ func TestParsePrevConditions(t *testing.T) { t.Fatalf("expected Reconciled to take precedence, got %v", rc) } }) + + t.Run("LastKnownReconciled takes precedence over legacy Available", func(t *testing.T) { + t.Parallel() + lkrCond := api.ResourceCondition{Type: api.ConditionTypeLastKnownReconciled, Status: api.ConditionTrue} + _, a, _ := parsePrevConditions(context.Background(), encode(availCond, lkrCond)) + if a == nil || a.Type != api.ConditionTypeLastKnownReconciled { + t.Fatalf("expected LastKnownReconciled to take precedence over Available, got %v", a) + } + }) + + t.Run("LastKnownReconciled takes precedence regardless of order", func(t *testing.T) { + t.Parallel() + lkrCond := api.ResourceCondition{Type: api.ConditionTypeLastKnownReconciled, Status: api.ConditionTrue} + _, a, _ := parsePrevConditions(context.Background(), encode(lkrCond, availCond)) + if a == nil || a.Type != api.ConditionTypeLastKnownReconciled { + t.Fatalf("expected LastKnownReconciled to take precedence over Available, got %v", a) + } + }) } // --------------------------------------------------------------------------- @@ -670,8 +688,8 @@ func TestComputeReconciled(t *testing.T) { if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False (child resources still exist)", cond.Status) } - if cond.Reason == nil || *cond.Reason != reasonWaitingForChildResources { - t.Errorf("Reason got %v, want %q", cond.Reason, reasonWaitingForChildResources) + if cond.Reason == nil || *cond.Reason != reasonReadyWaitingForChildren { + t.Errorf("Reason got %v, want %q", cond.Reason, reasonReadyWaitingForChildren) } }) @@ -686,21 +704,21 @@ func TestComputeReconciled(t *testing.T) { if cond.Status != api.ConditionTrue { t.Errorf("got %v, want True (no child resources)", cond.Status) } - if cond.Reason == nil || *cond.Reason != reasonAllAdaptersReconciled { - t.Errorf("Reason got %v, want %q", cond.Reason, reasonAllAdaptersReconciled) + if cond.Reason == nil || *cond.Reason != reasonReadyAllReconciled { + t.Errorf("Reason got %v, want %q", cond.Reason, reasonReadyAllReconciled) } }) } // --------------------------------------------------------------------------- -// computeAvailableLastUpdatedTime +// computeLastKnownReconciledLastUpdatedTime // --------------------------------------------------------------------------- -func TestComputeAvailableLastUpdatedTime(t *testing.T) { +func TestComputeLastKnownReconciledLastUpdatedTime(t *testing.T) { t.Parallel() t.Run("empty required → refTime", func(t *testing.T) { t.Parallel() - got := computeAvailableLastUpdatedTime(api.ConditionFalse, nil, aggTRef, nil, nil, 1, true, false) + got := computeLastKnownReconciledLastUpdatedTime(api.ConditionFalse, nil, aggTRef, nil, nil, 1, true, false) if !got.Equal(aggTRef) { t.Errorf("got %v, want refTime=%v", got, aggTRef) } @@ -713,7 +731,7 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "a": snap(2, true, aggT3), // later "b": snap(2, true, aggT1), // earlier → min } - got := computeAvailableLastUpdatedTime(api.ConditionTrue, nil, aggTRef, required, byAdapter, 2, true, false) + got := computeLastKnownReconciledLastUpdatedTime(api.ConditionTrue, nil, aggTRef, required, byAdapter, 2, true, false) if !got.Equal(aggT1) { t.Errorf("got %v, want min=%v", got, aggT1) } @@ -727,7 +745,7 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "b": snap(2, true, aggT2), } prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - got := computeAvailableLastUpdatedTime(api.ConditionTrue, prev, aggTRef, required, byAdapter, 1, true, true) + got := computeLastKnownReconciledLastUpdatedTime(api.ConditionTrue, prev, aggTRef, required, byAdapter, 1, true, true) if !got.Equal(aggT0) { t.Errorf("got %v, want prev.LastUpdatedTime=%v", got, aggT0) } @@ -740,7 +758,7 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "a": snap(1, true, aggT1), "b": snap(2, true, aggT2), } - got := computeAvailableLastUpdatedTime(api.ConditionTrue, nil, aggTRef, required, byAdapter, 1, true, true) + got := computeLastKnownReconciledLastUpdatedTime(api.ConditionTrue, nil, aggTRef, required, byAdapter, 1, true, true) if !got.Equal(aggTRef) { t.Errorf("got %v, want refTime=%v", got, aggTRef) } @@ -756,7 +774,9 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "b": snap(2, true, aggT2), } prev := mkPrevAvail(api.ConditionFalse, 1, aggT0, aggT0) - got := computeAvailableLastUpdatedTime(api.ConditionFalse, prev, aggTRef, required, byAdapter, 2, true, true) + got := computeLastKnownReconciledLastUpdatedTime( + api.ConditionFalse, prev, aggTRef, required, byAdapter, 2, true, true, + ) if !got.Equal(aggT0) { t.Errorf("got %v, want prev.LastUpdatedTime=%v", got, aggT0) } @@ -770,7 +790,9 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "b": snap(2, true, aggT1), // True at gen 2 (still included in atX) } // observedGen=2, hasFalseAtX=true; atX=[t3,t1], min=t1 (oldest, matches Ready semantics) - got := computeAvailableLastUpdatedTime(api.ConditionFalse, nil, aggTRef, required, byAdapter, 2, false, false) + got := computeLastKnownReconciledLastUpdatedTime( + api.ConditionFalse, nil, aggTRef, required, byAdapter, 2, false, false, + ) if !got.Equal(aggT1) { t.Errorf("got %v, want min of gen-2 adapters=%v", got, aggT1) } @@ -785,7 +807,9 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { "b": snap(2, false, aggT2), } prev := mkPrevAvail(api.ConditionFalse, 2, aggT0, aggT0) - got := computeAvailableLastUpdatedTime(api.ConditionFalse, prev, aggTRef, required, byAdapter, 3, false, false) + got := computeLastKnownReconciledLastUpdatedTime( + api.ConditionFalse, prev, aggTRef, required, byAdapter, 3, false, false, + ) if !got.Equal(aggT0) { t.Errorf("got %v, want prev.LastUpdatedTime=%v", got, aggT0) } @@ -798,7 +822,9 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { byAdapter := map[string]adapterAvailableSnapshot{ "a": snap(1, false, aggT1), } - got := computeAvailableLastUpdatedTime(api.ConditionFalse, nil, aggTRef, required, byAdapter, 2, false, false) + got := computeLastKnownReconciledLastUpdatedTime( + api.ConditionFalse, nil, aggTRef, required, byAdapter, 2, false, false, + ) if !got.Equal(aggTRef) { t.Errorf("got %v, want refTime=%v", got, aggTRef) } @@ -806,17 +832,20 @@ func TestComputeAvailableLastUpdatedTime(t *testing.T) { } // --------------------------------------------------------------------------- -// computeAvailable +// computeLastKnownReconciled // --------------------------------------------------------------------------- -func TestComputeAvailable(t *testing.T) { +func TestComputeLastKnownReconciled(t *testing.T) { t.Parallel() t.Run("empty required list → False", func(t *testing.T) { t.Parallel() - cond := computeAvailable(aggTRef, nil, nil, map[string]adapterAvailableSnapshot{}) + cond := computeLastKnownReconciled(aggTRef, nil, nil, map[string]adapterAvailableSnapshot{}) if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False", cond.Status) } + if cond.Reason == nil || *cond.Reason != reasonLKRMissingReports { + t.Errorf("Reason got %v, want %s", cond.Reason, reasonLKRMissingReports) + } }) t.Run("required adapter missing from byAdapter → False", func(t *testing.T) { @@ -826,10 +855,13 @@ func TestComputeAvailable(t *testing.T) { "a": snap(1, true, aggT1), // "b" absent } - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False", cond.Status) } + if cond.Reason == nil || *cond.Reason != reasonLKRMissingReports { + t.Errorf("Reason got %v, want %s", cond.Reason, reasonLKRMissingReports) + } }) t.Run("all True same generation → True", func(t *testing.T) { @@ -839,10 +871,13 @@ func TestComputeAvailable(t *testing.T) { "a": snap(1, true, aggT1), "b": snap(1, true, aggT2), } - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.Status != api.ConditionTrue { t.Errorf("got %v, want True", cond.Status) } + if cond.Reason == nil || *cond.Reason != reasonLKRAllReconciled { + t.Errorf("Reason got %v, want %s", cond.Reason, reasonLKRAllReconciled) + } }) t.Run("all True mixed generations, no prev → False", func(t *testing.T) { @@ -852,10 +887,13 @@ func TestComputeAvailable(t *testing.T) { "a": snap(1, true, aggT1), "b": snap(2, true, aggT2), } - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False (mixed gens, no prev=True)", cond.Status) } + if cond.Reason == nil || *cond.Reason != reasonLKRNotAtSameGeneration { + t.Errorf("Reason got %v, want %s", cond.Reason, reasonLKRNotAtSameGeneration) + } }) t.Run("all True mixed generations, prev True → True (sticky)", func(t *testing.T) { @@ -866,7 +904,7 @@ func TestComputeAvailable(t *testing.T) { "b": snap(2, true, aggT2), } prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if cond.Status != api.ConditionTrue { t.Errorf("got %v, want True (sticky from prev=True)", cond.Status) } @@ -879,10 +917,13 @@ func TestComputeAvailable(t *testing.T) { "a": snap(1, true, aggT1), "b": snap(1, false, aggT2), } - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False", cond.Status) } + if cond.Reason == nil || *cond.Reason != reasonLKRNotAvailable { + t.Errorf("Reason got %v, want %s", cond.Reason, reasonLKRNotAvailable) + } }) t.Run("some adapter False at tracked generation, prev True → False (breaks sticky)", func(t *testing.T) { @@ -894,7 +935,7 @@ func TestComputeAvailable(t *testing.T) { "b": snap(1, false, aggT2), } prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if cond.Status != api.ConditionFalse { t.Errorf("got %v, want False (False at tracked gen breaks sticky)", cond.Status) } @@ -909,7 +950,7 @@ func TestComputeAvailable(t *testing.T) { "b": snap(2, false, aggT2), // False at gen 2 ≠ tracked gen 1 } prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if cond.Status != api.ConditionTrue { t.Errorf("got %v, want True (False not at tracked gen → stays sticky)", cond.Status) } @@ -919,7 +960,7 @@ func TestComputeAvailable(t *testing.T) { t.Parallel() required := []string{"a"} byAdapter := map[string]adapterAvailableSnapshot{"a": snap(3, true, aggT1)} - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.ObservedGeneration != 3 { t.Errorf("ObservedGeneration got %d, want 3", cond.ObservedGeneration) } @@ -934,7 +975,7 @@ func TestComputeAvailable(t *testing.T) { "a": snap(1, false, aggT1), "b": snap(2, true, aggT2), } - cond := computeAvailable(aggTRef, nil, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, nil, required, byAdapter) if cond.Status != api.ConditionFalse { t.Errorf("Status got %v, want False", cond.Status) } @@ -953,7 +994,7 @@ func TestComputeAvailable(t *testing.T) { "b": snap(2, true, aggT2), } prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if cond.Status != api.ConditionTrue { t.Errorf("Status got %v, want True", cond.Status) } @@ -967,7 +1008,7 @@ func TestComputeAvailable(t *testing.T) { required := []string{"a"} byAdapter := map[string]adapterAvailableSnapshot{"a": snap(1, true, aggT1)} prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if !cond.CreatedTime.Equal(aggT0) { t.Errorf("CreatedTime got %v, want %v", cond.CreatedTime, aggT0) } @@ -978,7 +1019,7 @@ func TestComputeAvailable(t *testing.T) { required := []string{"a"} byAdapter := map[string]adapterAvailableSnapshot{"a": snap(1, true, aggT1)} prev := mkPrevAvail(api.ConditionTrue, 1, aggT0, aggT0) - cond := computeAvailable(aggTRef, prev, required, byAdapter) + cond := computeLastKnownReconciled(aggTRef, prev, required, byAdapter) if !cond.LastTransitionTime.Equal(aggT0) { t.Errorf("LastTransitionTime got %v, want prev.LastTransitionTime=%v", cond.LastTransitionTime, aggT0) } @@ -1212,8 +1253,8 @@ func TestAggregateResourceStatus(t *testing.T) { if reconciled.Type != api.ConditionTypeReconciled { t.Errorf("reconciled.Type=%q, want %q", reconciled.Type, api.ConditionTypeReconciled) } - if avail.Type != api.ConditionTypeAvailable { - t.Errorf("avail.Type=%q, want %q", avail.Type, api.ConditionTypeAvailable) + if avail.Type != api.ConditionTypeLastKnownReconciled { + t.Errorf("avail.Type=%q, want %q", avail.Type, api.ConditionTypeLastKnownReconciled) } }) diff --git a/pkg/services/cluster.go b/pkg/services/cluster.go index af72e49a..51b0252d 100644 --- a/pkg/services/cluster.go +++ b/pkg/services/cluster.go @@ -222,7 +222,7 @@ func (s *sqlClusterService) recomputeAndSaveClusterStatus( hasChildResources = exists } - reconciled, available, adapterConditions := AggregateResourceStatus(ctx, AggregateResourceStatusInput{ + reconciled, lastKnownReconciled, adapterConditions := AggregateResourceStatus(ctx, AggregateResourceStatusInput{ ResourceGeneration: cluster.Generation, RefTime: refTime, DeletedTime: cluster.DeletedTime, @@ -237,7 +237,7 @@ func (s *sqlClusterService) recomputeAndSaveClusterStatus( ready.Type = api.ConditionTypeReady allConditions := make([]api.ResourceCondition, 0, fixedConditionCount+len(adapterConditions)) - allConditions = append(allConditions, ready, reconciled, available) + allConditions = append(allConditions, ready, reconciled, lastKnownReconciled) allConditions = append(allConditions, adapterConditions...) conditionsJSON, err := json.Marshal(allConditions) diff --git a/pkg/services/cluster_test.go b/pkg/services/cluster_test.go index 9b0e52ca..8fe9d91b 100644 --- a/pkg/services/cluster_test.go +++ b/pkg/services/cluster_test.go @@ -717,7 +717,7 @@ func TestClusterAvailableReadyTransitions(t *testing.T) { var available, ready, reconciled *api.ResourceCondition for i := range conds { switch conds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: available = &conds[i] case api.ConditionTypeReady: ready = &conds[i] @@ -870,7 +870,7 @@ func TestClusterStaleAdapterStatusUpdatePolicy(t *testing.T) { var conds []api.ResourceCondition g.Expect(json.Unmarshal(stored.StatusConditions, &conds)).To(Succeed()) for i := range conds { - if conds[i].Type == api.ConditionTypeAvailable { + if conds[i].Type == api.ConditionTypeLastKnownReconciled { return conds[i] } } @@ -974,7 +974,7 @@ func TestClusterSyntheticTimestampsStableWithoutAdapterStatus(t *testing.T) { var createdAvailable, createdReady, createdReconciled *api.ResourceCondition for i := range createdConds { switch createdConds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: createdAvailable = &createdConds[i] case api.ConditionTypeReady: createdReady = &createdConds[i] @@ -1005,7 +1005,7 @@ func TestClusterSyntheticTimestampsStableWithoutAdapterStatus(t *testing.T) { var updatedAvailable, updatedReady, updatedReconciled *api.ResourceCondition for i := range updatedConds { switch updatedConds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: updatedAvailable = &updatedConds[i] case api.ConditionTypeReady: updatedReady = &updatedConds[i] @@ -1477,7 +1477,7 @@ func TestReconciled_DuringDeletion_ChildResources(t *testing.T) { g.Expect(reconciled).NotTo(BeNil()) g.Expect(reconciled.Status).To(Equal(api.ConditionFalse)) g.Expect(reconciled.Reason).NotTo(BeNil()) - g.Expect(*reconciled.Reason).To(Equal(reasonWaitingForChildResources)) + g.Expect(*reconciled.Reason).To(Equal(reasonReadyWaitingForChildren)) g.Expect(ready).NotTo(BeNil()) g.Expect(ready.Status).To(Equal(api.ConditionFalse)) @@ -1523,7 +1523,7 @@ func TestReconciled_DuringDeletion_ChildResources(t *testing.T) { g.Expect(reconciled).NotTo(BeNil()) g.Expect(reconciled.Status).To(Equal(api.ConditionTrue)) g.Expect(reconciled.Reason).NotTo(BeNil()) - g.Expect(*reconciled.Reason).To(Equal(reasonAllAdaptersReconciled)) + g.Expect(*reconciled.Reason).To(Equal(reasonReadyAllReconciled)) g.Expect(ready).NotTo(BeNil()) g.Expect(ready.Status).To(Equal(api.ConditionTrue)) diff --git a/pkg/services/node_pool_test.go b/pkg/services/node_pool_test.go index 6031aacf..3c905e05 100644 --- a/pkg/services/node_pool_test.go +++ b/pkg/services/node_pool_test.go @@ -587,7 +587,7 @@ func TestNodePoolAvailableReadyTransitions(t *testing.T) { var available, ready, reconciled *api.ResourceCondition for i := range conds { switch conds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: available = &conds[i] case api.ConditionTypeReady: ready = &conds[i] @@ -753,7 +753,7 @@ func TestNodePoolStaleAdapterStatusUpdatePolicy(t *testing.T) { var conds []api.ResourceCondition g.Expect(json.Unmarshal(stored.StatusConditions, &conds)).To(Succeed()) for i := range conds { - if conds[i].Type == api.ConditionTypeAvailable { + if conds[i].Type == api.ConditionTypeLastKnownReconciled { return conds[i] } } @@ -822,7 +822,7 @@ func TestNodePoolSyntheticTimestampsStableWithoutAdapterStatus(t *testing.T) { fixedNow := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) initialConditions := []api.ResourceCondition{ { - Type: api.ConditionTypeAvailable, + Type: api.ConditionTypeLastKnownReconciled, Status: api.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: fixedNow, @@ -857,7 +857,7 @@ func TestNodePoolSyntheticTimestampsStableWithoutAdapterStatus(t *testing.T) { var createdAvailable, createdReady, createdReconciled *api.ResourceCondition for i := range createdConds { switch createdConds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: createdAvailable = &createdConds[i] case api.ConditionTypeReady: createdReady = &createdConds[i] @@ -888,7 +888,7 @@ func TestNodePoolSyntheticTimestampsStableWithoutAdapterStatus(t *testing.T) { var updatedAvailable, updatedReady, updatedReconciled *api.ResourceCondition for i := range updatedConds { switch updatedConds[i].Type { - case api.ConditionTypeAvailable: + case api.ConditionTypeLastKnownReconciled: updatedAvailable = &updatedConds[i] case api.ConditionTypeReady: updatedReady = &updatedConds[i] diff --git a/pkg/services/status_helpers.go b/pkg/services/status_helpers.go index eda3d0a9..269c1cc6 100644 --- a/pkg/services/status_helpers.go +++ b/pkg/services/status_helpers.go @@ -19,7 +19,7 @@ func computeNodePoolConditionsJSON( adapterStatuses []*api.AdapterStatus, requiredAdapters []string, ) ([]byte, *errors.ServiceError) { - reconciled, available, adapterConditions := AggregateResourceStatus(ctx, AggregateResourceStatusInput{ + reconciled, lastKnownReconciled, adapterConditions := AggregateResourceStatus(ctx, AggregateResourceStatusInput{ ResourceGeneration: np.Generation, RefTime: nodePoolRefTime(np), DeletedTime: np.DeletedTime, @@ -33,7 +33,7 @@ func computeNodePoolConditionsJSON( ready.Type = api.ConditionTypeReady allConditions := make([]api.ResourceCondition, 0, fixedConditionCount+len(adapterConditions)) - allConditions = append(allConditions, ready, reconciled, available) + allConditions = append(allConditions, ready, reconciled, lastKnownReconciled) allConditions = append(allConditions, adapterConditions...) conditionsJSON, err := json.Marshal(allConditions)