Skip to content

Commit

Permalink
cache /find endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoShaka committed Oct 28, 2024
1 parent 239723a commit 3adf8b1
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 47 deletions.
122 changes: 75 additions & 47 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ const (
// DefaultFeatureWatchInterval is the default time in which the feature watcher
// should ping the auth server to check for updated features
DefaultFeatureWatchInterval = time.Minute * 5
// findEndpointCacheTTL is the cache TTL for the find endpoint generic answer.
// This cache is here to protect against accidental or intentional DDoS, the TTL must be low to quickly reflect
// cluster configuration changes.
findEndpointCacheTTL = 10 * time.Second
)

// healthCheckAppServerFunc defines a function used to perform a health check
Expand Down Expand Up @@ -173,6 +177,11 @@ type Handler struct {
// an authenticated websocket so unauthenticated sockets dont get left
// open.
wsIODeadline time.Duration

// findCache is used to cache the find endpoint answer. As this endpoint is unprotected and has high rate-limits,
// each call must cause minimal work. The cached answer can be modulated after, for example if the caller specified
// a specific Automatic Updates UUID or group.
findEndpointCache *utils.FnCache
}

// HandlerOption is a functional argument - an option that can be passed
Expand Down Expand Up @@ -452,16 +461,28 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
const apiPrefix = "/" + teleport.WebAPIVersion

cfg.SetDefaults()
clock := clockwork.NewRealClock()

findCache, err := utils.NewFnCache(utils.FnCacheConfig{
TTL: findEndpointCacheTTL,
Clock: clock,
Context: cfg.Context,
ReloadOnErr: false,
})
if err != nil {
return nil, trace.Wrap(err, "creating /find cache")
}

h := &Handler{
cfg: cfg,
log: newPackageLogger(),
logger: slog.Default().With(teleport.ComponentKey, teleport.ComponentWeb),
clock: clockwork.NewRealClock(),
clock: clock,
clusterFeatures: cfg.ClusterFeatures,
healthCheckAppServer: cfg.HealthCheckAppServer,
tracer: cfg.TracerProvider.Tracer(teleport.ComponentWeb),
wsIODeadline: wsIODeadline,
findEndpointCache: findCache,
}

if automaticUpgrades(cfg.ClusterFeatures) && h.cfg.AutomaticUpgradesChannels == nil {
Expand Down Expand Up @@ -1521,56 +1542,63 @@ func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Para
}

