reverseproxy: Active health checks request body option (#6520)

* Add an option to specify the body used for active health checks

* Replacer on request body
This commit is contained in:
Jesper Brix Rosenkilde 2024-08-19 18:55:55 +02:00 committed by GitHub
parent 043fe41ab8
commit 54a0c8f948
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 12 deletions

View file

@ -0,0 +1,40 @@
:8884
reverse_proxy 127.0.0.1:65535 {
health_uri /health
health_request_body "test body"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"body": "test body",
"uri": "/health"
}
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}

View file

@ -69,19 +69,20 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_retry_match <request-matcher> // lb_retry_match <request-matcher>
// //
// # active health checking // # active health checking
// health_uri <uri> // health_uri <uri>
// health_port <port> // health_port <port>
// health_interval <interval> // health_interval <interval>
// health_passes <num> // health_passes <num>
// health_fails <num> // health_fails <num>
// health_timeout <duration> // health_timeout <duration>
// health_status <status> // health_status <status>
// health_body <regexp> // health_body <regexp>
// health_method <value>
// health_request_body <value>
// health_follow_redirects // health_follow_redirects
// health_headers { // health_headers {
// <field> [<values...>] // <field> [<values...>]
// } // }
// health_method <value>
// //
// # passive health checking // # passive health checking
// fail_duration <duration> // fail_duration <duration>
@ -425,6 +426,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
h.HealthChecks.Active.Method = d.Val() h.HealthChecks.Active.Method = d.Val()
case "health_request_body":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Body = d.Val()
case "health_interval": case "health_interval":
if !d.NextArg() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()

View file

@ -24,6 +24,7 @@ import (
"regexp" "regexp"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"strings"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
@ -93,6 +94,9 @@ type ActiveHealthChecks struct {
// The HTTP method to use for health checks (default "GET"). // The HTTP method to use for health checks (default "GET").
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
// The body to send with the health check request.
Body string `json:"body,omitempty"`
// Whether to follow HTTP redirects in response to active health checks (default off). // Whether to follow HTTP redirects in response to active health checks (default off).
FollowRedirects bool `json:"follow_redirects,omitempty"` FollowRedirects bool `json:"follow_redirects,omitempty"`
@ -396,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
u.Path = h.HealthChecks.Active.Path u.Path = h.HealthChecks.Active.Path
} }
// replacer used for both body and headers. Only globals (env vars, system info, etc.) are available
repl := caddy.NewReplacer()
// if body is provided, create a reader for it, otherwise nil
var requestBody io.Reader
if h.HealthChecks.Active.Body != "" {
// set body, using replacer
requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, ""))
}
// attach dialing information to this request, as well as context values that // attach dialing information to this request, as well as context values that
// may be expected by handlers of this request // may be expected by handlers of this request
ctx := h.ctx.Context ctx := h.ctx.Context
@ -403,15 +417,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{ ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
dialInfoVarKey: dialInfo, dialInfoVarKey: dialInfo,
}) })
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil) req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
if err != nil { if err != nil {
return fmt.Errorf("making request: %v", err) return fmt.Errorf("making request: %v", err)
} }
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req) ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
req = req.WithContext(ctx) req = req.WithContext(ctx)
// set headers, using a replacer with only globals (env vars, system info, etc.) // set headers, using replacer
repl := caddy.NewReplacer()
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr) repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
for key, vals := range h.HealthChecks.Active.Headers { for key, vals := range h.HealthChecks.Active.Headers {
key = repl.ReplaceAll(key, "") key = repl.ReplaceAll(key, "")