mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Extract availability probe execution into a shared package
Move the ICMP, TCP, HTTP and UDP probe core from the monitoring availability poller into internal/availabilityprobe so the host agent can execute the same checks without importing the monitoring package. Monitoring keeps type aliases and thin wrappers, so callers and the canonical guardrail contracts are unchanged. Pure refactor for the upcoming external probe feature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package availabilityprobe
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// Branch-coverage test set for pingArgs, moved verbatim (aside from the
|
||||
// package clause) from
|
||||
// internal/monitoring/availability_poller_purehelpers_branchcov0722am_test.go
|
||||
// when the probe execution core was extracted into this package. It is still
|
||||
// selected via `-run "^TestBranchcov0722"` and covers:
|
||||
//
|
||||
// - pingArgs: the timeoutMillis <= 0 fallback to the config default, the
|
||||
// runtime.GOOS switch (windows / darwin+BSD / default), and the default
|
||||
// arm's ceiling arithmetic ((ms+999)/1000) plus the clamp-to-1 guard.
|
||||
//
|
||||
// Conventions match the sibling monitoring tests: stdlib `testing` only,
|
||||
// table-driven subtests, t.Fatalf assertions, no testify.
|
||||
|
||||
func TestBranchcov0722PingArgs(t *testing.T) {
|
||||
const host = "probe.example.test"
|
||||
|
||||
t.Run("positive timeout reaches the platform arm verbatim", func(t *testing.T) {
|
||||
got := pingArgs(host, 5000)
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
want := []string{"-n", "1", "-w", "5000", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on windows = %v, want %v", host, got, want)
|
||||
}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
want := []string{"-n", "-c", "1", "-W", "5000", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on %s = %v, want %v", host, runtime.GOOS, got, want)
|
||||
}
|
||||
default:
|
||||
// Default arm: (5000 + 999) / 1000 = 5 whole seconds.
|
||||
want := []string{"-n", "-c", "1", "-W", "5", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on %s = %v, want %v", host, runtime.GOOS, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero or negative timeout falls back to config default", func(t *testing.T) {
|
||||
defaultMillis := config.DefaultAvailabilityTimeoutMillis
|
||||
for _, ms := range []int{0, -1, -99999} {
|
||||
got := pingArgs(host, ms)
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
want := []string{"-n", "1", "-w", strconv.Itoa(defaultMillis), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on windows = %v, want %v", host, ms, got, want)
|
||||
}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(defaultMillis), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on %s = %v, want %v", host, ms, runtime.GOOS, got, want)
|
||||
}
|
||||
default:
|
||||
secs := (defaultMillis + 999) / 1000
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(secs), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on %s = %v, want %v", host, ms, runtime.GOOS, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TestBranchcov0722PingArgsDefaultArmCeilingArithmetic targets the
|
||||
// non-Windows / non-BSD ("default") arm of the runtime.GOOS switch, which is
|
||||
// the only arm that performs the (ms+999)/1000 ceiling division and the
|
||||
// clamp-to-1 guard. pingArgs reads runtime.GOOS directly, so this arm can only
|
||||
// be reached on platforms such as linux; on windows/darwin/BSD the test skips
|
||||
// honestly rather than asserting behaviour it cannot reach.
|
||||
func TestBranchcov0722PingArgsDefaultArmCeilingArithmetic(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin", "freebsd", "openbsd", "netbsd":
|
||||
t.Skipf("ceiling arithmetic lives only in the default (linux-like) pingArgs arm; skipping on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
const host = "probe.example.test"
|
||||
cases := []struct {
|
||||
name string
|
||||
timeoutMs int
|
||||
wantSeconds int
|
||||
}{
|
||||
// Ceiling division: any sub-second value rounds up to 1 second.
|
||||
{"one millisecond rounds up to one second", 1, 1},
|
||||
{"just under a second rounds up to one second", 999, 1},
|
||||
// Exact second boundary stays put.
|
||||
{"exact one thousand millis is one second", 1000, 1},
|
||||
// Fractional seconds round up to the next whole second.
|
||||
{"one thousand one millis rounds up to two seconds", 1001, 2},
|
||||
{"fifteen hundred millis rounds up to two seconds", 1500, 2},
|
||||
{"five thousand nine hundred ninety nine rounds up to six seconds", 5999, 6},
|
||||
// NOTE: the `if timeoutSeconds <= 0 { timeoutSeconds = 1 }` clamp is
|
||||
// unreachable here. The non-positive fallback guarantees timeoutMillis
|
||||
// is at least DefaultAvailabilityTimeoutMillis (2000) before this arm,
|
||||
// and any positive value yields (ms+999)/1000 >= 1. No table case can
|
||||
// exercise the clamp; this is documented in the report, not "fixed".
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := pingArgs(host, tc.timeoutMs)
|
||||
// Default arm shape: ["-n", "-c", "1", "-W", <seconds>, host].
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(tc.wantSeconds), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) = %v, want %v", host, tc.timeoutMs, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0722PingArgsCloneIndependence asserts that the returned slice
|
||||
// is freshly allocated on every call: mutating one result must not corrupt a
|
||||
// previously returned or subsequently returned slice, and vice versa. (pingArgs
|
||||
// takes only immutable inputs — a string and an int — so the relevant
|
||||
// invariant is that distinct calls do not alias backing arrays.)
|
||||
func TestBranchcov0722PingArgsCloneIndependence(t *testing.T) {
|
||||
first := pingArgs("alpha", 1000)
|
||||
second := pingArgs("beta", 2000)
|
||||
|
||||
snapshot := append([]string(nil), first...)
|
||||
for i := range second {
|
||||
second[i] = "MUTATED"
|
||||
}
|
||||
if !reflect.DeepEqual(first, snapshot) {
|
||||
t.Fatalf("mutating a pingArgs result corrupted a distinct result: first = %v, want %v", first, snapshot)
|
||||
}
|
||||
|
||||
third := pingArgs("gamma", 3000)
|
||||
snapshotThird := append([]string(nil), third...)
|
||||
for i := range first {
|
||||
first[i] = "MUTATED"
|
||||
}
|
||||
if !reflect.DeepEqual(third, snapshotThird) {
|
||||
t.Fatalf("mutating an earlier pingArgs result corrupted a later result: third = %v, want %v", third, snapshotThird)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// Package availabilityprobe executes a single agentless availability check
|
||||
// against a configured target (ICMP, TCP, UDP, HTTP/HTTPS).
|
||||
//
|
||||
// It holds only the probe execution core, deliberately free of scheduling,
|
||||
// status bookkeeping and resource projection, so that it can be shared between
|
||||
// the monitoring poller (which schedules probes and records their outcomes) and
|
||||
// the host agent's external-probe module (which runs the same checks from a
|
||||
// remote vantage point) without either side pulling in the other's
|
||||
// dependencies.
|
||||
package availabilityprobe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// Outcome describes what a completed probe proved about the target.
|
||||
type Outcome string
|
||||
|
||||
const (
|
||||
// OutcomeReachable means the probe proved the endpoint responds.
|
||||
OutcomeReachable Outcome = "reachable"
|
||||
// OutcomeUnreachable means the probe ran and the endpoint did not respond.
|
||||
OutcomeUnreachable Outcome = "unreachable"
|
||||
// OutcomeIndeterminate means the probe ran cleanly but could not prove
|
||||
// reachability either way (an open-or-filtered UDP timeout).
|
||||
OutcomeIndeterminate Outcome = "indeterminate"
|
||||
)
|
||||
|
||||
// Run executes one agentless availability check.
|
||||
func Run(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
_, err := Result(ctx, target)
|
||||
return err
|
||||
}
|
||||
|
||||
// Result preserves UDP's open-or-filtered state rather than incorrectly
|
||||
// claiming that a silent UDP endpoint was proven reachable.
|
||||
func Result(ctx context.Context, target config.AvailabilityTarget) (Outcome, error) {
|
||||
target = config.NormalizeAvailabilityTarget(target)
|
||||
if err := target.Validate(); err != nil {
|
||||
return OutcomeUnreachable, err
|
||||
}
|
||||
|
||||
timeout := time.Duration(target.EffectiveTimeoutMillis()) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = time.Duration(config.DefaultAvailabilityTimeoutMillis) * time.Millisecond
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
switch target.Protocol {
|
||||
case config.AvailabilityProbeICMP:
|
||||
return outcomeFromError(probeICMP(probeCtx, target))
|
||||
case config.AvailabilityProbeTCP:
|
||||
return outcomeFromError(probeTCP(probeCtx, target))
|
||||
case config.AvailabilityProbeUDP:
|
||||
return probeUDP(probeCtx, target)
|
||||
case config.AvailabilityProbeHTTP, config.AvailabilityProbeHTTPS:
|
||||
return outcomeFromError(probeHTTP(probeCtx, target, timeout))
|
||||
default:
|
||||
return OutcomeUnreachable, fmt.Errorf("unsupported availability protocol %q", target.Protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func outcomeFromError(err error) (Outcome, error) {
|
||||
if err != nil {
|
||||
return OutcomeUnreachable, err
|
||||
}
|
||||
return OutcomeReachable, nil
|
||||
}
|
||||
|
||||
func probeUDP(ctx context.Context, target config.AvailabilityTarget) (Outcome, error) {
|
||||
host := target.ProbeAddress()
|
||||
addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return OutcomeUnreachable, fmt.Errorf("resolve UDP availability target: %w", err)
|
||||
}
|
||||
var selected net.IP
|
||||
for _, address := range addresses {
|
||||
if address.IP == nil || address.IP.IsUnspecified() || address.IP.IsMulticast() || address.IP.Equal(net.IPv4bcast) {
|
||||
continue
|
||||
}
|
||||
selected = address.IP
|
||||
break
|
||||
}
|
||||
if selected == nil {
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP availability target did not resolve to an allowed unicast address")
|
||||
}
|
||||
|
||||
dialer := net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, "udp", net.JoinHostPort(selected.String(), strconv.Itoa(target.Port)))
|
||||
if err != nil {
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP probe dial failed: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return OutcomeUnreachable, fmt.Errorf("set UDP probe deadline: %w", err)
|
||||
}
|
||||
}
|
||||
payload := []byte(target.UDPRequest)
|
||||
if len(payload) == 0 {
|
||||
// A one-byte datagram gives the kernel an opportunity to surface an
|
||||
// ICMP port-unreachable result in open-or-filtered mode.
|
||||
payload = []byte{0}
|
||||
}
|
||||
if _, err := conn.Write(payload); err != nil {
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP probe write failed: %w", err)
|
||||
}
|
||||
|
||||
response := make([]byte, 4096)
|
||||
n, err := conn.Read(response)
|
||||
if err == nil {
|
||||
if target.UDPExpected != "" && string(response[:n]) != target.UDPExpected {
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP response did not match the expected payload")
|
||||
}
|
||||
return OutcomeReachable, nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if target.UDPMode == config.AvailabilityUDPOpenOrFiltered && ctxErr == context.DeadlineExceeded {
|
||||
return OutcomeIndeterminate, nil
|
||||
}
|
||||
return OutcomeUnreachable, ctxErr
|
||||
}
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if target.UDPMode == config.AvailabilityUDPOpenOrFiltered {
|
||||
return OutcomeIndeterminate, nil
|
||||
}
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP probe timed out waiting for a response")
|
||||
}
|
||||
return OutcomeUnreachable, fmt.Errorf("UDP probe failed: %w", err)
|
||||
}
|
||||
|
||||
func probeICMP(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
host := target.ProbeAddress()
|
||||
if host == "" {
|
||||
return fmt.Errorf("icmp availability target host is required")
|
||||
}
|
||||
args := pingArgs(host, target.EffectiveTimeoutMillis())
|
||||
cmd := exec.CommandContext(ctx, "ping", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
details := strings.TrimSpace(string(output))
|
||||
if details == "" {
|
||||
return fmt.Errorf("icmp probe failed: %w", err)
|
||||
}
|
||||
// Units written before v6.1.0-rc.1 lack AmbientCapabilities=CAP_NET_RAW and
|
||||
// in-place updates never rewrite the unit, so ping fails like this on every
|
||||
// upgraded install (#1554). Point at the unit instead of echoing ping stderr.
|
||||
if strings.Contains(details, "Operation not permitted") || strings.Contains(details, "cap_net_raw") {
|
||||
return fmt.Errorf("icmp probe blocked. The Pulse service unit does not grant CAP_NET_RAW, so ping cannot open a socket. Re-run the Pulse installer to regenerate the unit, or add a systemd override with AmbientCapabilities=CAP_NET_RAW and CapabilityBoundingSet=CAP_NET_RAW, then restart the service")
|
||||
}
|
||||
if len(details) > 240 {
|
||||
details = details[:240]
|
||||
}
|
||||
return fmt.Errorf("icmp probe failed: %s", details)
|
||||
}
|
||||
|
||||
func pingArgs(host string, timeoutMillis int) []string {
|
||||
if timeoutMillis <= 0 {
|
||||
timeoutMillis = config.DefaultAvailabilityTimeoutMillis
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return []string{"-n", "1", "-w", strconv.Itoa(timeoutMillis), host}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
return []string{"-n", "-c", "1", "-W", strconv.Itoa(timeoutMillis), host}
|
||||
default:
|
||||
timeoutSeconds := (timeoutMillis + 999) / 1000
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 1
|
||||
}
|
||||
return []string{"-n", "-c", "1", "-W", strconv.Itoa(timeoutSeconds), host}
|
||||
}
|
||||
}
|
||||
|
||||
func probeTCP(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
host := target.ProbeAddress()
|
||||
if host == "" {
|
||||
return fmt.Errorf("tcp availability target host is required")
|
||||
}
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(target.Port))
|
||||
|
||||
dialer := net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
return probeTCPViaSystem(ctx, host, target.Port, target.EffectiveTimeoutMillis())
|
||||
}
|
||||
|
||||
func probeTCPViaSystem(ctx context.Context, host string, port, timeoutMillis int) error {
|
||||
timeoutSecs := (timeoutMillis + 999) / 1000
|
||||
if timeoutSecs < 1 {
|
||||
timeoutSecs = 1
|
||||
}
|
||||
portStr := strconv.Itoa(port)
|
||||
|
||||
var args []string
|
||||
if runtime.GOOS == "darwin" {
|
||||
args = []string{"-z", "-G", strconv.Itoa(timeoutSecs), host, portStr}
|
||||
} else {
|
||||
args = []string{"-z", "-w", strconv.Itoa(timeoutSecs), host, portStr}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nc", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
details := strings.TrimSpace(string(output))
|
||||
if details == "" {
|
||||
return fmt.Errorf("tcp probe failed: %w", err)
|
||||
}
|
||||
if len(details) > 240 {
|
||||
details = details[:240]
|
||||
}
|
||||
return fmt.Errorf("tcp probe failed: %s", details)
|
||||
}
|
||||
|
||||
func probeHTTP(ctx context.Context, target config.AvailabilityTarget, timeout time.Duration) error {
|
||||
u, err := target.HTTPURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := httpOutboundOptions()
|
||||
u, err = securityutil.ValidateOutboundFetchURL(ctx, u.String(), opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http availability target URL validation failed: %w", err)
|
||||
}
|
||||
client := securityutil.NewRestrictedOutboundHTTPClient(timeout, opts)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build http availability request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Pulse availability probe")
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
return probeHTTPGet(ctx, client, u)
|
||||
}
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return fmt.Errorf("http probe returned %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
return fmt.Errorf("http probe failed: %w", err)
|
||||
}
|
||||
|
||||
func httpOutboundOptions() securityutil.RestrictedOutboundHTTPOptions {
|
||||
return securityutil.RestrictedOutboundHTTPOptions{
|
||||
AllowedSchemes: []string{"http", "https"},
|
||||
AllowPrivateIPs: true,
|
||||
AllowLoopback: true,
|
||||
TLSConfig: tlsutil.UnverifiedPeerCertificateCaptureTLSConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func probeHTTPGet(ctx context.Context, client *http.Client, u *url.URL) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build http availability fallback request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Pulse availability probe")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
return fmt.Errorf("http probe failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return fmt.Errorf("http probe returned %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package availabilityprobe
|
||||
|
||||
import "testing"
|
||||
|
||||
// Moved verbatim from internal/monitoring/availability_poller_test.go when the
|
||||
// probe execution core was extracted into this package.
|
||||
func TestAvailabilityHTTPOutboundOptionsUsesSharedPeerCertificateCapture(t *testing.T) {
|
||||
tlsConfig := httpOutboundOptions().TLSConfig
|
||||
if tlsConfig == nil || !tlsConfig.InsecureSkipVerify {
|
||||
t.Fatal("availability TLS config must enter explicit peer-certificate capture mode")
|
||||
}
|
||||
if tlsConfig.VerifyPeerCertificate == nil {
|
||||
t.Fatal("availability TLS config must reject missing or malformed peer certificates")
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/availabilityprobe"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// AvailabilityProbeStatus captures the last observed state of an agentless
|
||||
@@ -40,12 +34,14 @@ type AvailabilityProbeStatus struct {
|
||||
FailureThreshold int `json:"failureThreshold,omitempty"`
|
||||
}
|
||||
|
||||
type AvailabilityProbeOutcome string
|
||||
// AvailabilityProbeOutcome and its values are aliases for the shared probe
|
||||
// package so existing monitoring and API callers keep their spelling.
|
||||
type AvailabilityProbeOutcome = availabilityprobe.Outcome
|
||||
|
||||
const (
|
||||
AvailabilityProbeReachable AvailabilityProbeOutcome = "reachable"
|
||||
AvailabilityProbeUnreachable AvailabilityProbeOutcome = "unreachable"
|
||||
AvailabilityProbeIndeterminate AvailabilityProbeOutcome = "indeterminate"
|
||||
AvailabilityProbeReachable = availabilityprobe.OutcomeReachable
|
||||
AvailabilityProbeUnreachable = availabilityprobe.OutcomeUnreachable
|
||||
AvailabilityProbeIndeterminate = availabilityprobe.OutcomeIndeterminate
|
||||
)
|
||||
|
||||
type availabilityPollProvider struct{}
|
||||
@@ -331,271 +327,18 @@ func (m *Monitor) setAvailabilityStatus(target config.AvailabilityTarget, checke
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// ProbeAvailabilityTarget executes one agentless availability check.
|
||||
// ProbeAvailabilityTarget executes one agentless availability check. The probe
|
||||
// execution core lives in internal/availabilityprobe so the host agent can run
|
||||
// the same checks without importing the monitoring package; this wrapper keeps
|
||||
// the historical monitoring entry point for existing callers.
|
||||
func ProbeAvailabilityTarget(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
_, err := ProbeAvailabilityTargetResult(ctx, target)
|
||||
return err
|
||||
return availabilityprobe.Run(ctx, target)
|
||||
}
|
||||
|
||||
// ProbeAvailabilityTargetResult preserves UDP's open-or-filtered state rather
|
||||
// than incorrectly claiming that a silent UDP endpoint was proven reachable.
|
||||
func ProbeAvailabilityTargetResult(ctx context.Context, target config.AvailabilityTarget) (AvailabilityProbeOutcome, error) {
|
||||
target = config.NormalizeAvailabilityTarget(target)
|
||||
if err := target.Validate(); err != nil {
|
||||
return AvailabilityProbeUnreachable, err
|
||||
}
|
||||
|
||||
timeout := time.Duration(target.EffectiveTimeoutMillis()) * time.Millisecond
|
||||
if timeout <= 0 {
|
||||
timeout = time.Duration(config.DefaultAvailabilityTimeoutMillis) * time.Millisecond
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
switch target.Protocol {
|
||||
case config.AvailabilityProbeICMP:
|
||||
return outcomeFromProbeError(probeICMP(probeCtx, target))
|
||||
case config.AvailabilityProbeTCP:
|
||||
return outcomeFromProbeError(probeTCP(probeCtx, target))
|
||||
case config.AvailabilityProbeUDP:
|
||||
return probeUDP(probeCtx, target)
|
||||
case config.AvailabilityProbeHTTP, config.AvailabilityProbeHTTPS:
|
||||
return outcomeFromProbeError(probeHTTP(probeCtx, target, timeout))
|
||||
default:
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("unsupported availability protocol %q", target.Protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func outcomeFromProbeError(err error) (AvailabilityProbeOutcome, error) {
|
||||
if err != nil {
|
||||
return AvailabilityProbeUnreachable, err
|
||||
}
|
||||
return AvailabilityProbeReachable, nil
|
||||
}
|
||||
|
||||
func probeUDP(ctx context.Context, target config.AvailabilityTarget) (AvailabilityProbeOutcome, error) {
|
||||
host := target.ProbeAddress()
|
||||
addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("resolve UDP availability target: %w", err)
|
||||
}
|
||||
var selected net.IP
|
||||
for _, address := range addresses {
|
||||
if address.IP == nil || address.IP.IsUnspecified() || address.IP.IsMulticast() || address.IP.Equal(net.IPv4bcast) {
|
||||
continue
|
||||
}
|
||||
selected = address.IP
|
||||
break
|
||||
}
|
||||
if selected == nil {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP availability target did not resolve to an allowed unicast address")
|
||||
}
|
||||
|
||||
dialer := net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, "udp", net.JoinHostPort(selected.String(), strconv.Itoa(target.Port)))
|
||||
if err != nil {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP probe dial failed: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("set UDP probe deadline: %w", err)
|
||||
}
|
||||
}
|
||||
payload := []byte(target.UDPRequest)
|
||||
if len(payload) == 0 {
|
||||
// A one-byte datagram gives the kernel an opportunity to surface an
|
||||
// ICMP port-unreachable result in open-or-filtered mode.
|
||||
payload = []byte{0}
|
||||
}
|
||||
if _, err := conn.Write(payload); err != nil {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP probe write failed: %w", err)
|
||||
}
|
||||
|
||||
response := make([]byte, 4096)
|
||||
n, err := conn.Read(response)
|
||||
if err == nil {
|
||||
if target.UDPExpected != "" && string(response[:n]) != target.UDPExpected {
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP response did not match the expected payload")
|
||||
}
|
||||
return AvailabilityProbeReachable, nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if target.UDPMode == config.AvailabilityUDPOpenOrFiltered && ctxErr == context.DeadlineExceeded {
|
||||
return AvailabilityProbeIndeterminate, nil
|
||||
}
|
||||
return AvailabilityProbeUnreachable, ctxErr
|
||||
}
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if target.UDPMode == config.AvailabilityUDPOpenOrFiltered {
|
||||
return AvailabilityProbeIndeterminate, nil
|
||||
}
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP probe timed out waiting for a response")
|
||||
}
|
||||
return AvailabilityProbeUnreachable, fmt.Errorf("UDP probe failed: %w", err)
|
||||
}
|
||||
|
||||
func probeICMP(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
host := target.ProbeAddress()
|
||||
if host == "" {
|
||||
return fmt.Errorf("icmp availability target host is required")
|
||||
}
|
||||
args := pingArgs(host, target.EffectiveTimeoutMillis())
|
||||
cmd := exec.CommandContext(ctx, "ping", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
details := strings.TrimSpace(string(output))
|
||||
if details == "" {
|
||||
return fmt.Errorf("icmp probe failed: %w", err)
|
||||
}
|
||||
// Units written before v6.1.0-rc.1 lack AmbientCapabilities=CAP_NET_RAW and
|
||||
// in-place updates never rewrite the unit, so ping fails like this on every
|
||||
// upgraded install (#1554). Point at the unit instead of echoing ping stderr.
|
||||
if strings.Contains(details, "Operation not permitted") || strings.Contains(details, "cap_net_raw") {
|
||||
return fmt.Errorf("icmp probe blocked. The Pulse service unit does not grant CAP_NET_RAW, so ping cannot open a socket. Re-run the Pulse installer to regenerate the unit, or add a systemd override with AmbientCapabilities=CAP_NET_RAW and CapabilityBoundingSet=CAP_NET_RAW, then restart the service")
|
||||
}
|
||||
if len(details) > 240 {
|
||||
details = details[:240]
|
||||
}
|
||||
return fmt.Errorf("icmp probe failed: %s", details)
|
||||
}
|
||||
|
||||
func pingArgs(host string, timeoutMillis int) []string {
|
||||
if timeoutMillis <= 0 {
|
||||
timeoutMillis = config.DefaultAvailabilityTimeoutMillis
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return []string{"-n", "1", "-w", strconv.Itoa(timeoutMillis), host}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
return []string{"-n", "-c", "1", "-W", strconv.Itoa(timeoutMillis), host}
|
||||
default:
|
||||
timeoutSeconds := (timeoutMillis + 999) / 1000
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 1
|
||||
}
|
||||
return []string{"-n", "-c", "1", "-W", strconv.Itoa(timeoutSeconds), host}
|
||||
}
|
||||
}
|
||||
|
||||
func probeTCP(ctx context.Context, target config.AvailabilityTarget) error {
|
||||
host := target.ProbeAddress()
|
||||
if host == "" {
|
||||
return fmt.Errorf("tcp availability target host is required")
|
||||
}
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(target.Port))
|
||||
|
||||
dialer := net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
return probeTCPViaSystem(ctx, host, target.Port, target.EffectiveTimeoutMillis())
|
||||
}
|
||||
|
||||
func probeTCPViaSystem(ctx context.Context, host string, port, timeoutMillis int) error {
|
||||
timeoutSecs := (timeoutMillis + 999) / 1000
|
||||
if timeoutSecs < 1 {
|
||||
timeoutSecs = 1
|
||||
}
|
||||
portStr := strconv.Itoa(port)
|
||||
|
||||
var args []string
|
||||
if runtime.GOOS == "darwin" {
|
||||
args = []string{"-z", "-G", strconv.Itoa(timeoutSecs), host, portStr}
|
||||
} else {
|
||||
args = []string{"-z", "-w", strconv.Itoa(timeoutSecs), host, portStr}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nc", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
details := strings.TrimSpace(string(output))
|
||||
if details == "" {
|
||||
return fmt.Errorf("tcp probe failed: %w", err)
|
||||
}
|
||||
if len(details) > 240 {
|
||||
details = details[:240]
|
||||
}
|
||||
return fmt.Errorf("tcp probe failed: %s", details)
|
||||
}
|
||||
|
||||
func probeHTTP(ctx context.Context, target config.AvailabilityTarget, timeout time.Duration) error {
|
||||
u, err := target.HTTPURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := availabilityHTTPOutboundOptions()
|
||||
u, err = securityutil.ValidateOutboundFetchURL(ctx, u.String(), opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http availability target URL validation failed: %w", err)
|
||||
}
|
||||
client := securityutil.NewRestrictedOutboundHTTPClient(timeout, opts)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build http availability request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Pulse availability probe")
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
return probeHTTPGet(ctx, client, u)
|
||||
}
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return fmt.Errorf("http probe returned %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
return fmt.Errorf("http probe failed: %w", err)
|
||||
}
|
||||
|
||||
func availabilityHTTPOutboundOptions() securityutil.RestrictedOutboundHTTPOptions {
|
||||
return securityutil.RestrictedOutboundHTTPOptions{
|
||||
AllowedSchemes: []string{"http", "https"},
|
||||
AllowPrivateIPs: true,
|
||||
AllowLoopback: true,
|
||||
TLSConfig: tlsutil.UnverifiedPeerCertificateCaptureTLSConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func probeHTTPGet(ctx context.Context, client *http.Client, u *url.URL) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build http availability fallback request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Pulse availability probe")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
return fmt.Errorf("http probe failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return fmt.Errorf("http probe returned %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
return availabilityprobe.Result(ctx, target)
|
||||
}
|
||||
|
||||
func availabilityStatusFromTarget(target config.AvailabilityTarget) AvailabilityProbeStatus {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// This file is a purpose-built branch-coverage test set (selected via
|
||||
// `-run "^TestBranchcov0722"`) for three pure helpers in
|
||||
// `-run "^TestBranchcov0722"`) for two pure helpers in
|
||||
// availability_poller.go that previously had 0.0% coverage:
|
||||
//
|
||||
// - availabilityConnectionKey(targetID string) string
|
||||
// - (availabilityPollProvider).ConnectionHealthKey(_ *Monitor, instanceName string) string
|
||||
// - pingArgs(host string, timeoutMillis int) []string
|
||||
//
|
||||
// The third helper originally covered here, pingArgs(host string, timeoutMillis
|
||||
// int) []string, moved with the probe execution core to
|
||||
// internal/availabilityprobe; its cases live in
|
||||
// internal/availabilityprobe/ping_args_branchcov0722am_test.go.
|
||||
//
|
||||
// Every arm of each function is exercised directly here:
|
||||
//
|
||||
@@ -25,9 +24,6 @@ import (
|
||||
// gets the prefix appended a second time.
|
||||
// - ConnectionHealthKey: pure delegation to availabilityConnectionKey with a
|
||||
// nil *Monitor (the receiver ignores the monitor entirely).
|
||||
// - pingArgs: the timeoutMillis <= 0 fallback to the config default, the
|
||||
// runtime.GOOS switch (windows / darwin+BSD / default), and the default
|
||||
// arm's ceiling arithmetic ((ms+999)/1000) plus the clamp-to-1 guard.
|
||||
//
|
||||
// Conventions match sibling in-package tests in this directory (see
|
||||
// monitoring_infra_keys_branchcov0716_test.go and the connected-infrastructure
|
||||
@@ -97,128 +93,3 @@ func TestBranchcov0722ConnectionHealthKey(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchcov0722PingArgs(t *testing.T) {
|
||||
const host = "probe.example.test"
|
||||
|
||||
t.Run("positive timeout reaches the platform arm verbatim", func(t *testing.T) {
|
||||
got := pingArgs(host, 5000)
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
want := []string{"-n", "1", "-w", "5000", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on windows = %v, want %v", host, got, want)
|
||||
}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
want := []string{"-n", "-c", "1", "-W", "5000", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on %s = %v, want %v", host, runtime.GOOS, got, want)
|
||||
}
|
||||
default:
|
||||
// Default arm: (5000 + 999) / 1000 = 5 whole seconds.
|
||||
want := []string{"-n", "-c", "1", "-W", "5", host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, 5000) on %s = %v, want %v", host, runtime.GOOS, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero or negative timeout falls back to config default", func(t *testing.T) {
|
||||
defaultMillis := config.DefaultAvailabilityTimeoutMillis
|
||||
for _, ms := range []int{0, -1, -99999} {
|
||||
got := pingArgs(host, ms)
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
want := []string{"-n", "1", "-w", strconv.Itoa(defaultMillis), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on windows = %v, want %v", host, ms, got, want)
|
||||
}
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(defaultMillis), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on %s = %v, want %v", host, ms, runtime.GOOS, got, want)
|
||||
}
|
||||
default:
|
||||
secs := (defaultMillis + 999) / 1000
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(secs), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) on %s = %v, want %v", host, ms, runtime.GOOS, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TestBranchcov0722PingArgsDefaultArmCeilingArithmetic targets the
|
||||
// non-Windows / non-BSD ("default") arm of the runtime.GOOS switch, which is
|
||||
// the only arm that performs the (ms+999)/1000 ceiling division and the
|
||||
// clamp-to-1 guard. pingArgs reads runtime.GOOS directly, so this arm can only
|
||||
// be reached on platforms such as linux; on windows/darwin/BSD the test skips
|
||||
// honestly rather than asserting behaviour it cannot reach.
|
||||
func TestBranchcov0722PingArgsDefaultArmCeilingArithmetic(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin", "freebsd", "openbsd", "netbsd":
|
||||
t.Skipf("ceiling arithmetic lives only in the default (linux-like) pingArgs arm; skipping on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
const host = "probe.example.test"
|
||||
cases := []struct {
|
||||
name string
|
||||
timeoutMs int
|
||||
wantSeconds int
|
||||
}{
|
||||
// Ceiling division: any sub-second value rounds up to 1 second.
|
||||
{"one millisecond rounds up to one second", 1, 1},
|
||||
{"just under a second rounds up to one second", 999, 1},
|
||||
// Exact second boundary stays put.
|
||||
{"exact one thousand millis is one second", 1000, 1},
|
||||
// Fractional seconds round up to the next whole second.
|
||||
{"one thousand one millis rounds up to two seconds", 1001, 2},
|
||||
{"fifteen hundred millis rounds up to two seconds", 1500, 2},
|
||||
{"five thousand nine hundred ninety nine rounds up to six seconds", 5999, 6},
|
||||
// NOTE: the `if timeoutSeconds <= 0 { timeoutSeconds = 1 }` clamp is
|
||||
// unreachable here. The non-positive fallback guarantees timeoutMillis
|
||||
// is at least DefaultAvailabilityTimeoutMillis (2000) before this arm,
|
||||
// and any positive value yields (ms+999)/1000 >= 1. No table case can
|
||||
// exercise the clamp; this is documented in the report, not "fixed".
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := pingArgs(host, tc.timeoutMs)
|
||||
// Default arm shape: ["-n", "-c", "1", "-W", <seconds>, host].
|
||||
want := []string{"-n", "-c", "1", "-W", strconv.Itoa(tc.wantSeconds), host}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pingArgs(%q, %d) = %v, want %v", host, tc.timeoutMs, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchcov0722PingArgsCloneIndependence asserts that the returned slice
|
||||
// is freshly allocated on every call: mutating one result must not corrupt a
|
||||
// previously returned or subsequently returned slice, and vice versa. (pingArgs
|
||||
// takes only immutable inputs — a string and an int — so the relevant
|
||||
// invariant is that distinct calls do not alias backing arrays.)
|
||||
func TestBranchcov0722PingArgsCloneIndependence(t *testing.T) {
|
||||
first := pingArgs("alpha", 1000)
|
||||
second := pingArgs("beta", 2000)
|
||||
|
||||
snapshot := append([]string(nil), first...)
|
||||
for i := range second {
|
||||
second[i] = "MUTATED"
|
||||
}
|
||||
if !reflect.DeepEqual(first, snapshot) {
|
||||
t.Fatalf("mutating a pingArgs result corrupted a distinct result: first = %v, want %v", first, snapshot)
|
||||
}
|
||||
|
||||
third := pingArgs("gamma", 3000)
|
||||
snapshotThird := append([]string(nil), third...)
|
||||
for i := range first {
|
||||
first[i] = "MUTATED"
|
||||
}
|
||||
if !reflect.DeepEqual(third, snapshotThird) {
|
||||
t.Fatalf("mutating an earlier pingArgs result corrupted a later result: third = %v, want %v", third, snapshotThird)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,15 +38,8 @@ func TestProbeAvailabilityTargetHTTPFallsBackToGETWhenHeadNotAllowed(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityHTTPOutboundOptionsUsesSharedPeerCertificateCapture(t *testing.T) {
|
||||
tlsConfig := availabilityHTTPOutboundOptions().TLSConfig
|
||||
if tlsConfig == nil || !tlsConfig.InsecureSkipVerify {
|
||||
t.Fatal("availability TLS config must enter explicit peer-certificate capture mode")
|
||||
}
|
||||
if tlsConfig.VerifyPeerCertificate == nil {
|
||||
t.Fatal("availability TLS config must reject missing or malformed peer certificates")
|
||||
}
|
||||
}
|
||||
// TestAvailabilityHTTPOutboundOptionsUsesSharedPeerCertificateCapture moved to
|
||||
// internal/availabilityprobe alongside the outbound options it asserts on.
|
||||
|
||||
func TestProbeAvailabilityTargetHTTPTreatsServerErrorsAsUnavailable(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
@@ -30,17 +30,25 @@ var bannedSnapshotResourceAccessPatterns = []struct {
|
||||
}
|
||||
|
||||
func TestAvailabilityTLSUsesSharedCertificateCaptureBoundary(t *testing.T) {
|
||||
source, err := os.ReadFile("availability_poller.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read availability_poller.go: %v", err)
|
||||
// The probe execution core moved to internal/availabilityprobe so the host
|
||||
// agent can share it; the TLS boundary guardrail follows the code and still
|
||||
// covers the monitoring poller that schedules the probes.
|
||||
const probeCore = "../availabilityprobe/probe.go"
|
||||
sources := make(map[string]string, 2)
|
||||
for _, path := range []string{"availability_poller.go", probeCore} {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
text := string(data)
|
||||
sources[path] = text
|
||||
if strings.Contains(text, "InsecureSkipVerify: true") {
|
||||
t.Fatalf("%s must not define a local skip-verification TLS config", path)
|
||||
}
|
||||
}
|
||||
text := string(source)
|
||||
if !strings.Contains(text, "tlsutil.UnverifiedPeerCertificateCaptureTLSConfig()") {
|
||||
if !strings.Contains(sources[probeCore], "tlsutil.UnverifiedPeerCertificateCaptureTLSConfig()") {
|
||||
t.Fatal("availability probes must use the shared peer-certificate capture boundary")
|
||||
}
|
||||
if strings.Contains(text, "InsecureSkipVerify: true") {
|
||||
t.Fatal("availability probes must not define a local skip-verification TLS config")
|
||||
}
|
||||
}
|
||||
|
||||
type guardrailSupplementalProvider struct {
|
||||
|
||||
Reference in New Issue
Block a user