Skip to content

Commit f358437

Browse files
committed
HYPERFLEET-774 - feat: aggregation logic
1 parent 34ae499 commit f358437

18 files changed

Lines changed: 3205 additions & 1332 deletions

docs/api-operator-guide.md

Lines changed: 230 additions & 81 deletions
Large diffs are not rendered by default.

docs/api-resources.md

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
3232
```
3333

3434
**Response (201 Created):**
35+
<details>
36+
<summary>JSON response 201 created</summary>
3537

3638
```json
3739
{
@@ -55,7 +57,7 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
5557
"status": "False",
5658
"reason": "AwaitingAdapters",
5759
"message": "Waiting for adapters to report status",
58-
"observed_generation": 0,
60+
"observed_generation": 1,
5961
"created_time": "2025-01-01T00:00:00Z",
6062
"last_updated_time": "2025-01-01T00:00:00Z",
6163
"last_transition_time": "2025-01-01T00:00:00Z"
@@ -65,7 +67,7 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
6567
"status": "False",
6668
"reason": "AwaitingAdapters",
6769
"message": "Waiting for adapters to report status",
68-
"observed_generation": 0,
70+
"observed_generation": 1,
6971
"created_time": "2025-01-01T00:00:00Z",
7072
"last_updated_time": "2025-01-01T00:00:00Z",
7173
"last_transition_time": "2025-01-01T00:00:00Z"
@@ -75,6 +77,8 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
7577
}
7678
```
7779

80+
</details>
81+
7882
**Note**: Status initially has `Available=False` and `Ready=False` conditions until adapters report status.
7983

8084
### Get Cluster
@@ -83,6 +87,9 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
8387

8488
**Response (200 OK):**
8589

90+
<details>
91+
<summary>JSON response</summary>
92+
8693
```json
8794
{
8895
"kind": "Cluster",
@@ -125,6 +132,8 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/statuses
125132
}
126133
```
127134

135+
</details>
136+
128137
### List Clusters
129138

130139
**GET** `/api/hyperfleet/v1/clusters?page=1&pageSize=10`
@@ -156,6 +165,9 @@ Adapters use this endpoint to report their status.
156165

157166
**Request Body:**
158167

168+
<details>
169+
<summary>JSON response</summary>
170+
159171
```json
160172
{
161173
"adapter": "validator",
@@ -188,8 +200,13 @@ Adapters use this endpoint to report their status.
188200
}
189201
```
190202

203+
</details>
204+
191205
**Response (201 Created):**
192206

207+
<details>
208+
<summary>JSON response</summary>
209+
193210
```json
194211
{
195212
"adapter": "validator",
@@ -226,28 +243,9 @@ Adapters use this endpoint to report their status.
226243
}
227244
```
228245

229-
**Note**: The API automatically sets `created_time`, `last_report_time`, and `last_transition_time` fields.
230-
231-
### Status Conditions
232-
233-
The status uses Kubernetes-style conditions instead of a single phase field:
246+
</details>
234247

235-
- **Ready** - Whether all adapters report successfully at the current generation
236-
- `True`: All required adapters report `Available=True` at current spec generation
237-
- `False`: One or more adapters report Available=False at current generation
238-
- After every spec change, `Ready` becomes `False` since adapters take some time to report at current spec generation
239-
- Default value when creating the cluster, when no adapters have reported yet any value
240-
241-
- **Available** - Aggregated adapter result for a common `observed_generation`
242-
- `True`: All required adapters report Available=True for the same observed_generation
243-
- `False`: At least one adapter reports Available=False when all adapters report the same observed_generation
244-
- Default value when creating the cluster, when no adapters have reported yet any value
245-
246-
`Available` keeps its value unchanged in case adapters report from a different `observed_generation` or there is already a mix of `observed_generation` statuses
247-
248-
- e.g. `Available=True` for `observed_generation==1`
249-
- One adapter reports `Available=False` for `observed_generation=1` `Available` transitions to `False`
250-
- One adapter reports `Available=False` for `observed_generation=2` `Available` keeps its `True` status
248+
**Note**: The API automatically sets `created_time`, `last_report_time`, and `last_transition_time` fields.
251249

252250
## NodePool Management
253251

@@ -281,6 +279,9 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses
281279

282280
**Response (201 Created):**
283281

282+
<details>
283+
<summary>JSON response</summary>
284+
284285
```json
285286
{
286287
"kind": "NodePool",
@@ -307,7 +308,7 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses
307308
"status": "False",
308309
"reason": "AwaitingAdapters",
309310
"message": "Waiting for adapters to report status",
310-
"observed_generation": 0,
311+
"observed_generation": 1,
311312
"created_time": "2025-01-01T00:00:00Z",
312313
"last_updated_time": "2025-01-01T00:00:00Z",
313314
"last_transition_time": "2025-01-01T00:00:00Z"
@@ -317,7 +318,7 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses
317318
"status": "False",
318319
"reason": "AwaitingAdapters",
319320
"message": "Waiting for adapters to report status",
320-
"observed_generation": 0,
321+
"observed_generation": 1,
321322
"created_time": "2025-01-01T00:00:00Z",
322323
"last_updated_time": "2025-01-01T00:00:00Z",
323324
"last_transition_time": "2025-01-01T00:00:00Z"
@@ -327,12 +328,17 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses
327328
}
328329
```
329330

331+
</details>
332+
330333
### Get NodePool
331334

332335
**GET** `/api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}`
333336

334337
**Response (200 OK):**
335338

339+
<details>
340+
<summary>JSON response</summary>
341+
336342
```json
337343
{
338344
"kind": "NodePool",
@@ -373,6 +379,8 @@ POST /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses
373379
}
374380
```
375381

382+
</details>
383+
376384
### Report NodePool Status
377385

378386
**POST** `/api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses`

pkg/api/adapter_status_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99

1010
// AdapterStatus database model
1111
type AdapterStatus struct {
12-
LastReportTime *time.Time `json:"last_report_time" gorm:"not null"`
13-
CreatedTime *time.Time `json:"created_time" gorm:"not null"`
12+
LastReportTime time.Time `json:"last_report_time" gorm:"not null"`
13+
CreatedTime time.Time `json:"created_time" gorm:"not null"`
1414
Meta
1515
ResourceType string `json:"resource_type" gorm:"size:20;index:idx_resource;not null"`
1616
ResourceID string `json:"resource_id" gorm:"size:255;index:idx_resource;not null"`

pkg/api/presenters/adapter_status.go

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ func ConvertAdapterStatus(
6767
Conditions: conditionsJSON,
6868
Data: dataJSON,
6969
Metadata: metadataJSON,
70-
CreatedTime: &now,
71-
LastReportTime: &now,
70+
CreatedTime: now,
71+
LastReportTime: now,
7272
}, nil
7373
}
7474

@@ -138,23 +138,12 @@ func PresentAdapterStatus(adapterStatus *api.AdapterStatus) (openapi.AdapterStat
138138
}
139139
}
140140

141-
// Set default times if nil (shouldn't happen in normal operation)
142-
createdTime := time.Time{}
143-
if adapterStatus.CreatedTime != nil {
144-
createdTime = *adapterStatus.CreatedTime
145-
}
146-
147-
lastReportTime := time.Time{}
148-
if adapterStatus.LastReportTime != nil {
149-
lastReportTime = *adapterStatus.LastReportTime
150-
}
151-
152141
return openapi.AdapterStatus{
153142
Adapter: adapterStatus.Adapter,
154143
Conditions: openapiConditions,
155-
CreatedTime: createdTime,
144+
CreatedTime: adapterStatus.CreatedTime,
156145
Data: &data,
157-
LastReportTime: lastReportTime,
146+
LastReportTime: adapterStatus.LastReportTime,
158147
Metadata: openapiMetadata,
159148
ObservedGeneration: adapterStatus.ObservedGeneration,
160149
}, nil

pkg/api/presenters/adapter_status_test.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ func TestPresentAdapterStatus_Complete(t *testing.T) {
278278
Conditions: conditionsJSON,
279279
Data: dataJSON,
280280
Metadata: metadataJSON,
281-
CreatedTime: &now,
282-
LastReportTime: &now,
281+
CreatedTime: now,
282+
LastReportTime: now,
283283
}
284284

285285
result, err := PresentAdapterStatus(adapterStatus)
@@ -310,8 +310,8 @@ func TestPresentAdapterStatus_Complete(t *testing.T) {
310310
Expect(result.LastReportTime.Unix()).To(Equal(now.Unix()))
311311
}
312312

313-
// TestPresentAdapterStatus_NilTimestamps tests handling of nil timestamps
314-
func TestPresentAdapterStatus_NilTimestamps(t *testing.T) {
313+
// TestPresentAdapterStatus_ZeroTimestamps tests that zero timestamps pass through as zero time.Time values.
314+
func TestPresentAdapterStatus_ZeroTimestamps(t *testing.T) {
315315
RegisterTestingT(t)
316316

317317
adapterStatus := &api.AdapterStatus{
@@ -321,14 +321,12 @@ func TestPresentAdapterStatus_NilTimestamps(t *testing.T) {
321321
ObservedGeneration: 5,
322322
Conditions: []byte("[]"),
323323
Data: []byte("{}"),
324-
CreatedTime: nil, // Nil timestamp
325-
LastReportTime: nil, // Nil timestamp
324+
// CreatedTime and LastReportTime are zero-valued (unset)
326325
}
327326

328327
result, err := PresentAdapterStatus(adapterStatus)
329328
Expect(err).To(BeNil())
330329

331-
// Verify zero time.Time is returned (not nil)
332330
Expect(result.CreatedTime.IsZero()).To(BeTrue())
333331
Expect(result.LastReportTime.IsZero()).To(BeTrue())
334332
}
@@ -345,8 +343,8 @@ func TestPresentAdapterStatus_EmptyConditions(t *testing.T) {
345343
ObservedGeneration: 2,
346344
Conditions: []byte("[]"), // Empty array JSON
347345
Data: []byte("{}"),
348-
CreatedTime: &now,
349-
LastReportTime: &now,
346+
CreatedTime: now,
347+
LastReportTime: now,
350348
}
351349

352350
result, err := PresentAdapterStatus(adapterStatus)
@@ -368,8 +366,8 @@ func TestPresentAdapterStatus_EmptyData(t *testing.T) {
368366
ObservedGeneration: 3,
369367
Conditions: []byte("[]"),
370368
Data: []byte("{}"), // Empty object JSON
371-
CreatedTime: &now,
372-
LastReportTime: &now,
369+
CreatedTime: now,
370+
LastReportTime: now,
373371
}
374372

375373
result, err := PresentAdapterStatus(adapterStatus)
@@ -410,8 +408,8 @@ func TestPresentAdapterStatus_ConditionStatusConversion(t *testing.T) {
410408
ObservedGeneration: 1,
411409
Conditions: conditionsJSON,
412410
Data: []byte("{}"),
413-
CreatedTime: &now,
414-
LastReportTime: &now,
411+
CreatedTime: now,
412+
LastReportTime: now,
415413
}
416414

417415
result, err := PresentAdapterStatus(adapterStatus)
@@ -471,8 +469,8 @@ func TestPresentAdapterStatus_MalformedConditions(t *testing.T) {
471469
ObservedGeneration: 5,
472470
Conditions: []byte("{invalid json}"), // Malformed JSON
473471
Data: []byte("{}"),
474-
CreatedTime: &now,
475-
LastReportTime: &now,
472+
CreatedTime: now,
473+
LastReportTime: now,
476474
}
477475
adapterStatus.ID = "adapter-status-malformed-conditions"
478476

@@ -494,8 +492,8 @@ func TestPresentAdapterStatus_MalformedData(t *testing.T) {
494492
ObservedGeneration: 5,
495493
Conditions: []byte("[]"),
496494
Data: []byte("{not valid json"), // Malformed JSON
497-
CreatedTime: &now,
498-
LastReportTime: &now,
495+
CreatedTime: now,
496+
LastReportTime: now,
499497
}
500498
adapterStatus.ID = "adapter-status-malformed-data"
501499

@@ -518,8 +516,8 @@ func TestPresentAdapterStatus_MalformedMetadata(t *testing.T) {
518516
Conditions: []byte("[]"),
519517
Data: []byte("{}"),
520518
Metadata: []byte("[{incomplete"), // Malformed JSON
521-
CreatedTime: &now,
522-
LastReportTime: &now,
519+
CreatedTime: now,
520+
LastReportTime: now,
523521
}
524522
adapterStatus.ID = "adapter-status-malformed-metadata"
525523

0 commit comments

Comments
 (0)