11package services
22
33import (
4+ "context"
45 "encoding/json"
6+ "fmt"
57 "math"
68 "strings"
79 "time"
810 "unicode"
911
1012 "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
13+ "github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
1114)
1215
1316// Mandatory condition types that must be present in all adapter status updates
@@ -19,6 +22,9 @@ const (
1922 ConditionValidationErrorMissing = "missing"
2023)
2124
25+ // adapterConditionStatusTrue is the string value for a True adapter condition status.
26+ const adapterConditionStatusTrue = "True"
27+
2228// Required adapter lists configured via pkg/config/adapter.go (see AdapterRequirementsConfig)
2329
2430// adapterConditionSuffixMap allows overriding the default suffix for specific adapters
@@ -118,18 +124,21 @@ func ComputeAvailableCondition(adapterStatuses api.AdapterStatusList, requiredAd
118124 Status string `json:"status"`
119125 }
120126 if len (adapterStatus .Conditions ) > 0 {
121- if err := json .Unmarshal (adapterStatus .Conditions , & conditions ); err == nil {
122- for _ , cond := range conditions {
123- if cond .Type == api .ConditionTypeAvailable {
124- adapterMap [adapterStatus .Adapter ] = struct {
125- available string
126- observedGeneration int32
127- }{
128- available : cond .Status ,
129- observedGeneration : adapterStatus .ObservedGeneration ,
130- }
131- break
127+ if err := json .Unmarshal (adapterStatus .Conditions , & conditions ); err != nil {
128+ logger .WithError (context .Background (), err ).Warn (
129+ fmt .Sprintf ("failed to parse adapter conditions for adapter %s" , adapterStatus .Adapter ))
130+ continue
131+ }
132+ for _ , cond := range conditions {
133+ if cond .Type == api .ConditionTypeAvailable {
134+ adapterMap [adapterStatus .Adapter ] = struct {
135+ available string
136+ observedGeneration int32
137+ }{
138+ available : cond .Status ,
139+ observedGeneration : adapterStatus .ObservedGeneration ,
132140 }
141+ break
133142 }
134143 }
135144 }
@@ -149,7 +158,7 @@ func ComputeAvailableCondition(adapterStatuses api.AdapterStatusList, requiredAd
149158
150159 // For Available condition, we don't check generation matching
151160 // We just need Available=True at ANY generation
152- if adapterInfo .available == "True" {
161+ if adapterInfo .available == adapterConditionStatusTrue {
153162 numAvailable ++
154163 if adapterInfo .observedGeneration < minObservedGeneration {
155164 minObservedGeneration = adapterInfo .observedGeneration
@@ -189,18 +198,21 @@ func ComputeReadyCondition(
189198 Status string `json:"status"`
190199 }
191200 if len (adapterStatus .Conditions ) > 0 {
192- if err := json .Unmarshal (adapterStatus .Conditions , & conditions ); err == nil {
193- for _ , cond := range conditions {
194- if cond .Type == api .ConditionTypeAvailable {
195- adapterMap [adapterStatus .Adapter ] = struct {
196- available string
197- observedGeneration int32
198- }{
199- available : cond .Status ,
200- observedGeneration : adapterStatus .ObservedGeneration ,
201- }
202- break
201+ if err := json .Unmarshal (adapterStatus .Conditions , & conditions ); err != nil {
202+ logger .WithError (context .Background (), err ).Warn (
203+ fmt .Sprintf ("failed to parse adapter conditions for adapter %s" , adapterStatus .Adapter ))
204+ continue
205+ }
206+ for _ , cond := range conditions {
207+ if cond .Type == api .ConditionTypeAvailable {
208+ adapterMap [adapterStatus .Adapter ] = struct {
209+ available string
210+ observedGeneration int32
211+ }{
212+ available : cond .Status ,
213+ observedGeneration : adapterStatus .ObservedGeneration ,
203214 }
215+ break
204216 }
205217 }
206218 }
@@ -224,7 +236,7 @@ func ComputeReadyCondition(
224236 }
225237
226238 // Check available status
227- if adapterInfo .available == "True" {
239+ if adapterInfo .available == adapterConditionStatusTrue {
228240 numReady ++
229241 }
230242 }
@@ -234,6 +246,79 @@ func ComputeReadyCondition(
234246 return numReady == numRequired
235247}
236248
249+ // findAdapterStatus returns the first adapter status in the list with the given adapter name, or (nil, false).
250+ func findAdapterStatus (adapterStatuses api.AdapterStatusList , adapterName string ) (* api.AdapterStatus , bool ) {
251+ for _ , s := range adapterStatuses {
252+ if s .Adapter == adapterName {
253+ return s , true
254+ }
255+ }
256+ return nil , false
257+ }
258+
259+ // adapterConditionsHasAvailableTrue returns true if the adapter conditions JSON
260+ // contains a condition with type Available and status True.
261+ func adapterConditionsHasAvailableTrue (conditions []byte ) bool {
262+ if len (conditions ) == 0 {
263+ return false
264+ }
265+ var conds []struct {
266+ Type string `json:"type"`
267+ Status string `json:"status"`
268+ }
269+ if err := json .Unmarshal (conditions , & conds ); err != nil {
270+ return false
271+ }
272+ for _ , c := range conds {
273+ if c .Type == api .ConditionTypeAvailable && c .Status == adapterConditionStatusTrue {
274+ return true
275+ }
276+ }
277+ return false
278+ }
279+
280+ // computeReadyLastUpdated returns the timestamp to use for the Ready condition's LastUpdatedTime.
281+ // When isReady is false, it returns now (Ready=False changes frequently; 10s threshold applies).
282+ // When isReady is true, it returns the minimum LastReportTime across all required adapters
283+ // that have Available=True at the current generation. Falls back to now if no timestamps found.
284+ func computeReadyLastUpdated (
285+ adapterStatuses api.AdapterStatusList ,
286+ requiredAdapters []string ,
287+ resourceGeneration int32 ,
288+ now time.Time ,
289+ isReady bool ,
290+ ) time.Time {
291+ if ! isReady {
292+ return now
293+ }
294+
295+ var minTime * time.Time
296+ for _ , adapterName := range requiredAdapters {
297+ status , ok := findAdapterStatus (adapterStatuses , adapterName )
298+ if ! ok {
299+ return now // safety: required adapter missing
300+ }
301+ if status .LastReportTime == nil {
302+ return now // safety: no timestamp
303+ }
304+ if status .ObservedGeneration != resourceGeneration {
305+ continue // not at current gen, skip
306+ }
307+ if ! adapterConditionsHasAvailableTrue (status .Conditions ) {
308+ continue
309+ }
310+ if minTime == nil || status .LastReportTime .Before (* minTime ) {
311+ t := * status .LastReportTime
312+ minTime = & t
313+ }
314+ }
315+
316+ if minTime == nil {
317+ return now // safety fallback
318+ }
319+ return * minTime
320+ }
321+
237322func BuildSyntheticConditions (
238323 existingConditionsJSON []byte ,
239324 adapterStatuses api.AdapterStatusList ,
@@ -271,7 +356,14 @@ func BuildSyntheticConditions(
271356 CreatedTime : now ,
272357 LastUpdatedTime : now ,
273358 }
274- preserveSyntheticCondition (& availableCondition , existingAvailable , now )
359+ availableLastUpdated := now
360+ if existingAvailable != nil &&
361+ existingAvailable .Status == availableStatus &&
362+ existingAvailable .ObservedGeneration == minObservedGeneration &&
363+ ! existingAvailable .LastUpdatedTime .IsZero () {
364+ availableLastUpdated = existingAvailable .LastUpdatedTime
365+ }
366+ applyConditionHistory (& availableCondition , existingAvailable , now , availableLastUpdated )
275367
276368 isReady := ComputeReadyCondition (adapterStatuses , requiredAdapters , resourceGeneration )
277369 readyStatus := api .ConditionFalse
@@ -286,13 +378,25 @@ func BuildSyntheticConditions(
286378 CreatedTime : now ,
287379 LastUpdatedTime : now ,
288380 }
289- preserveSyntheticCondition (& readyCondition , existingReady , now )
381+ readyLastUpdated := computeReadyLastUpdated (
382+ adapterStatuses , requiredAdapters , resourceGeneration , now , isReady ,
383+ )
384+ applyConditionHistory (& readyCondition , existingReady , now , readyLastUpdated )
290385
291386 return availableCondition , readyCondition
292387}
293388
294- func preserveSyntheticCondition (target * api.ResourceCondition , existing * api.ResourceCondition , now time.Time ) {
389+ // applyConditionHistory copies stable timestamps and metadata from an existing condition.
390+ // lastUpdatedTime is used unconditionally for LastUpdatedTime — the caller is responsible
391+ // for computing the correct value (e.g. now, computeReadyLastUpdated(...)).
392+ func applyConditionHistory (
393+ target * api.ResourceCondition ,
394+ existing * api.ResourceCondition ,
395+ now time.Time ,
396+ lastUpdatedTime time.Time ,
397+ ) {
295398 if existing == nil {
399+ target .LastUpdatedTime = lastUpdatedTime
296400 return
297401 }
298402
@@ -303,9 +407,7 @@ func preserveSyntheticCondition(target *api.ResourceCondition, existing *api.Res
303407 if ! existing .LastTransitionTime .IsZero () {
304408 target .LastTransitionTime = existing .LastTransitionTime
305409 }
306- if ! existing .LastUpdatedTime .IsZero () {
307- target .LastUpdatedTime = existing .LastUpdatedTime
308- }
410+ target .LastUpdatedTime = lastUpdatedTime
309411 if target .Reason == nil && existing .Reason != nil {
310412 target .Reason = existing .Reason
311413 }
@@ -319,5 +421,5 @@ func preserveSyntheticCondition(target *api.ResourceCondition, existing *api.Res
319421 target .CreatedTime = existing .CreatedTime
320422 }
321423 target .LastTransitionTime = now
322- target .LastUpdatedTime = now
424+ target .LastUpdatedTime = lastUpdatedTime
323425}
0 commit comments