A leader-only runnable checks every configured provider concurrently under one timeout per round (first round at start, then every interval), keeps the latest results for the metrics collector, logs targets going down, changing reason or coming back, and publishes one ProviderHealth object per provider: created with a managed-by label when missing, then a status merge patch. Per-target lastTransitionTime survives rounds in which ready does not flip. Objects of providers that are no longer configured are deleted once at start; with checks turned off, all are. Reads go through the API reader, so no cluster-wide informer is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
99 lines
3.0 KiB
Go
99 lines
3.0 KiB
Go
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]) + "…"
|
|
}
|