This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeHTTPDoer struct {
|
||||
response *http.Response
|
||||
err error
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (f *fakeHTTPDoer) Do(request *http.Request) (*http.Response, error) {
|
||||
f.request = request
|
||||
return f.response, f.err
|
||||
}
|
||||
|
||||
func probeDefinition(probeType string) Definition {
|
||||
return Definition{
|
||||
ID: "probe-1",
|
||||
ServiceID: "service-1",
|
||||
Name: "probe",
|
||||
Type: probeType,
|
||||
Target: Target{Scheme: "http", Host: "service.internal", Port: 8080, Path: "/health"},
|
||||
Interval: 30 * time.Second,
|
||||
Timeout: 2 * time.Second,
|
||||
Enabled: true,
|
||||
Revision: 1,
|
||||
VerifyTLS: false,
|
||||
FollowRedirects: false,
|
||||
}
|
||||
}
|
||||
|
||||
func executorPolicy() NetworkPolicy {
|
||||
return NetworkPolicy{Revision: 1, AllowedNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("127.0.0.0/8")}}
|
||||
}
|
||||
|
||||
func executorResolver(_ context.Context, _ string) ([]netip.Addr, error) {
|
||||
return []netip.Addr{netip.MustParseAddr("10.10.0.8")}, nil
|
||||
}
|
||||
|
||||
func TestProbeExecutorHTTPAssertionsAndStatus(t *testing.T) {
|
||||
body := `{"ok":true,"message":"ready"}`
|
||||
doer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}}
|
||||
definition := probeDefinition(TypeHTTP)
|
||||
definition.ContentAssertion = map[string]any{"keyword": "ready", "json": map[string]any{"ok": true}}
|
||||
executor := ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: doer}
|
||||
result, err := executor.Execute(context.Background(), definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.State != "up" || result.StatusCode == nil || *result.StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected HTTP result: %+v", result)
|
||||
}
|
||||
if doer.request == nil || doer.request.Method != http.MethodGet || doer.request.URL.String() != "http://service.internal:8080/health" {
|
||||
t.Fatalf("unexpected request: %+v", doer.request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeExecutorHTTPFailureBodyLimitAndRedaction(t *testing.T) {
|
||||
definition := probeDefinition(TypeHTTP)
|
||||
definition.ExpectedStatusCodes = []int{http.StatusOK}
|
||||
definition.SecretReference = "secret-ref-should-never-leak"
|
||||
failureDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusServiceUnavailable, Body: io.NopCloser(strings.NewReader("down"))}}
|
||||
failure, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: failureDoer}).Execute(context.Background(), definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if failure.State != "down" || failure.ErrorClass != "status_not_expected" || strings.Contains(failure.ErrorMessage, definition.SecretReference) {
|
||||
t.Fatalf("unexpected failure result: %+v", failure)
|
||||
}
|
||||
if failureDoer.request == nil || failureDoer.request.Header.Get("Authorization") != "" {
|
||||
t.Fatal("probe credentials were placed in the request")
|
||||
}
|
||||
|
||||
limitedPolicy := executorPolicy()
|
||||
limitedPolicy.MaxResponseBytes = 4
|
||||
largeDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("12345"))}}
|
||||
large, err := (ProbeExecutor{Policy: limitedPolicy, Resolver: resolverFunc(executorResolver), HTTP: largeDoer}).Execute(context.Background(), definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if large.State != "unknown" || large.ErrorClass != "response_too_large" {
|
||||
t.Fatalf("unexpected bounded response result: %+v", large)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeExecutorHTTPRedirectPolicyAndTimeout(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == "/start" {
|
||||
http.Redirect(response, request, "/final", http.StatusFound)
|
||||
return
|
||||
}
|
||||
_, _ = response.Write([]byte("final"))
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := mustPort(serverURL.Port())
|
||||
resolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
||||
if host == "service.internal" {
|
||||
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
|
||||
}
|
||||
return nil, errors.New("unexpected host")
|
||||
})
|
||||
definition := probeDefinition(TypeHTTP)
|
||||
definition.Target = Target{Scheme: "http", Host: "service.internal", Port: port, Path: "/start"}
|
||||
definition.FollowRedirects = false
|
||||
withoutRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withoutRedirect.State != "up" || withoutRedirect.StatusCode == nil || *withoutRedirect.StatusCode != http.StatusFound {
|
||||
t.Fatalf("redirect was not safely stopped: %+v", withoutRedirect)
|
||||
}
|
||||
definition.FollowRedirects = true
|
||||
withRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withRedirect.State != "up" || withRedirect.StatusCode == nil || *withRedirect.StatusCode != http.StatusOK {
|
||||
t.Fatalf("redirect was not followed within policy: %+v", withRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeExecutorTimeoutAndDNS(t *testing.T) {
|
||||
definition := probeDefinition(TypeHTTP)
|
||||
definition.Timeout = time.Second
|
||||
// Use a doer that releases only when the executor's per-probe context expires.
|
||||
blockingDoer := httpDoerFunc(func(request *http.Request) (*http.Response, error) {
|
||||
<-request.Context().Done()
|
||||
return nil, request.Context().Err()
|
||||
})
|
||||
result, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: blockingDoer}).Execute(context.Background(), definition)
|
||||
if !errors.Is(err, context.DeadlineExceeded) || result.ErrorClass != "transport_error" {
|
||||
t.Fatalf("unexpected timeout result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
dnsDefinition := probeDefinition(TypeDNS)
|
||||
dnsDefinition.Target = Target{Host: "dns.internal"}
|
||||
dnsResolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
||||
if host != "dns.internal" {
|
||||
return nil, errors.New("unexpected host")
|
||||
}
|
||||
return []netip.Addr{netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("10.0.0.2")}, nil
|
||||
})
|
||||
dnsResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: dnsResolver}).Execute(context.Background(), dnsDefinition)
|
||||
if err != nil || dnsResult.State != "up" || dnsResult.Attributes["addressCount"] != 2 {
|
||||
t.Fatalf("unexpected DNS result=%+v err=%v", dnsResult, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeExecutorTCPAndUnsupportedICMP(t *testing.T) {
|
||||
definition := probeDefinition(TypeTCP)
|
||||
definition.Target = Target{Host: "tcp.internal", Port: 443}
|
||||
calledNetwork := ""
|
||||
tcpResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
||||
calledNetwork = network
|
||||
client, peer := net.Pipe()
|
||||
_ = peer.Close()
|
||||
return client, nil
|
||||
}}).Execute(context.Background(), definition)
|
||||
if err != nil || tcpResult.State != "up" || calledNetwork != "tcp" {
|
||||
t.Fatalf("unexpected TCP result=%+v err=%v network=%s", tcpResult, err, calledNetwork)
|
||||
}
|
||||
|
||||
icmpDefinition := probeDefinition(TypeICMP)
|
||||
icmpDefinition.Target = Target{Host: "gateway.internal"}
|
||||
icmpResult, err := (ProbeExecutor{Policy: executorPolicy()}).Execute(context.Background(), icmpDefinition)
|
||||
if err != nil || icmpResult.State != "unknown" || icmpResult.ErrorClass != "unsupported" {
|
||||
t.Fatalf("unexpected ICMP fallback result=%+v err=%v", icmpResult, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeExecutorTLSCertificateFacts(t *testing.T) {
|
||||
observed := time.Date(2026, time.August, 2, 12, 0, 0, 0, time.UTC)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificateTemplate := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "tls.internal"}, DNSNames: []string{"tls.internal"}, NotBefore: observed.Add(-time.Hour), NotAfter: observed.Add(90 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}
|
||||
certificateDER, err := x509.CreateCertificate(rand.Reader, certificateTemplate, certificateTemplate, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverCertificate := tls.Certificate{Certificate: [][]byte{certificateDER}, PrivateKey: privateKey}
|
||||
definition := probeDefinition(TypeTLS)
|
||||
definition.Target = Target{Scheme: "https", Host: "tls.internal", Port: 443}
|
||||
definition.VerifyTLS = false
|
||||
tlsResult, err := (ProbeExecutor{
|
||||
Policy: executorPolicy(),
|
||||
Resolver: resolverFunc(func(context.Context, string) ([]netip.Addr, error) {
|
||||
return []netip.Addr{netip.MustParseAddr("10.0.0.3")}, nil
|
||||
}),
|
||||
Now: func() time.Time { return observed },
|
||||
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
||||
if network != "tcp" {
|
||||
return nil, errors.New("unexpected TLS network")
|
||||
}
|
||||
client, server := net.Pipe()
|
||||
go func() {
|
||||
defer server.Close()
|
||||
_ = tls.Server(server, &tls.Config{Certificates: []tls.Certificate{serverCertificate}}).Handshake()
|
||||
}()
|
||||
return client, nil
|
||||
},
|
||||
}).Execute(context.Background(), definition)
|
||||
if err != nil || tlsResult.State != "up" || tlsResult.Certificate == nil || tlsResult.Certificate.VerificationState != "valid" || tlsResult.Certificate.HostnameValid == nil || !*tlsResult.Certificate.HostnameValid {
|
||||
t.Fatalf("unexpected TLS result=%+v err=%v", tlsResult, err)
|
||||
}
|
||||
|
||||
attention := *certificateTemplate
|
||||
attention.NotAfter = observed.Add(7 * 24 * time.Hour)
|
||||
attentionResult := certificateFromX509(&attention, definition, func() time.Time { return observed })
|
||||
if attentionResult.VerificationState != "attention" {
|
||||
t.Fatalf("expected certificate attention, got %+v", attentionResult)
|
||||
}
|
||||
invalid := *certificateTemplate
|
||||
invalid.DNSNames = []string{"other.internal"}
|
||||
invalidResult := certificateFromX509(&invalid, definition, func() time.Time { return observed })
|
||||
if invalidResult.VerificationState != "invalid" || invalidResult.HostnameValid == nil || *invalidResult.HostnameValid {
|
||||
t.Fatalf("expected invalid hostname, got %+v", invalidResult)
|
||||
}
|
||||
}
|
||||
|
||||
type httpDoerFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f httpDoerFunc) Do(request *http.Request) (*http.Response, error) { return f(request) }
|
||||
Reference in New Issue
Block a user