Skip to content

Commit

Permalink
Implement immediate schedule support for automatic updates (#47920)
Browse files Browse the repository at this point in the history
* Implement immediate schedule support

* expose edition, fips, and ensure ping endpoint answers

* fix after rebase

* fix cache tests
  • Loading branch information
hugoShaka authored Oct 29, 2024
1 parent 074f806 commit 1807dfd
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 74 deletions.
10 changes: 10 additions & 0 deletions api/client/webclient/webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions api/types/autoupdate/rollout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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",
},
Expand Down
4 changes: 3 additions & 1 deletion api/types/autoupdate/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 6 additions & 6 deletions api/types/autoupdate/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
Expand All @@ -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,
},
},
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions lib/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
139 changes: 97 additions & 42 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 1807dfd

Please sign in to comment.