diff --git a/hyperfleet/README.md b/hyperfleet/README.md
index fdef2d9..9f7c2f1 100644
--- a/hyperfleet/README.md
+++ b/hyperfleet/README.md
@@ -143,7 +143,7 @@ Kubernetes resources (Jobs, Secrets, ConfigMaps, Services) created by adapters t
- Fetches cluster details from API
- Evaluates preconditions
- Creates Kubernetes resources if conditions met
- - Reports status → POST /clusters/{id}/statuses
+ - Reports status → PUT /clusters/{id}/statuses
6. API aggregates adapter statuses → Updates cluster status
7. Cycle repeats until cluster reaches Ready phase
```
diff --git a/hyperfleet/components/adapter/adapter-versioning.md b/hyperfleet/components/adapter/adapter-versioning.md
index 4c14f75..9bfde8e 100644
--- a/hyperfleet/components/adapter/adapter-versioning.md
+++ b/hyperfleet/components/adapter/adapter-versioning.md
@@ -233,7 +233,7 @@ When Sentinel introduces a breaking event schema change, follow the expand-contr
- Report adapter status and provisioning results (WRITE)
**Adapter status report payloads:**
-- When adapters report status or provisioning results, they POST/PUT to HyperFleet API endpoints
+- When adapters report status or provisioning results, they PUT to HyperFleet API endpoints
- Status payload structure follows the API schema version from the imported API client library
**API version targeting:**
diff --git a/hyperfleet/components/adapter/framework/adapter-deletion-flow-design.md b/hyperfleet/components/adapter/framework/adapter-deletion-flow-design.md
index af33915..55e211f 100644
--- a/hyperfleet/components/adapter/framework/adapter-deletion-flow-design.md
+++ b/hyperfleet/components/adapter/framework/adapter-deletion-flow-design.md
@@ -585,7 +585,7 @@ post:
post_actions:
- name: updateStatus
api_call:
- method: POST
+ method: PUT
url: /clusters/{{ .clusterId }}/statuses
body: '{{ .clusterStatusPayload }}'
```
diff --git a/hyperfleet/components/adapter/framework/adapter-design-decisions.md b/hyperfleet/components/adapter/framework/adapter-design-decisions.md
index 2a10d65..c784b4d 100644
--- a/hyperfleet/components/adapter/framework/adapter-design-decisions.md
+++ b/hyperfleet/components/adapter/framework/adapter-design-decisions.md
@@ -23,7 +23,7 @@ This document captures the key design decisions, trade-offs, and rationale behin
2. [Kubernetes Resource Management](#2-kubernetes-resource-management-vs-in-process-execution)
3. [Anemic Events Pattern](#3-anemic-events-pattern)
4. [Condition-Based Status Reporting](#4-condition-based-status-reporting)
-5. [Adapters POST Status Updates](#5-adapters-post-status-updates)
+5. [Adapters PUT Status Updates](#5-adapters-put-status-updates)
6. [Helm-Based Deployment](#6-helm-based-deployment)
7. [Layered Configuration Architecture](#7-layered-configuration-architecture)
8. [CEL Expression Language](#8-cel-expression-language)
@@ -178,20 +178,20 @@ observed_time: "..." # Timestamp when status was reported
---
-## 5. Adapters POST Status Updates
+## 5. Adapters PUT Status Updates
-**Decision:** Adapters POST status updates directly to HyperFleet API without checking if status exists first.
+**Decision:** Adapters PUT status updates directly to HyperFleet API without checking if status exists first.
**Why:**
- **Simple flow** - single API call per status update
- **API handles create-or-update** - server-side logic determines if creating or updating
-- **Idempotent** - same POST multiple times produces same result
-- **Less latency** - no GET call before POST
+- **Idempotent** - same PUT multiple times produces same result
+- **Less latency** - no GET call before PUT
- **Stateless adapter** - adapter doesn't need to track if status exists
**Implementation:**
```
-POST /api/hyperfleet/v1/clusters/{clusterId}/statuses
+PUT /api/hyperfleet/v1/clusters/{clusterId}/statuses
{
"adapter": "validation",
"observed_generation": 1,
diff --git a/hyperfleet/components/adapter/framework/adapter-flow-diagrams.md b/hyperfleet/components/adapter/framework/adapter-flow-diagrams.md
index 023d818..fbde3ce 100644
--- a/hyperfleet/components/adapter/framework/adapter-flow-diagrams.md
+++ b/hyperfleet/components/adapter/framework/adapter-flow-diagrams.md
@@ -123,13 +123,7 @@ sequenceDiagram
A->>A: Log skip reason (debug)
Note over A: Report status - not applied
- A->>API: GET /api/hyperfleet/v1/clusters/cls-123/statuses
-
- alt ClusterStatus exists (200 OK)
- A->>API: PATCH /statuses/{statusId}
Applied=False, Available=False, Health=True
- else ClusterStatus not found (404)
- A->>API: POST /statuses
Applied=False, Available=False, Health=True
- end
+ A->>API: PUT /statuses
Applied=False, Available=False, Health=True
API-->>A: Status updated
A->>B: Acknowledge message
@@ -143,13 +137,7 @@ sequenceDiagram
K-->>A: Resources created
Note over A: Report status - resources created
- A->>API: GET /api/hyperfleet/v1/clusters/cls-123/statuses
-
- alt ClusterStatus exists (200 OK)
- A->>API: PATCH /statuses/{statusId}
Applied=True, Available=False, Health=True
- else ClusterStatus not found (404)
- A->>API: POST /statuses
Applied=True, Available=False, Health=True
- end
+ A->>API: PUT /statuses
Applied=True, Available=False, Health=True
API-->>A: Status updated
A->>B: Acknowledge message
@@ -160,13 +148,7 @@ sequenceDiagram
alt Postconditions NOT met (workload in progress)
Note over A: Workload still running
- A->>API: GET /api/hyperfleet/v1/clusters/cls-123/statuses
-
- alt ClusterStatus exists (200 OK)
- A->>API: PATCH /statuses/{statusId}
Applied=True, Available=False, Health=True
- else ClusterStatus not found (404)
- A->>API: POST /statuses
Applied=True, Available=False, Health=True
- end
+ A->>API: PUT /statuses
Applied=True, Available=False, Health=True
API-->>A: Status updated
A->>B: Acknowledge message
@@ -175,7 +157,7 @@ sequenceDiagram
alt Workload Succeeded
Note over A: Aggregate conditions
A->>A: Available=True (all conditions True)
- A->>API: PATCH /statuses/{statusId}
Available=True, Applied=True, Health=True
+ A->>API: PUT /statuses/{statusId}
Available=True, Applied=True, Health=True
API-->>A: Status updated
Note over A: Check resource management
@@ -190,7 +172,7 @@ sequenceDiagram
else Workload Failed
Note over A: Aggregate conditions
A->>A: Available=False (workload failed)
- A->>API: PATCH /statuses/{statusId}
Available=False, Applied=True, Health=True
+ A->>API: PUT /statuses/{statusId}
Available=False, Applied=True, Health=True
API-->>A: Status updated
Note over A: Check resource management
@@ -266,9 +248,9 @@ flowchart LR
- `clusterId` enables parent-child relationships (e.g., nodepools → cluster)
### Status Upsert Pattern
-- Adapters POST status updates to HyperFleet API
+- Adapters PUT status updates to HyperFleet API
- API handles create-or-update logic server-side
-- Idempotent: same POST multiple times = same result
+- Idempotent: same PUT multiple times = same result
- Prevents race conditions between adapters
### Status Reporting Pattern
@@ -396,7 +378,7 @@ sequenceDiagram
rect rgb(240, 255, 240)
Note over Adapter: Post-Processing (always runs)
Adapter->>Adapter: Evaluate conditions (CEL)
- Adapter->>API: POST /resources/{id}/statuses (Applied=False)
+ Adapter->>API: PUT /resources/{id}/statuses (Applied=False)
end
Note over User, K8s: Phase 4 - API Aggregates & Deletes (Hierarchical)
@@ -524,12 +506,12 @@ sequenceDiagram
Sentinel->>Sub_Adapter: CloudEvent (subresource)
Sub_Adapter->>Sub_Adapter: Capture deleted_time, evaluate lifecycle.delete
Sub_Adapter->>Sub_Adapter: Clean up subresource resources (per-resource ordering)
- Sub_Adapter->>API: POST status (Applied=False, Health=True, Finalized=True)
+ Sub_Adapter->>API: PUT status (Applied=False, Health=True, Finalized=True)
and Resource cleanup (in parallel)
Sentinel->>Res_Adapter: CloudEvent (resource)
Res_Adapter->>Res_Adapter: Capture deleted_time, evaluate lifecycle.delete
Res_Adapter->>Res_Adapter: Clean up resource resources (per-resource ordering)
- Res_Adapter->>API: POST status (Applied=False, Health=True, Finalized=True)
+ Res_Adapter->>API: PUT status (Applied=False, Health=True, Finalized=True)
end
API->>API: Subresource Reconciled=True?
diff --git a/hyperfleet/components/adapter/framework/adapter-frame-design.md b/hyperfleet/components/adapter/framework/adapter-frame-design.md
index 9cc1116..44c202a 100644
--- a/hyperfleet/components/adapter/framework/adapter-frame-design.md
+++ b/hyperfleet/components/adapter/framework/adapter-frame-design.md
@@ -103,7 +103,7 @@ graph TB
BrokerLib -->|Connects| MessageBroker
K8sClient -->|CRUD Operations| K8sAPI
APIClient -->|REST Calls| HyperFleetAPI
- Reporter -->|POST Status| HyperFleetAPI
+ Reporter -->|PUT Status| HyperFleetAPI
Logger -.->|Used by| Components
Errors -.->|Used by| Components
@@ -634,7 +634,7 @@ subscriber.Subscribe(ctx, func(ctx context.Context, msg []byte) error {
#### Purpose
- Evaluate tracked Kubernetes resources using CEL expressions
- Build status payload with conditions (applied, available, health) and custom data
-- Report status to HyperFleet API via POST requests
+- Report status to HyperFleet API via PUT requests
- Support conditional reporting based on expression evaluation
- **Always executes**, even when preconditions fail or resources weren't created
@@ -772,7 +772,7 @@ subscriber.Subscribe(ctx, func(ctx context.Context, msg []byte) error {
- Build status payload with evaluated conditions and data
- Execute postActions (from `post.postActions`):
- Evaluate `when` condition (if specified)
- - POST status payload to HyperFleet API endpoint
+ - PUT status payload to HyperFleet API endpoint
- Execute additional actions (e.g., logging)
- Acknowledge message to broker
@@ -972,9 +972,9 @@ These patterns align with the workflow described in [Adapter Flow Diagrams](./ad
- Kubernetes standard fields remain unchanged: `metadata.name`, `status.phase`
### Status Upsert Pattern
-- Adapters POST status updates to HyperFleet API: `POST /api/hyperfleet/v1/clusters/{resourceId}/statuses`
+- Adapters PUT status updates to HyperFleet API: `PUT /api/hyperfleet/v1/clusters/{resourceId}/statuses`
- API handles create-or-update logic server-side (idempotent)
-- Same POST multiple times = same result
+- Same PUT multiple times = same result
- Prevents race conditions between adapters
### Idempotency Pattern
diff --git a/hyperfleet/components/adapter/framework/adapter-integration-tests.md b/hyperfleet/components/adapter/framework/adapter-integration-tests.md
index 1bd26a8..fe7f7d1 100644
--- a/hyperfleet/components/adapter/framework/adapter-integration-tests.md
+++ b/hyperfleet/components/adapter/framework/adapter-integration-tests.md
@@ -95,7 +95,7 @@ pubsubContainer, err := testcontainers.GenericContainer(ctx, testcontainers.Gene
apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Handle GET /clusters/{id}
// Handle GET /clusters/{id}/statuses
- // Handle POST /clusters/{id}/statuses
+ // Handle PUT /clusters/{id}/statuses
// Handle PATCH /clusters/{id}/statuses/{statusId}
}))
defer apiServer.Close()
@@ -104,7 +104,7 @@ defer apiServer.Close()
**API Endpoints to Mock**:
- `GET /api/hyperfleet/v1/clusters/{clusterId}` - Return cluster object
- `GET /api/hyperfleet/v1/clusters/{clusterId}/statuses` - Return existing status or 404
-- `POST /api/hyperfleet/v1/clusters/{clusterId}/statuses` - Create/upsert status
+- `PUT /api/hyperfleet/v1/clusters/{clusterId}/statuses` - Create/upsert status
- `PATCH /api/hyperfleet/v1/clusters/{clusterId}/statuses/{statusId}` - Update status
**Request/Response Tracking**:
@@ -391,7 +391,7 @@ spec:
postActions:
- name: "reportStatus"
apiCall:
- method: "POST"
+ method: "PUT"
url: "{{ .hyperfleetApiBaseUrl }}/api/hyperfleet/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/statuses"
body: "{{ .statusPayload }}"
timeout: 30s
@@ -513,7 +513,7 @@ spec:
2. Wait for adapter to process event (max 5 seconds)
3. Verify API was called: `GET /clusters/cls-test-001`
4. Verify Job was created in Kubernetes
-5. Verify status was reported: `POST /clusters/cls-test-001/statuses`
+5. Verify status was reported: `PUT /clusters/cls-test-001/statuses`
**Expected Outcomes**:
- ✅ Event consumed from broker
diff --git a/hyperfleet/components/adapter/framework/adapter-metrics.md b/hyperfleet/components/adapter/framework/adapter-metrics.md
index 123a79d..6b541a2 100644
--- a/hyperfleet/components/adapter/framework/adapter-metrics.md
+++ b/hyperfleet/components/adapter/framework/adapter-metrics.md
@@ -215,14 +215,14 @@ hyperfleet_adapter_resources_deleted_total{component="adapter-validation",versio
**Labels**:
- `adapter_name` - Name of the adapter
- `api` - API being called: `hyperfleet`, `kubernetes`, `external`
-- `method` - HTTP method: `GET`, `POST`, `PATCH`, `DELETE`
+- `method` - HTTP method: `GET`, `POST`, `PATCH`, `DELETE`, `PUT`
- `endpoint` - API endpoint (sanitized, no IDs): e.g., `/clusters/{id}`, `/statuses`
- `status_code` - HTTP status code: `200`, `404`, `500`, etc.
**Example**:
```prometheus
hyperfleet_adapter_api_requests_total{component="adapter-validation",version="v1.0.0",adapter_name="validation",api="hyperfleet",method="GET",endpoint="/clusters/{id}",status_code="200"} 1523
-hyperfleet_adapter_api_requests_total{component="adapter-validation",version="v1.0.0",adapter_name="validation",api="hyperfleet",method="POST",endpoint="/statuses",status_code="200"} 1487
+hyperfleet_adapter_api_requests_total{component="adapter-validation",version="v1.0.0",adapter_name="validation",api="hyperfleet",method="PUT",endpoint="/statuses",status_code="200"} 1487
hyperfleet_adapter_api_requests_total{component="adapter-validation",version="v1.0.0",adapter_name="validation",api="kubernetes",method="POST",endpoint="/namespaces/{ns}/jobs",status_code="201"} 1432
hyperfleet_adapter_api_requests_total{component="adapter-validation",version="v1.0.0",adapter_name="validation",api="kubernetes",method="GET",endpoint="/namespaces/{ns}/jobs/{name}",status_code="200"} 2145
```
diff --git a/hyperfleet/components/adapter/framework/adapter-status-contract.md b/hyperfleet/components/adapter/framework/adapter-status-contract.md
index 63c53fd..4850407 100644
--- a/hyperfleet/components/adapter/framework/adapter-status-contract.md
+++ b/hyperfleet/components/adapter/framework/adapter-status-contract.md
@@ -13,7 +13,7 @@ Last Updated: 2025-12-09
- [Status Reporting Endpoint](#status-reporting-endpoint)
- [Upsert Pattern](#upsert-pattern)
- [Status Payload Structure](#status-payload-structure)
- - [POST Request (Upsert ClusterStatus)](#post-request-upsert-clusterstatus)
+ - [PUT Request (Upsert ClusterStatus)](#put-request-upsert-clusterstatus)
- [Required Fields](#required-fields)
- [Adapter Status Request Fields](#adapter-status-request-fields)
- [Condition Request Fields](#condition-request-fields)
@@ -48,7 +48,7 @@ Last Updated: 2025-12-09
- [5. Always Report observed_generation and observed_time](#5-always-report-observed_generation-and-observed_time)
- [6. Use Data Field for Structured Information](#6-use-data-field-for-structured-information)
- [7. Handle Errors Gracefully](#7-handle-errors-gracefully)
- - [8. Always Use POST](#8-always-use-post)
+ - [8. Always Use PUT](#8-always-use-put)
- [9. Conditions: reason and message Are Optional](#9-conditions-reason-and-message-are-optional)
- [Versioning](#versioning)
- [References](#references)
@@ -71,21 +71,21 @@ This document defines the contract between HyperFleet adapters and the HyperFlee
**Base URL**: `{hyperfleetApiBaseUrl}/api/hyperfleet/{hyperfleetApiVersion}/clusters/{clusterId}/statuses`
**Method**:
-- `POST` - Upsert ClusterStatus (create or update)
+- `PUT` - Upsert ClusterStatus (create or update)
### Upsert Pattern
-Adapters **always use POST** for status reporting:
+Adapters **always use PUT** for status reporting:
**API Behavior**:
- The HyperFleet API handles the upsert logic server-side
- If ClusterStatus doesn't exist: API creates it
- If ClusterStatus exists: API updates the adapter's status within it
-- Idempotent: Same POST multiple times = same result
+- Idempotent: Same PUT multiple times = same result
**Adapter Implementation**:
- No need to GET first to check if status exists
-- Always POST to the same endpoint
+- Always PUT to the same endpoint
- API handles create-or-update logic automatically
- Simpler adapter code, fewer HTTP requests
@@ -93,9 +93,9 @@ Adapters **always use POST** for status reporting:
## Status Payload Structure
-### POST Request (Upsert ClusterStatus)
+### PUT Request (Upsert ClusterStatus)
-Always POST the adapter status in this structure:
+Always PUT the adapter status in this structure:
```json
{
@@ -136,7 +136,7 @@ Always POST the adapter status in this structure:
**Notes**:
- The API will upsert this adapter status (create or update based on adapter name)
-- Other adapter statuses for this cluster are preserved (not affected by this POST)
+- Other adapter statuses for this cluster are preserved (not affected by this PUT)
- API will set `created_time` and `last_report_time` automatically
- API will add `last_transition_time` to each condition automatically
@@ -637,7 +637,7 @@ post:
description: "Example resource must exist"
postActions:
- type: "api_call"
- method: "POST"
+ method: "PUT"
endpoint: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/statuses"
headers:
- name: "Authorization"
@@ -654,7 +654,7 @@ post:
3. **Evaluate Conditions**: Evaluate CEL expressions for applied, available, health
4. **Evaluate Data**: Evaluate CEL expressions for custom data fields
5. **Build Payload**: Construct status payload with conditions and data
-6. **Execute PostActions**: POST to HyperFleet API endpoint
+6. **Execute PostActions**: PUT to HyperFleet API endpoint
---
@@ -675,7 +675,7 @@ Content-Type: application/json
### Example Request
```http
-POST /api/v1/clusters/cls-123/statuses HTTP/1.1
+PUT /api/v1/clusters/cls-123/statuses HTTP/1.1
Host: api.hyperfleet.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
@@ -769,9 +769,29 @@ Use the `data` field for adapter-specific structured data that other components
Report adapter errors with `Health=False` and appropriate error messages.
-### 8. Always Use POST
-
-Always POST to the same endpoint - the API handles upsert logic server-side for idempotency.
+### 8. Always Use PUT
+
+Report status with **PUT** only. The API applies [upsert semantics](#upsert-pattern) on the status resource: the first write creates your adapter’s entry if needed; later writes replace that entry (other adapters’ statuses on the same cluster are unchanged). Repeating the same PUT is safe, so **retries after timeouts or `5xx` responses should replay the same request** rather than using a new verb or URL.
+
+- **One URL per cluster**: `PUT …/api/hyperfleet/{hyperfleetApiVersion}/clusters/{clusterId}/statuses` (same path every reconcile; no “create vs update” branch in the adapter).
+- **No prerequisite GET**: you do not need to read existing status before reporting.
+- **Headers**: `Authorization: Bearer ` and `Content-Type: application/json` (see [HTTP Headers](#http-headers)).
+
+```text
+ Adapter HyperFleet API
+ | |
+ | PUT /api/hyperfleet/{hyperfleetApiVersion}/ |
+ | clusters/{clusterId}/statuses |
+ | |
+ | Authorization: Bearer |
+ | Content-Type: application/json |
+ | -----------------------------------------------> |
+ | adapter, observed_generation, observed_time, |
+ | conditions[], optional data, optional meta |
+ | |
+ | 200 OK (created or updated) |
+ | <----------------------------------------------- |
+```
### 9. Conditions: reason and message Are Optional
diff --git a/hyperfleet/components/api-service/api-service.md b/hyperfleet/components/api-service/api-service.md
index a794ca9..b2b6ba7 100644
--- a/hyperfleet/components/api-service/api-service.md
+++ b/hyperfleet/components/api-service/api-service.md
@@ -34,7 +34,7 @@ The API is intentionally simple by design — it stores state and aggregates ada
graph TD
User([User / Operator]) -->|CRUD /clusters /nodepools| API[HyperFleet API\nport :8000]
Sentinel[Sentinel] -->|Poll GET /clusters?labels=...| API
- Adapter[Adapters] -->|POST /clusters/:id/statuses| API
+ Adapter[Adapters] -->|PUT /clusters/:id/statuses| API
API -->|Read/Write| DB[(PostgreSQL)]
API -->|Expose| Health[Health :8080\n/healthz /readyz]
API -->|Expose| Metrics[Metrics :9090\n/metrics]
@@ -54,7 +54,7 @@ The primary HyperFleet resource. A cluster tracks desired state (`spec`) and cur
| `/api/hyperfleet/v1/clusters/{id}` | GET | Get cluster by ID |
| `/api/hyperfleet/v1/clusters/{id}` | PATCH | Update cluster spec |
| `/api/hyperfleet/v1/clusters/{id}/statuses` | GET | List adapter status reports |
-| `/api/hyperfleet/v1/clusters/{id}/statuses` | POST | Report adapter status |
+| `/api/hyperfleet/v1/clusters/{id}/statuses` | PUT | Report adapter status |
#### Node Pools
@@ -66,7 +66,7 @@ Groups of compute nodes nested under a cluster.
| `/api/hyperfleet/v1/clusters/{id}/nodepools` | GET | List node pools for a cluster |
| `/api/hyperfleet/v1/clusters/{id}/nodepools` | POST | Create node pool |
| `/api/hyperfleet/v1/clusters/{id}/nodepools/{np_id}` | GET | Get node pool |
-| `/api/hyperfleet/v1/clusters/{id}/nodepools/{np_id}/statuses` | POST | Report adapter status |
+| `/api/hyperfleet/v1/clusters/{id}/nodepools/{np_id}/statuses` | PUT | Report adapter status |
Both resources support:
- **Pagination**: `page` and `size` query parameters
@@ -109,7 +109,7 @@ The `Ready` condition determines if the "state of the world" have been reconcile
It is computed out of the different status reports coming from adapters that have to provide information about the their validity at current API resource `generation`
-This aggregated `Ready` condition is the primary signal consumed by Sentinel's CEL decision logic. The API performs this aggregation on every `POST /statuses` call so Sentinel always reads current state.
+This aggregated `Ready` condition is the primary signal consumed by Sentinel's CEL decision logic. The API performs this aggregation on every `PUT /statuses` call so Sentinel always reads current state.
### Generation Tracking
@@ -222,7 +222,7 @@ Why Rejected: the API gets an additional concern (publishing) and needs to guara
| PostgreSQL 13+ | Primary data store for all resources and statuses |
| Red Hat SSO / OCM | JWT token issuance and validation (production auth) |
| Sentinel | Polls this API for resources; drives reconciliation |
-| Adapters | POST status updates to this API |
+| Adapters | PUT status updates to this API |
---
diff --git a/hyperfleet/components/api-service/hard-delete-design.md b/hyperfleet/components/api-service/hard-delete-design.md
index 57aa71e..47b2d0f 100644
--- a/hyperfleet/components/api-service/hard-delete-design.md
+++ b/hyperfleet/components/api-service/hard-delete-design.md
@@ -48,13 +48,13 @@ Last Updated: 2026-04-23
### Overview
-The API hard-deletes DB records within the same `POST /adapter_statuses` request that computes `Reconciled=True`. No new endpoint or component is introduced. The API is the natural owner because it receives every adapter status report, aggregates conditions to compute `Reconciled`, and can hard-delete atomically within the same database transaction.
+The API hard-deletes DB records within the same `PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses` request that computes `Reconciled=True`. No new endpoint or component is introduced. The API is the natural owner because it receives every adapter status report, aggregates conditions to compute `Reconciled`, and can hard-delete atomically within the same database transaction.
### Service-Layer Ordering Enforcement (Primary Control)
The **service layer** is the primary enforcement mechanism for bottom-up deletion ordering. This is a business rule, not a database concern.
-When the API receives `POST /clusters/{id}/adapter_statuses` with `Finalized=True`, the service layer:
+When the API receives `PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses` with `Finalized=True`, the service layer:
1. Stores the adapter conditions as reported
2. Computes `Reconciled` by checking **both**:
@@ -94,13 +94,13 @@ sequenceDiagram
Sentinel-->>NPAdapter: CloudEvent (nodepool)
CLAdapter->>CLAdapter: Clean up cluster-level K8s resources
- CLAdapter->>API: POST /clusters/{id}/adapter_statuses (Finalized=True)
+ CLAdapter->>API: PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses (Finalized=True)
API->>DB: Store cluster adapter conditions as reported
API->>DB: Reconciled=False (nodepools still exist)
API-->>CLAdapter: 200 OK
NPAdapter->>NPAdapter: Clean up nodepool-level K8s resources
- NPAdapter->>API: POST /nodepools/{id}/adapter_statuses (Finalized=True)
+ NPAdapter->>API: PUT /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses (Finalized=True)
API->>DB: Store nodepool adapter conditions as reported
API->>DB: Nodepool Reconciled=True
API->>DB: DELETE nodepool record and adapter_statuses
@@ -109,7 +109,7 @@ sequenceDiagram
Sentinel->>API: Poll: cluster Reconciled=False
Sentinel-->>CLAdapter: CloudEvent (cluster)
- CLAdapter->>API: POST /clusters/{id}/adapter_statuses (Finalized=True)
+ CLAdapter->>API: PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses (Finalized=True)
API->>DB: Store cluster adapter conditions as reported
API->>DB: Reconciled=True (no nodepools remain)
API->>DB: DELETE cluster record and adapter_statuses
@@ -131,7 +131,7 @@ sequenceDiagram
participant API
participant DB
- CLAdapter->>API: POST /clusters/{id}/adapter_statuses (Finalized=True)
+ CLAdapter->>API: PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses (Finalized=True)
API->>DB: BEGIN TRANSACTION
API->>DB: Store cluster adapter conditions
API->>DB: Compute Reconciled=True (no nodepools remain)
@@ -146,7 +146,7 @@ sequenceDiagram
API-->>CLAdapter: 500 Internal Server Error
Note over DB: Cluster remains with deleted_time set, Reconciled=False
Note over CLAdapter: Sentinel will re-trigger on next poll cycle
- CLAdapter->>API: POST /clusters/{id}/adapter_statuses (Finalized=True)
+ CLAdapter->>API: PUT /api/hyperfleet/v1/clusters/{cluster_id}/statuses (Finalized=True)
Note over API: Retry hard-delete logic
end
```
diff --git a/hyperfleet/components/sentinel/sentinel.md b/hyperfleet/components/sentinel/sentinel.md
index a6658f8..781203f 100644
--- a/hyperfleet/components/sentinel/sentinel.md
+++ b/hyperfleet/components/sentinel/sentinel.md
@@ -61,7 +61,7 @@ Implement a "HyperFleet Sentinel" service that continuously polls the HyperFleet
Without the Sentinel, the cluster provisioning workflow has a critical gap:
-1. **No Reconciliation Loop**: After adapters complete their work and post status updates, nothing triggers subsequent adapters to check if they can now proceed
+1. **No Reconciliation Loop**: After adapters complete their work and put status updates, nothing triggers subsequent adapters to check if they can now proceed
2. **Stuck Clusters**: Clusters remain in "pending" state indefinitely with no mechanism to retry failed operations
3. **Manual Intervention Required**: Operators must manually trigger reconciliation or restart adapters
4. **No Failure Recovery**: Transient failures cannot self-heal without a retry mechanism
diff --git a/hyperfleet/docs/force-deletion-design.md b/hyperfleet/docs/force-deletion-design.md
index 0ca1357..6e92391 100644
--- a/hyperfleet/docs/force-deletion-design.md
+++ b/hyperfleet/docs/force-deletion-design.md
@@ -102,7 +102,7 @@ Force delete also works on individual subresources. For example, a single stuck
Force delete requires no changes to Sentinel's polling or event publishing. Once records are removed from the DB, Sentinel has nothing to poll.
-Adapters may receive events for resources that have been force-deleted. When an adapter tries to GET the resource as a precondition or POST its status back to the API, the API returns 404. Adapters must handle this gracefully (log and move on, do not retry).
+Adapters may receive events for resources that have been force-deleted. When an adapter tries to GET the resource as a precondition or PUT its status back to the API, the API returns 404. Adapters must handle this gracefully (log and move on, do not retry).
---
diff --git a/hyperfleet/docs/status-guide.md b/hyperfleet/docs/status-guide.md
index 004f391..e65e3a6 100644
--- a/hyperfleet/docs/status-guide.md
+++ b/hyperfleet/docs/status-guide.md
@@ -16,7 +16,7 @@ Last Updated: 2026-03-11
- [Adapter Status Reporting Flow](#adapter-status-reporting-flow)
- [Adapter Implementation Pattern](#adapter-implementation-pattern)
- [The Adapter Status Contract](#the-adapter-status-contract)
- - [Reporting Status: Always POST](#reporting-status-always-post)
+ - [Reporting Status: Always PUT](#reporting-status-always-put)
- [CRITICAL: Always Update `last_updated_time`](#critical-always-update-last_updated_time)
- [Implementation via Adapter Configuration (PR #18)](#implementation-via-adapter-configuration-pr-18)
- [ClusterStatus Object Structure](#clusterstatus-object-structure)
@@ -130,17 +130,17 @@ HyperFleet uses a **condition-based status reporting contract** where adapters r
|--------|----------|-------------|
| **GET** | `/v1/clusters/{clusterId}` | Get cluster with aggregated status (phase + adapter availability) |
| **GET** | `/v1/clusters/{clusterId}/statuses` | Get the ClusterStatus with all adapter statuses (optional - for querying) |
-| **POST** | `/v1/clusters/{clusterId}/statuses` | Adapter reports status (API handles upsert internally) |
+| **PUT** | `/v1/clusters/{clusterId}/statuses` | Adapter reports status (API handles upsert internally) |
> **Note**: This document will be updated with references to the Adapter Configuration Framework from [PR #18](https://github.com/openshift-hyperfleet/architecture/pull/18) once it is merged. The PR introduces a declarative YAML-based system for adapter configuration, event handling, and status reporting.
### Adapter Status Reporting Flow
-When an adapter needs to report its status, it **always POSTs**. The API handles the upsert logic internally.
+When an adapter needs to report its status, it **always PUTs**. The API handles the upsert logic internally.
-#### Adapter Action: POST Status Report
+#### Adapter Action: PUT Status Report
-**POST** `/v1/clusters/{clusterId}/statuses`
+**PUT** `/v1/clusters/{clusterId}/statuses`
```json
{
@@ -185,7 +185,7 @@ When an adapter needs to report its status, it **always POSTs**. The API handles
**Response**: `200 OK` with updated ClusterStatus object (whether first report or subsequent update)
**What Happens (API-side)**:
-1. API receives POST with `adapter` field identifying which adapter is reporting
+1. API receives PUT with `adapter` field identifying which adapter is reporting
2. API finds the adapter entry in `adapter_statuses` array (or creates if first time)
3. If first report: API INSERTs adapter with `created_at = now()`
4. If subsequent report: API UPDATEs adapter, preserving `created_at` and updating `updated_at`
@@ -202,8 +202,8 @@ When an adapter needs to report its status, it **always POSTs**. The API handles
```javascript
function reportStatus(clusterId, adapterStatus) {
- // Adapter always POSTs - API decides if INSERT or UPDATE
- POST /v1/clusters/{clusterId}/statuses
+ // Adapter always PUTs - API decides if INSERT or UPDATE
+ PUT /v1/clusters/{clusterId}/statuses
body = {
adapter: "dns", // Identifies which adapter
observed_generation: 1,
@@ -228,11 +228,11 @@ function reportStatus(clusterId, adapterStatus) {
## The Adapter Status Contract
-### Reporting Status: Always POST
+### Reporting Status: Always PUT
-Adapters **always POST** to report status. The API handles upsert internally (INSERT if first report, UPDATE if adapter already reported).
+Adapters **always PUT** to report status. The API handles upsert internally (INSERT if first report, UPDATE if adapter already reported).
-**Endpoint**: `POST /v1/clusters/{clusterId}/statuses`
+**Endpoint**: `PUT /v1/clusters/{clusterId}/statuses`
**Payload Structure**: Adapters send just their status with `adapter` field identifying themselves:
@@ -355,9 +355,9 @@ Adapters generate this status payload using declarative configuration that defin
1. **Status Evaluation Rules** - How to calculate each condition (Applied, Available, Health) based on resource state
2. **Payload Templates** - How to construct the JSON payload with dynamic data
-3. **API Reporting Actions** - When and how to POST/PATCH to the HyperFleet API
+3. **API Reporting Actions** - When and how to PUT to the HyperFleet API
-**Example configuration snippet** (from PR #18):
+**Example configuration snippet**
```yaml
postProcessing:
statusEvaluation:
@@ -377,7 +377,7 @@ postProcessing:
actions:
- type: "api_call"
- method: "PATCH"
+ method: "PUT"
endpoint: "{{.hyperfleetApi}}/api/{{.version}}/clusters/{{.clusterId}}/statuses"
body: "{{.clusterStatusPayload}}"
```
@@ -501,7 +501,7 @@ The ClusterStatus object is a **RESTful resource** that contains ALL adapter sta
- ClusterStatus does NOT have a generation field - it reflects current observed state
- Each adapter in `adapter_statuses` has `observed_generation` indicating which cluster generation it has reconciled
- Cluster spec has `generation` (user's intent), adapters report `observed_generation` (observed state)
-- Adapters always POST with `adapter` field in payload - API handles upsert internally
+- Adapters always PUT with `adapter` field in payload - API handles upsert internally
- API creates ClusterStatus on first report, updates adapter entry on subsequent reports
- This is much more RESTful: `/clusters/{id}/statuses` represents the complete status of the cluster
- Prevents scattered status objects - everything in one cohesive resource
@@ -1696,51 +1696,46 @@ policies:
The following examples show **individual adapter status payloads** that adapters send. These become entries in the ClusterStatus `adapter_statuses` array.
-> **Implementation Note**: Once [PR #18](https://github.com/openshift-hyperfleet/architecture/pull/18) is merged, adapters will generate these payloads using declarative configuration. The `postProcessing.statusEvaluation` section in the adapter config defines how to calculate condition states (Applied, Available, Health) by evaluating resource state, and the `actions` section defines when to POST/PATCH these payloads to the HyperFleet API.
+> **Implementation Note**: Once [PR #18](https://github.com/openshift-hyperfleet/architecture/pull/18) is merged, adapters will generate these payloads using declarative configuration. The `postProcessing.statusEvaluation` section in the adapter config defines how to calculate condition states (Applied, Available, Health) by evaluating resource state, and the `actions` section defines when to PUT these payloads to the HyperFleet API.
### 1. Adapter Started (Job Created)
-**Scenario**: Validation adapter received event, created Job. This is the first adapter to report, so it POSTs to create the ClusterStatus.
+**Scenario**: Validation adapter received event, created Job. This is the first adapter to report. The API will create the ClusterStatus resource and add this adapter's status to the `adapter_statuses` array.
-**POST** `/v1/clusters/cls-123/statuses`
+**PUT** `/v1/clusters/cls-123/statuses`
```json
{
- "generation": 1,
- "adapter_statuses": [
+ "adapter": "validation",
+ "observed_generation": 1,
+ "conditions": [
{
- "adapter": "validation",
- "observed_generation": 1,
- "conditions": [
- {
- "type": "Available",
- "status": "False",
- "reason": "JobRunning",
- "message": "Validation Job is executing",
- "last_transition_time": "2025-10-17T12:00:05Z"
- },
- {
- "type": "Applied",
- "status": "True",
- "reason": "JobLaunched",
- "message": "Kubernetes Job 'validation-cls-123-gen1' created successfully",
- "last_transition_time": "2025-10-17T12:00:05Z"
- },
- {
- "type": "Health",
- "status": "True",
- "reason": "NoErrors",
- "message": "Adapter is healthy",
- "last_transition_time": "2025-10-17T12:00:05Z"
- }
- ],
- "metadata": {
- "job_name": "validation-cls-123-gen1",
- "job_namespace": "hyperfleet-jobs"
- },
- "last_updated_time": "2025-10-17T12:00:05Z"
+ "type": "Available",
+ "status": "False",
+ "reason": "JobRunning",
+ "message": "Validation Job is executing",
+ "last_transition_time": "2025-10-17T12:00:05Z"
+ },
+ {
+ "type": "Applied",
+ "status": "True",
+ "reason": "JobLaunched",
+ "message": "Kubernetes Job 'validation-cls-123-gen1' created successfully",
+ "last_transition_time": "2025-10-17T12:00:05Z"
+ },
+ {
+ "type": "Health",
+ "status": "True",
+ "reason": "NoErrors",
+ "message": "Adapter is healthy",
+ "last_transition_time": "2025-10-17T12:00:05Z"
}
- ]
+ ],
+ "metadata": {
+ "job_name": "validation-cls-123-gen1",
+ "job_namespace": "hyperfleet-jobs"
+ },
+ "last_updated_time": "2025-10-17T12:00:05Z"
}
```
@@ -1753,9 +1748,9 @@ The following examples show **individual adapter status payloads** that adapters
### 2. Adapter Succeeded
-**Scenario**: Validation Job completed successfully. Validation adapter POSTs to update its status (API handles upsert).
+**Scenario**: Validation Job completed successfully. Validation adapter PUTs to update its status (API handles upsert).
-**POST** `/v1/clusters/cls-123/statuses`
+**PUT** `/v1/clusters/cls-123/statuses`
```json
{
@@ -1817,7 +1812,7 @@ The following examples show **individual adapter status payloads** that adapters
**Scenario**: Validation Job ran but found missing Route53 zone
-**POST** `/v1/clusters/cls-123/statuses`
+**PUT** `/v1/clusters/cls-123/statuses`
```json
{
@@ -1874,7 +1869,7 @@ The following examples show **individual adapter status payloads** that adapters
**Scenario**: Adapter couldn't create Job due to quota exceeded.
-**POST** `/v1/clusters/cls-123/statuses`
+**PUT** `/v1/clusters/cls-123/statuses`
```json
{
@@ -2818,7 +2813,7 @@ Automated API calls to report status using the contract defined in this document
```yaml
actions:
- type: "api_call"
- method: "PATCH"
+ method: "PUT"
endpoint: "{{.hyperfleetApi}}/api/{{.version}}/clusters/{{.clusterId}}/statuses"
body: "{{.clusterStatusPayload}}"
```
@@ -2863,7 +2858,7 @@ The configuration-driven approach provides:
The adapter configuration system implements the status contract defined in this document:
- **Three Required Conditions** - `applied`, `available`, `health` are explicit sections in `statusEvaluation`
-- **Simple POST Pattern** - Framework automatically POSTs status reports (API handles upsert)
+- **Simple PUT Pattern** - Framework automatically PUTs status reports (API handles upsert)
- **observed_generation** - Automatically included in status payloads from event parameters
- **Condition Structure** - Templates generate proper `type`, `status`, `reason`, `message` fields
- **Data Field** - Custom data can be included via configuration templates
@@ -2880,7 +2875,7 @@ For a complete example of an adapter configuration that implements this status c
**ClusterStatus Object** (detailed, verbose):
- ONE ClusterStatus per cluster containing all adapter statuses
-- Adapters always POST: `POST /v1/clusters/{clusterId}/statuses` with `adapter` field in payload
+- Adapters always PUT: `PUT /v1/clusters/{clusterId}/statuses` with `adapter` field in payload
- API handles upsert internally: INSERT on first report, UPDATE on subsequent reports
- Contains `adapter_statuses` array with full conditions, data, and metadata for each adapter
- Retrieved via `GET /v1/clusters/{clusterId}/statuses` (optional - for querying)