Skip to content

Commit

Permalink
reverse_proxy: Allow dynamic backends (closes #990 and #1539)
Browse files Browse the repository at this point in the history
This PR enables the use of placeholders in an upstream's Dial address.

A Dial address must represent precisely one socket after replacements.

See also #998 and #1639.
  • Loading branch information
mholt committed Oct 11, 2019
1 parent 4aa3af4 commit 1e31be8
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 134 deletions.
7 changes: 4 additions & 3 deletions listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,10 @@ func JoinNetworkAddress(network, host, port string) string {
if network != "" {
a = network + "/"
}
a += host
if port != "" {
a += ":" + port
if host != "" && port == "" {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
}
return a
}
4 changes: 4 additions & 0 deletions listeners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ func TestJoinNetworkAddress(t *testing.T) {
network: "unix", host: "/foo/bar", port: "",
expect: "unix//foo/bar",
},
{
network: "", host: "::1", port: "1234",
expect: "[::1]:1234",
},
} {
actual := JoinNetworkAddress(tc.network, tc.host, tc.port)
if actual != tc.expect {
Expand Down
10 changes: 4 additions & 6 deletions modules/caddyhttp/reverseproxy/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for _, up := range d.RemainingArgs() {
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: up,
})
h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
}

for d.NextBlock(0) {
Expand All @@ -94,9 +92,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
for _, up := range args {
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: up,
})
h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
}

case "lb_policy":
Expand Down Expand Up @@ -502,6 +498,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if d.Val() == "off" {
var disable bool
h.KeepAlive.Enabled = &disable
break
}
dur, err := time.ParseDuration(d.Val())
if err != nil {
Expand All @@ -521,6 +518,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.MaxIdleConns = num
h.KeepAlive.MaxIdleConnsPerHost = num

default:
return d.Errf("unrecognized subdirective %s", d.Val())
Expand Down
6 changes: 3 additions & 3 deletions modules/caddyhttp/reverseproxy/healthchecks.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (h *Handler) doActiveHealthChecksForAllHosts() {
// so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost"
}
err = h.doActiveHealthCheck(NewDialInfo(network, addrs[0]), hostAddr, host)
err = h.doActiveHealthCheck(DialInfo{Network: network, Address: addrs[0]}, hostAddr, host)
if err != nil {
log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", networkAddr, err)
}
Expand Down Expand Up @@ -259,7 +259,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
err := upstream.Host.CountFail(1)
if err != nil {
log.Printf("[ERROR] proxy: upstream %s: counting failure: %v",
upstream.dialInfo, err)
upstream.Dial, err)
}

// forget it later
Expand All @@ -268,7 +268,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
err := host.CountFail(-1)
if err != nil {
log.Printf("[ERROR] proxy: upstream %s: expiring failure: %v",
upstream.dialInfo, err)
upstream.Dial, err)
}
}(upstream.Host, failDuration)
}
62 changes: 37 additions & 25 deletions modules/caddyhttp/reverseproxy/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ type Upstream struct {

healthCheckPolicy *PassiveHealthChecks
cb CircuitBreaker
dialInfo DialInfo
}

// Available returns true if the remote host
Expand Down Expand Up @@ -149,8 +148,7 @@ func (uh *upstreamHost) CountFail(delta int) error {
}

// SetHealthy sets the upstream has healthy or unhealthy
// and returns true if the value was different from before,
// or an error if the adjustment failed.
// and returns true if the new value is different.
func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
var unhealthy, compare int32 = 1, 0
if healthy {
Expand All @@ -167,42 +165,56 @@ func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
// a host that can be represented in a URL, but
// they certainly have a network name and address).
type DialInfo struct {
// The network to use. This should be one of the
// values that is accepted by net.Dial:
// Upstream is the Upstream associated with
// this DialInfo. It may be nil.
Upstream *Upstream

// The network to use. This should be one of
// the values that is accepted by net.Dial:
// https://golang.org/pkg/net/#Dial
Network string

// The address to dial. Follows the same
// semantics and rules as net.Dial.
Address string

// Host and Port are components of Address,
// pre-split for convenience.
// Host and Port are components of Address.
Host, Port string
}

// NewDialInfo creates and populates a DialInfo
// for the given network and address. It splits
// the address into host and port values if the
// network type supports them, or uses the whole
// address as the port if splitting fails.
func NewDialInfo(network, address string) DialInfo {
var addrHost, addrPort string
if !strings.Contains(network, "unix") {
var err error
addrHost, addrPort, err = net.SplitHostPort(address)
if err != nil {
addrHost = address // assume there was no port
}
}
return DialInfo{network, address, addrHost, addrPort}
}

// String returns the Caddy network address form
// by joining the network and address with a
// forward slash.
func (di DialInfo) String() string {
return di.Network + "/" + di.Address
return caddy.JoinNetworkAddress(di.Network, di.Host, di.Port)
}

// fillDialInfo returns a filled DialInfo for the given upstream, using
// the given Replacer. Note that the returned value is not a pointer.
func fillDialInfo(upstream *Upstream, repl caddy.Replacer) (DialInfo, error) {
dial := repl.ReplaceAll(upstream.Dial, "")
netw, addrs, err := caddy.ParseNetworkAddress(dial)
if err != nil {
return DialInfo{}, fmt.Errorf("upstream %s: invalid dial address %s: %v", upstream.Dial, dial, err)
}
if len(addrs) != 1 {
return DialInfo{}, fmt.Errorf("upstream %s: dial address must represent precisely one socket: %s represents %d",
upstream.Dial, dial, len(addrs))
}
var dialHost, dialPort string
if !strings.Contains(netw, "unix") {
dialHost, dialPort, err = net.SplitHostPort(addrs[0])
if err != nil {
dialHost = addrs[0] // assume there was no port
}
}
return DialInfo{
Upstream: upstream,
Network: netw,
Address: addrs[0],
Host: dialHost,
Port: dialPort,
}, nil
}

// DialInfoCtxKey is used to store a DialInfo
Expand Down

0 comments on commit 1e31be8

Please sign in to comment.