Skip to content

Commit

Permalink
Add 'tctl autoupdate agents status'
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoShaka committed Jan 17, 2025
1 parent cb7a4d5 commit 31a9ffd
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 37 deletions.
1 change: 1 addition & 0 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindWorkloadIdentity, RW()),
types.NewRule(types.KindAutoUpdateVersion, RW()),
types.NewRule(types.KindAutoUpdateConfig, RW()),
types.NewRule(types.KindAutoUpdateAgentRollout, RO()),
},
},
},
Expand Down
169 changes: 132 additions & 37 deletions tool/tctl/common/autoupdate_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"fmt"
"io"
"os"
"strings"
"time"

"github.com/alecthomas/kingpin/v2"
"github.com/coreos/go-semver/semver"
Expand All @@ -32,7 +34,7 @@ import (
"github.com/gravitational/teleport/api/client/webclient"
autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
"github.com/gravitational/teleport/api/types/autoupdate"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
Expand All @@ -48,10 +50,11 @@ type AutoUpdateCommand struct {
app *kingpin.Application
ccf *tctlcfg.GlobalCLIFlags

targetCmd *kingpin.CmdClause
enableCmd *kingpin.CmdClause
disableCmd *kingpin.CmdClause
statusCmd *kingpin.CmdClause
toolsTargetCmd *kingpin.CmdClause
toolsEnableCmd *kingpin.CmdClause
toolsDisableCmd *kingpin.CmdClause
toolsStatusCmd *kingpin.CmdClause
agentsStatusCmd *kingpin.CmdClause

toolsTargetVersion string
proxy string
Expand All @@ -71,16 +74,19 @@ func (c *AutoUpdateCommand) Initialize(app *kingpin.Application, ccf *tctlcfg.Gl

clientToolsCmd := autoUpdateCmd.Command("client-tools", "Manage client tools auto update configuration.")

c.statusCmd = clientToolsCmd.Command("status", "Prints if the client tools updates are enabled/disabled, and the target version in specified format.")
c.statusCmd.Flag("proxy", "Address of the Teleport proxy. When defined this address will be used to retrieve client tools auto update configuration.").StringVar(&c.proxy)
c.statusCmd.Flag("format", "Output format: 'yaml' or 'json'").Default(teleport.YAML).StringVar(&c.format)
c.toolsStatusCmd = clientToolsCmd.Command("status", "Prints if the client tools updates are enabled/disabled, and the target version in specified format.")
c.toolsStatusCmd.Flag("proxy", "Address of the Teleport proxy. When defined this address will be used to retrieve client tools auto update configuration.").StringVar(&c.proxy)
c.toolsStatusCmd.Flag("format", "Output format: 'yaml' or 'json'").Default(teleport.YAML).StringVar(&c.format)

c.enableCmd = clientToolsCmd.Command("enable", "Enables client tools auto updates. Clients will be told to update to the target version.")
c.disableCmd = clientToolsCmd.Command("disable", "Disables client tools auto updates. Clients will not be told to update to the target version.")
c.toolsEnableCmd = clientToolsCmd.Command("enable", "Enables client tools auto updates. Clients will be told to update to the target version.")
c.toolsDisableCmd = clientToolsCmd.Command("disable", "Disables client tools auto updates. Clients will not be told to update to the target version.")

c.targetCmd = clientToolsCmd.Command("target", "Sets the client tools target version. This command is not supported on Teleport Cloud.")
c.targetCmd.Arg("version", "Client tools target version. Clients will be told to update to this version.").StringVar(&c.toolsTargetVersion)
c.targetCmd.Flag("clear", "removes the target version, Teleport will default to its current proxy version.").BoolVar(&c.clear)
c.toolsTargetCmd = clientToolsCmd.Command("target", "Sets the client tools target version. This command is not supported on Teleport Cloud.")
c.toolsTargetCmd.Arg("version", "Client tools target version. Clients will be told to update to this version.").StringVar(&c.toolsTargetVersion)
c.toolsTargetCmd.Flag("clear", "removes the target version, Teleport will default to its current proxy version.").BoolVar(&c.clear)

agentsCmd := autoUpdateCmd.Command("agents", "Manage agents auto update configuration.")
c.agentsStatusCmd = agentsCmd.Command("status", "Prints agents auto update status.")

if c.stdout == nil {
c.stdout = os.Stdout
Expand All @@ -89,19 +95,21 @@ func (c *AutoUpdateCommand) Initialize(app *kingpin.Application, ccf *tctlcfg.Gl

// TryRun takes the CLI command as an argument and executes it.
func (c *AutoUpdateCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
var commandFunc func(ctx context.Context, client *authclient.Client) error
var commandFunc func(ctx context.Context, client autoupdateClient) error
switch {
case cmd == c.targetCmd.FullCommand():
case cmd == c.toolsTargetCmd.FullCommand():
commandFunc = c.TargetVersion
case cmd == c.enableCmd.FullCommand():
case cmd == c.toolsEnableCmd.FullCommand():
commandFunc = c.SetModeCommand(true)
case cmd == c.disableCmd.FullCommand():
case cmd == c.toolsDisableCmd.FullCommand():
commandFunc = c.SetModeCommand(false)
case c.proxy == "" && cmd == c.statusCmd.FullCommand():
commandFunc = c.Status
case c.proxy != "" && cmd == c.statusCmd.FullCommand():
err = c.StatusByProxy(ctx)
case c.proxy == "" && cmd == c.toolsStatusCmd.FullCommand():
commandFunc = c.ToolsStatus
case c.proxy != "" && cmd == c.toolsStatusCmd.FullCommand():
err = c.ToolsStatusByProxy(ctx)
return true, trace.Wrap(err)
case cmd == c.agentsStatusCmd.FullCommand():
commandFunc = c.agentsStatusCommand
default:
return false, nil
}
Expand All @@ -117,17 +125,17 @@ func (c *AutoUpdateCommand) TryRun(ctx context.Context, cmd string, clientFunc c
}

// TargetVersion creates or updates AutoUpdateVersion resource with client tools target version.
func (c *AutoUpdateCommand) TargetVersion(ctx context.Context, client *authclient.Client) error {
func (c *AutoUpdateCommand) TargetVersion(ctx context.Context, client autoupdateClient) error {
var err error
switch {
case c.clear:
err = c.clearTargetVersion(ctx, client)
err = c.clearToolsTargetVersion(ctx, client)
case c.toolsTargetVersion != "":
// For parallel requests where we attempt to create a resource simultaneously, retries should be implemented.
// The same approach applies to updates if the resource has been deleted during the process.
// Second create request must return `AlreadyExists` error, update for deleted resource `NotFound` error.
for i := 0; i < maxRetries; i++ {
err = c.setTargetVersion(ctx, client)
err = c.setToolsTargetVersion(ctx, client)
if err == nil {
break
}
Expand All @@ -140,13 +148,13 @@ func (c *AutoUpdateCommand) TargetVersion(ctx context.Context, client *authclien
}

// SetModeCommand returns a command to enable or disable client tools auto-updates in the cluster.
func (c *AutoUpdateCommand) SetModeCommand(enabled bool) func(ctx context.Context, client *authclient.Client) error {
return func(ctx context.Context, client *authclient.Client) error {
func (c *AutoUpdateCommand) SetModeCommand(enabled bool) func(ctx context.Context, client autoupdateClient) error {
return func(ctx context.Context, client autoupdateClient) error {
// For parallel requests where we attempt to create a resource simultaneously, retries should be implemented.
// The same approach applies to updates if the resource has been deleted during the process.
// Second create request must return `AlreadyExists` error, update for deleted resource `NotFound` error.
for i := 0; i < maxRetries; i++ {
err := c.setMode(ctx, client, enabled)
err := c.setToolsMode(ctx, client, enabled)
if err == nil {
break
}
Expand All @@ -164,8 +172,95 @@ type getResponse struct {
TargetVersion string `json:"target_version"`
}

// Status makes request to auth service to fetch client tools auto update version and mode.
func (c *AutoUpdateCommand) Status(ctx context.Context, client *authclient.Client) error {
// autoupdateClient is a subset of the Teleport client, with functions used to interact with automatic update resources.
// Not every AU function is part of the interface, we'll add them as we need.
type autoupdateClient interface {
GetAutoUpdateAgentRollout(context.Context) (*autoupdatev1pb.AutoUpdateAgentRollout, error)
GetAutoUpdateVersion(context.Context) (*autoupdatev1pb.AutoUpdateVersion, error)
GetAutoUpdateConfig(context.Context) (*autoupdatev1pb.AutoUpdateConfig, error)
CreateAutoUpdateConfig(context.Context, *autoupdatev1pb.AutoUpdateConfig) (*autoupdatev1pb.AutoUpdateConfig, error)
CreateAutoUpdateVersion(context.Context, *autoupdatev1pb.AutoUpdateVersion) (*autoupdatev1pb.AutoUpdateVersion, error)
UpdateAutoUpdateConfig(context.Context, *autoupdatev1pb.AutoUpdateConfig) (*autoupdatev1pb.AutoUpdateConfig, error)
UpdateAutoUpdateVersion(context.Context, *autoupdatev1pb.AutoUpdateVersion) (*autoupdatev1pb.AutoUpdateVersion, error)
}

func (c *AutoUpdateCommand) agentsStatusCommand(ctx context.Context, client autoupdateClient) error {
rollout, err := client.GetAutoUpdateAgentRollout(ctx)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}

sb := strings.Builder{}
if rollout.GetSpec() == nil {
sb.WriteString("No active agent rollout (autoupdate_agent_rollout).\n")
}
if mode := rollout.GetSpec().GetAutoupdateMode(); mode != "" {
sb.WriteString("Agent autoupdate mode: " + mode + "\n")
}
if st := formatTimeIfNotEmpty(rollout.GetStatus().GetStartTime().AsTime(), time.DateTime); st != "" {
sb.WriteString("Rollout creation date: " + st + "\n")
}
if start := rollout.GetSpec().GetStartVersion(); start != "" {
sb.WriteString("Start version: " + start + "\n")
}
if target := rollout.GetSpec().GetTargetVersion(); target != "" {
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")
}
if schedule := rollout.GetSpec().GetSchedule(); schedule == autoupdate.AgentsScheduleImmediate {
sb.WriteString("Schedule is immediate. Every group immediately updates to the target version.\n")
}
if strategy := rollout.GetSpec().GetStrategy(); strategy != "" {
sb.WriteString("Strategy: " + strategy + "\n")
}

if groups := rollout.GetStatus().GetGroups(); len(groups) > 0 {
sb.WriteRune('\n')
headers := []string{"Group Name", "State", "Start Time", "State Reason"}
table := asciitable.MakeTable(headers)
for _, group := range groups {
table.AddRow([]string{
group.GetName(),
userFriendlyState(group.GetState()),
formatTimeIfNotEmpty(group.GetStartTime().AsTime(), time.DateTime),
group.GetLastUpdateReason()})
}
sb.Write(table.AsBuffer().Bytes())
}

fmt.Fprint(c.stdout, sb.String())
return nil
}

func formatTimeIfNotEmpty(t time.Time, format string) string {
if t.IsZero() || t.Unix() == 0 {
return ""
}
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"
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
config, err := client.GetAutoUpdateConfig(ctx)
if err != nil && !trace.IsNotFound(err) {
Expand All @@ -183,12 +278,12 @@ func (c *AutoUpdateCommand) Status(ctx context.Context, client *authclient.Clien
response.TargetVersion = version.Spec.Tools.TargetVersion
}

return c.printResponse(response)
return c.printToolsResponse(response)
}

// StatusByProxy makes request to `webapi/find` endpoint to fetch tools auto update version and mode
// ToolsStatusByProxy makes request to `webapi/find` endpoint to fetch tools auto update version and mode
// without authentication.
func (c *AutoUpdateCommand) StatusByProxy(ctx context.Context) error {
func (c *AutoUpdateCommand) ToolsStatusByProxy(ctx context.Context) error {
find, err := webclient.Find(&webclient.Config{
Context: ctx,
ProxyAddr: c.proxy,
Expand All @@ -201,13 +296,13 @@ func (c *AutoUpdateCommand) StatusByProxy(ctx context.Context) error {
if find.AutoUpdate.ToolsAutoUpdate {
mode = autoupdate.ToolsUpdateModeEnabled
}
return c.printResponse(getResponse{
return c.printToolsResponse(getResponse{
TargetVersion: find.AutoUpdate.ToolsVersion,
Mode: mode,
})
}

func (c *AutoUpdateCommand) setMode(ctx context.Context, client *authclient.Client, enabled bool) error {
func (c *AutoUpdateCommand) setToolsMode(ctx context.Context, client autoupdateClient, enabled bool) error {
setMode := client.UpdateAutoUpdateConfig
config, err := client.GetAutoUpdateConfig(ctx)
if trace.IsNotFound(err) {
Expand Down Expand Up @@ -235,7 +330,7 @@ func (c *AutoUpdateCommand) setMode(ctx context.Context, client *authclient.Clie
return nil
}

func (c *AutoUpdateCommand) setTargetVersion(ctx context.Context, client *authclient.Client) error {
func (c *AutoUpdateCommand) setToolsTargetVersion(ctx context.Context, client autoupdateClient) error {
if _, err := semver.NewVersion(c.toolsTargetVersion); err != nil {
return trace.WrapWithMessage(err, "not semantic version")
}
Expand All @@ -262,7 +357,7 @@ func (c *AutoUpdateCommand) setTargetVersion(ctx context.Context, client *authcl
return nil
}

func (c *AutoUpdateCommand) clearTargetVersion(ctx context.Context, client *authclient.Client) error {
func (c *AutoUpdateCommand) clearToolsTargetVersion(ctx context.Context, client autoupdateClient) error {
version, err := client.GetAutoUpdateVersion(ctx)
if trace.IsNotFound(err) {
return nil
Expand All @@ -279,7 +374,7 @@ func (c *AutoUpdateCommand) clearTargetVersion(ctx context.Context, client *auth
return nil
}

func (c *AutoUpdateCommand) printResponse(response getResponse) error {
func (c *AutoUpdateCommand) printToolsResponse(response getResponse) error {
switch c.format {
case teleport.JSON:
if err := utils.WriteJSON(c.stdout, response); err != nil {
Expand Down
Loading

0 comments on commit 31a9ffd

Please sign in to comment.