cmd/containerboot: serve health on local endpoint (#14246)
* cmd/containerboot: serve health on local endpoint We introduced stable (user) metrics in #14035, and `TS_LOCAL_ADDR_PORT` with it. Rather than requiring users to specify a new addr/port combination for each new local endpoint they want the container to serve, this combines the health check endpoint onto the local addr/port used by metrics if `TS_ENABLE_HEALTH_CHECK` is used instead of `TS_HEALTHCHECK_ADDR_PORT`. `TS_LOCAL_ADDR_PORT` now defaults to binding to all interfaces on 9002 so that it works more seamlessly and with less configuration in environments other than Kubernetes, where the operator always overrides the default anyway. In particular, listening on localhost would not be accessible from outside the container, and many scripted container environments do not know the IP address of the container before it's started. Listening on all interfaces allows users to just set one env var (`TS_ENABLE_METRICS` or `TS_ENABLE_HEALTH_CHECK`) to get a fully functioning local endpoint they can query from outside the container. Updates #14035, #12898 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
@ -101,6 +101,24 @@ func TestContainerBoot(t *testing.T) {
|
||||
|
||||
argFile := filepath.Join(d, "args")
|
||||
runningSockPath := filepath.Join(d, "tmp/tailscaled.sock")
|
||||
var localAddrPort, healthAddrPort int
|
||||
for _, p := range []*int{&localAddrPort, &healthAddrPort} {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open listener: %v", err)
|
||||
}
|
||||
if err := ln.Close(); err != nil {
|
||||
t.Fatalf("Failed to close listener: %v", err)
|
||||
}
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
*p = port
|
||||
}
|
||||
metricsURL := func(port int) string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/metrics", port)
|
||||
}
|
||||
healthURL := func(port int) string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/healthz", port)
|
||||
}
|
||||
|
||||
type phase struct {
|
||||
// If non-nil, send this IPN bus notification (and remember it as the
|
||||
@ -119,6 +137,8 @@ func TestContainerBoot(t *testing.T) {
|
||||
// WantFatalLog is the fatal log message we expect from containerboot.
|
||||
// If set for a phase, the test will finish on that phase.
|
||||
WantFatalLog string
|
||||
|
||||
EndpointStatuses map[string]int
|
||||
}
|
||||
runningNotify := &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
@ -147,6 +167,11 @@ func TestContainerBoot(t *testing.T) {
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
// No metrics or health by default.
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(9002): -1,
|
||||
healthURL(9002): -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
@ -700,6 +725,104 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "metrics_enabled",
|
||||
Env: map[string]string{
|
||||
"TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
|
||||
"TS_ENABLE_METRICS": "true",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): 200,
|
||||
healthURL(localAddrPort): -1,
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "health_enabled",
|
||||
Env: map[string]string{
|
||||
"TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
|
||||
"TS_ENABLE_HEALTH_CHECK": "true",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): -1,
|
||||
healthURL(localAddrPort): 503, // Doesn't start passing until the next phase.
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): -1,
|
||||
healthURL(localAddrPort): 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "metrics_and_health_on_same_port",
|
||||
Env: map[string]string{
|
||||
"TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
|
||||
"TS_ENABLE_METRICS": "true",
|
||||
"TS_ENABLE_HEALTH_CHECK": "true",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): 200,
|
||||
healthURL(localAddrPort): 503, // Doesn't start passing until the next phase.
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): 200,
|
||||
healthURL(localAddrPort): 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local_metrics_and_deprecated_health",
|
||||
Env: map[string]string{
|
||||
"TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
|
||||
"TS_ENABLE_METRICS": "true",
|
||||
"TS_HEALTHCHECK_ADDR_PORT": fmt.Sprintf("[::]:%d", healthAddrPort),
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): 200,
|
||||
healthURL(healthAddrPort): 503, // Doesn't start passing until the next phase.
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
EndpointStatuses: map[string]int{
|
||||
metricsURL(localAddrPort): 200,
|
||||
healthURL(healthAddrPort): 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -796,7 +919,26 @@ func TestContainerBoot(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("phase %d: %v", i, err)
|
||||
}
|
||||
|
||||
for url, want := range p.EndpointStatuses {
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil && want != -1 {
|
||||
return fmt.Errorf("GET %s: %v", url, err)
|
||||
}
|
||||
if want > 0 && resp.StatusCode != want {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("GET %s, want %d, got %d\n%s", url, want, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("phase %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
|
||||
@ -955,6 +1097,12 @@ func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
panic(fmt.Sprintf("unsupported method %q", r.Method))
|
||||
}
|
||||
case "/localapi/v0/usermetrics":
|
||||
if r.Method != "GET" {
|
||||
panic(fmt.Sprintf("unsupported method %q", r.Method))
|
||||
}
|
||||
w.Write([]byte("fake metrics"))
|
||||
return
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
|
||||
}
|
||||
|
Reference in New Issue
Block a user