122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1"
|
|
)
|
|
|
|
// Reasons used on the Provisioned condition, plus the two the reconciler
|
|
// writes on the Healthy condition when representing the health engine's
|
|
// verdict (the engine owns the state; only the reconciler writes status).
|
|
const (
|
|
ReasonProvisioning = "Provisioning"
|
|
ReasonCreated = "Created"
|
|
ReasonReplacing = "Replacing"
|
|
ReasonRecreating = "Recreating"
|
|
ReasonQuotaExceeded = "QuotaExceeded"
|
|
ReasonPermanentError = "PermanentError"
|
|
ReasonCloudInitError = "CloudInitError"
|
|
ReasonExternalEndpoint = "ExternalEndpoint"
|
|
ReasonDeleting = "Deleting"
|
|
|
|
ReasonProbeSucceeded = "ProbeSucceeded"
|
|
ReasonProbeFailed = "ProbeFailed"
|
|
)
|
|
|
|
// setProvisioned stages the Provisioned condition on p. Nothing is written
|
|
// to the API server here; the deferred patch in Reconcile flushes it.
|
|
// ObservedGeneration is passed explicitly — SetStatusCondition does not
|
|
// populate it, and without it every condition would report generation 0.
|
|
func setProvisioned(p *crawlv1alpha1.Proxy, status metav1.ConditionStatus, reason, message string) {
|
|
apimeta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
|
|
Type: crawlv1alpha1.ConditionProvisioned,
|
|
Status: status,
|
|
Reason: reason,
|
|
Message: message,
|
|
ObservedGeneration: p.Generation,
|
|
})
|
|
}
|
|
|
|
// applyHealth stages the Healthy condition and the latency fields from the
|
|
// health engine's current snapshot. Called only from states where the proxy
|
|
// is reachable (Running, External); everywhere else the condition is either
|
|
// left as-is or removed by the create branch.
|
|
func (r *ProxyReconciler) applyHealth(p *crawlv1alpha1.Proxy) {
|
|
if r.Health == nil {
|
|
return
|
|
}
|
|
snap, ok := r.Health.Snapshot(client.ObjectKeyFromObject(p))
|
|
if !ok {
|
|
return
|
|
}
|
|
cond := metav1.Condition{
|
|
Type: crawlv1alpha1.ConditionHealthy,
|
|
ObservedGeneration: p.Generation,
|
|
}
|
|
if snap.Healthy {
|
|
cond.Status = metav1.ConditionTrue
|
|
cond.Reason = ReasonProbeSucceeded
|
|
cond.Message = "probe succeeded through the proxy"
|
|
} else {
|
|
cond.Status = metav1.ConditionFalse
|
|
cond.Reason = ReasonProbeFailed
|
|
cond.Message = fmt.Sprintf("%d consecutive probe failures; last: %s",
|
|
snap.ConsecutiveFailures, snap.LastError)
|
|
}
|
|
apimeta.SetStatusCondition(&p.Status.Conditions, cond)
|
|
p.Status.LatencyMillis = snap.Latency.Milliseconds()
|
|
if !snap.LastProbe.IsZero() {
|
|
p.Status.LastHealthCheckTime = &metav1.Time{Time: snap.LastProbe}
|
|
}
|
|
}
|
|
|
|
// computePhase derives status.phase from deletionTimestamp and the
|
|
// Provisioned/Healthy conditions. Pure, so the truth table is unit-testable.
|
|
func computePhase(p *crawlv1alpha1.Proxy) crawlv1alpha1.ProxyPhase {
|
|
if !p.DeletionTimestamp.IsZero() {
|
|
return crawlv1alpha1.PhaseDeleting
|
|
}
|
|
prov := apimeta.FindStatusCondition(p.Status.Conditions, crawlv1alpha1.ConditionProvisioned)
|
|
if prov == nil {
|
|
return crawlv1alpha1.PhasePending
|
|
}
|
|
if prov.Status != metav1.ConditionTrue {
|
|
// Quota exhaustion is a slow-retry wait, not a terminal state — only
|
|
// a permanent error latches Failed.
|
|
if prov.Reason == ReasonPermanentError {
|
|
return crawlv1alpha1.PhaseFailed
|
|
}
|
|
return crawlv1alpha1.PhaseProvisioning
|
|
}
|
|
healthy := apimeta.FindStatusCondition(p.Status.Conditions, crawlv1alpha1.ConditionHealthy)
|
|
switch {
|
|
case healthy == nil || healthy.Status == metav1.ConditionUnknown:
|
|
// Provisioned but no health verdict yet: still being brought into
|
|
// service.
|
|
return crawlv1alpha1.PhaseProvisioning
|
|
case healthy.Status == metav1.ConditionTrue:
|
|
return crawlv1alpha1.PhaseReady
|
|
default:
|
|
return crawlv1alpha1.PhaseUnhealthy
|
|
}
|
|
}
|
|
|
|
// patchStatusIfChanged recomputes the derived status fields and issues one
|
|
// status patch — or none, when nothing changed. This is the only place the
|
|
// reconciler writes status.
|
|
func (r *ProxyReconciler) patchStatusIfChanged(ctx context.Context, base, p *crawlv1alpha1.Proxy) error {
|
|
p.Status.ObservedGeneration = p.Generation
|
|
p.Status.Phase = computePhase(p)
|
|
if equality.Semantic.DeepEqual(base.Status, p.Status) {
|
|
return nil
|
|
}
|
|
return r.Status().Patch(ctx, p, client.MergeFrom(base))
|
|
}
|