cmd/{containerboot,k8s-operator},kube/kubetypes: unadvertise ingress services on shutdown

Add a PreStop lifecycle hook to the ProxyGroup ingress spec so that we
can explicitly notify control that the service is being unadvertised and
prompt it to update the netmap for clients faster. Without this, control
just treats the device as temporarily offline and allows it a grace period
before no longer considering it part of the service.

Updates tailscale/corp#24795

Change-Id: I0a9a4fe7a5395ca76135ceead05cbc3ee32b3d3c
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Tom Proctor 2025-03-26 01:28:41 +00:00
parent 4777cc2cda
commit 996a1ebb44
7 changed files with 72 additions and 10 deletions

View File

@ -47,10 +47,10 @@ func (h *healthz) update(healthy bool) {
h.hasAddrs = healthy h.hasAddrs = healthy
} }
// healthHandlers registers a simple health handler at /healthz. // registerHealthHandlers registers a simple health handler at /healthz.
// A containerized tailscale instance is considered healthy if // A containerized tailscale instance is considered healthy if
// it has at least one tailnet IP address. // it has at least one tailnet IP address.
func healthHandlers(mux *http.ServeMux, podIPv4 string) *healthz { func registerHealthHandlers(mux *http.ServeMux, podIPv4 string) *healthz {
h := &healthz{podIPv4: podIPv4} h := &healthz{podIPv4: podIPv4}
mux.Handle("GET /healthz", h) mux.Handle("GET /healthz", h)
return h return h

View File

@ -226,29 +226,38 @@ func run() error {
mux := http.NewServeMux() mux := http.NewServeMux()
log.Printf("Running healthcheck endpoint at %s/healthz", cfg.HealthCheckAddrPort) log.Printf("Running healthcheck endpoint at %s/healthz", cfg.HealthCheckAddrPort)
healthCheck = healthHandlers(mux, cfg.PodIPv4) healthCheck = registerHealthHandlers(mux, cfg.PodIPv4)
close := runHTTPServer(mux, cfg.HealthCheckAddrPort) close := runHTTPServer(mux, cfg.HealthCheckAddrPort)
defer close() defer close()
} }
if cfg.localMetricsEnabled() || cfg.localHealthEnabled() || cfg.egressSvcsTerminateEPEnabled() { if cfg.localMetricsEnabled() ||
cfg.localHealthEnabled() ||
cfg.egressSvcsTerminateEPEnabled() ||
cfg.ServeConfigPath != "" {
mux := http.NewServeMux() mux := http.NewServeMux()
if cfg.localMetricsEnabled() { if cfg.localMetricsEnabled() {
log.Printf("Running metrics endpoint at %s/metrics", cfg.LocalAddrPort) log.Printf("Running metrics endpoint at %s/metrics", cfg.LocalAddrPort)
metricsHandlers(mux, client, cfg.DebugAddrPort) registerMetricsHandlers(mux, client, cfg.DebugAddrPort)
} }
if cfg.localHealthEnabled() { if cfg.localHealthEnabled() {
log.Printf("Running healthcheck endpoint at %s/healthz", cfg.LocalAddrPort) log.Printf("Running healthcheck endpoint at %s/healthz", cfg.LocalAddrPort)
healthCheck = healthHandlers(mux, cfg.PodIPv4) healthCheck = registerHealthHandlers(mux, cfg.PodIPv4)
} }
if cfg.EgressProxiesCfgPath != "" {
log.Printf("Running preshutdown hook at %s%s", cfg.LocalAddrPort, kubetypes.EgessServicesPreshutdownEP) if cfg.egressSvcsTerminateEPEnabled() {
log.Printf("Running egress preshutdown hook at %s%s", cfg.LocalAddrPort, kubetypes.EgessServicesPreshutdownEP)
ep.registerHandlers(mux) ep.registerHandlers(mux)
} }
if cfg.ServeConfigPath != "" {
log.Printf("Running serve preshutdown hook at %s%s", cfg.LocalAddrPort, kubetypes.ServePreshutdownEP)
registerServeShutdownHandlers(mux, client)
}
close := runHTTPServer(mux, cfg.LocalAddrPort) close := runHTTPServer(mux, cfg.LocalAddrPort)
defer close() defer close()
} }

View File

@ -62,13 +62,13 @@ func (m *metrics) handleDebug(w http.ResponseWriter, r *http.Request) {
proxy(w, r, debugURL, http.DefaultClient.Do) proxy(w, r, debugURL, http.DefaultClient.Do)
} }
// metricsHandlers registers a simple HTTP metrics handler at /metrics, forwarding // registerMetricsHandlers registers a simple HTTP metrics handler at /metrics, forwarding
// requests to tailscaled's /localapi/v0/usermetrics API. // requests to tailscaled's /localapi/v0/usermetrics API.
// //
// In 1.78.x and 1.80.x, it also proxies debug paths to tailscaled's debug // In 1.78.x and 1.80.x, it also proxies debug paths to tailscaled's debug
// endpoint if configured to ease migration for a breaking change serving user // endpoint if configured to ease migration for a breaking change serving user
// metrics instead of debug metrics on the "metrics" port. // metrics instead of debug metrics on the "metrics" port.
func metricsHandlers(mux *http.ServeMux, lc *local.Client, debugAddrPort string) { func registerMetricsHandlers(mux *http.ServeMux, lc *local.Client, debugAddrPort string) {
m := &metrics{ m := &metrics{
lc: lc, lc: lc,
debugEndpoint: debugAddrPort, debugEndpoint: debugAddrPort,

View File

@ -9,7 +9,9 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -169,3 +171,32 @@ func readServeConfig(path, certDomain string) (*ipn.ServeConfig, error) {
} }
return &sc, nil return &sc, nil
} }
func registerServeShutdownHandlers(mux *http.ServeMux, lc *local.Client) {
// Register the ingress shutdown handler.
mux.Handle(fmt.Sprintf("GET %s", kubetypes.ServePreshutdownEP), serveShutdownHandler(lc))
}
func serveShutdownHandler(lc *local.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
prefs, err := lc.GetPrefs(r.Context())
if err != nil {
http.Error(w, fmt.Sprintf("error getting prefs: %v", err), http.StatusInternalServerError)
return
}
if len(prefs.AdvertiseServices) == 0 {
return
}
log.Printf("serve proxy: unadvertising services: %v", prefs.AdvertiseServices)
if _, err := lc.EditPrefs(r.Context(), &ipn.MaskedPrefs{
AdvertiseServicesSet: true,
Prefs: ipn.Prefs{
AdvertiseServices: nil,
},
}); err != nil {
http.Error(w, fmt.Sprintf("error setting prefs AdvertiseServices: %v", err), http.StatusInternalServerError)
return
}
}
}

