implement webapi for managed updates (#62909)

This commit is contained in:
Yassine Bounekhla
2026-01-23 18:44:54 +00:00
committed by GitHub
parent 07e85c50af
commit 8d26811669
6 changed files with 1210 additions and 34 deletions
+77
View File
@@ -0,0 +1,77 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package report provides utilities for working with AutoUpdateAgentReport resources.
package report
import (
"fmt"
"slices"
"time"
autoupdatepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
)
// UserFriendlyState converts an AutoUpdateAgentGroupState or AutoUpdateAgentRolloutState
// to a user-friendly string representation.
func UserFriendlyState[T autoupdatepb.AutoUpdateAgentGroupState | autoupdatepb.AutoUpdateAgentRolloutState](state T) string {
switch state {
case 0:
return "Unknown"
case 1:
return "Unstarted"
case 2:
return "Active"
case 3:
return "Done"
case 4:
return "Rolledback"
case 5:
return "Canary"
default:
// If we don't know anything about this state, we display its integer
return fmt.Sprintf("Unknown state (%d)", state)
}
}
// ValidReports filters out stale reports and returns only valid ones.
// Reports are generated every 1 minute, so any that are older than that must be stale.
func ValidReports(reports []*autoupdatepb.AutoUpdateAgentReport, now time.Time) []*autoupdatepb.AutoUpdateAgentReport {
isStale := func(r *autoupdatepb.AutoUpdateAgentReport) bool {
return now.Sub(r.GetSpec().GetTimestamp().AsTime()) > time.Minute
}
return slices.DeleteFunc(slices.Clone(reports), isStale)
}
// AggregateVersionCounts aggregates agent reports into version counts by group
func AggregateVersionCounts(reports []*autoupdatepb.AutoUpdateAgentReport) map[string]map[string]int {
out := make(map[string]map[string]int)
for _, report := range reports {
for groupName, group := range report.GetSpec().GetGroups() {
if out[groupName] == nil {
out[groupName] = make(map[string]int)
}
for version, versionCount := range group.GetVersions() {
out[groupName][version] += int(versionCount.GetCount())
}
}
}
return out
}
+6
View File
@@ -1181,6 +1181,12 @@ func (h *Handler) bindDefaultEndpoints() {
// Channel can contain "/", hence the use of a catch-all parameter
h.GET("/webapi/automaticupgrades/channel/*request", h.WithUnauthenticatedHighLimiter(h.automaticUpgrades109))
// Managed updates
h.GET("/webapi/managedupdates", h.WithAuth(h.getManagedUpdatesDetails))
h.POST("/webapi/managedupdates/groups/:groupName/start", h.WithAuth(h.startGroupUpdate))
h.POST("/webapi/managedupdates/groups/:groupName/done", h.WithAuth(h.markGroupDone))
h.POST("/webapi/managedupdates/groups/:groupName/rollback", h.WithAuth(h.rollbackGroup))
// GET Machine ID bot by name
h.GET("/webapi/sites/:site/machine-id/bot/:name", h.WithClusterAuth(h.getBot))
// GET Machine ID bots
+384
View File
@@ -0,0 +1,384 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package web
import (
"net/http"
"strings"
"time"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
"github.com/gravitational/teleport"
autoupdatepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/autoupdate"
"github.com/gravitational/teleport/api/utils/clientutils"
aur "github.com/gravitational/teleport/lib/autoupdate/report"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/itertools/stream"
"github.com/gravitational/teleport/lib/web/ui"
)
// getManagedUpdatesDetails returns managed updates details.
func (h *Handler) getManagedUpdatesDetails(w http.ResponseWriter, r *http.Request, params httprouter.Params, sctx *SessionContext) (any, error) {
ctx := r.Context()
clt, err := sctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}
response := &ui.ManagedUpdatesDetails{}
autoUpdateConfig, err := clt.GetAutoUpdateConfig(ctx)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
autoUpdateConfig = nil
}
autoUpdateVersion, err := clt.GetAutoUpdateVersion(ctx)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
autoUpdateVersion = nil
}
response.Tools = getToolsInfo(autoUpdateConfig, autoUpdateVersion)
rollout, err := clt.GetAutoUpdateAgentRollout(ctx)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
rollout = nil
}
if rollout != nil {
response.Rollout = getRolloutInfo(rollout)
}
reports, err := stream.Collect(clientutils.Resources(ctx, clt.ListAutoUpdateAgentReports))
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
reports = nil
}
// Filter and aggregate version counts from the reports
validReports := aur.ValidReports(reports, time.Now())
versionCountsByGroup := aur.AggregateVersionCounts(validReports)
if rollout != nil {
response.Groups = getGroupsInfo(rollout, versionCountsByGroup)
response.OrphanedAgentVersionCounts = getOrphanedAgentCounts(rollout, versionCountsByGroup)
}
// Get cluster maintenance info if this is a cloud cluster
if features := h.GetClusterFeatures(); features.GetCloud() {
maintenanceConfig, err := clt.GetClusterMaintenanceConfig(ctx)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
maintenanceConfig = nil
}
if maintenanceConfig != nil {
response.ClusterMaintenance = getClusterMaintenanceInfo(maintenanceConfig)
}
}
return response, nil
}
// getToolsInfo builds the ToolsAutoUpdateInfo object.
func getToolsInfo(config *autoupdatepb.AutoUpdateConfig, version *autoupdatepb.AutoUpdateVersion) *ui.ToolsAutoUpdateInfo {
var mode, targetVersion string
if config != nil {
mode = config.GetSpec().GetTools().GetMode()
}
if version != nil {
targetVersion = version.GetSpec().GetTools().GetTargetVersion()
}
// If empty, return nil
if mode == "" && targetVersion == "" {
return nil
}
return &ui.ToolsAutoUpdateInfo{
Mode: mode,
TargetVersion: targetVersion,
}
}
// getRolloutInfo builds the RolloutInfo object.
func getRolloutInfo(rollout *autoupdatepb.AutoUpdateAgentRollout) *ui.RolloutInfo {
if rollout == nil || rollout.GetSpec() == nil {
return nil
}
spec := rollout.GetSpec()
status := rollout.GetStatus()
info := &ui.RolloutInfo{
StartVersion: spec.GetStartVersion(),
TargetVersion: spec.GetTargetVersion(),
Strategy: spec.GetStrategy(),
Schedule: spec.GetSchedule(),
State: strings.ToLower(aur.UserFriendlyState(status.GetState())),
Mode: spec.GetAutoupdateMode(),
}
// Set the rollout start time
if status != nil {
if startTime := status.GetStartTime(); startTime != nil && startTime.IsValid() {
t := startTime.AsTime()
if !t.IsZero() && t.Unix() != 0 {
info.StartTime = &t
}
}
}
return info
}
// getGroupsInfo builds the list of RolloutGroupInfo objects.
func getGroupsInfo(rollout *autoupdatepb.AutoUpdateAgentRollout, versionCountsByGroup map[string]map[string]int) []ui.RolloutGroupInfo {
if rollout == nil {
return nil
}
groups := rollout.GetStatus().GetGroups()
if len(groups) == 0 {
return nil
}
out := make([]ui.RolloutGroupInfo, 0, len(groups))
for i, group := range groups {
groupInfo := ui.RolloutGroupInfo{
Name: group.GetName(),
State: strings.ToLower(aur.UserFriendlyState(group.GetState())),
InitialCount: group.GetInitialCount(),
PresentCount: group.GetPresentCount(),
UpToDateCount: group.GetUpToDateCount(),
StateReason: group.GetLastUpdateReason(),
CanaryCount: group.GetCanaryCount(),
IsCatchAll: i == len(groups)-1,
}
// Only set the position if the strategy is halt-on-error
if rollout.GetSpec().GetStrategy() == autoupdate.AgentsStrategyHaltOnError {
groupInfo.Position = i + 1
}
// Set the group start time
if startTime := group.GetStartTime(); startTime != nil && startTime.IsValid() {
t := startTime.AsTime()
if !t.IsZero() && t.Unix() != 0 {
groupInfo.StartTime = &t
}
}
// Set the last update time
if lastUpdateTime := group.GetLastUpdateTime(); lastUpdateTime != nil && lastUpdateTime.IsValid() {
t := lastUpdateTime.AsTime()
if !t.IsZero() && t.Unix() != 0 {
groupInfo.LastUpdateTime = &t
}
}
// Add the version counts from aggregated reports
if counts, ok := versionCountsByGroup[group.GetName()]; ok && len(counts) > 0 {
groupInfo.AgentVersionCounts = counts
}
// Calculate the CanarySuccessCount
if groupInfo.CanaryCount > 0 {
var successCount uint64
for _, canary := range group.GetCanaries() {
if canary.GetSuccess() {
successCount++
}
}
groupInfo.CanarySuccessCount = successCount
}
out = append(out, groupInfo)
}
return out
}
// getClusterMaintenaceInfo builds the ClusterMaintenanceInfo object.
func getClusterMaintenanceInfo(cmc types.ClusterMaintenanceConfig) *ui.ClusterMaintenanceInfo {
window, ok := cmc.GetAgentUpgradeWindow()
if !ok {
return nil
}
return &ui.ClusterMaintenanceInfo{
ControlPlaneVersion: teleport.Version,
MaintenanceWeekdays: window.Weekdays,
MaintenanceStartHour: int(window.UTCStartHour),
}
}
// getOrphanedAgentCounts returns version counts for agents reporting group names
// that don't match any defined rollout group.
func getOrphanedAgentCounts(rollout *autoupdatepb.AutoUpdateAgentRollout, versionCountsByGroup map[string]map[string]int) map[string]int {
if rollout == nil || len(versionCountsByGroup) == 0 {
return nil
}
// Get the defined rollout groups
definedGroups := make(map[string]bool)
for _, group := range rollout.GetStatus().GetGroups() {
definedGroups[group.GetName()] = true
}
// Calculate how many agents don't belong to any of those groups, and their version.
orphanedCounts := make(map[string]int)
for groupName, versionCounts := range versionCountsByGroup {
if !definedGroups[groupName] {
for version, count := range versionCounts {
orphanedCounts[version] += count
}
}
}
if len(orphanedCounts) == 0 {
return nil
}
return orphanedCounts
}
func getAutoUpdateServiceClient(sctx *SessionContext) autoupdatepb.AutoUpdateServiceClient {
return autoupdatepb.NewAutoUpdateServiceClient(sctx.GetClientConnection())
}
// startGroupUpdate starts an update for a specified rollout group.
func (h *Handler) startGroupUpdate(w http.ResponseWriter, r *http.Request, params httprouter.Params, sctx *SessionContext) (any, error) {
ctx := r.Context()
groupName := params.ByName("groupName")
if groupName == "" {
return nil, trace.BadParameter("group name is required")
}
var req ui.StartGroupUpdateRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
state := autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSPECIFIED
// If the force flag is set to true, set the desired state to active to skip canary phase.
if req.Force {
state = autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE
}
client := getAutoUpdateServiceClient(sctx)
rollout, err := client.TriggerAutoUpdateAgentGroup(ctx, &autoupdatepb.TriggerAutoUpdateAgentGroupRequest{
Groups: []string{groupName},
DesiredState: state,
})
if err != nil {
return nil, trace.Wrap(err)
}
group, err := findGroupInfo(rollout, groupName)
if err != nil {
return nil, trace.Wrap(err)
}
return &ui.GroupActionResponse{Group: group}, nil
}
// markGroupDone marks a specified rollout group as done.
func (h *Handler) markGroupDone(w http.ResponseWriter, r *http.Request, params httprouter.Params, sctx *SessionContext) (any, error) {
ctx := r.Context()
groupName := params.ByName("groupName")
if groupName == "" {
return nil, trace.BadParameter("group name is required")
}
client := getAutoUpdateServiceClient(sctx)
rollout, err := client.ForceAutoUpdateAgentGroup(ctx, &autoupdatepb.ForceAutoUpdateAgentGroupRequest{
Groups: []string{groupName},
})
if err != nil {
return nil, trace.Wrap(err)
}
group, err := findGroupInfo(rollout, groupName)
if err != nil {
return nil, trace.Wrap(err)
}
return &ui.GroupActionResponse{Group: group}, nil
}
// rollbackGroup rolls back a specified rollout group.
func (h *Handler) rollbackGroup(w http.ResponseWriter, r *http.Request, params httprouter.Params, sctx *SessionContext) (any, error) {
ctx := r.Context()
groupName := params.ByName("groupName")
if groupName == "" {
return nil, trace.BadParameter("group name is required")
}
auClient := getAutoUpdateServiceClient(sctx)
rollout, err := auClient.RollbackAutoUpdateAgentGroup(ctx, &autoupdatepb.RollbackAutoUpdateAgentGroupRequest{
Groups: []string{groupName},
AllStartedGroups: false,
})
if err != nil {
return nil, trace.Wrap(err)
}
group, err := findGroupInfo(rollout, groupName)
if err != nil {
return nil, trace.Wrap(err)
}
return &ui.GroupActionResponse{Group: group}, nil
}
// findGroupInfo gets the RolloutGroupInfo for a specified group name.
func findGroupInfo(rollout *autoupdatepb.AutoUpdateAgentRollout, groupName string) (*ui.RolloutGroupInfo, error) {
if rollout == nil || rollout.GetStatus() == nil {
return nil, trace.NotFound("group %q not found in rollout", groupName)
}
groups := getGroupsInfo(rollout, nil)
for i := range groups {
if groups[i].Name == groupName {
return &groups[i], nil
}
}
return nil, trace.NotFound("group %q not found in rollout", groupName)
}
+620
View File
@@ -0,0 +1,620 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package web
import (
"encoding/json"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
autoupdatepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/autoupdate"
"github.com/gravitational/teleport/lib/web/ui"
)
// TestGetManagedUpdateDetails tests fetching managed updates details.
func TestGetManagedUpdatesDetails(t *testing.T) {
t.Parallel()
ctx := t.Context()
env := newWebPack(t, 1)
proxy := env.proxies[0]
// Create a role with permissions to read autoupdate resources
role, err := types.NewRole("testrole", types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindAutoUpdateConfig, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateVersion, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateAgentRollout, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateAgentReport, []string{types.VerbRead, types.VerbList}),
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
pack := proxy.authPack(t, "testuser", []types.Role{role})
// Create an auto update config
config, err := autoupdate.NewAutoUpdateConfig(&autoupdatepb.AutoUpdateConfigSpec{
Tools: &autoupdatepb.AutoUpdateConfigSpecTools{
Mode: autoupdate.ToolsUpdateModeEnabled,
},
Agents: &autoupdatepb.AutoUpdateConfigSpecAgents{
Mode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyTimeBased,
MaintenanceWindowDuration: durationpb.New(time.Hour),
Schedules: &autoupdatepb.AgentAutoUpdateSchedules{
Regular: []*autoupdatepb.AgentAutoUpdateGroup{
{Name: "all", Days: []string{"Mon", "Tue", "Wed"}, StartHour: 14},
},
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateConfig(ctx, config)
require.NoError(t, err)
// Create AutoUpdateVersion
version, err := autoupdate.NewAutoUpdateVersion(&autoupdatepb.AutoUpdateVersionSpec{
Tools: &autoupdatepb.AutoUpdateVersionSpecTools{
TargetVersion: "18.2.0",
},
Agents: &autoupdatepb.AutoUpdateVersionSpecAgents{
StartVersion: "18.1.0",
TargetVersion: "18.2.0",
Schedule: autoupdate.AgentsScheduleRegular,
Mode: autoupdate.AgentsUpdateModeEnabled,
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateVersion(ctx, version)
require.NoError(t, err)
// Create AutoUpdateAgentRollout
rollout := &autoupdatepb.AutoUpdateAgentRollout{
Kind: types.KindAutoUpdateAgentRollout,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: types.MetaNameAutoUpdateAgentRollout,
},
Spec: &autoupdatepb.AutoUpdateAgentRolloutSpec{
StartVersion: "18.1.0",
TargetVersion: "18.2.0",
Schedule: autoupdate.AgentsScheduleRegular,
AutoupdateMode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyTimeBased,
},
Status: &autoupdatepb.AutoUpdateAgentRolloutStatus{
State: autoupdatepb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_ACTIVE,
Groups: []*autoupdatepb.AutoUpdateAgentRolloutStatusGroup{
{
Name: "all",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
PresentCount: 100,
UpToDateCount: 75,
ConfigDays: []string{"Mon", "Tue", "Wed"},
ConfigStartHour: 14,
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentRollout(ctx, rollout)
require.NoError(t, err)
// Make the request
resp, err := pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "managedupdates"), url.Values{})
require.NoError(t, err)
var result ui.ManagedUpdatesDetails
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
// Verify tools info
require.NotNil(t, result.Tools)
require.Equal(t, autoupdate.ToolsUpdateModeEnabled, result.Tools.Mode)
require.Equal(t, "18.2.0", result.Tools.TargetVersion)
// Verify rollout info
require.NotNil(t, result.Rollout)
require.Equal(t, "18.1.0", result.Rollout.StartVersion)
require.Equal(t, "18.2.0", result.Rollout.TargetVersion)
require.Equal(t, autoupdate.AgentsStrategyTimeBased, result.Rollout.Strategy)
require.Equal(t, autoupdate.AgentsScheduleRegular, result.Rollout.Schedule)
require.Equal(t, "active", result.Rollout.State)
require.Equal(t, autoupdate.AgentsUpdateModeEnabled, result.Rollout.Mode)
// Verify groups info
require.Len(t, result.Groups, 1)
require.Equal(t, "all", result.Groups[0].Name)
require.Equal(t, "active", result.Groups[0].State)
require.Equal(t, uint64(100), result.Groups[0].PresentCount)
require.Equal(t, uint64(75), result.Groups[0].UpToDateCount)
// Verify cluster maintenance info is not set (not a cloud cluster)
require.Nil(t, result.ClusterMaintenance)
}
// TestGetOrphanedAgentCounts tests that orphaned agents (agents in a group that doesn't exist) are correctly included in the response.
func TestGetOrphanedAgentCounts(t *testing.T) {
t.Parallel()
ctx := t.Context()
env := newWebPack(t, 1)
proxy := env.proxies[0]
// Create a role with permissions to read autoupdate resources
role, err := types.NewRole("testrole", types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindAutoUpdateConfig, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateVersion, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateAgentRollout, []string{types.VerbRead}),
types.NewRule(types.KindAutoUpdateAgentReport, []string{types.VerbRead, types.VerbList}),
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
pack := proxy.authPack(t, "testuser", []types.Role{role})
// Create AutoUpdateConfig with "prod" as the only group
config, err := autoupdate.NewAutoUpdateConfig(&autoupdatepb.AutoUpdateConfigSpec{
Agents: &autoupdatepb.AutoUpdateConfigSpecAgents{
Mode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyTimeBased,
MaintenanceWindowDuration: durationpb.New(time.Hour),
Schedules: &autoupdatepb.AgentAutoUpdateSchedules{
Regular: []*autoupdatepb.AgentAutoUpdateGroup{
{Name: "prod", Days: []string{"*"}, StartHour: 10},
},
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateConfig(ctx, config)
require.NoError(t, err)
// Create AutoUpdateVersion
version, err := autoupdate.NewAutoUpdateVersion(&autoupdatepb.AutoUpdateVersionSpec{
Agents: &autoupdatepb.AutoUpdateVersionSpecAgents{
StartVersion: "18.1.0",
TargetVersion: "18.2.0",
Schedule: autoupdate.AgentsScheduleRegular,
Mode: autoupdate.AgentsUpdateModeEnabled,
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateVersion(ctx, version)
require.NoError(t, err)
// Create the AutoUpdateAgentRollout
rollout := &autoupdatepb.AutoUpdateAgentRollout{
Kind: types.KindAutoUpdateAgentRollout,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: types.MetaNameAutoUpdateAgentRollout,
},
Spec: &autoupdatepb.AutoUpdateAgentRolloutSpec{
StartVersion: "18.1.0",
TargetVersion: "18.2.0",
Schedule: autoupdate.AgentsScheduleRegular,
AutoupdateMode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyTimeBased,
},
Status: &autoupdatepb.AutoUpdateAgentRolloutStatus{
State: autoupdatepb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_ACTIVE,
Groups: []*autoupdatepb.AutoUpdateAgentRolloutStatusGroup{
{
Name: "prod",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
PresentCount: 50,
UpToDateCount: 25,
ConfigDays: []string{"*"},
ConfigStartHour: 10,
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentRollout(ctx, rollout)
require.NoError(t, err)
// Create AutoUpdateAgentReport with some agents in "prod" (valid) and some in "invalidgroup" (orphaned)
report := &autoupdatepb.AutoUpdateAgentReport{
Kind: types.KindAutoUpdateAgentReport,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: "test-auth-server",
},
Spec: &autoupdatepb.AutoUpdateAgentReportSpec{
Timestamp: timestamppb.Now(),
Groups: map[string]*autoupdatepb.AutoUpdateAgentReportSpecGroup{
"prod": {
Versions: map[string]*autoupdatepb.AutoUpdateAgentReportSpecGroupVersion{
"18.1.0": {Count: 30},
"18.2.0": {Count: 20},
},
},
"invalidgroup": {
Versions: map[string]*autoupdatepb.AutoUpdateAgentReportSpecGroupVersion{
"18.1.0": {Count: 5},
"18.2.0": {Count: 3},
},
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentReport(ctx, report)
require.NoError(t, err)
// Make the request
resp, err := pack.clt.Get(ctx, pack.clt.Endpoint("webapi", "managedupdates"), url.Values{})
require.NoError(t, err)
var result ui.ManagedUpdatesDetails
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
// Verify that the orphaned agent version counts are present and correct
require.NotNil(t, result.OrphanedAgentVersionCounts)
require.Equal(t, 5, result.OrphanedAgentVersionCounts["18.1.0"])
require.Equal(t, 3, result.OrphanedAgentVersionCounts["18.2.0"])
// Verify that the prod group agent version counts are present and correct
require.Len(t, result.Groups, 1)
require.Equal(t, "prod", result.Groups[0].Name)
require.Equal(t, 30, result.Groups[0].AgentVersionCounts["18.1.0"])
require.Equal(t, 20, result.Groups[0].AgentVersionCounts["18.2.0"])
}
// TestStartGroupUpdate tests starting an update for a group.
func TestStartGroupUpdate(t *testing.T) {
t.Parallel()
ctx := t.Context()
env := newWebPack(t, 1)
proxy := env.proxies[0]
role, err := types.NewRole("testrole", types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindAutoUpdateConfig, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateVersion, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateAgentRollout, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
pack := proxy.authPack(t, "testuser", []types.Role{role})
config, err := autoupdate.NewAutoUpdateConfig(&autoupdatepb.AutoUpdateConfigSpec{
Agents: &autoupdatepb.AutoUpdateConfigSpecAgents{
Mode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
Schedules: &autoupdatepb.AgentAutoUpdateSchedules{
Regular: []*autoupdatepb.AgentAutoUpdateGroup{
{Name: "dev", Days: []string{"*"}, StartHour: 10, WaitHours: 0},
{Name: "staging", Days: []string{"*"}, StartHour: 10, WaitHours: 24},
},
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateConfig(ctx, config)
require.NoError(t, err)
version, err := autoupdate.NewAutoUpdateVersion(&autoupdatepb.AutoUpdateVersionSpec{
Agents: &autoupdatepb.AutoUpdateVersionSpecAgents{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
Mode: autoupdate.AgentsUpdateModeEnabled,
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateVersion(ctx, version)
require.NoError(t, err)
rollout := &autoupdatepb.AutoUpdateAgentRollout{
Kind: types.KindAutoUpdateAgentRollout,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: types.MetaNameAutoUpdateAgentRollout,
},
Spec: &autoupdatepb.AutoUpdateAgentRolloutSpec{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
AutoupdateMode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
},
Status: &autoupdatepb.AutoUpdateAgentRolloutStatus{
State: autoupdatepb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_ACTIVE,
Groups: []*autoupdatepb.AutoUpdateAgentRolloutStatusGroup{
{
Name: "dev",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
ConfigDays: []string{"*"},
ConfigStartHour: 10,
ConfigWaitHours: 0,
CanaryCount: 1,
},
{
Name: "staging",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
ConfigDays: []string{"*"},
ConfigStartHour: 10,
ConfigWaitHours: 24,
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentRollout(ctx, rollout)
require.NoError(t, err)
// Start the update
resp, err := pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "dev", "start"), nil)
require.NoError(t, err)
// Verify the group goes to canary state
var result ui.GroupActionResponse
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "canary", result.Group.State)
// Start the update with the force flag set
resp, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "staging", "start"), ui.StartGroupUpdateRequest{
Force: true,
})
require.NoError(t, err)
// Verify that the group goes straight to active
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "staging", result.Group.Name)
require.Equal(t, "active", result.Group.State)
// Trying to start a nonexistent group returns an error
_, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "nonexistent", "start"), nil)
require.Error(t, err)
}
// TestMarkGroupDone tests marking a managed update group as done.
func TestMarkGroupDone(t *testing.T) {
t.Parallel()
ctx := t.Context()
env := newWebPack(t, 1)
proxy := env.proxies[0]
role, err := types.NewRole("testrole", types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindAutoUpdateConfig, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateVersion, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateAgentRollout, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
pack := proxy.authPack(t, "testuser", []types.Role{role})
config, err := autoupdate.NewAutoUpdateConfig(&autoupdatepb.AutoUpdateConfigSpec{
Agents: &autoupdatepb.AutoUpdateConfigSpecAgents{
Mode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
Schedules: &autoupdatepb.AgentAutoUpdateSchedules{
Regular: []*autoupdatepb.AgentAutoUpdateGroup{
{Name: "dev", Days: []string{"*"}, StartHour: 10, WaitHours: 0},
},
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateConfig(ctx, config)
require.NoError(t, err)
version, err := autoupdate.NewAutoUpdateVersion(&autoupdatepb.AutoUpdateVersionSpec{
Agents: &autoupdatepb.AutoUpdateVersionSpecAgents{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
Mode: autoupdate.AgentsUpdateModeEnabled,
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateVersion(ctx, version)
require.NoError(t, err)
rollout := &autoupdatepb.AutoUpdateAgentRollout{
Kind: types.KindAutoUpdateAgentRollout,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: types.MetaNameAutoUpdateAgentRollout,
},
Spec: &autoupdatepb.AutoUpdateAgentRolloutSpec{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
AutoupdateMode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
},
Status: &autoupdatepb.AutoUpdateAgentRolloutStatus{
State: autoupdatepb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_ACTIVE,
Groups: []*autoupdatepb.AutoUpdateAgentRolloutStatusGroup{
{
Name: "dev",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
ConfigDays: []string{"*"},
ConfigStartHour: 10,
ConfigWaitHours: 0,
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentRollout(ctx, rollout)
require.NoError(t, err)
// Force-start the group
resp, err := pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "dev", "start"), ui.StartGroupUpdateRequest{
Force: true,
})
require.NoError(t, err)
// Verify that the state is now active
var result ui.GroupActionResponse
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "active", result.Group.State)
// Mark the group as done
resp, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "dev", "done"), nil)
require.NoError(t, err)
// Verify that the state is now done
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "dev", result.Group.Name)
require.Equal(t, "done", result.Group.State)
// Trying to mark a nonexistent group as done returns an error
_, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "nonexistent", "done"), nil)
require.Error(t, err)
}
// TestRollbackGroup tests rolling back a managed update group.
func TestRollbackGroup(t *testing.T) {
t.Parallel()
ctx := t.Context()
env := newWebPack(t, 1)
proxy := env.proxies[0]
role, err := types.NewRole("testrole", types.RoleSpecV6{
Allow: types.RoleConditions{
Rules: []types.Rule{
types.NewRule(types.KindAutoUpdateConfig, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateVersion, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
types.NewRule(types.KindAutoUpdateAgentRollout, []string{types.VerbRead, types.VerbCreate, types.VerbUpdate}),
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
pack := proxy.authPack(t, "testuser", []types.Role{role})
config, err := autoupdate.NewAutoUpdateConfig(&autoupdatepb.AutoUpdateConfigSpec{
Agents: &autoupdatepb.AutoUpdateConfigSpecAgents{
Mode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
Schedules: &autoupdatepb.AgentAutoUpdateSchedules{
Regular: []*autoupdatepb.AgentAutoUpdateGroup{
{Name: "prod", Days: []string{"*"}, StartHour: 10, WaitHours: 0},
},
},
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateConfig(ctx, config)
require.NoError(t, err)
version, err := autoupdate.NewAutoUpdateVersion(&autoupdatepb.AutoUpdateVersionSpec{
Agents: &autoupdatepb.AutoUpdateVersionSpecAgents{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
Mode: autoupdate.AgentsUpdateModeEnabled,
},
})
require.NoError(t, err)
_, err = env.server.Auth().UpsertAutoUpdateVersion(ctx, version)
require.NoError(t, err)
rollout := &autoupdatepb.AutoUpdateAgentRollout{
Kind: types.KindAutoUpdateAgentRollout,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: types.MetaNameAutoUpdateAgentRollout,
},
Spec: &autoupdatepb.AutoUpdateAgentRolloutSpec{
StartVersion: "18.0.0",
TargetVersion: "18.1.0",
Schedule: autoupdate.AgentsScheduleRegular,
AutoupdateMode: autoupdate.AgentsUpdateModeEnabled,
Strategy: autoupdate.AgentsStrategyHaltOnError,
},
Status: &autoupdatepb.AutoUpdateAgentRolloutStatus{
State: autoupdatepb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_ACTIVE,
Groups: []*autoupdatepb.AutoUpdateAgentRolloutStatusGroup{
{
Name: "prod",
State: autoupdatepb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
ConfigDays: []string{"*"},
ConfigStartHour: 10,
ConfigWaitHours: 0,
},
},
},
}
_, err = env.server.Auth().UpsertAutoUpdateAgentRollout(ctx, rollout)
require.NoError(t, err)
// Force-start the group
resp, err := pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "prod", "start"), ui.StartGroupUpdateRequest{
Force: true,
})
require.NoError(t, err)
// Verify that the state is now active
var result ui.GroupActionResponse
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "active", result.Group.State)
// Rollback the group
resp, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "prod", "rollback"), nil)
require.NoError(t, err)
// Verify that the state is now rolledback
require.NoError(t, json.Unmarshal(resp.Bytes(), &result))
require.NotNil(t, result.Group)
require.Equal(t, "prod", result.Group.Name)
require.Equal(t, "rolledback", result.Group.State)
// Trying to rollback a nonexistent group returns an error
_, err = pack.clt.PostJSON(ctx, pack.clt.Endpoint("webapi", "managedupdates", "groups", "nonexistent", "rollback"), nil)
require.Error(t, err)
}
+118
View File
@@ -0,0 +1,118 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ui
import "time"
// ManagedUpdatesDetails is the response to the request for managed updates details.
type ManagedUpdatesDetails struct {
// Rollout contains information about the agent rollout configuration.
Rollout *RolloutInfo `json:"rollout,omitempty"`
// Tools contains information about the tools autoupdates configuration.
Tools *ToolsAutoUpdateInfo `json:"tools,omitempty"`
// ClusterMaintenance contains information about the cluster maintenance configuration. This is for Cloud only.
ClusterMaintenance *ClusterMaintenanceInfo `json:"clusterMaintenance,omitempty"`
// Groups contains information about rollout groups.
Groups []RolloutGroupInfo `json:"groups,omitempty"`
// OrphanedAgentVersionCounts is a map representing how many orphaned agents are in each version.
// An orphaned agent is an agent belonging to a group that is not defined in the rollout.
OrphanedAgentVersionCounts map[string]int `json:"orphanedAgentVersionCounts,omitempty"`
}
// RolloutInfo contains information about the agent rollout configuration.
type RolloutInfo struct {
// StartVersion is the starting version for the rollout.
StartVersion string `json:"startVersion,omitempty"`
// TargetVersion is the version to update to in the rollout.
TargetVersion string `json:"targetVersion,omitempty"`
// Strategy is the rollout strategy, either halt-on-error or time-based.
Strategy string `json:"strategy,omitempty"`
// Schedule is the rollout schedule.
Schedule string `json:"schedule,omitempty"`
// State is the current state of the rollout.
State string `json:"state,omitempty"`
// Mode is the autoupdate mode option.
Mode string `json:"mode,omitempty"`
// StartTime is when the rollout was created/started.
StartTime *time.Time `json:"startTime,omitempty"`
}
// ToolsAutoUpdateInfo contains information about the tools autoupdates configuration.
type ToolsAutoUpdateInfo struct {
// Mode is the tools autoupdate mode, either enabled or disabled.
Mode string `json:"mode,omitempty"`
// TargetVersion is the target version.
TargetVersion string `json:"targetVersion,omitempty"`
}
// ClusterMaintenanceInfo contains information about the cluster maintenance configuration. This is for Cloud only.
type ClusterMaintenanceInfo struct {
// ControlPlaneVersion is the current version of the control plane.
ControlPlaneVersion string `json:"controlPlaneVersion,omitempty"`
// MaintenanceWeekdays is the list of days when maintenance can occur.
MaintenanceWeekdays []string `json:"maintenanceWeekdays,omitempty"`
// MaintenanceStartHour is the maintenance window start hour (UTC time).
MaintenanceStartHour int `json:"maintenanceStartHour"`
}
// RolloutGroupInfo contains information about a rollout group.
type RolloutGroupInfo struct {
// Name is the group name.
Name string `json:"name"`
// Position is the position of this group in the rollout order, this is only applicable for halt-on-error strategy.
Position int `json:"position,omitempty"`
// State is this group's current state in the rollout.
State string `json:"state"`
// InitialCount is the number of agents in this group when the rollout started.
InitialCount uint64 `json:"initialCount"`
// PresentCount is the number of agents in this group currently connected.
PresentCount uint64 `json:"presentCount"`
// UpToDateCount is the number of agents in this group running the target version.
UpToDateCount uint64 `json:"upToDateCount"`
// StateReason is the optional state reason text.
StateReason string `json:"stateReason,omitempty"`
// StartTime is the time when the group rollout started.
StartTime *time.Time `json:"startTime,omitempty"`
// LastUpdateTime is the time of the last state update for this group.
LastUpdateTime *time.Time `json:"lastUpdateTime,omitempty"`
// AgentVersionCounts is a map representing how many agents in this group are on each version.
AgentVersionCounts map[string]int `json:"agentVersionCounts,omitempty"`
// CanaryCount is the number of canary agents that need to be updated before the whole group is updated.
CanaryCount uint64 `json:"canaryCount,omitempty"`
// CanarySuccessCount is the number of canary agents that have been successfully updated.
CanarySuccessCount uint64 `json:"canarySuccessCount,omitempty"`
// IsCatchAll indicates whether this group is the catch-all group for orphaned agents.
IsCatchAll bool `json:"isCatchAll,omitempty"`
}
// StartGroupUpdateRequest is the request body for starting a group update.
type StartGroupUpdateRequest struct {
// Force, if true, skips canary phase and goes directly to active state.
Force bool `json:"force,omitempty"`
}
// GroupActionResponse is the response for a group action like start update, mark done, and rollback.
type GroupActionResponse struct {
// Group contains the updated group information.
Group *RolloutGroupInfo `json:"group,omitempty"`
}
+5 -34
View File
@@ -38,6 +38,7 @@ import (
autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
"github.com/gravitational/teleport/api/types/autoupdate"
"github.com/gravitational/teleport/lib/asciitable"
aur "github.com/gravitational/teleport/lib/autoupdate/report"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
@@ -243,7 +244,7 @@ func (c *AutoUpdateCommand) agentsStatusCommand(ctx context.Context, client auto
sb.WriteString("Target version: " + target + "\n")
}
if state := rollout.GetStatus().GetState(); state != autoupdatev1pb.AutoUpdateAgentRolloutState_AUTO_UPDATE_AGENT_ROLLOUT_STATE_UNSPECIFIED {
sb.WriteString("Rollout state: " + userFriendlyState(state) + "\n")
sb.WriteString("Rollout state: " + aur.UserFriendlyState(state) + "\n")
}
if schedule := rollout.GetSpec().GetSchedule(); schedule == autoupdate.AgentsScheduleImmediate {
sb.WriteString("Schedule is immediate. Every group immediately updates to the target version.\n")
@@ -279,7 +280,7 @@ func (c *AutoUpdateCommand) agentsReportCommand(ctx context.Context, client auto
return trace.BadParameter("no reports returned, but the server did not return a NotFoundError, this ia a bug")
}
validReports := filterValidReports(reports, now)
validReports := aur.ValidReports(reports, now)
if len(validReports) == 0 {
fmt.Fprintf(c.stdout, "Read %d reports, but they are expired. If you just (re)deployed the Auth service, you might want to retry after 60 seconds.\n", len(reports))
@@ -329,16 +330,6 @@ func (c *AutoUpdateCommand) agentsReportCommand(ctx context.Context, client auto
return trace.Wrap(err)
}
func filterValidReports(reports []*autoupdatev1pb.AutoUpdateAgentReport, now time.Time) []*autoupdatev1pb.AutoUpdateAgentReport {
var validReports []*autoupdatev1pb.AutoUpdateAgentReport
for _, report := range reports {
if now.Sub(report.GetSpec().GetTimestamp().AsTime()) <= time.Minute {
validReports = append(validReports, report)
}
}
return validReports
}
func (c *AutoUpdateCommand) omittedSummary(reports []*autoupdatev1pb.AutoUpdateAgentReport) string {
aggregated := make(map[string]int)
var totalOmitted int
@@ -403,7 +394,7 @@ func rolloutGroupTable(rollout *autoupdatev1pb.AutoUpdateAgentRollout, writer io
if i == len(groups)-1 {
groupName = groupName + " (catch-all)"
}
state := userFriendlyState(group.GetState())
state := aur.UserFriendlyState(group.GetState())
// If this is the canary state, we annotate the group state with the canary progress
if group.GetState() == autoupdatev1pb.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_CANARY {
@@ -434,7 +425,7 @@ func rolloutGroupTable(rollout *autoupdatev1pb.AutoUpdateAgentRollout, writer io
groupName := group.GetName()
table.AddRow([]string{
groupName,
userFriendlyState(group.GetState()),
aur.UserFriendlyState(group.GetState()),
formatTimeIfNotEmpty(group.GetStartTime().AsTime(), time.DateTime),
group.GetLastUpdateReason()})
}
@@ -528,26 +519,6 @@ func formatTimeIfNotEmpty(t time.Time, format string) string {
return t.Format(format)
}
func userFriendlyState[T autoupdatev1pb.AutoUpdateAgentGroupState | autoupdatev1pb.AutoUpdateAgentRolloutState](state T) string {
switch state {
case 0:
return "Unknown"
case 1:
return "Unstarted"
case 2:
return "Active"
case 3:
return "Done"
case 4:
return "Rolledback"
case 5:
return "Canary"
default:
// If we don't know anything about this state, we display its integer
return fmt.Sprintf("Unknown state (%d)", state)
}
}
// ToolsStatus makes request to auth service to fetch client tools auto update version and mode.
func (c *AutoUpdateCommand) ToolsStatus(ctx context.Context, client autoupdateClient) error {
var response getResponse