From 1807dfdf527e61d293ca05153fac153876c3baaf Mon Sep 17 00:00:00 2001 From: Hugo Shaka Date: Tue, 29 Oct 2024 11:11:29 -0400 Subject: [PATCH] Implement immediate schedule support for automatic updates (#47920) * Implement immediate schedule support * expose edition, fips, and ensure ping endpoint answers * fix after rebase * fix cache tests --- api/client/webclient/webclient.go | 10 ++ api/types/autoupdate/rollout_test.go | 12 +-- api/types/autoupdate/utils.go | 4 +- api/types/autoupdate/version_test.go | 12 +-- lib/cache/cache_test.go | 6 +- lib/web/apiserver.go | 139 +++++++++++++++++++-------- lib/web/apiserver_ping_test.go | 100 ++++++++++++++++--- 7 files changed, 209 insertions(+), 74 deletions(-) diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index 95ae0ea9747c3..f3b6ba5586768 100644 --- a/api/client/webclient/webclient.go +++ b/api/client/webclient/webclient.go @@ -305,6 +305,10 @@ type PingResponse struct { // reserved: license_warnings ([]string) // AutomaticUpgrades describes whether agents should automatically upgrade. AutomaticUpgrades bool `json:"automatic_upgrades"` + // Edition represents the Teleport edition. Possible values are "oss", "ent", and "community". + Edition string `json:"edition"` + // FIPS represents if Teleport is using FIPS-compliant cryptography. + FIPS bool `json:"fips"` } // PingErrorResponse contains the error from /webapi/ping. @@ -336,6 +340,12 @@ type AutoUpdateSettings struct { ToolsVersion string `json:"tools_version"` // ToolsMode defines mode client auto update feature `enabled|disabled`. ToolsMode string `json:"tools_mode"` + // AgentVersion defines the version of teleport that agents enrolled into autoupdates should run. + AgentVersion string `json:"agent_version"` + // AgentAutoUpdate indicates if the requesting agent should attempt to update now. + AgentAutoUpdate bool `json:"agent_auto_update"` + // AgentUpdateJitterSeconds defines the jitter time an agent should wait before updating. + AgentUpdateJitterSeconds int `json:"agent_update_jitter_seconds"` } // KubeProxySettings is kubernetes proxy settings diff --git a/api/types/autoupdate/rollout_test.go b/api/types/autoupdate/rollout_test.go index cce4dc8495d83..66c1b705d1568 100644 --- a/api/types/autoupdate/rollout_test.go +++ b/api/types/autoupdate/rollout_test.go @@ -41,7 +41,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { spec: &autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2.3.4-dev", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: AgentsUpdateModeEnabled, Strategy: AgentsStrategyHaltOnError, }, @@ -57,7 +57,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { Spec: &autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2.3.4-dev", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: AgentsUpdateModeEnabled, Strategy: AgentsStrategyHaltOnError, }, @@ -74,7 +74,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { name: "missing start version", spec: &autoupdate.AutoUpdateAgentRolloutSpec{ TargetVersion: "2.3.4-dev", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: AgentsUpdateModeEnabled, Strategy: AgentsStrategyHaltOnError, }, @@ -87,7 +87,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { spec: &autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2-3-4", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: AgentsUpdateModeEnabled, Strategy: AgentsStrategyHaltOnError, }, @@ -100,7 +100,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { spec: &autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2.3.4-dev", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: "invalid-mode", Strategy: AgentsStrategyHaltOnError, }, @@ -126,7 +126,7 @@ func TestNewAutoUpdateAgentRollout(t *testing.T) { spec: &autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2.3.4-dev", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, AutoupdateMode: AgentsUpdateModeEnabled, Strategy: "invalid-strategy", }, diff --git a/api/types/autoupdate/utils.go b/api/types/autoupdate/utils.go index 4772ff8a94411..30658c80d71ec 100644 --- a/api/types/autoupdate/utils.go +++ b/api/types/autoupdate/utils.go @@ -51,8 +51,10 @@ func checkToolsMode(mode string) error { func checkScheduleName(schedule string) error { switch schedule { - case AgentsScheduleRegular, AgentsScheduleImmediate: + case AgentsScheduleImmediate: return nil + case AgentsScheduleRegular: + return trace.BadParameter("regular schedule is not implemented yet") default: return trace.BadParameter("unsupported schedule type: %q", schedule) } diff --git a/api/types/autoupdate/version_test.go b/api/types/autoupdate/version_test.go index a59a4f6fe6c22..793d7d6a2a145 100644 --- a/api/types/autoupdate/version_test.go +++ b/api/types/autoupdate/version_test.go @@ -94,7 +94,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { Agents: &autoupdate.AutoUpdateVersionSpecAgents{ StartVersion: "1.2.3-dev.1", TargetVersion: "1.2.3-dev.2", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, Mode: AgentsUpdateModeEnabled, }, }, @@ -111,7 +111,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { Agents: &autoupdate.AutoUpdateVersionSpecAgents{ StartVersion: "1.2.3-dev.1", TargetVersion: "1.2.3-dev.2", - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, Mode: AgentsUpdateModeEnabled, }, }, @@ -124,7 +124,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { StartVersion: "", TargetVersion: "1.2.3", Mode: AgentsUpdateModeEnabled, - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, }, }, assertErr: func(t *testing.T, err error, a ...any) { @@ -138,7 +138,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { StartVersion: "1.2.3-dev", TargetVersion: "", Mode: AgentsUpdateModeEnabled, - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, }, }, assertErr: func(t *testing.T, err error, a ...any) { @@ -152,7 +152,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { StartVersion: "17-0-0", TargetVersion: "1.2.3", Mode: AgentsUpdateModeEnabled, - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, }, }, assertErr: func(t *testing.T, err error, a ...any) { @@ -166,7 +166,7 @@ func TestNewAutoUpdateVersion(t *testing.T) { StartVersion: "1.2.3", TargetVersion: "17-0-0", Mode: AgentsUpdateModeEnabled, - Schedule: AgentsScheduleRegular, + Schedule: AgentsScheduleImmediate, }, }, assertErr: func(t *testing.T, err error, a ...any) { diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go index 9c11b4f3a1145..f74ca9eeef151 100644 --- a/lib/cache/cache_test.go +++ b/lib/cache/cache_test.go @@ -4083,9 +4083,9 @@ func newAutoUpdateAgentRollout(t *testing.T) *autoupdate.AutoUpdateAgentRollout r, err := update.NewAutoUpdateAgentRollout(&autoupdate.AutoUpdateAgentRolloutSpec{ StartVersion: "1.2.3", TargetVersion: "2.3.4", - Schedule: "regular", - AutoupdateMode: "enabled", - Strategy: "time-based", + Schedule: update.AgentsScheduleImmediate, + AutoupdateMode: update.AgentsUpdateModeEnabled, + Strategy: update.AgentsStrategyTimeBased, }) require.NoError(t, err) return r diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index de4d085377c3c..2bbb2bcbdd561 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -65,6 +65,7 @@ import ( "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" apidefaults "github.com/gravitational/teleport/api/defaults" + autoupdatepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1" mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" "github.com/gravitational/teleport/api/mfa" @@ -135,6 +136,8 @@ const ( // 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 + // DefaultAgentUpdateJitterSeconds is the default jitter agents should wait before updating. + DefaultAgentUpdateJitterSeconds = 60 ) // healthCheckAppServerFunc defines a function used to perform a health check @@ -1539,69 +1542,65 @@ func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Para MinClientVersion: teleport.MinClientVersion, ClusterName: h.auth.clusterName, AutomaticUpgrades: pr.ServerFeatures.GetAutomaticUpgrades(), + AutoUpdate: h.automaticUpdateSettings(r.Context()), + Edition: modules.GetModules().BuildType(), + FIPS: modules.IsBoringBinary(), }, nil } func (h *Handler) find(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { // 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, - } - - proxyConfig, err := h.cfg.ProxySettings.GetProxySettings(r.Context()) + proxyConfig, err := h.cfg.ProxySettings.GetProxySettings(ctx) if err != nil { return nil, trace.Wrap(err) } - response.Proxy = *proxyConfig - authPref, err := h.cfg.AccessPoint.GetAuthPreference(r.Context()) + authPref, err := h.cfg.AccessPoint.GetAuthPreference(ctx) if err != nil { return nil, 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 + return &webclient.PingResponse{ + Proxy: *proxyConfig, + Auth: webclient.AuthenticationSettings{SignatureAlgorithmSuite: authPref.GetSignatureAlgorithmSuite()}, + ServerVersion: teleport.Version, + MinClientVersion: teleport.MinClientVersion, + ClusterName: h.auth.clusterName, + Edition: modules.GetModules().BuildType(), + FIPS: modules.IsBoringBinary(), + AutoUpdate: h.automaticUpdateSettings(ctx), + }, nil }) if err != nil { return nil, trace.Wrap(err) } - - // 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 } +// TODO: add the request as a parameter when we'll need to modulate the content based on the UUID and group +func (h *Handler) automaticUpdateSettings(ctx context.Context) webclient.AutoUpdateSettings { + autoUpdateConfig, err := h.cfg.AccessPoint.GetAutoUpdateConfig(ctx) + // 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(ctx, "failed to receive AutoUpdateConfig", "error", err) + } + + autoUpdateVersion, err := h.cfg.AccessPoint.GetAutoUpdateVersion(ctx) + // 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(ctx, "failed to receive AutoUpdateVersion", "error", err) + } + + return webclient.AutoUpdateSettings{ + ToolsMode: getToolsMode(autoUpdateConfig), + ToolsVersion: getToolsVersion(autoUpdateVersion), + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentVersion: getAgentVersion(autoUpdateVersion), + AgentAutoUpdate: agentShouldUpdate(autoUpdateConfig, autoUpdateVersion), + } +} + func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { authClient := h.cfg.ProxyClient connectorName := p.ByName("connector") @@ -5154,3 +5153,59 @@ func readEtagFromAppHash(fs http.FileSystem) (string, error) { return etag, nil } + +func getToolsMode(config *autoupdatepb.AutoUpdateConfig) string { + // If we can't get the AU config or if AUs 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 config.GetSpec().GetTools() == nil { + return autoupdate.ToolsUpdateModeDisabled + } + return config.GetSpec().GetTools().GetMode() +} + +func getToolsVersion(version *autoupdatepb.AutoUpdateVersion) string { + // 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 version.GetSpec().GetTools() == nil { + return api.Version + } + return version.GetSpec().GetTools().GetTargetVersion() +} + +func getAgentVersion(version *autoupdatepb.AutoUpdateVersion) string { + // 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. + // TODO: read the version from the autoupdate_agent_rollout when the resource is implemented + if version.GetSpec().GetAgents() == nil { + return api.Version + } + + return version.GetSpec().GetAgents().GetTargetVersion() +} + +func agentShouldUpdate(config *autoupdatepb.AutoUpdateConfig, version *autoupdatepb.AutoUpdateVersion) bool { + // TODO: read the data from the autoupdate_agent_rollout when the resource is implemented + + // If we can't get the AU config or if AUs 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 config.GetSpec().GetAgents() == nil { + return false + } + if version.GetSpec().GetAgents() == nil { + return false + } + configMode := config.GetSpec().GetAgents().GetMode() + versionMode := version.GetSpec().GetAgents().GetMode() + + // We update only if both version and config agent modes are "enabled" + if configMode != autoupdate.AgentsUpdateModeEnabled || versionMode != autoupdate.AgentsUpdateModeEnabled { + return false + } + + scheduleName := version.GetSpec().GetAgents().GetSchedule() + return scheduleName == autoupdate.AgentsScheduleImmediate +} diff --git a/lib/web/apiserver_ping_test.go b/lib/web/apiserver_ping_test.go index 231c8625ffacd..5ce3720375c46 100644 --- a/lib/web/apiserver_ping_test.go +++ b/lib/web/apiserver_ping_test.go @@ -305,48 +305,110 @@ func TestPing_autoUpdateResources(t *testing.T) { { name: "resources not defined", expected: webclient.AutoUpdateSettings{ - ToolsVersion: api.Version, - ToolsMode: autoupdate.ToolsUpdateModeDisabled, + ToolsVersion: api.Version, + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, }, { - name: "enable auto update", + name: "enable tools auto update", config: &autoupdatev1pb.AutoUpdateConfigSpec{ Tools: &autoupdatev1pb.AutoUpdateConfigSpecTools{ Mode: autoupdate.ToolsUpdateModeEnabled, }, }, expected: webclient.AutoUpdateSettings{ - ToolsMode: autoupdate.ToolsUpdateModeEnabled, - ToolsVersion: api.Version, + ToolsMode: autoupdate.ToolsUpdateModeEnabled, + ToolsVersion: api.Version, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, cleanup: true, }, { - name: "no autoupdate tool config nor version", + name: "enable agent auto update, immediate schedule", + config: &autoupdatev1pb.AutoUpdateConfigSpec{ + Agents: &autoupdatev1pb.AutoUpdateConfigSpecAgents{ + Mode: autoupdate.AgentsUpdateModeEnabled, + Strategy: autoupdate.AgentsStrategyHaltOnError, + }, + }, + version: &autoupdatev1pb.AutoUpdateVersionSpec{ + Agents: &autoupdatev1pb.AutoUpdateVersionSpecAgents{ + Mode: autoupdate.AgentsUpdateModeEnabled, + StartVersion: "1.2.3", + TargetVersion: "1.2.4", + Schedule: autoupdate.AgentsScheduleImmediate, + }, + }, + expected: webclient.AutoUpdateSettings{ + ToolsVersion: api.Version, + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: true, + AgentVersion: "1.2.4", + }, + cleanup: true, + }, + { + name: "version enable agent auto update, but config disables them", + config: &autoupdatev1pb.AutoUpdateConfigSpec{ + Agents: &autoupdatev1pb.AutoUpdateConfigSpecAgents{ + Mode: autoupdate.AgentsUpdateModeDisabled, + Strategy: autoupdate.AgentsStrategyHaltOnError, + }, + }, + version: &autoupdatev1pb.AutoUpdateVersionSpec{ + Agents: &autoupdatev1pb.AutoUpdateVersionSpecAgents{ + Mode: autoupdate.AgentsUpdateModeEnabled, + StartVersion: "1.2.3", + TargetVersion: "1.2.4", + Schedule: autoupdate.AgentsScheduleImmediate, + }, + }, + expected: webclient.AutoUpdateSettings{ + ToolsVersion: api.Version, + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: "1.2.4", + }, + cleanup: true, + }, + { + name: "empty config and version", config: &autoupdatev1pb.AutoUpdateConfigSpec{}, version: &autoupdatev1pb.AutoUpdateVersionSpec{}, expected: webclient.AutoUpdateSettings{ - ToolsVersion: api.Version, - ToolsMode: autoupdate.ToolsUpdateModeDisabled, + ToolsVersion: api.Version, + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, cleanup: true, }, { - name: "set auto update version", + name: "set tools auto update version", version: &autoupdatev1pb.AutoUpdateVersionSpec{ Tools: &autoupdatev1pb.AutoUpdateVersionSpecTools{ TargetVersion: "1.2.3", }, }, expected: webclient.AutoUpdateSettings{ - ToolsVersion: "1.2.3", - ToolsMode: autoupdate.ToolsUpdateModeDisabled, + ToolsVersion: "1.2.3", + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, cleanup: true, }, { - name: "enable auto update and set version", + name: "enable tools auto update and set version", config: &autoupdatev1pb.AutoUpdateConfigSpec{ Tools: &autoupdatev1pb.AutoUpdateConfigSpecTools{ Mode: autoupdate.ToolsUpdateModeEnabled, @@ -358,8 +420,11 @@ func TestPing_autoUpdateResources(t *testing.T) { }, }, expected: webclient.AutoUpdateSettings{ - ToolsMode: autoupdate.ToolsUpdateModeEnabled, - ToolsVersion: "1.2.3", + ToolsMode: autoupdate.ToolsUpdateModeEnabled, + ToolsVersion: "1.2.3", + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, }, { @@ -375,8 +440,11 @@ func TestPing_autoUpdateResources(t *testing.T) { }, }, expected: webclient.AutoUpdateSettings{ - ToolsMode: autoupdate.ToolsUpdateModeDisabled, - ToolsVersion: "3.2.1", + ToolsMode: autoupdate.ToolsUpdateModeDisabled, + ToolsVersion: "3.2.1", + AgentUpdateJitterSeconds: DefaultAgentUpdateJitterSeconds, + AgentAutoUpdate: false, + AgentVersion: api.Version, }, }, }