Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cache /find endpoint #48016

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved
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
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved
for _, proxy := range env.proxies {
proxy.handler.handler.findEndpointCache.Remove("find")
}

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

Expand Down
Loading