Add the health engine: through-proxy probes, thresholds, channel-fed transitions
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
66
internal/health/probe.go
Normal file
66
internal/health/probe.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Package health actively probes every proxy by fetching a URL through the
|
||||
// proxy itself, keeps per-proxy threshold state, and pushes status-affecting
|
||||
// transitions to the reconciler over a channel. The engine owns health
|
||||
// state; the reconciler owns its representation in the Proxy's status.
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
// probeResult is the outcome of a single through-the-proxy probe.
|
||||
type probeResult struct {
|
||||
ok bool
|
||||
latency time.Duration
|
||||
err error
|
||||
}
|
||||
|
||||
// probe fetches hc.ProbeURL through the proxy at proxyURL. For an https
|
||||
// probe URL the transport issues CONNECT to the proxy and TLS-handshakes
|
||||
// through the tunnel; a proxy that accepts TCP but cannot egress answers
|
||||
// CONNECT with a non-200, which client.Do surfaces as an error, not a
|
||||
// response — so success requires err == nil AND an expected status code.
|
||||
// tlsCfg is nil in production (system roots); tests and private-CA setups
|
||||
// inject their own.
|
||||
func probe(ctx context.Context, proxyURL *url.URL, hc crawlv1alpha1.HealthCheckSpec, tlsCfg *tls.Config) probeResult {
|
||||
timeout := time.Duration(hc.TimeoutSeconds) * time.Second
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
// Load-bearing: with keep-alives on, net/http caches the established
|
||||
// CONNECT tunnel and later probes would never re-exercise CONNECT —
|
||||
// exactly the failure this probe exists to catch.
|
||||
DisableKeepAlives: true,
|
||||
ForceAttemptHTTP2: false,
|
||||
TLSHandshakeTimeout: timeout,
|
||||
ResponseHeaderTimeout: timeout,
|
||||
TLSClientConfig: tlsCfg,
|
||||
DialContext: (&net.Dialer{Timeout: timeout}).DialContext,
|
||||
}
|
||||
defer transport.CloseIdleConnections()
|
||||
|
||||
client := &http.Client{Transport: transport, Timeout: timeout}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hc.ProbeURL, nil)
|
||||
if err != nil {
|
||||
return probeResult{err: fmt.Errorf("building probe request: %w", err)}
|
||||
}
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
latency := time.Since(start)
|
||||
if err != nil {
|
||||
return probeResult{latency: latency, err: err}
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if !slices.Contains(hc.ExpectedStatusCodes, int32(resp.StatusCode)) {
|
||||
return probeResult{latency: latency, err: fmt.Errorf("unexpected status %d", resp.StatusCode)}
|
||||
}
|
||||
return probeResult{ok: true, latency: latency}
|
||||
}
|
||||
Reference in New Issue
Block a user