Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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