67 lines
2.4 KiB
Go
67 lines
2.4 KiB
Go
// 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}
|
|
}
|