diff --git a/configs/adapter-task-config-template.yaml b/configs/adapter-task-config-template.yaml index 431c2a9..bbac138 100644 --- a/configs/adapter-task-config-template.yaml +++ b/configs/adapter-task-config-template.yaml @@ -351,7 +351,7 @@ post: # Report cluster status to HyperFleet API (always executed) - name: "reportClusterStatus" api_call: - method: "POST" + method: "PUT" # NOTE: API path includes /api/hyperfleet/ prefix and ends with /statuses url: "/clusters/{{ .clusterId }}/statuses" body: "{{ .clusterStatusPayload }}" diff --git a/docs/adapter-authoring-guide.md b/docs/adapter-authoring-guide.md index 93833ff..d1ddf31 100644 --- a/docs/adapter-authoring-guide.md +++ b/docs/adapter-authoring-guide.md @@ -82,7 +82,7 @@ sequenceDiagram Sentinel->>Adapter: CloudEvent {id, generation: N+1} Adapter->>API: GET /clusters/{id} Adapter->>Adapter: Create/update resources - Adapter->>API: POST status {observed_generation: N+1} + Adapter->>API: PUT status {observed_generation: N+1} API->>API: All adapters at N+1 → Reconciled=True ``` @@ -378,7 +378,7 @@ To implement time-based stability checks, you need to know how long a cluster ha | Field | Updates when | Use for | |-------|-------------|---------| | **`last_transition_time`** | Condition status **changes** (True→False or False→True) | **Stability windows** — "cluster has been Reconciled for N minutes" | -| **`last_updated_time`** | Adapter **reports status** (every POST, even if unchanged) | **Liveness checks** — "adapter reported recently" | +| **`last_updated_time`** | Adapter **reports status** (every PUT, even if unchanged) | **Liveness checks** — "adapter reported recently" | **Critical:** For stability windows, always use `last_transition_time`. The `last_updated_time` field has special aggregation behavior that makes it unsuitable for measuring state duration. @@ -896,7 +896,7 @@ sequenceDiagram Workload->>K8s: Update status (conditions, phase) Adapter->>K8s: Discover resource (read status back) Note over Adapter: Evaluate CEL expressions against
discovered resource status - Adapter->>API: POST /statuses {Applied, Available, Health} + Adapter->>API: PUT /statuses {Applied, Available, Health} ``` The adapter does **not** wait for the workload to complete. It reads whatever status is available at discovery time and reports it. If the object is still pending, the adapter reports `Available=False`. The Sentinel will trigger another reconciliation cycle later, and the adapter will read the updated status then. @@ -1259,7 +1259,7 @@ Mock responses matched by HTTP method and URL regex. Supports sequential respons }, { "match": { - "method": "POST", + "method": "PUT", "urlPattern": "/api/hyperfleet/v1/clusters/.*/statuses" }, "responses": [ @@ -1327,7 +1327,7 @@ Phase 3.5: Discovery Results ................. (available as resources.* in payl Phase 4: Post Actions ..................... SUCCESS [1/1] update-status EXECUTED - API Call: POST /api/hyperfleet/v1/clusters/abc123/statuses -> 200 + API Call: PUT /api/hyperfleet/v1/clusters/abc123/statuses -> 200 Result: SUCCESS ``` @@ -1414,7 +1414,7 @@ Post-actions target the NodePool status endpoint instead of the cluster one: post_actions: - name: "reportNodepoolStatus" api_call: - method: "POST" + method: "PUT" url: "/api/hyperfleet/v1/clusters/{{ .clusterId }}/nodepools/{{ .nodepoolId }}/statuses" body: "{{ .nodepoolStatusPayload }}" ``` diff --git a/internal/configloader/validator_test.go b/internal/configloader/validator_test.go index 9867822..b870c91 100644 --- a/internal/configloader/validator_test.go +++ b/internal/configloader/validator_test.go @@ -60,7 +60,7 @@ func TestValidateActionBaseName(t *testing.T) { PostActions: []PostAction{{ ActionBase: ActionBase{ Name: "update-status", - APICall: &APICall{Method: "POST", URL: "/status"}, + APICall: &APICall{Method: "PUT", URL: "/status"}, }, }}, } diff --git a/internal/executor/README.md b/internal/executor/README.md index d19148a..4e9a199 100644 --- a/internal/executor/README.md +++ b/internal/executor/README.md @@ -345,7 +345,7 @@ post: post_actions: - name: "reportStatus" api_call: - method: "POST" + method: "PUT" url: "{{ .apiBaseUrl }}/clusters/{{ .clusterId }}/statuses" body: "{{ .statusPayload }}" ``` @@ -446,7 +446,7 @@ post: post_actions: - name: "reportStatus" api_call: - method: "POST" + method: "PUT" url: "{{ .apiBaseUrl }}/clusters/{{ .clusterId }}/statuses" body: "{{ .statusPayload }}" ``` diff --git a/internal/executor/utils.go b/internal/executor/utils.go index 7d1a565..fb7360e 100644 --- a/internal/executor/utils.go +++ b/internal/executor/utils.go @@ -149,7 +149,7 @@ func ExecuteAPICall( } log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url, string(body)) resp, err = apiClient.Post(ctx, url, body, opts...) - // Log body on failure for debugging + // Log error message on failure for debugging purposes if err != nil || (resp != nil && !resp.IsSuccess()) { var logErr error if err != nil { @@ -158,7 +158,7 @@ func ExecuteAPICall( logErr = fmt.Errorf("POST %s returned non-success status: %d", url, resp.StatusCode) } errCtx := logger.WithErrorField(ctx, logErr) - log.Error(errCtx, "Request failed") + log.Error(errCtx, "POST Request failed") } case http.MethodPut: body := []byte(apiCall.Body) @@ -170,6 +170,17 @@ func ExecuteAPICall( } log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url, string(body)) resp, err = apiClient.Put(ctx, url, body, opts...) + // Log error message on failure for debugging purposes + if err != nil || (resp != nil && !resp.IsSuccess()) { + var logErr error + if err != nil { + logErr = err + } else { + logErr = fmt.Errorf("PUT %s returned non-success status: %d", url, resp.StatusCode) + } + errCtx := logger.WithErrorField(ctx, logErr) + log.Error(errCtx, "PUT Request failed") + } case http.MethodPatch: body := []byte(apiCall.Body) if apiCall.Body != "" { diff --git a/test/integration/executor/executor_integration_test.go b/test/integration/executor/executor_integration_test.go index 2f0a87a..7dd2f96 100644 --- a/test/integration/executor/executor_integration_test.go +++ b/test/integration/executor/executor_integration_test.go @@ -142,7 +142,7 @@ func createTestConfig(apiBaseURL string) *configloader.Config { ActionBase: configloader.ActionBase{ Name: "reportClusterStatus", APICall: &configloader.APICall{ - Method: "POST", + Method: "PUT", URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/statuses", Body: "{{ .clusterStatusPayload }}", Timeout: "5s", @@ -1142,21 +1142,21 @@ func TestExecutor_PostActionAPIFailure(t *testing.T) { // Verify the phase is post_actions assert.Equal(t, executor.PhasePostActions, result.CurrentPhase, "Expected failure in post_actions phase") - // Verify precondition API was called, but status POST failed + // Verify precondition API was called, but status PUT failed requests := mockAPI.GetRequests() - assert.GreaterOrEqual(t, len(requests), 2, "Expected at least 2 API calls (GET cluster + POST status)") + assert.GreaterOrEqual(t, len(requests), 2, "Expected at least 2 API calls (GET cluster + PUT status)") // Find the status POST request - var statusPostFound bool + var statusPutFound bool for _, req := range requests { - if req.Method == http.MethodPost && strings.Contains(req.Path, "/statuses") { - statusPostFound = true - t.Logf("Status POST was attempted: %s %s", req.Method, req.Path) + if req.Method == http.MethodPut && strings.Contains(req.Path, "/statuses") { + statusPutFound = true + t.Logf("Status PUT was attempted: %s %s", req.Method, req.Path) } } - assert.True(t, statusPostFound, "Expected status POST to be attempted") + assert.True(t, statusPutFound, "Expected status PUT to be attempted") - // No status should be successfully stored since POST failed + // No status should be successfully stored since PUT failed statusResponses := mockAPI.GetStatusResponses() assert.Empty(t, statusResponses, "Expected no successful status responses due to API failure") @@ -1254,7 +1254,7 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { ActionBase: configloader.ActionBase{ Name: "reportError", APICall: &configloader.APICall{ - Method: "POST", + Method: "PUT", URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/error-report", Body: "{{ .errorReportPayload }}", Timeout: "5s", @@ -1299,7 +1299,7 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { requests := mockAPI.GetRequests() var errorReportRequest *testutil.MockRequest for i := range requests { - if requests[i].Method == http.MethodPost && strings.Contains(requests[i].Path, "/error-report") { + if requests[i].Method == http.MethodPut && strings.Contains(requests[i].Path, "/error-report") { errorReportRequest = &requests[i] break } @@ -1391,7 +1391,7 @@ func TestExecutor_PayloadBuildFailure(t *testing.T) { ActionBase: configloader.ActionBase{ Name: "shouldNotExecute", APICall: &configloader.APICall{ - Method: "POST", + Method: "PUT", URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/statuses", Body: "{{ .badPayload }}", Timeout: "5s", @@ -1457,7 +1457,7 @@ func TestExecutor_PayloadBuildFailure(t *testing.T) { // Verify NO API call was made to the post action endpoint (blocked) requests := mockAPI.GetRequests() for _, req := range requests { - if req.Method == http.MethodPost && strings.Contains(req.Path, "/statuses") { + if req.Method == http.MethodPut && strings.Contains(req.Path, "/statuses") { t.Errorf("Post action API call should NOT have been made (blocked by payload build failure)") } } diff --git a/test/integration/executor/executor_k8s_integration_test.go b/test/integration/executor/executor_k8s_integration_test.go index 0dafcc3..776c026 100644 --- a/test/integration/executor/executor_k8s_integration_test.go +++ b/test/integration/executor/executor_k8s_integration_test.go @@ -83,7 +83,7 @@ func newK8sTestAPIServer(t *testing.T) *k8sTestAPIServer { switch { case strings.Contains(r.URL.Path, "/clusters/") && strings.HasSuffix(r.URL.Path, "/statuses"): - if r.Method == http.MethodPost { + if r.Method == http.MethodPut { var statusBody map[string]interface{} if err := json.Unmarshal([]byte(bodyStr), &statusBody); err == nil { mock.statusResponses = append(mock.statusResponses, statusBody) @@ -295,7 +295,7 @@ func createK8sTestConfig(testNamespace string) *configloader.Config { ActionBase: configloader.ActionBase{ Name: "reportClusterStatus", APICall: &configloader.APICall{ - Method: "POST", + Method: "PUT", URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/" + "{{ .clusterID }}/statuses", Body: "{{ .clusterStatusPayload }}",