mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
implement webapi for managed updates (#62909)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user