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)) }