package providerhealth import ( "fmt" "slices" "strings" "unicode/utf8" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1" ) // Reasons on the Ready condition. const ( reasonAllTargetsReady = "AllTargetsReady" reasonTargetsNotReady = "TargetsNotReady" reasonChecksNotImplemented = "ChecksNotImplemented" reasonNoTargets = "NoTargets" ) // maxMessageLen matches the CRD's limit on a target's message. const maxMessageLen = 1024 // buildStatus derives a ProviderHealth status from the previous one and a // fresh result. Every timestamp comes from r.CheckedAt, and prev is left // untouched. A target keeps its lastTransitionTime while ready does not // flip, which is how several hosts each show when they went down. func buildStatus(prev crawlv1alpha1.ProviderHealthStatus, r Result) crawlv1alpha1.ProviderHealthStatus { now := metav1.NewTime(r.CheckedAt) out := crawlv1alpha1.ProviderHealthStatus{ Type: r.Type, Conditions: slices.Clone(prev.Conditions), LastCheckTime: &now, } before := make(map[string]crawlv1alpha1.ProviderTarget, len(prev.Targets)) for _, t := range prev.Targets { before[t.Name] = t } ready := 0 var failing []string for _, ts := range r.Targets { t := crawlv1alpha1.ProviderTarget{ Name: ts.Name, Address: ts.Address, Ready: ts.Ready, Reason: ts.Reason, Message: truncate(ts.Message, maxMessageLen), LatencyMillis: ts.Latency.Milliseconds(), LastTransitionTime: now, Details: ts.Details, } if b, ok := before[ts.Name]; ok && b.Ready == ts.Ready { t.LastTransitionTime = b.LastTransitionTime } if ts.Ready { ready++ } else { failing = append(failing, ts.Name+": "+ts.Reason) } out.Targets = append(out.Targets, t) } if r.Supported { out.TargetsReady = fmt.Sprintf("%d/%d", ready, len(r.Targets)) } cond := metav1.Condition{Type: crawlv1alpha1.ConditionReady, LastTransitionTime: now} switch { case !r.Supported: cond.Status = metav1.ConditionUnknown cond.Reason = reasonChecksNotImplemented cond.Message = fmt.Sprintf("%s providers have no health check", r.Type) case len(r.Targets) == 0: cond.Status = metav1.ConditionUnknown cond.Reason = reasonNoTargets cond.Message = "the provider has nothing to check" case len(failing) == 0: cond.Status = metav1.ConditionTrue cond.Reason = reasonAllTargetsReady cond.Message = fmt.Sprintf("%d of %d targets ready", ready, len(r.Targets)) default: cond.Status = metav1.ConditionFalse cond.Reason = reasonTargetsNotReady cond.Message = truncate(strings.Join(failing, "; "), maxMessageLen) } apimeta.SetStatusCondition(&out.Conditions, cond) return out } // truncate cuts s to at most n characters, which is how the API server // counts maxLength. func truncate(s string, n int) string { if utf8.RuneCountInString(s) <= n { return s } return string([]rune(s)[:n-1]) + "…" }