func (h *Handler) find(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
// TODO(jent,espadolini): add a time-based cache to further reduce load on this endpoint
proxyConfig, err := h.cfg.ProxySettings.GetProxySettings(r.Context())
if err != nil {
return nil, trace.Wrap(err)
}
authPref, err := h.cfg.AccessPoint.GetAuthPreference(r.Context())
if err != nil {
return nil, trace.Wrap(err)
}
response := webclient.PingResponse{
Auth: webclient.AuthenticationSettings{
// Nodes need the signature algorithm suite when joining to generate
// keys with the correct algorithm.
SignatureAlgorithmSuite: authPref.GetSignatureAlgorithmSuite(),
},
Proxy: *proxyConfig,
ServerVersion: teleport.Version,
MinClientVersion: teleport.MinClientVersion,
ClusterName: h.auth.clusterName,
}
// cache the generic answer to avoid doing work for each request
resp, err := utils.FnCacheGet[webclient.PingResponse](r.Context(), h.findEndpointCache, "find", func(ctx context.Context) (webclient.PingResponse, error) {
response := webclient.PingResponse{
ServerVersion: teleport.Version,
MinClientVersion: teleport.MinClientVersion,
ClusterName: h.auth.clusterName,
}

autoUpdateConfig, err := h.cfg.AccessPoint.GetAutoUpdateConfig(r.Context())
// TODO(vapopov) DELETE IN v18.0.0 check of IsNotImplemented, must be backported to all latest supported versions.
if err != nil && !trace.IsNotFound(err) && !trace.IsNotImplemented(err) {
h.logger.ErrorContext(r.Context(), "failed to receive AutoUpdateConfig", "error", err)
}
// If we can't get the AU config or tools AU are not configured, we default to "disabled".
// This ensures we fail open and don't accidentally update agents if something is going wrong.
// If we want to enable AUs by default, it would be better to create a default "autoupdate_config" resource
// than changing this logic.
if autoUpdateConfig.GetSpec().GetTools() == nil {
response.AutoUpdate.ToolsMode = autoupdate.ToolsUpdateModeDisabled
} else {
response.AutoUpdate.ToolsMode = autoUpdateConfig.GetSpec().GetTools().GetMode()
}
proxyConfig, err := h.cfg.ProxySettings.GetProxySettings(r.Context())
if err != nil {
return response, trace.Wrap(err)
}
response.Proxy = *proxyConfig

autoUpdateVersion, err := h.cfg.AccessPoint.GetAutoUpdateVersion(r.Context())
// TODO(vapopov) DELETE IN v18.0.0 check of IsNotImplemented, must be backported to all latest supported versions.
if err != nil && !trace.IsNotFound(err) && !trace.IsNotImplemented(err) {
h.logger.ErrorContext(r.Context(), "failed to receive AutoUpdateVersion", "error", err)
}
// If we can't get the AU version or tools AU version is not specified, we default to the current proxy version.
// This ensures we always advertise a version compatible with the cluster.
if autoUpdateVersion.GetSpec().GetTools() == nil {
response.AutoUpdate.ToolsVersion = api.Version
} else {
response.AutoUpdate.ToolsVersion = autoUpdateVersion.GetSpec().GetTools().GetTargetVersion()
authPref, err := h.cfg.AccessPoint.GetAuthPreference(r.Context())
if err != nil {
return response, trace.Wrap(err)
}
response.Auth = webclient.AuthenticationSettings{SignatureAlgorithmSuite: authPref.GetSignatureAlgorithmSuite()}

autoUpdateConfig, err := h.cfg.AccessPoint.GetAutoUpdateConfig(r.Context())
// TODO(vapopov) DELETE IN v18.0.0 check of IsNotImplemented, must be backported to all latest supported versions.
if err != nil && !trace.IsNotFound(err) && !trace.IsNotImplemented(err) {
h.logger.ErrorContext(r.Context(), "failed to receive AutoUpdateConfig", "error", err)
}
// If we can't get the AU config or tools AU are not configured, we default to "disabled".
// This ensures we fail open and don't accidentally update agents if something is going wrong.
// If we want to enable AUs by default, it would be better to create a default "autoupdate_config" resource
// than changing this logic.
if autoUpdateConfig.GetSpec().GetTools() == nil {
response.AutoUpdate.ToolsMode = autoupdate.ToolsUpdateModeDisabled
} else {
response.AutoUpdate.ToolsMode = autoUpdateConfig.GetSpec().GetTools().GetMode()
}

autoUpdateVersion, err := h.cfg.AccessPoint.GetAutoUpdateVersion(r.Context())
// TODO(vapopov) DELETE IN v18.0.0 check of IsNotImplemented, must be backported to all latest supported versions.
if err != nil && !trace.IsNotFound(err) && !trace.IsNotImplemented(err) {
h.logger.ErrorContext(r.Context(), "failed to receive AutoUpdateVersion", "error", err)
}
// If we can't get the AU version or tools AU version is not specified, we default to the current proxy version.
// This ensures we always advertise a version compatible with the cluster.
if autoUpdateVersion.GetSpec().GetTools() == nil {
response.AutoUpdate.ToolsVersion = api.Version
} else {
response.AutoUpdate.ToolsVersion = autoUpdateVersion.GetSpec().GetTools().GetTargetVersion()
}

return response, nil
})
if err != nil {
return nil, trace.Wrap(err)
}

return response, nil
// If you need to modulate the response based on the request params (will need to do this for automatic updates)
// Do it here.
return resp, nil
}

func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
Expand Down
5 changes: 5 additions & 0 deletions lib/web/apiserver_ping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ func TestPing_autoUpdateResources(t *testing.T) {
require.NoError(t, err)
}

// clear the fn cache to force the next answer to be fresh
for _, proxy := range env.proxies {
proxy.handler.handler.findEndpointCache.Remove("find")
}

resp, err := client.NewInsecureWebClient().Do(req)
require.NoError(t, err)

Expand Down

0 comments on commit 3adf8b1

Please sign in to comment.