View File

@ -209,6 +209,15 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode string
// Set the deletion grace period to 6 minutes to ensure that the pre-stop hook has enough time to terminate // Set the deletion grace period to 6 minutes to ensure that the pre-stop hook has enough time to terminate
// gracefully. // gracefully.
ss.Spec.Template.DeletionGracePeriodSeconds = ptr.To(deletionGracePeriodSeconds) ss.Spec.Template.DeletionGracePeriodSeconds = ptr.To(deletionGracePeriodSeconds)
} else if pg.Spec.Type == tsapi.ProxyGroupTypeIngress {
c.Lifecycle = &corev1.Lifecycle{
PreStop: &corev1.LifecycleHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: kubetypes.ServePreshutdownEP,
Port: intstr.FromInt(defaultLocalAddrPort),
},
},
}
} }
return ss, nil return ss, nil
} }

View File

@ -418,6 +418,18 @@ func TestProxyGroupTypes(t *testing.T) {
verifyEnvVar(t, sts, "TS_SERVE_CONFIG", "/etc/proxies/serve-config.json") verifyEnvVar(t, sts, "TS_SERVE_CONFIG", "/etc/proxies/serve-config.json")
verifyEnvVar(t, sts, "TS_EXPERIMENTAL_CERT_SHARE", "true") verifyEnvVar(t, sts, "TS_EXPERIMENTAL_CERT_SHARE", "true")
expectedLifecycle := corev1.Lifecycle{
PreStop: &corev1.LifecycleHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: kubetypes.ServePreshutdownEP,
Port: intstr.FromInt(defaultLocalAddrPort),
},
},
}
if diff := cmp.Diff(expectedLifecycle, *sts.Spec.Template.Spec.Containers[0].Lifecycle); diff != "" {
t.Errorf("unexpected lifecycle (-want +got):\n%s", diff)
}
// Verify ConfigMap volume mount // Verify ConfigMap volume mount
cmName := fmt.Sprintf("%s-ingress-config", pg.Name) cmName := fmt.Sprintf("%s-ingress-config", pg.Name)
expectedVolume := corev1.Volume{ expectedVolume := corev1.Volume{

View File

@ -48,6 +48,7 @@ const (
PodIPv4Header string = "Pod-IPv4" PodIPv4Header string = "Pod-IPv4"
EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown" EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown"
ServePreshutdownEP = "/internal-serve-preshutdown"
LabelManaged = "tailscale.com/managed" LabelManaged = "tailscale.com/managed"
LabelSecretType = "tailscale.com/secret-type" // "config", "state" "certs" LabelSecretType = "tailscale.com/secret-type" // "config", "state" "certs"