From 1beca9d3a1505eba68b6138532a117990847e2d6 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Tue, 17 Dec 2024 12:54:12 -0800 Subject: [PATCH] Reorganize tctl commands to not require an auth client by default (#48894) * Reorganize tctl commands to have commands not required auth client * Replace auth client with lazy loading approach * Fix linter warning * Replace camel case in import alias Replace logrus to use slog * Rename close function * Refactor plugin commands to use interface of auth client and plugin client Code review changes * Refactor workload identity commands * Add access to global config for the commands * Add test checking all tctl commands match process * Fix golangci-lint warnings --- integration/tctl_terraform_env_test.go | 4 +- tool/tctl/common/access_request_command.go | 31 +- tool/tctl/common/accessmonitoring/command.go | 16 +- tool/tctl/common/acl_command.go | 24 +- tool/tctl/common/admin_action_test.go | 7 +- tool/tctl/common/alert_command.go | 20 +- tool/tctl/common/app_command.go | 16 +- tool/tctl/common/auth_command.go | 43 ++- tool/tctl/common/bots_command.go | 29 +- tool/tctl/common/client/auth.go | 118 ++++++ tool/tctl/common/cmds.go | 1 + tool/tctl/common/config/global.go | 196 ++++++++++ tool/tctl/common/config/profile.go | 121 ++++++ tool/tctl/common/db_command.go | 16 +- tool/tctl/common/desktop_command.go | 17 +- tool/tctl/common/devices.go | 15 +- tool/tctl/common/edit_command.go | 16 +- .../common/externalauditstorage_command.go | 17 +- tool/tctl/common/fido2.go | 9 +- tool/tctl/common/helpers_test.go | 12 +- tool/tctl/common/idp_command.go | 20 +- tool/tctl/common/inventory_command.go | 20 +- tool/tctl/common/kube_command.go | 15 +- tool/tctl/common/loadtest_command.go | 19 +- tool/tctl/common/lock_command.go | 16 +- tool/tctl/common/loginrule/command.go | 19 +- tool/tctl/common/node_command.go | 18 +- tool/tctl/common/notification_command.go | 28 +- tool/tctl/common/plugin/plugins_command.go | 52 +-- .../common/plugin/plugins_command_test.go | 20 + tool/tctl/common/proxy_command.go | 15 +- tool/tctl/common/recordings_command.go | 16 +- tool/tctl/common/resource_command.go | 22 +- tool/tctl/common/saml_command.go | 13 +- tool/tctl/common/status_command.go | 16 +- tool/tctl/common/tctl.go | 355 +----------------- tool/tctl/common/tctl_test.go | 72 +++- tool/tctl/common/terraform_command.go | 14 +- tool/tctl/common/token_command.go | 20 +- tool/tctl/common/top_command.go | 7 +- tool/tctl/common/touchid.go | 9 +- tool/tctl/common/user_command.go | 24 +- tool/tctl/common/version_command.go | 56 +++ tool/tctl/common/webauthnwin.go | 9 +- tool/tctl/common/workload_identity_command.go | 18 +- tool/tctl/sso/configure/command.go | 14 +- tool/tctl/sso/tester/command.go | 14 +- tool/tsh/common/tctl_test.go | 13 +- 48 files changed, 1101 insertions(+), 561 deletions(-) create mode 100644 tool/tctl/common/client/auth.go create mode 100644 tool/tctl/common/config/global.go create mode 100644 tool/tctl/common/config/profile.go create mode 100644 tool/tctl/common/version_command.go diff --git a/integration/tctl_terraform_env_test.go b/integration/tctl_terraform_env_test.go index 9ebea1c95bc35..942d359cc73b1 100644 --- a/integration/tctl_terraform_env_test.go +++ b/integration/tctl_terraform_env_test.go @@ -102,7 +102,7 @@ func TestTCTLTerraformCommand_ProxyJoin(t *testing.T) { tctlCommand := common.TerraformCommand{} app := kingpin.New("test", "test") - tctlCommand.Initialize(app, tctlCfg) + tctlCommand.Initialize(app, nil, tctlCfg) _, err = app.Parse([]string{"terraform", "env"}) require.NoError(t, err) // Create io buffer writer @@ -175,7 +175,7 @@ func TestTCTLTerraformCommand_AuthJoin(t *testing.T) { tctlCommand := common.TerraformCommand{} app := kingpin.New("test", "test") - tctlCommand.Initialize(app, tctlCfg) + tctlCommand.Initialize(app, nil, tctlCfg) _, err = app.Parse([]string{"terraform", "env"}) require.NoError(t, err) // Create io buffer writer diff --git a/tool/tctl/common/access_request_command.go b/tool/tctl/common/access_request_command.go index 96f04f1e23b1f..ef62637dda8ca 100644 --- a/tool/tctl/common/access_request_command.go +++ b/tool/tctl/common/access_request_command.go @@ -39,6 +39,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // AccessRequestCommand implements `tctl users` set of commands @@ -76,7 +78,7 @@ type AccessRequestCommand struct { } // Initialize allows AccessRequestCommand to plug itself into the CLI parser -func (c *AccessRequestCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *AccessRequestCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config requests := app.Command("requests", "Manage access requests.").Alias("request") @@ -125,27 +127,36 @@ func (c *AccessRequestCommand) Initialize(app *kingpin.Application, config *serv } // TryRun takes the CLI command as an argument (like "access-request list") and executes it. -func (c *AccessRequestCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *AccessRequestCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.requestList.FullCommand(): - err = c.List(ctx, client) + commandFunc = c.List case c.requestGet.FullCommand(): - err = c.Get(ctx, client) + commandFunc = c.Get case c.requestApprove.FullCommand(): - err = c.Approve(ctx, client) + commandFunc = c.Approve case c.requestDeny.FullCommand(): - err = c.Deny(ctx, client) + commandFunc = c.Deny case c.requestCreate.FullCommand(): - err = c.Create(ctx, client) + commandFunc = c.Create case c.requestDelete.FullCommand(): - err = c.Delete(ctx, client) + commandFunc = c.Delete case c.requestCaps.FullCommand(): - err = c.Caps(ctx, client) + commandFunc = c.Caps case c.requestReview.FullCommand(): - err = c.Review(ctx, client) + commandFunc = c.Review default: return false, nil } + + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/accessmonitoring/command.go b/tool/tctl/common/accessmonitoring/command.go index ca9b627b2b29b..d4483c6ed91b2 100644 --- a/tool/tctl/common/accessmonitoring/command.go +++ b/tool/tctl/common/accessmonitoring/command.go @@ -35,6 +35,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // Command implements `tctl audit` group of commands. @@ -44,7 +46,7 @@ type Command struct { } // Initialize allows to implement Command interface. -func (c *Command) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (c *Command) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { c.innerCmdMap = map[string]runFunc{} auditCmd := app.Command("audit", "Audit command.") @@ -114,13 +116,19 @@ func (c *Command) initAuditReportsCommands(auditCmd *kingpin.CmdClause, cfg *ser type runFunc func(context.Context, *authclient.Client) error -func (c *Command) TryRun(ctx context.Context, selectedCommand string, authClient *authclient.Client) (match bool, err error) { - handler, ok := c.innerCmdMap[selectedCommand] +func (c *Command) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + handler, ok := c.innerCmdMap[cmd] if !ok { return false, nil } - switch err := trail.FromGRPC(handler(ctx, authClient)); { + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) + + switch err := trail.FromGRPC(handler(ctx, client)); { case trace.IsNotImplemented(err): return true, trace.AccessDenied("Access Monitoring requires a Teleport Enterprise Auth Server.") default: diff --git a/tool/tctl/common/acl_command.go b/tool/tctl/common/acl_command.go index 93fd4c122a8f7..189ba01b232ed 100644 --- a/tool/tctl/common/acl_command.go +++ b/tool/tctl/common/acl_command.go @@ -35,6 +35,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // ACLCommand implements the `tctl acl` family of commands. @@ -57,7 +59,7 @@ type ACLCommand struct { } // Initialize allows ACLCommand to plug itself into the CLI parser -func (c *ACLCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) { +func (c *ACLCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { acl := app.Command("acl", "Manage access lists.").Alias("access-lists") c.ls = acl.Command("ls", "List cluster access lists.") @@ -85,21 +87,29 @@ func (c *ACLCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) } // TryRun takes the CLI command as an argument (like "acl ls") and executes it. -func (c *ACLCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *ACLCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.ls.FullCommand(): - err = c.List(ctx, client) + commandFunc = c.List case c.get.FullCommand(): - err = c.Get(ctx, client) + commandFunc = c.Get case c.usersAdd.FullCommand(): - err = c.UsersAdd(ctx, client) + commandFunc = c.UsersAdd case c.usersRemove.FullCommand(): - err = c.UsersRemove(ctx, client) + commandFunc = c.UsersRemove case c.usersList.FullCommand(): - err = c.UsersList(ctx, client) + commandFunc = c.UsersList default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/admin_action_test.go b/tool/tctl/common/admin_action_test.go index b910cf239b5ab..1bac280312f04 100644 --- a/tool/tctl/common/admin_action_test.go +++ b/tool/tctl/common/admin_action_test.go @@ -58,6 +58,7 @@ import ( "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/hostid" tctl "github.com/gravitational/teleport/tool/tctl/common" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" testserver "github.com/gravitational/teleport/tool/teleport/testenv" tsh "github.com/gravitational/teleport/tool/tsh/common" ) @@ -1156,13 +1157,15 @@ func runTestCase(t *testing.T, ctx context.Context, client *authclient.Client, t app := utils.InitCLIParser("tctl", tctl.GlobalHelpString) cfg := servicecfg.MakeDefaultConfig() - tc.cliCommand.Initialize(app, cfg) + tc.cliCommand.Initialize(app, &tctlcfg.GlobalCLIFlags{}, cfg) args := strings.Split(tc.command, " ") commandName, err := app.Parse(args) require.NoError(t, err) - match, err := tc.cliCommand.TryRun(ctx, commandName, client) + match, err := tc.cliCommand.TryRun(ctx, commandName, func(context.Context) (*authclient.Client, func(context.Context), error) { + return client, func(context.Context) {}, nil + }) require.True(t, match) return err } diff --git a/tool/tctl/common/alert_command.go b/tool/tctl/common/alert_command.go index 30638816b49a5..e7940457fb780 100644 --- a/tool/tctl/common/alert_command.go +++ b/tool/tctl/common/alert_command.go @@ -37,6 +37,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // AlertCommand implements the `tctl alerts` family of commands. @@ -62,7 +64,7 @@ type AlertCommand struct { } // Initialize allows AlertCommand to plug itself into the CLI parser -func (c *AlertCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *AlertCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config alert := app.Command("alerts", "Manage cluster alerts.").Alias("alert") @@ -93,17 +95,25 @@ func (c *AlertCommand) Initialize(app *kingpin.Application, config *servicecfg.C } // TryRun takes the CLI command as an argument (like "alerts ls") and executes it. -func (c *AlertCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *AlertCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.alertList.FullCommand(): - err = c.List(ctx, client) + commandFunc = c.List case c.alertCreate.FullCommand(): - err = c.Create(ctx, client) + commandFunc = c.Create case c.alertAck.FullCommand(): - err = c.Ack(ctx, client) + commandFunc = c.Ack default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/app_command.go b/tool/tctl/common/app_command.go index bb9f232d81c5d..a271c93d901bc 100644 --- a/tool/tctl/common/app_command.go +++ b/tool/tctl/common/app_command.go @@ -34,6 +34,8 @@ import ( libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // AppsCommand implements "tctl apps" group of commands. @@ -55,7 +57,7 @@ type AppsCommand struct { } // Initialize allows AppsCommand to plug itself into the CLI parser -func (c *AppsCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *AppsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config apps := app.Command("apps", "Operate on applications registered with the cluster.") @@ -68,13 +70,21 @@ func (c *AppsCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun attempts to run subcommands like "apps ls". -func (c *AppsCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *AppsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.appsList.FullCommand(): - err = c.ListApps(ctx, client) + commandFunc = c.ListApps default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index e32f2a56dc2fb..f52dcfd0d1136 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -51,8 +51,17 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) +// authCommandClient is aggregated client interface for auth command. +type authCommandClient interface { + certificateSigner + crlGenerator + authclient.ClientI +} + // AuthCommand implements `tctl auth` group of commands type AuthCommand struct { config *servicecfg.Config @@ -104,7 +113,7 @@ type AuthCommand struct { } // Initialize allows TokenCommand to plug itself into the CLI parser -func (a *AuthCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (a *AuthCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { a.config = config // operations with authorities auth := app.Command("auth", "Operations with user and host certificate authorities (CAs).").Hidden() @@ -171,23 +180,31 @@ func (a *AuthCommand) Initialize(app *kingpin.Application, config *servicecfg.Co // TryRun takes the CLI command as an argument (like "auth gen") and executes it // or returns match=false if 'cmd' does not belong to it -func (a *AuthCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (a *AuthCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client authCommandClient) error switch cmd { case a.authGenerate.FullCommand(): - err = a.GenerateKeys(ctx) + return true, trace.Wrap(a.GenerateKeys(ctx)) case a.authExport.FullCommand(): - err = a.ExportAuthorities(ctx, client) + commandFunc = a.ExportAuthorities case a.authSign.FullCommand(): - err = a.GenerateAndSignKeys(ctx, client) + commandFunc = a.GenerateAndSignKeys case a.authRotate.FullCommand(): - err = a.RotateCertAuthority(ctx, client) + commandFunc = a.RotateCertAuthority case a.authLS.FullCommand(): - err = a.ListAuthServers(ctx, client) + commandFunc = a.ListAuthServers case a.authCRL.FullCommand(): - err = a.GenerateCRLForCA(ctx, client) + commandFunc = a.GenerateCRLForCA default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } @@ -219,7 +236,7 @@ var allowedCRLCertificateTypes = []string{ // ExportAuthorities outputs the list of authorities in OpenSSH compatible formats // If --type flag is given, only prints keys for CAs of this type, otherwise // prints all keys -func (a *AuthCommand) ExportAuthorities(ctx context.Context, clt *authclient.Client) error { +func (a *AuthCommand) ExportAuthorities(ctx context.Context, clt authCommandClient) error { exportFunc := client.ExportAuthorities if a.exportPrivateKeys { exportFunc = client.ExportAuthoritiesSecrets @@ -285,7 +302,7 @@ type certificateSigner interface { } // GenerateAndSignKeys generates a new keypair and signs it for role -func (a *AuthCommand) GenerateAndSignKeys(ctx context.Context, clusterAPI certificateSigner) error { +func (a *AuthCommand) GenerateAndSignKeys(ctx context.Context, clusterAPI authCommandClient) error { if a.streamTarfile { tarWriter := newTarWriter(clockwork.NewRealClock()) defer tarWriter.Archive(os.Stdout) @@ -421,7 +438,7 @@ func (a *AuthCommand) generateSnowflakeKey(ctx context.Context, clusterAPI certi } // RotateCertAuthority starts or restarts certificate authority rotation process -func (a *AuthCommand) RotateCertAuthority(ctx context.Context, client *authclient.Client) error { +func (a *AuthCommand) RotateCertAuthority(ctx context.Context, client authCommandClient) error { req := types.RotateRequest{ Type: types.CertAuthType(a.rotateType), GracePeriod: &a.rotateGracePeriod, @@ -445,7 +462,7 @@ func (a *AuthCommand) RotateCertAuthority(ctx context.Context, client *authclien } // ListAuthServers prints a list of connected auth servers -func (a *AuthCommand) ListAuthServers(ctx context.Context, clusterAPI *authclient.Client) error { +func (a *AuthCommand) ListAuthServers(ctx context.Context, clusterAPI authCommandClient) error { servers, err := clusterAPI.GetAuthServers() if err != nil { return trace.Wrap(err) @@ -473,7 +490,7 @@ type crlGenerator interface { // GenerateCRLForCA generates a certificate revocation list for a certificate // authority. -func (a *AuthCommand) GenerateCRLForCA(ctx context.Context, clusterAPI crlGenerator) error { +func (a *AuthCommand) GenerateCRLForCA(ctx context.Context, clusterAPI authCommandClient) error { certType := types.CertAuthType(a.caType) if err := certType.Check(); err != nil { return trace.Wrap(err) diff --git a/tool/tctl/common/bots_command.go b/tool/tctl/common/bots_command.go index 306969eec1f26..6a5de45f5afb5 100644 --- a/tool/tctl/common/bots_command.go +++ b/tool/tctl/common/bots_command.go @@ -49,6 +49,8 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) type BotsCommand struct { @@ -82,7 +84,7 @@ type BotsCommand struct { } // Initialize sets up the "tctl bots" command. -func (c *BotsCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *BotsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { bots := app.Command("bots", "Manage Machine ID bots on the cluster.").Alias("bot") c.botsList = bots.Command("ls", "List all certificate renewal bots registered with the cluster.") @@ -131,27 +133,34 @@ func (c *BotsCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun attempts to run subcommands. -func (c *BotsCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *BotsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.botsList.FullCommand(): - err = c.ListBots(ctx, client) + commandFunc = c.ListBots case c.botsAdd.FullCommand(): - err = c.AddBot(ctx, client) + commandFunc = c.AddBot case c.botsRemove.FullCommand(): - err = c.RemoveBot(ctx, client) + commandFunc = c.RemoveBot case c.botsLock.FullCommand(): - err = c.LockBot(ctx, client) + commandFunc = c.LockBot case c.botsUpdate.FullCommand(): - err = c.UpdateBot(ctx, client) + commandFunc = c.UpdateBot case c.botsInstancesShow.FullCommand(): - err = c.ShowBotInstance(ctx, client) + commandFunc = c.ShowBotInstance case c.botsInstancesList.FullCommand(): - err = c.ListBotInstances(ctx, client) + commandFunc = c.ListBotInstances case c.botsInstancesAdd.FullCommand(): - err = c.AddBotInstance(ctx, client) + commandFunc = c.AddBotInstance default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/client/auth.go b/tool/tctl/common/client/auth.go new file mode 100644 index 0000000000000..8f88d6fd4c479 --- /dev/null +++ b/tool/tctl/common/client/auth.go @@ -0,0 +1,118 @@ +/* + * Teleport + * Copyright (C) 2024 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 . + */ + +package client + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/client/webclient" + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/api/mfa" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/authclient" + libmfa "github.com/gravitational/teleport/lib/client/mfa" + "github.com/gravitational/teleport/lib/reversetunnelclient" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/tool/common" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" +) + +// InitFunc initiates connection to auth service, makes ping request and return the client instance. +// If the function does not return an error, the caller is responsible for calling the client close function +// once it does not need the client anymore. +type InitFunc func(ctx context.Context) (client *authclient.Client, close func(context.Context), err error) + +// GetInitFunc wraps lazy loading auth init function for commands which requires the auth client. +func GetInitFunc(ccf tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) InitFunc { + return func(ctx context.Context) (*authclient.Client, func(context.Context), error) { + clientConfig, err := tctlcfg.ApplyConfig(&ccf, cfg) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + resolver, err := reversetunnelclient.CachingResolver( + ctx, + reversetunnelclient.WebClientResolver(&webclient.Config{ + Context: ctx, + ProxyAddr: clientConfig.AuthServers[0].String(), + Insecure: clientConfig.Insecure, + Timeout: clientConfig.DialTimeout, + }), + nil /* clock */) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + dialer, err := reversetunnelclient.NewTunnelAuthDialer(reversetunnelclient.TunnelAuthDialerConfig{ + Resolver: resolver, + ClientConfig: clientConfig.SSH, + Log: clientConfig.Log, + InsecureSkipTLSVerify: clientConfig.Insecure, + ClusterCAs: clientConfig.TLS.RootCAs, + }) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + clientConfig.ProxyDialer = dialer + + client, err := authclient.Connect(ctx, clientConfig) + if err != nil { + if utils.IsUntrustedCertErr(err) { + err = trace.WrapWithMessage(err, utils.SelfSignedCertsMsg) + } + fmt.Fprintf(os.Stderr, + "ERROR: Cannot connect to the auth server. Is the auth server running on %q?\n", + cfg.AuthServerAddresses()[0].Addr) + return nil, nil, trace.NewAggregate(&common.ExitCodeError{Code: 1}, err) + } + + // Get the proxy address and set the MFA prompt constructor. + resp, err := client.Ping(ctx) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + proxyAddr := resp.ProxyPublicAddr + client.SetMFAPromptConstructor(func(opts ...mfa.PromptOpt) mfa.Prompt { + promptCfg := libmfa.NewPromptConfig(proxyAddr, opts...) + return libmfa.NewCLIPrompt(&libmfa.CLIPromptConfig{ + PromptConfig: *promptCfg, + }) + }) + + return client, func(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, constants.TimeoutGetClusterAlerts) + defer cancel() + if err := common.ShowClusterAlerts(ctx, client, os.Stderr, nil, + types.AlertSeverity_HIGH); err != nil { + slog.WarnContext(ctx, "Failed to display cluster alerts.", "error", err) + } + if err := client.Close(); err != nil { + slog.WarnContext(ctx, "Failed to close client.", "error", err) + } + }, nil + } +} diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go index c233761e044b9..4b9745ac38a10 100644 --- a/tool/tctl/common/cmds.go +++ b/tool/tctl/common/cmds.go @@ -29,6 +29,7 @@ import ( // Commands returns the set of available subcommands for tctl. func Commands() []CLICommand { return []CLICommand{ + &VersionCommand{}, &UserCommand{}, &NodeCommand{}, &TokensCommand{}, diff --git a/tool/tctl/common/config/global.go b/tool/tctl/common/config/global.go new file mode 100644 index 0000000000000..e3110a0f794ca --- /dev/null +++ b/tool/tctl/common/config/global.go @@ -0,0 +1,196 @@ +/* + * Teleport + * Copyright (C) 2024 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 . + */ + +package config + +import ( + "errors" + "io/fs" + "log/slog" + "path/filepath" + "runtime" + + "github.com/gravitational/trace" + log "github.com/sirupsen/logrus" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/api/metadata" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/auth/state" + "github.com/gravitational/teleport/lib/auth/storage" + "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" +) + +// GlobalCLIFlags keeps the CLI flags that apply to all tctl commands +type GlobalCLIFlags struct { + // Debug enables verbose logging mode to the console + Debug bool + // ConfigFile is the path to the Teleport configuration file + ConfigFile string + // ConfigString is the base64-encoded string with Teleport configuration + ConfigString string + // AuthServerAddr lists addresses of auth or proxy servers to connect to, + AuthServerAddr []string + // IdentityFilePath is the path to the identity file + IdentityFilePath string + // Insecure, when set, skips validation of server TLS certificate when + // connecting through a proxy (specified in AuthServerAddr). + Insecure bool +} + +// ApplyConfig takes configuration values from the config file and applies them +// to 'servicecfg.Config' object. +// +// The returned authclient.Config has the credentials needed to dial the auth +// server. +func ApplyConfig(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { + // --debug flag + if ccf.Debug { + cfg.Debug = ccf.Debug + utils.InitLogger(utils.LoggingForCLI, slog.LevelDebug) + log.Debugf("Debug logging has been enabled.") + } + cfg.Log = log.StandardLogger() + + if cfg.Version == "" { + cfg.Version = defaults.TeleportConfigVersionV1 + } + + // If the config file path provided is not a blank string, load the file and apply its values + var fileConf *config.FileConfig + var err error + if ccf.ConfigFile != "" { + fileConf, err = config.ReadConfigFile(ccf.ConfigFile) + if err != nil { + return nil, trace.Wrap(err) + } + } + + // if configuration is passed as an environment variable, + // try to decode it and override the config file + if ccf.ConfigString != "" { + fileConf, err = config.ReadFromString(ccf.ConfigString) + if err != nil { + return nil, trace.Wrap(err) + } + } + + // It only makes sense to use file config when tctl is run on the same + // host as the auth server. + // If this is any other host, then it's remote tctl usage. + // Remote tctl usage will require ~/.tsh or an identity file. + // ~/.tsh which will provide credentials AND config to reach auth server. + // Identity file requires --auth-server flag. + localAuthSvcConf := fileConf != nil && fileConf.Auth.Enabled() + if localAuthSvcConf { + if err = config.ApplyFileConfig(fileConf, cfg); err != nil { + return nil, trace.Wrap(err) + } + } + + // --auth-server flag(-s) + if len(ccf.AuthServerAddr) != 0 { + authServers, err := utils.ParseAddrs(ccf.AuthServerAddr) + if err != nil { + return nil, trace.Wrap(err) + } + // Overwrite any existing configuration with flag values. + if err := cfg.SetAuthServerAddresses(authServers); err != nil { + return nil, trace.Wrap(err) + } + } + + // Config file (for an auth_service) should take precedence. + if !localAuthSvcConf { + // Try profile or identity file. + if fileConf == nil { + log.Debug("no config file, loading auth config via extension") + } else { + log.Debug("auth_service disabled in config file, loading auth config via extension") + } + authConfig, err := LoadConfigFromProfile(ccf, cfg) + if err == nil { + return authConfig, nil + } + if !trace.IsNotFound(err) { + return nil, trace.Wrap(err) + } else if runtime.GOOS == constants.WindowsOS { + // On macOS/Linux, a not found error here is okay, as we can attempt + // to use the local auth identity. The auth server itself doesn't run + // on Windows though, so exit early with a clear error. + return nil, trace.BadParameter("tctl requires a tsh profile on Windows. " + + "Try logging in with tsh first.") + } + } + + // If auth server is not provided on the command line or in file + // configuration, use the default. + if len(cfg.AuthServerAddresses()) == 0 { + authServers, err := utils.ParseAddrs([]string{defaults.AuthConnectAddr().Addr}) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := cfg.SetAuthServerAddresses(authServers); err != nil { + return nil, trace.Wrap(err) + } + } + + authConfig := new(authclient.Config) + // read the host UUID only in case the identity was not provided, + // because it will be used for reading local auth server identity + cfg.HostUUID, err = hostid.ReadFile(cfg.DataDir) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, trace.Wrap(err, "Could not load Teleport host UUID file at %s. "+ + "Please make sure that a Teleport Auth Service instance is running on this host prior to using tctl or provide credentials by logging in with tsh first.", + filepath.Join(cfg.DataDir, hostid.FileName)) + } else if errors.Is(err, fs.ErrPermission) { + return nil, trace.Wrap(err, "Teleport does not have permission to read Teleport host UUID file at %s. "+ + "Ensure that you are running as a user with appropriate permissions or provide credentials by logging in with tsh first.", + filepath.Join(cfg.DataDir, hostid.FileName)) + } + return nil, trace.Wrap(err) + } + identity, err := storage.ReadLocalIdentity(filepath.Join(cfg.DataDir, teleport.ComponentProcess), state.IdentityID{Role: types.RoleAdmin, HostUUID: cfg.HostUUID}) + if err != nil { + // The "admin" identity is not present? This means the tctl is running + // NOT on the auth server + if trace.IsNotFound(err) { + return nil, trace.AccessDenied("tctl must be used on an Auth Service host or provided with credentials by logging in with tsh first.") + } + return nil, trace.Wrap(err) + } + authConfig.TLS, err = identity.TLSConfig(cfg.CipherSuites) + if err != nil { + return nil, trace.Wrap(err) + } + authConfig.TLS.InsecureSkipVerify = ccf.Insecure + authConfig.Insecure = ccf.Insecure + authConfig.AuthServers = cfg.AuthServerAddresses() + authConfig.Log = cfg.Log + authConfig.DialOpts = append(authConfig.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTCTL)) + + return authConfig, nil +} diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go new file mode 100644 index 0000000000000..ffe062621dbc2 --- /dev/null +++ b/tool/tctl/common/config/profile.go @@ -0,0 +1,121 @@ +/* + * Teleport + * Copyright (C) 2024 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 . + */ + +package config + +import ( + "errors" + "time" + + "github.com/gravitational/trace" + log "github.com/sirupsen/logrus" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/metadata" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/client/identityfile" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" +) + +// LoadConfigFromProfile applies config from ~/.tsh/ profile if it's present +func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { + proxyAddr := "" + if len(ccf.AuthServerAddr) != 0 { + proxyAddr = ccf.AuthServerAddr[0] + } + + clientStore := client.NewFSClientStore(cfg.TeleportHome) + if ccf.IdentityFilePath != "" { + var err error + clientStore, err = identityfile.NewClientStoreFromIdentityFile(ccf.IdentityFilePath, proxyAddr, "") + if err != nil { + return nil, trace.Wrap(err) + } + } + + profile, err := clientStore.ReadProfileStatus(proxyAddr) + if err != nil { + return nil, trace.Wrap(err) + } + if profile.IsExpired(time.Now()) { + if profile.GetKeyRingError != nil { + if errors.As(profile.GetKeyRingError, new(*client.FutureCertPathError)) { + // Intentionally avoid wrapping the error because the caller + // ignores NotFound errors. + return nil, trace.Errorf("it appears tsh v17 or newer was used to log in, make sure to use tsh and tctl on the same major version\n\t%v", profile.GetKeyRingError) + } + return nil, trace.Wrap(profile.GetKeyRingError) + } + return nil, trace.BadParameter("your credentials have expired, please login using `tsh login`") + } + + c := client.MakeDefaultConfig() + log.WithFields(log.Fields{"proxy": profile.ProxyURL.String(), "user": profile.Username}).Debugf("Found profile.") + if err := c.LoadProfile(clientStore, proxyAddr); err != nil { + return nil, trace.Wrap(err) + } + + webProxyHost, _ := c.WebProxyHostPort() + idx := client.KeyIndex{ProxyHost: webProxyHost, Username: c.Username, ClusterName: profile.Cluster} + key, err := clientStore.GetKey(idx, client.WithSSHCerts{}) + if err != nil { + return nil, trace.Wrap(err) + } + + // Auth config can be created only using a key associated with the root cluster. + rootCluster, err := key.RootClusterName() + if err != nil { + return nil, trace.Wrap(err) + } + if profile.Cluster != rootCluster { + return nil, trace.BadParameter("your credentials are for cluster %q, please run `tsh login %q` to log in to the root cluster", profile.Cluster, rootCluster) + } + + authConfig := &authclient.Config{} + authConfig.TLS, err = key.TeleportClientTLSConfig(cfg.CipherSuites, []string{rootCluster}) + if err != nil { + return nil, trace.Wrap(err) + } + authConfig.TLS.InsecureSkipVerify = ccf.Insecure + authConfig.Insecure = ccf.Insecure + authConfig.SSH, err = key.ProxyClientSSHConfig(rootCluster) + if err != nil { + return nil, trace.Wrap(err) + } + // Do not override auth servers from command line + if len(ccf.AuthServerAddr) == 0 { + webProxyAddr, err := utils.ParseAddr(c.WebProxyAddr) + if err != nil { + return nil, trace.Wrap(err) + } + log.Debugf("Setting auth server to web proxy %v.", webProxyAddr) + cfg.SetAuthServerAddress(*webProxyAddr) + } + authConfig.AuthServers = cfg.AuthServerAddresses() + authConfig.Log = cfg.Log + authConfig.DialOpts = append(authConfig.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTCTL)) + + if c.TLSRoutingEnabled { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + } + + return authConfig, nil +} diff --git a/tool/tctl/common/db_command.go b/tool/tctl/common/db_command.go index 98da6cc93113c..d23f2ebe51aa2 100644 --- a/tool/tctl/common/db_command.go +++ b/tool/tctl/common/db_command.go @@ -34,6 +34,8 @@ import ( libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // DBCommand implements "tctl db" group of commands. @@ -55,7 +57,7 @@ type DBCommand struct { } // Initialize allows DBCommand to plug itself into the CLI parser. -func (c *DBCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *DBCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config db := app.Command("db", "Operate on databases registered with the cluster.") @@ -68,13 +70,21 @@ func (c *DBCommand) Initialize(app *kingpin.Application, config *servicecfg.Conf } // TryRun attempts to run subcommands like "db ls". -func (c *DBCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *DBCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.dbList.FullCommand(): - err = c.ListDatabases(ctx, client) + commandFunc = c.ListDatabases default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/desktop_command.go b/tool/tctl/common/desktop_command.go index 464d27bd27c14..9b3eae8c7958e 100644 --- a/tool/tctl/common/desktop_command.go +++ b/tool/tctl/common/desktop_command.go @@ -30,6 +30,8 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // DesktopCommand implements "tctl desktop" group of commands. @@ -50,7 +52,7 @@ type DesktopCommand struct { } // Initialize allows DesktopCommand to plug itself into the CLI parser -func (c *DesktopCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *DesktopCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config desktop := app.Command("desktop", "Operate on registered desktops.").Alias("desktops").Alias("windows_desktop").Alias("windows_desktops") @@ -63,15 +65,22 @@ func (c *DesktopCommand) Initialize(app *kingpin.Application, config *servicecfg } // TryRun attempts to run subcommands like "desktop ls". -func (c *DesktopCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *DesktopCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.desktopList.FullCommand(): - err = c.ListDesktop(ctx, client) + commandFunc = c.ListDesktop case c.desktopBootstrap.FullCommand(): - err = c.BootstrapAD(ctx, client) + commandFunc = c.BootstrapAD default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/devices.go b/tool/tctl/common/devices.go index 0b1623581beeb..f8bfc9412ab60 100644 --- a/tool/tctl/common/devices.go +++ b/tool/tctl/common/devices.go @@ -38,6 +38,8 @@ import ( "github.com/gravitational/teleport/lib/devicetrust" dtnative "github.com/gravitational/teleport/lib/devicetrust/native" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // DevicesCommand implements the `tctl devices` command. @@ -67,7 +69,7 @@ var osTypeToEnum = map[osType]devicepb.OSType{ windowsType: devicepb.OSType_OS_TYPE_WINDOWS, } -func (c *DevicesCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (c *DevicesCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { devicesCmd := app.Command("devices", "Register and manage trusted devices").Hidden() addCmd := devicesCmd.Command("add", "Register managed devices.") @@ -112,19 +114,24 @@ type runner interface { Run(context.Context, *authclient.Client) error } -func (c *DevicesCommand) TryRun(ctx context.Context, selectedCommand string, authClient *authclient.Client) (match bool, err error) { +func (c *DevicesCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { innerCmd, ok := map[string]runner{ "devices add": &c.add, "devices ls": &c.ls, "devices rm": &c.rm, "devices enroll": &c.enroll, "devices lock": &c.lock, - }[selectedCommand] + }[cmd] if !ok { return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) - switch err := trail.FromGRPC(innerCmd.Run(ctx, authClient)); { + switch err := trail.FromGRPC(innerCmd.Run(ctx, client)); { case trace.IsNotImplemented(err): return true, trace.AccessDenied("Device Trust requires a Teleport Enterprise Auth Server running v12 or later.") default: diff --git a/tool/tctl/common/edit_command.go b/tool/tctl/common/edit_command.go index 5c3b2f9efbdf4..196fe653bd756 100644 --- a/tool/tctl/common/edit_command.go +++ b/tool/tctl/common/edit_command.go @@ -39,6 +39,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // EditCommand implements the `tctl edit` command for modifying @@ -55,7 +57,7 @@ type EditCommand struct { Editor func(filename string) error } -func (e *EditCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (e *EditCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { e.app = app e.config = config e.cmd = app.Command("edit", "Edit a Teleport resource.") @@ -68,12 +70,16 @@ func (e *EditCommand) Initialize(app *kingpin.Application, config *servicecfg.Co e.cmd.Flag("confirm", "Confirm an unsafe or temporary resource update").Hidden().BoolVar(&e.confirm) } -func (e *EditCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (bool, error) { +func (e *EditCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (bool, error) { if cmd != e.cmd.FullCommand() { return false, nil } - - err := e.editResource(ctx, client) + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) + err = e.editResource(ctx, client) return true, trace.Wrap(err) } @@ -119,7 +125,7 @@ func (e *EditCommand) editResource(ctx context.Context, client *authclient.Clien withSecrets: true, confirm: e.confirm, } - rc.Initialize(e.app, e.config) + rc.Initialize(e.app, nil, e.config) err = rc.Get(ctx, client) if closeErr := f.Close(); closeErr != nil { diff --git a/tool/tctl/common/externalauditstorage_command.go b/tool/tctl/common/externalauditstorage_command.go index 585e388f805de..44d73d7044dde 100644 --- a/tool/tctl/common/externalauditstorage_command.go +++ b/tool/tctl/common/externalauditstorage_command.go @@ -26,6 +26,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // ExternalAuditStorageCommand implements "tctl externalauditstorage" group of commands. @@ -42,7 +44,7 @@ type ExternalAuditStorageCommand struct { } // Initialize allows ExternalAuditStorageCommand to plug itself into the CLI parser. -func (c *ExternalAuditStorageCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *ExternalAuditStorageCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config externalAuditStorage := app.Command("externalauditstorage", "Operate on External Audit Storage configuration.").Hidden() @@ -55,15 +57,22 @@ func (c *ExternalAuditStorageCommand) Initialize(app *kingpin.Application, confi } // TryRun attempts to run subcommands. -func (c *ExternalAuditStorageCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *ExternalAuditStorageCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.promote.FullCommand(): - err = c.Promote(ctx, client) + commandFunc = c.Promote case c.generate.FullCommand(): - err = c.Generate(ctx, client) + commandFunc = c.Generate default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/fido2.go b/tool/tctl/common/fido2.go index 8563ffaeeced1..5ae848d4cfd39 100644 --- a/tool/tctl/common/fido2.go +++ b/tool/tctl/common/fido2.go @@ -21,9 +21,10 @@ import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/tool/common/fido2" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // fido2Command adapts fido2.Command for tctl. @@ -31,10 +32,10 @@ type fido2Command struct { impl *fido2.Command } -func (c *fido2Command) Initialize(app *kingpin.Application, _ *servicecfg.Config) { +func (c *fido2Command) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { c.impl = fido2.NewCommand(app) } -func (c *fido2Command) TryRun(ctx context.Context, selectedCommand string, _ *authclient.Client) (match bool, err error) { - return c.impl.TryRun(ctx, selectedCommand) +func (c *fido2Command) TryRun(ctx context.Context, cmd string, _ commonclient.InitFunc) (match bool, err error) { + return c.impl.TryRun(ctx, cmd) } diff --git a/tool/tctl/common/helpers_test.go b/tool/tctl/common/helpers_test.go index a9c17e4cef4fd..d9649391427a7 100644 --- a/tool/tctl/common/helpers_test.go +++ b/tool/tctl/common/helpers_test.go @@ -44,6 +44,8 @@ import ( "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) type options struct { @@ -59,8 +61,8 @@ func withEditor(editor func(string) error) optionsFunc { } type cliCommand interface { - Initialize(app *kingpin.Application, cfg *servicecfg.Config) - TryRun(ctx context.Context, cmd string, client *authclient.Client) (bool, error) + Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) + TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (bool, error) } func runCommand(t *testing.T, client *authclient.Client, cmd cliCommand, args []string) error { @@ -68,13 +70,15 @@ func runCommand(t *testing.T, client *authclient.Client, cmd cliCommand, args [] cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() app := utils.InitCLIParser("tctl", GlobalHelpString) - cmd.Initialize(app, cfg) + cmd.Initialize(app, &tctlcfg.GlobalCLIFlags{}, cfg) selectedCmd, err := app.Parse(args) require.NoError(t, err) ctx := context.Background() - _, err = cmd.TryRun(ctx, selectedCmd, client) + _, err = cmd.TryRun(ctx, selectedCmd, func(ctx context.Context) (*authclient.Client, func(context.Context), error) { + return client, func(context.Context) {}, nil + }) return err } diff --git a/tool/tctl/common/idp_command.go b/tool/tctl/common/idp_command.go index 2555ba80e36a1..e29beb102ee0d 100644 --- a/tool/tctl/common/idp_command.go +++ b/tool/tctl/common/idp_command.go @@ -15,6 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + package common import ( @@ -37,6 +38,8 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // subcommandRunner is used to create pluggable subcommand under @@ -45,7 +48,7 @@ import ( // $ tctl idp oidc [ ...] type subcommandRunner interface { initialize(parent *kingpin.CmdClause, cfg *servicecfg.Config) - tryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) + tryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) } // IdPCommand implements all commands under "tctl idp". @@ -61,7 +64,7 @@ type samlIdPCommand struct { } // Initialize installs the base "idp" command and all subcommands. -func (t *IdPCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (t *IdPCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { idp := app.Command("idp", "Teleport Identity Provider") idp.Alias(` @@ -79,9 +82,9 @@ Examples: } // TryRun calls tryRun for each subcommand, and returns (false, nil) if none of them match. -func (i *IdPCommand) TryRun(ctx context.Context, cmd string, c *authclient.Client) (match bool, err error) { +func (i *IdPCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { for _, subcommandRunner := range i.subcommandRunners { - match, err = subcommandRunner.tryRun(ctx, cmd, c) + match, err = subcommandRunner.tryRun(ctx, cmd, clientFunc) if err != nil { return match, trace.Wrap(err) } @@ -121,10 +124,15 @@ Examples: s.testAttributeMapping.cmd = testAttrMap } -func (s *samlIdPCommand) tryRun(ctx context.Context, cmd string, c *authclient.Client) (match bool, err error) { +func (s *samlIdPCommand) tryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { switch cmd { case s.testAttributeMapping.cmd.FullCommand(): - return true, trace.Wrap(s.testAttributeMapping.run(ctx, c)) + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) + return true, trace.Wrap(s.testAttributeMapping.run(ctx, client)) default: return false, nil } diff --git a/tool/tctl/common/inventory_command.go b/tool/tctl/common/inventory_command.go index bac450d83c5e9..56bdc48ad912c 100644 --- a/tool/tctl/common/inventory_command.go +++ b/tool/tctl/common/inventory_command.go @@ -35,6 +35,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" vc "github.com/gravitational/teleport/lib/versioncontrol" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // InventoryCommand implements the `tctl inventory` family of commands. @@ -64,7 +66,7 @@ type InventoryCommand struct { } // Initialize allows AccessRequestCommand to plug itself into the CLI parser -func (c *InventoryCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *InventoryCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config inventory := app.Command("inventory", "Manage Teleport instance inventory.").Hidden() @@ -85,17 +87,25 @@ func (c *InventoryCommand) Initialize(app *kingpin.Application, config *servicec } // TryRun takes the CLI command as an argument (like "inventory status") and executes it. -func (c *InventoryCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *InventoryCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.inventoryStatus.FullCommand(): - err = c.Status(ctx, client) + commandFunc = c.Status case c.inventoryList.FullCommand(): - err = c.List(ctx, client) + commandFunc = c.List case c.inventoryPing.FullCommand(): - err = c.Ping(ctx, client) + commandFunc = c.Ping default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/kube_command.go b/tool/tctl/common/kube_command.go index 12a9ce9d08f9c..b0e2f69afe373 100644 --- a/tool/tctl/common/kube_command.go +++ b/tool/tctl/common/kube_command.go @@ -34,6 +34,8 @@ import ( libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // KubeCommand implements "tctl kube" group of commands. @@ -55,7 +57,7 @@ type KubeCommand struct { } // Initialize allows KubeCommand to plug itself into the CLI parser -func (c *KubeCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *KubeCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config kube := app.Command("kube", "Operate on registered Kubernetes clusters.") @@ -68,13 +70,20 @@ func (c *KubeCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun attempts to run subcommands like "kube ls". -func (c *KubeCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *KubeCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.kubeList.FullCommand(): - err = c.ListKube(ctx, client) + commandFunc = c.ListKube default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/loadtest_command.go b/tool/tctl/common/loadtest_command.go index 891568e200599..fb9075af180fd 100644 --- a/tool/tctl/common/loadtest_command.go +++ b/tool/tctl/common/loadtest_command.go @@ -44,6 +44,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // LoadtestCommand implements the `tctl loadtest` family of commands. @@ -71,7 +73,7 @@ type LoadtestCommand struct { } // Initialize allows LoadtestCommand to plug itself into the CLI parser -func (c *LoadtestCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *LoadtestCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config loadtest := app.Command("loadtest", "Tools for generating artificial load").Hidden() @@ -96,17 +98,24 @@ func (c *LoadtestCommand) Initialize(app *kingpin.Application, config *servicecf } // TryRun takes the CLI command as an argument (like "loadtest node-heartbeats") and executes it. -func (c *LoadtestCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *LoadtestCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.nodeHeartbeats.FullCommand(): - err = c.NodeHeartbeats(ctx, client) + commandFunc = c.NodeHeartbeats case c.watch.FullCommand(): - err = c.Watch(ctx, client) + commandFunc = c.Watch case c.auditEvents.FullCommand(): - err = c.AuditEvents(ctx, client) + commandFunc = c.AuditEvents default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/lock_command.go b/tool/tctl/common/lock_command.go index 058a2f57c91d4..3927c7ed91b28 100644 --- a/tool/tctl/common/lock_command.go +++ b/tool/tctl/common/lock_command.go @@ -30,6 +30,8 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // LockCommand implements `tctl lock` group of commands. @@ -42,7 +44,7 @@ type LockCommand struct { } // Initialize allows LockCommand to plug itself into the CLI parser. -func (c *LockCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *LockCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config c.mainCmd = app.Command("lock", "Create a new lock.") @@ -60,13 +62,21 @@ func (c *LockCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun attempts to run subcommands. -func (c *LockCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *LockCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.mainCmd.FullCommand(): - err = c.CreateLock(ctx, client) + commandFunc = c.CreateLock default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/loginrule/command.go b/tool/tctl/common/loginrule/command.go index 8c1d467d1a93e..022c389d88dd7 100644 --- a/tool/tctl/common/loginrule/command.go +++ b/tool/tctl/common/loginrule/command.go @@ -37,11 +37,13 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) type subcommand interface { initialize(parent *kingpin.CmdClause, cfg *servicecfg.Config) - tryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) + tryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) } // Command implements all commands under "tctl login_rule". @@ -50,7 +52,7 @@ type Command struct { } // Initialize installs the base "login_rule" command and all subcommands. -func (t *Command) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (t *Command) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { loginRuleCommand := app.Command("login_rule", "Test login rules") t.subcommands = []subcommand{ @@ -64,9 +66,9 @@ func (t *Command) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { // TryRun calls tryRun for each subcommand, and if none of them match returns // (false, nil) -func (t *Command) TryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) { +func (t *Command) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { for _, subcommand := range t.subcommands { - match, err = subcommand.tryRun(ctx, selectedCommand, c) + match, err = subcommand.tryRun(ctx, cmd, clientFunc) if err != nil { return match, trace.Wrap(err) } @@ -112,7 +114,7 @@ Examples: > echo '{"groups": ["example"]}' | tctl login_rule test --resource-file rule.yaml`) } -func (t *testCommand) tryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) { +func (t *testCommand) tryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) { if selectedCommand != t.cmd.FullCommand() { return false, nil } @@ -120,8 +122,13 @@ func (t *testCommand) tryRun(ctx context.Context, selectedCommand string, c *aut if len(t.inputResourceFiles) == 0 && !t.loadFromCluster { return true, trace.BadParameter("no login rules to test, --resource-file or --load-from-cluster must be set") } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) - return true, trace.Wrap(t.run(ctx, c)) + return true, trace.Wrap(t.run(ctx, client)) } func (t *testCommand) run(ctx context.Context, c *authclient.Client) error { diff --git a/tool/tctl/common/node_command.go b/tool/tctl/common/node_command.go index a92eff599e64a..f07021e2daa1e 100644 --- a/tool/tctl/common/node_command.go +++ b/tool/tctl/common/node_command.go @@ -42,6 +42,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // NodeCommand implements `tctl nodes` group of commands @@ -76,7 +78,7 @@ type NodeCommand struct { } // Initialize allows NodeCommand to plug itself into the CLI parser -func (c *NodeCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *NodeCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config // add node command @@ -99,16 +101,22 @@ func (c *NodeCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun takes the CLI command as an argument (like "nodes ls") and executes it. -func (c *NodeCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *NodeCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.nodeAdd.FullCommand(): - err = c.Invite(ctx, client) + commandFunc = c.Invite case c.nodeList.FullCommand(): - err = c.ListActive(ctx, client) - + commandFunc = c.ListActive default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/notification_command.go b/tool/tctl/common/notification_command.go index 860f8a9cf977c..9703ae65f6065 100644 --- a/tool/tctl/common/notification_command.go +++ b/tool/tctl/common/notification_command.go @@ -43,6 +43,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/tool/common" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // NotificationCommand implements the `tctl notifications` family of commands. @@ -69,7 +71,7 @@ type NotificationCommand struct { } // Initialize allows NotificationCommand command to plug itself into the CLI parser -func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) { +func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { notif := app.Command("notifications", "Manage cluster notifications.") n.create = notif.Command("create", "Create a cluster notification.").Alias("add") @@ -98,19 +100,24 @@ func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *servicecfg } // TryRun takes the CLI command as an argument and executes it. -func (n *NotificationCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { - nc := client.NotificationServiceClient() - +func (n *NotificationCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case n.create.FullCommand(): - err = n.Create(ctx, client) + commandFunc = n.Create case n.ls.FullCommand(): - err = n.List(ctx, nc) + commandFunc = n.List case n.rm.FullCommand(): - err = n.Remove(ctx, client) + commandFunc = n.Remove default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } @@ -232,7 +239,7 @@ func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Cli return nil } -func (n *NotificationCommand) List(ctx context.Context, client notificationspb.NotificationServiceClient) error { +func (n *NotificationCommand) List(ctx context.Context, client *authclient.Client) error { labels, err := libclient.ParseLabelSpec(n.labels) if err != nil { return trace.Wrap(err) @@ -240,13 +247,14 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N var result []*notificationspb.Notification var pageToken string + nc := client.NotificationServiceClient() for { var resp *notificationspb.ListNotificationsResponse var err error // If a user was specified, list user-specific notifications for them, if not, default to listing global notifications. if n.user != "" { - resp, err = client.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ + resp, err = nc.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ PageSize: defaults.DefaultChunkSize, PageToken: pageToken, Filters: ¬ificationspb.NotificationFilters{ @@ -259,7 +267,7 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N return trace.Wrap(err) } } else { - resp, err = client.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ + resp, err = nc.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ PageSize: defaults.DefaultChunkSize, PageToken: pageToken, Filters: ¬ificationspb.NotificationFilters{ diff --git a/tool/tctl/common/plugin/plugins_command.go b/tool/tctl/common/plugin/plugins_command.go index bcc2661caf8c7..b6c6ed57d85a1 100644 --- a/tool/tctl/common/plugin/plugins_command.go +++ b/tool/tctl/common/plugin/plugins_command.go @@ -34,8 +34,9 @@ import ( pluginsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) const ( @@ -81,7 +82,7 @@ type PluginsCommand struct { } // Initialize creates the plugins command and subcommands -func (p *PluginsCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (p *PluginsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { p.config = config p.dryRun = true @@ -139,12 +140,11 @@ func (p *PluginsCommand) initDelete(parent *kingpin.CmdClause) { } // Delete implements `tctl plugins delete`, deleting a plugin from the Teleport cluster -func (p *PluginsCommand) Delete(ctx context.Context, client *authclient.Client) error { +func (p *PluginsCommand) Delete(ctx context.Context, args installPluginArgs) error { log := p.config.Logger.With("plugin", p.delete.name) - plugins := client.PluginsClient() req := &pluginsv1.DeletePluginRequest{Name: p.delete.name} - if _, err := plugins.DeletePlugin(ctx, req); err != nil { + if _, err := args.plugins.DeletePlugin(ctx, req); err != nil { if trace.IsNotFound(err) { log.InfoContext(ctx, "Plugin not found") return nil @@ -156,8 +156,8 @@ func (p *PluginsCommand) Delete(ctx context.Context, client *authclient.Client) } // Cleanup cleans up the given plugin. -func (p *PluginsCommand) Cleanup(ctx context.Context, clusterAPI *authclient.Client) error { - needsCleanup, err := clusterAPI.PluginsClient().NeedsCleanup(ctx, &pluginsv1.NeedsCleanupRequest{ +func (p *PluginsCommand) Cleanup(ctx context.Context, args installPluginArgs) error { + needsCleanup, err := args.plugins.NeedsCleanup(ctx, &pluginsv1.NeedsCleanupRequest{ Type: p.pluginType, }) if err != nil { @@ -189,7 +189,7 @@ func (p *PluginsCommand) Cleanup(ctx context.Context, clusterAPI *authclient.Cli return nil } - if _, err := clusterAPI.PluginsClient().Cleanup(ctx, &pluginsv1.CleanupRequest{ + if _, err := args.plugins.Cleanup(ctx, &pluginsv1.CleanupRequest{ Type: p.pluginType, }); err != nil { return trace.Wrap(err) @@ -209,12 +209,16 @@ type authClient interface { UpdateIntegration(ctx context.Context, ig types.Integration) (types.Integration, error) Ping(ctx context.Context) (proto.PingResponse, error) PerformMFACeremony(ctx context.Context, challengeRequest *proto.CreateAuthenticateChallengeRequest, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) + GetRole(ctx context.Context, name string) (types.Role, error) } type pluginsClient interface { CreatePlugin(ctx context.Context, in *pluginsv1.CreatePluginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetPlugin(ctx context.Context, in *pluginsv1.GetPluginRequest, opts ...grpc.CallOption) (*types.PluginV1, error) UpdatePlugin(ctx context.Context, in *pluginsv1.UpdatePluginRequest, opts ...grpc.CallOption) (*types.PluginV1, error) + NeedsCleanup(ctx context.Context, in *pluginsv1.NeedsCleanupRequest, opts ...grpc.CallOption) (*pluginsv1.NeedsCleanupResponse, error) + Cleanup(ctx context.Context, in *pluginsv1.CleanupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeletePlugin(ctx context.Context, in *pluginsv1.DeletePluginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type installPluginArgs struct { @@ -224,11 +228,11 @@ type installPluginArgs struct { // InstallSCIM implements `tctl plugins install scim`, installing a SCIM integration // plugin into the teleport cluster -func (p *PluginsCommand) InstallSCIM(ctx context.Context, client *authclient.Client) error { +func (p *PluginsCommand) InstallSCIM(ctx context.Context, args installPluginArgs) error { log := p.config.Logger.With(logFieldPlugin, p.install.name) log.DebugContext(ctx, "Fetching cluster info...") - info, err := client.Ping(ctx) + info, err := args.authClient.Ping(ctx) if err != nil { return trace.Wrap(err, "failed fetching cluster info") } @@ -242,7 +246,7 @@ func (p *PluginsCommand) InstallSCIM(ctx context.Context, client *authclient.Cli connectorID := p.install.scim.samlConnector log.DebugContext(ctx, "Validating SAML Connector...", logFieldSAMLConnector, connectorID) - connector, err := client.GetSAMLConnector(ctx, p.install.scim.samlConnector, false) + connector, err := args.authClient.GetSAMLConnector(ctx, p.install.scim.samlConnector, false) if err != nil { if !p.install.scim.force { return trace.Wrap(err, "failed validating SAML connector") @@ -251,7 +255,7 @@ func (p *PluginsCommand) InstallSCIM(ctx context.Context, client *authclient.Cli role := p.install.scim.role log.DebugContext(ctx, "Validating Default Role...", logFieldRole, role) - if _, err := client.GetRole(ctx, role); err != nil { + if _, err := args.authClient.GetRole(ctx, role); err != nil { if !p.install.scim.force { return trace.Wrap(err, "failed validating role") } @@ -291,7 +295,7 @@ func (p *PluginsCommand) InstallSCIM(ctx context.Context, client *authclient.Cli } log.DebugContext(ctx, "Creating SCIM Plugin...") - if _, err := client.PluginsClient().CreatePlugin(ctx, request); err != nil { + if _, err := args.plugins.CreatePlugin(ctx, request); err != nil { log.ErrorContext(ctx, "Failed to create SCIM integration", logErrorMessage(err)) return trace.Wrap(err) } @@ -311,22 +315,28 @@ func (p *PluginsCommand) InstallSCIM(ctx context.Context, client *authclient.Cli } // TryRun runs the plugins command -func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, args installPluginArgs) error switch cmd { case p.cleanupCmd.FullCommand(): - err = p.Cleanup(ctx, client) + commandFunc = p.Cleanup case p.install.okta.cmd.FullCommand(): - args := installPluginArgs{authClient: client, plugins: client.PluginsClient()} - err = p.InstallOkta(ctx, args) + commandFunc = p.InstallOkta case p.install.scim.cmd.FullCommand(): - err = p.InstallSCIM(ctx, client) + commandFunc = p.InstallSCIM case p.install.entraID.cmd.FullCommand(): - args := installPluginArgs{authClient: client, plugins: client.PluginsClient()} - err = p.InstallEntra(ctx, args) + commandFunc = p.InstallEntra case p.delete.cmd.FullCommand(): - err = p.Delete(ctx, client) + commandFunc = p.Delete default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, installPluginArgs{authClient: client, plugins: client.PluginsClient()}) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/plugin/plugins_command_test.go b/tool/tctl/common/plugin/plugins_command_test.go index 3254e813ee205..cc0c2ef953c1b 100644 --- a/tool/tctl/common/plugin/plugins_command_test.go +++ b/tool/tctl/common/plugin/plugins_command_test.go @@ -460,6 +460,21 @@ func (m *mockPluginsClient) UpdatePlugin(ctx context.Context, in *pluginsv1.Upda return result.Get(0).(*types.PluginV1), result.Error(1) } +func (m *mockPluginsClient) NeedsCleanup(ctx context.Context, in *pluginsv1.NeedsCleanupRequest, opts ...grpc.CallOption) (*pluginsv1.NeedsCleanupResponse, error) { + result := m.Called(ctx, in, opts) + return result.Get(0).(*pluginsv1.NeedsCleanupResponse), result.Error(1) +} + +func (m *mockPluginsClient) Cleanup(ctx context.Context, in *pluginsv1.CleanupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + result := m.Called(ctx, in, opts) + return result.Get(0).(*emptypb.Empty), result.Error(1) +} + +func (m *mockPluginsClient) DeletePlugin(ctx context.Context, in *pluginsv1.DeletePluginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + result := m.Called(ctx, in, opts) + return result.Get(0).(*emptypb.Empty), result.Error(1) +} + type mockAuthClient struct { mock.Mock } @@ -499,5 +514,10 @@ func (m *mockAuthClient) PerformMFACeremony(ctx context.Context, challengeReques return &proto.MFAAuthenticateResponse{}, nil } +func (m *mockAuthClient) GetRole(ctx context.Context, name string) (types.Role, error) { + result := m.Called(ctx, name) + return result.Get(0).(types.Role), result.Error(1) +} + // anyContext is an argument matcher for testify mocks that matches any context. var anyContext any = mock.MatchedBy(func(context.Context) bool { return true }) diff --git a/tool/tctl/common/proxy_command.go b/tool/tctl/common/proxy_command.go index 47c23842f2e7e..cd8f868fa77a1 100644 --- a/tool/tctl/common/proxy_command.go +++ b/tool/tctl/common/proxy_command.go @@ -28,6 +28,8 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // ProxyCommand returns information about connected proxies @@ -39,7 +41,7 @@ type ProxyCommand struct { } // Initialize creates the proxy command and subcommands -func (p *ProxyCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (p *ProxyCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { p.config = config proxyCommand := app.Command("proxy", "Operations with information for cluster proxies.").Hidden() @@ -72,12 +74,19 @@ func (p *ProxyCommand) ListProxies(ctx context.Context, clusterAPI *authclient.C } // TryRun runs the proxy command -func (p *ProxyCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (p *ProxyCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case p.lsCmd.FullCommand(): - err = p.ListProxies(ctx, client) + commandFunc = p.ListProxies default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) return true, trace.Wrap(err) } diff --git a/tool/tctl/common/recordings_command.go b/tool/tctl/common/recordings_command.go index b74193dd42c5f..f2a2fdae8dfed 100644 --- a/tool/tctl/common/recordings_command.go +++ b/tool/tctl/common/recordings_command.go @@ -35,6 +35,8 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/tool/common" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // RecordingsCommand implements "tctl recordings" group of commands. @@ -56,7 +58,7 @@ type RecordingsCommand struct { } // Initialize allows RecordingsCommand to plug itself into the CLI parser -func (c *RecordingsCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *RecordingsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config recordings := app.Command("recordings", "View and control session recordings.") c.recordingsList = recordings.Command("ls", "List recorded sessions.") @@ -68,13 +70,21 @@ func (c *RecordingsCommand) Initialize(app *kingpin.Application, config *service } // TryRun attempts to run subcommands like "recordings ls". -func (c *RecordingsCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *RecordingsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.recordingsList.FullCommand(): - err = c.ListRecordings(ctx, client) + commandFunc = c.ListRecordings default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 1cc029a0a3b52..cfefe03e3a0c5 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -71,7 +71,9 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" clusterconfigrec "github.com/gravitational/teleport/tool/tctl/common/clusterconfig" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" "github.com/gravitational/teleport/tool/tctl/common/databaseobject" "github.com/gravitational/teleport/tool/tctl/common/databaseobjectimportrule" "github.com/gravitational/teleport/tool/tctl/common/loginrule" @@ -127,7 +129,7 @@ Same as above, but using JSON output: ` // Initialize allows ResourceCommand to plug itself into the CLI parser -func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (rc *ResourceCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { rc.CreateHandlers = map[ResourceKind]ResourceCreateHandler{ types.KindUser: rc.createUser, types.KindRole: rc.createRole, @@ -238,23 +240,31 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec // TryRun takes the CLI command as an argument (like "auth gen") and executes it // or returns match=false if 'cmd' does not belong to it -func (rc *ResourceCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (rc *ResourceCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { // tctl get case rc.getCmd.FullCommand(): - err = rc.Get(ctx, client) + commandFunc = rc.Get // tctl create case rc.createCmd.FullCommand(): - err = rc.Create(ctx, client) + commandFunc = rc.Create // tctl rm case rc.deleteCmd.FullCommand(): - err = rc.Delete(ctx, client) + commandFunc = rc.Delete // tctl update case rc.updateCmd.FullCommand(): - err = rc.UpdateFields(ctx, client) + commandFunc = rc.UpdateFields default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/saml_command.go b/tool/tctl/common/saml_command.go index 726fb55077c48..7500dcd21ad7d 100644 --- a/tool/tctl/common/saml_command.go +++ b/tool/tctl/common/saml_command.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // implements common.CLICommand interface @@ -41,7 +43,7 @@ type SAMLCommand struct { // Initialize allows a caller-defined command to plug itself into CLI // argument parsing -func (cmd *SAMLCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (cmd *SAMLCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { cmd.config = cfg saml := app.Command("saml", "Operations on SAML auth connectors.") @@ -51,9 +53,14 @@ func (cmd *SAMLCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Con // TryRun is executed after the CLI parsing is done. The command must // determine if selectedCommand belongs to it and return match=true -func (cmd *SAMLCommand) TryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) { +func (cmd *SAMLCommand) TryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) { if selectedCommand == cmd.exportCmd.FullCommand() { - return true, trace.Wrap(cmd.export(ctx, c)) + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + defer closeFn(ctx) + return true, trace.Wrap(cmd.export(ctx, client)) } return false, nil } diff --git a/tool/tctl/common/status_command.go b/tool/tctl/common/status_command.go index 8270fd26f0ea6..9c1ddea183a1b 100644 --- a/tool/tctl/common/status_command.go +++ b/tool/tctl/common/status_command.go @@ -31,6 +31,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/tlsca" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // StatusCommand implements `tctl token` group of commands. @@ -42,19 +44,27 @@ type StatusCommand struct { } // Initialize allows StatusCommand to plug itself into the CLI parser. -func (c *StatusCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *StatusCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config c.status = app.Command("status", "Report cluster status.") } // TryRun takes the CLI command as an argument (like "nodes ls") and executes it. -func (c *StatusCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *StatusCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.status.FullCommand(): - err = c.Status(ctx, client) + commandFunc = c.Status default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index b71301cb27981..0c5ebbc7887d9 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -22,39 +22,23 @@ import ( "context" "errors" "fmt" - "io/fs" "log/slog" "os" "path/filepath" - "runtime" - "time" "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/breaker" - "github.com/gravitational/teleport/api/client/webclient" - "github.com/gravitational/teleport/api/constants" - "github.com/gravitational/teleport/api/metadata" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/auth/authclient" - "github.com/gravitational/teleport/lib/auth/state" - "github.com/gravitational/teleport/lib/auth/storage" "github.com/gravitational/teleport/lib/autoupdate/tools" - "github.com/gravitational/teleport/lib/client" - "github.com/gravitational/teleport/lib/client/identityfile" - libmfa "github.com/gravitational/teleport/lib/client/mfa" - "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/modules" - "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" - "github.com/gravitational/teleport/lib/utils/hostid" "github.com/gravitational/teleport/tool/common" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) const ( @@ -68,23 +52,6 @@ const ( authAddrEnvVar = "TELEPORT_AUTH_SERVER" ) -// GlobalCLIFlags keeps the CLI flags that apply to all tctl commands -type GlobalCLIFlags struct { - // Debug enables verbose logging mode to the console - Debug bool - // ConfigFile is the path to the Teleport configuration file - ConfigFile string - // ConfigString is the base64-encoded string with Teleport configuration - ConfigString string - // AuthServerAddr lists addresses of auth or proxy servers to connect to, - AuthServerAddr []string - // IdentityFilePath is the path to the identity file - IdentityFilePath string - // Insecure, when set, skips validation of server TLS certificate when - // connecting through a proxy (specified in AuthServerAddr). - Insecure bool -} - // CLICommand interface must be implemented by every CLI command // // This allows OSS and Enterprise Teleport editions to plug their own @@ -93,11 +60,11 @@ type GlobalCLIFlags struct { type CLICommand interface { // Initialize allows a caller-defined command to plug itself into CLI // argument parsing - Initialize(*kingpin.Application, *servicecfg.Config) + Initialize(*kingpin.Application, *tctlcfg.GlobalCLIFlags, *servicecfg.Config) // TryRun is executed after the CLI parsing is done. The command must // determine if selectedCommand belongs to it and return match=true - TryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) + TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) } // Run is the same as 'make'. It helps to share the code between different @@ -132,19 +99,18 @@ func TryRun(commands []CLICommand, args []string) error { cfg := servicecfg.MakeDefaultConfig() cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() - // each command will add itself to the CLI parser: + var ccf tctlcfg.GlobalCLIFlags + + // Each command will add itself to the CLI parser. for i := range commands { - commands[i].Initialize(app, cfg) + commands[i].Initialize(app, &ccf, cfg) } - var ccf GlobalCLIFlags - // If the config file path is being overridden by environment variable, set that. // If not, check whether the default config file path exists and set that if so. // This preserves tctl's default behavior for backwards compatibility. - configFileEnvar, isSet := os.LookupEnv(defaults.ConfigFileEnvar) - if isSet { - ccf.ConfigFile = configFileEnvar + if configFileEnv, ok := os.LookupEnv(defaults.ConfigFileEnvar); ok { + ccf.ConfigFile = configFileEnv } else { if utils.FileExists(defaults.ConfigFilePath) { ccf.ConfigFile = defaults.ConfigFilePath @@ -171,9 +137,6 @@ func TryRun(commands []CLICommand, args []string) error { StringVar(&ccf.IdentityFilePath) app.Flag("insecure", "When specifying a proxy address in --auth-server, do not verify its TLS certificate. Danger: any data you send can be intercepted or modified by an attacker."). BoolVar(&ccf.Insecure) - - // "version" command is always available: - ver := app.Command("version", "Print the version of your tctl binary.") app.HelpFlag.Short('h') // parse CLI commands+flags: @@ -191,12 +154,6 @@ func TryRun(commands []CLICommand, args []string) error { return trace.BadParameter("tctl --identity also requires --auth-server") } - // "version" command? - if selectedCmd == ver.FullCommand() { - modules.GetModules().PrintVersion() - return nil - } - cfg.TeleportHome = os.Getenv(types.HomeEnvVar) if cfg.TeleportHome != "" { cfg.TeleportHome = filepath.Clean(cfg.TeleportHome) @@ -204,69 +161,11 @@ func TryRun(commands []CLICommand, args []string) error { cfg.Debug = ccf.Debug - // configure all commands with Teleport configuration (they share 'cfg') - clientConfig, err := ApplyConfig(&ccf, cfg) - if err != nil { - return trace.Wrap(err) - } - ctx := context.Background() - - resolver, err := reversetunnelclient.CachingResolver( - ctx, - reversetunnelclient.WebClientResolver(&webclient.Config{ - Context: ctx, - ProxyAddr: clientConfig.AuthServers[0].String(), - Insecure: clientConfig.Insecure, - Timeout: clientConfig.DialTimeout, - }), - nil /* clock */) - if err != nil { - return trace.Wrap(err) - } - - dialer, err := reversetunnelclient.NewTunnelAuthDialer(reversetunnelclient.TunnelAuthDialerConfig{ - Resolver: resolver, - ClientConfig: clientConfig.SSH, - Log: clientConfig.Log, - InsecureSkipTLSVerify: clientConfig.Insecure, - ClusterCAs: clientConfig.TLS.RootCAs, - }) - if err != nil { - return trace.Wrap(err) - } - - clientConfig.ProxyDialer = dialer - - client, err := authclient.Connect(ctx, clientConfig) - if err != nil { - if utils.IsUntrustedCertErr(err) { - err = trace.WrapWithMessage(err, utils.SelfSignedCertsMsg) - } - fmt.Fprintf(os.Stderr, - "ERROR: Cannot connect to the auth server. Is the auth server running on %q?\n", - cfg.AuthServerAddresses()[0].Addr) - return trace.NewAggregate(&common.ExitCodeError{Code: 1}, err) - } - - // Get the proxy address and set the MFA prompt constructor. - resp, err := client.Ping(ctx) - if err != nil { - return trace.Wrap(err) - } - - proxyAddr := resp.ProxyPublicAddr - client.SetMFAPromptConstructor(func(opts ...mfa.PromptOpt) mfa.Prompt { - promptCfg := libmfa.NewPromptConfig(proxyAddr, opts...) - return libmfa.NewCLIPrompt(&libmfa.CLIPromptConfig{ - PromptConfig: *promptCfg, - }) - }) - - // execute whatever is selected: - var match bool + clientFunc := commonclient.GetInitFunc(ccf, cfg) + // Execute whatever is selected. for _, c := range commands { - match, err = c.TryRun(ctx, selectedCmd, client) + match, err := c.TryRun(ctx, selectedCmd, clientFunc) if err != nil { return trace.Wrap(err) } @@ -275,233 +174,5 @@ func TryRun(commands []CLICommand, args []string) error { } } - ctx, cancel := context.WithTimeout(ctx, constants.TimeoutGetClusterAlerts) - defer cancel() - if err := common.ShowClusterAlerts(ctx, client, os.Stderr, nil, - types.AlertSeverity_HIGH); err != nil { - log.WithError(err).Warn("Failed to display cluster alerts.") - } - return nil } - -// ApplyConfig takes configuration values from the config file and applies them -// to 'servicecfg.Config' object. -// -// The returned authclient.Config has the credentials needed to dial the auth -// server. -func ApplyConfig(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { - // --debug flag - if ccf.Debug { - cfg.Debug = ccf.Debug - utils.InitLogger(utils.LoggingForCLI, slog.LevelDebug) - log.Debugf("Debug logging has been enabled.") - } - cfg.Log = log.StandardLogger() - - if cfg.Version == "" { - cfg.Version = defaults.TeleportConfigVersionV1 - } - - // If the config file path provided is not a blank string, load the file and apply its values - var fileConf *config.FileConfig - var err error - if ccf.ConfigFile != "" { - fileConf, err = config.ReadConfigFile(ccf.ConfigFile) - if err != nil { - return nil, trace.Wrap(err) - } - } - - // if configuration is passed as an environment variable, - // try to decode it and override the config file - if ccf.ConfigString != "" { - fileConf, err = config.ReadFromString(ccf.ConfigString) - if err != nil { - return nil, trace.Wrap(err) - } - } - - // It only makes sense to use file config when tctl is run on the same - // host as the auth server. - // If this is any other host, then it's remote tctl usage. - // Remote tctl usage will require ~/.tsh or an identity file. - // ~/.tsh which will provide credentials AND config to reach auth server. - // Identity file requires --auth-server flag. - localAuthSvcConf := fileConf != nil && fileConf.Auth.Enabled() - if localAuthSvcConf { - if err = config.ApplyFileConfig(fileConf, cfg); err != nil { - return nil, trace.Wrap(err) - } - } - - // --auth-server flag(-s) - if len(ccf.AuthServerAddr) != 0 { - authServers, err := utils.ParseAddrs(ccf.AuthServerAddr) - if err != nil { - return nil, trace.Wrap(err) - } - // Overwrite any existing configuration with flag values. - if err := cfg.SetAuthServerAddresses(authServers); err != nil { - return nil, trace.Wrap(err) - } - } - - // Config file (for an auth_service) should take precedence. - if !localAuthSvcConf { - // Try profile or identity file. - if fileConf == nil { - log.Debug("no config file, loading auth config via extension") - } else { - log.Debug("auth_service disabled in config file, loading auth config via extension") - } - authConfig, err := LoadConfigFromProfile(ccf, cfg) - if err == nil { - return authConfig, nil - } - if !trace.IsNotFound(err) { - return nil, trace.Wrap(err) - } else if runtime.GOOS == constants.WindowsOS { - // On macOS/Linux, a not found error here is okay, as we can attempt - // to use the local auth identity. The auth server itself doesn't run - // on Windows though, so exit early with a clear error. - return nil, trace.BadParameter("tctl requires a tsh profile on Windows. " + - "Try logging in with tsh first.") - } - } - - // If auth server is not provided on the command line or in file - // configuration, use the default. - if len(cfg.AuthServerAddresses()) == 0 { - authServers, err := utils.ParseAddrs([]string{defaults.AuthConnectAddr().Addr}) - if err != nil { - return nil, trace.Wrap(err) - } - - if err := cfg.SetAuthServerAddresses(authServers); err != nil { - return nil, trace.Wrap(err) - } - } - - authConfig := new(authclient.Config) - // read the host UUID only in case the identity was not provided, - // because it will be used for reading local auth server identity - cfg.HostUUID, err = hostid.ReadFile(cfg.DataDir) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil, trace.Wrap(err, "Could not load Teleport host UUID file at %s. "+ - "Please make sure that a Teleport Auth Service instance is running on this host prior to using tctl or provide credentials by logging in with tsh first.", - filepath.Join(cfg.DataDir, hostid.FileName)) - } else if errors.Is(err, fs.ErrPermission) { - return nil, trace.Wrap(err, "Teleport does not have permission to read Teleport host UUID file at %s. "+ - "Ensure that you are running as a user with appropriate permissions or provide credentials by logging in with tsh first.", - filepath.Join(cfg.DataDir, hostid.FileName)) - } - return nil, trace.Wrap(err) - } - identity, err := storage.ReadLocalIdentity(filepath.Join(cfg.DataDir, teleport.ComponentProcess), state.IdentityID{Role: types.RoleAdmin, HostUUID: cfg.HostUUID}) - if err != nil { - // The "admin" identity is not present? This means the tctl is running - // NOT on the auth server - if trace.IsNotFound(err) { - return nil, trace.AccessDenied("tctl must be used on an Auth Service host or provided with credentials by logging in with tsh first.") - } - return nil, trace.Wrap(err) - } - authConfig.TLS, err = identity.TLSConfig(cfg.CipherSuites) - if err != nil { - return nil, trace.Wrap(err) - } - authConfig.TLS.InsecureSkipVerify = ccf.Insecure - authConfig.Insecure = ccf.Insecure - authConfig.AuthServers = cfg.AuthServerAddresses() - authConfig.Log = cfg.Log - authConfig.DialOpts = append(authConfig.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTCTL)) - - return authConfig, nil -} - -// LoadConfigFromProfile applies config from ~/.tsh/ profile if it's present -func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Config, error) { - proxyAddr := "" - if len(ccf.AuthServerAddr) != 0 { - proxyAddr = ccf.AuthServerAddr[0] - } - - clientStore := client.NewFSClientStore(cfg.TeleportHome) - if ccf.IdentityFilePath != "" { - var err error - clientStore, err = identityfile.NewClientStoreFromIdentityFile(ccf.IdentityFilePath, proxyAddr, "") - if err != nil { - return nil, trace.Wrap(err) - } - } - - profile, err := clientStore.ReadProfileStatus(proxyAddr) - if err != nil { - return nil, trace.Wrap(err) - } - if profile.IsExpired(time.Now()) { - if profile.GetKeyRingError != nil { - if errors.As(profile.GetKeyRingError, new(*client.FutureCertPathError)) { - // Intentionally avoid wrapping the error because the caller - // ignores NotFound errors. - return nil, trace.Errorf("it appears tsh v17 or newer was used to log in, make sure to use tsh and tctl on the same major version\n\t%v", profile.GetKeyRingError) - } - return nil, trace.Wrap(profile.GetKeyRingError) - } - return nil, trace.BadParameter("your credentials have expired, please login using `tsh login`") - } - - c := client.MakeDefaultConfig() - log.WithFields(log.Fields{"proxy": profile.ProxyURL.String(), "user": profile.Username}).Debugf("Found profile.") - if err := c.LoadProfile(clientStore, proxyAddr); err != nil { - return nil, trace.Wrap(err) - } - - webProxyHost, _ := c.WebProxyHostPort() - idx := client.KeyIndex{ProxyHost: webProxyHost, Username: c.Username, ClusterName: profile.Cluster} - key, err := clientStore.GetKey(idx, client.WithSSHCerts{}) - if err != nil { - return nil, trace.Wrap(err) - } - - // Auth config can be created only using a key associated with the root cluster. - rootCluster, err := key.RootClusterName() - if err != nil { - return nil, trace.Wrap(err) - } - if profile.Cluster != rootCluster { - return nil, trace.BadParameter("your credentials are for cluster %q, please run `tsh login %q` to log in to the root cluster", profile.Cluster, rootCluster) - } - - authConfig := &authclient.Config{} - authConfig.TLS, err = key.TeleportClientTLSConfig(cfg.CipherSuites, []string{rootCluster}) - if err != nil { - return nil, trace.Wrap(err) - } - authConfig.TLS.InsecureSkipVerify = ccf.Insecure - authConfig.Insecure = ccf.Insecure - authConfig.SSH, err = key.ProxyClientSSHConfig(rootCluster) - if err != nil { - return nil, trace.Wrap(err) - } - // Do not override auth servers from command line - if len(ccf.AuthServerAddr) == 0 { - webProxyAddr, err := utils.ParseAddr(c.WebProxyAddr) - if err != nil { - return nil, trace.Wrap(err) - } - log.Debugf("Setting auth server to web proxy %v.", webProxyAddr) - cfg.SetAuthServerAddress(*webProxyAddr) - } - authConfig.AuthServers = cfg.AuthServerAddresses() - authConfig.Log = cfg.Log - authConfig.DialOpts = append(authConfig.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTCTL)) - - if c.TLSRoutingEnabled { - cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) - } - - return authConfig, nil -} diff --git a/tool/tctl/common/tctl_test.go b/tool/tctl/common/tctl_test.go index ccba39abaa958..f5593d46db036 100644 --- a/tool/tctl/common/tctl_test.go +++ b/tool/tctl/common/tctl_test.go @@ -20,6 +20,7 @@ package common import ( "context" + "errors" "os" "testing" @@ -31,6 +32,8 @@ import ( "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" "github.com/gravitational/teleport/tool/teleport/testenv" ) @@ -39,6 +42,55 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// TestCommandMatchBeforeAuthConnect verifies all defined `tctl` commands `TryRun` +// method, to ensure that auth client not initialized in matching process, +// so we don't require a client before command is executed. +func TestCommandMatchBeforeAuthConnect(t *testing.T) { + app := utils.InitCLIParser("tctl", GlobalHelpString) + cfg := servicecfg.MakeDefaultConfig() + cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() + + var ccf tctlcfg.GlobalCLIFlags + + commands := Commands() + for i := range commands { + commands[i].Initialize(app, &ccf, cfg) + } + + testError := errors.New("auth client must not be initialized before match") + + ctx := context.Background() + clientFunc := func(ctx context.Context) (client *authclient.Client, close func(context.Context), err error) { + return nil, nil, testError + } + + var match bool + var err error + + // We set the command which is not defined to go through + // all defined commands to ensure that auth client + // not initialized before command is matched. + for _, c := range commands { + match, err = c.TryRun(ctx, "non-existing-command", clientFunc) + if err != nil { + break + } + } + require.False(t, match) + require.NoError(t, err) + + // Iterate and expect that `tokens ls` command going to be executed + // and auth client is requested. + for _, c := range commands { + match, err = c.TryRun(ctx, "tokens ls", clientFunc) + if err != nil { + break + } + } + require.False(t, match) + require.ErrorIs(t, err, testError) +} + // TestConnect tests client config and connection logic. func TestConnect(t *testing.T) { dynAddr := helpers.NewDynamicServiceAddr(t) @@ -80,13 +132,13 @@ func TestConnect(t *testing.T) { for _, tc := range []struct { name string - cliFlags GlobalCLIFlags + cliFlags tctlcfg.GlobalCLIFlags modifyConfig func(*servicecfg.Config) wantErrContains string }{ { name: "default to data dir", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ AuthServerAddr: []string{fileConfig.Auth.ListenAddress}, Insecure: true, }, @@ -95,33 +147,33 @@ func TestConnect(t *testing.T) { }, }, { name: "auth config file", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ ConfigFile: mustWriteFileConfig(t, fileConfig), Insecure: true, }, }, { name: "auth config file string", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ ConfigString: mustGetBase64EncFileConfig(t, fileConfig), Insecure: true, }, }, { name: "ignores agent config file", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ ConfigFile: mustWriteFileConfig(t, fileConfigAgent), Insecure: true, }, wantErrContains: "make sure that a Teleport Auth Service instance is running", }, { name: "ignores agent config file string", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ ConfigString: mustGetBase64EncFileConfig(t, fileConfigAgent), Insecure: true, }, wantErrContains: "make sure that a Teleport Auth Service instance is running", }, { name: "ignores agent config file and loads identity file", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ AuthServerAddr: []string{fileConfig.Auth.ListenAddress}, IdentityFilePath: mustWriteIdentityFile(t, clt, username), ConfigFile: mustWriteFileConfig(t, fileConfigAgent), @@ -129,7 +181,7 @@ func TestConnect(t *testing.T) { }, }, { name: "ignores agent config file string and loads identity file", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ AuthServerAddr: []string{fileConfig.Auth.ListenAddress}, IdentityFilePath: mustWriteIdentityFile(t, clt, username), ConfigString: mustGetBase64EncFileConfig(t, fileConfigAgent), @@ -137,7 +189,7 @@ func TestConnect(t *testing.T) { }, }, { name: "identity file", - cliFlags: GlobalCLIFlags{ + cliFlags: tctlcfg.GlobalCLIFlags{ AuthServerAddr: []string{fileConfig.Auth.ListenAddress}, IdentityFilePath: mustWriteIdentityFile(t, clt, username), Insecure: true, @@ -154,7 +206,7 @@ func TestConnect(t *testing.T) { tc.modifyConfig(cfg) } - clientConfig, err := ApplyConfig(&tc.cliFlags, cfg) + clientConfig, err := tctlcfg.ApplyConfig(&tc.cliFlags, cfg) if tc.wantErrContains != "" { require.ErrorContains(t, err, tc.wantErrContains) return diff --git a/tool/tctl/common/terraform_command.go b/tool/tctl/common/terraform_command.go index ac2fc5ef593b2..f80aa331670e0 100644 --- a/tool/tctl/common/terraform_command.go +++ b/tool/tctl/common/terraform_command.go @@ -49,6 +49,8 @@ import ( "github.com/gravitational/teleport/lib/tbot/ssh" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) const ( @@ -84,7 +86,7 @@ type TerraformCommand struct { } // Initialize sets up the "tctl bots" command. -func (c *TerraformCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (c *TerraformCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { tfCmd := app.Command("terraform", "Helpers to run the Teleport Terraform Provider.") c.envCmd = tfCmd.Command("env", "Obtain certificates and load them into environments variables. This creates a temporary MachineID bot.") @@ -106,15 +108,19 @@ func (c *TerraformCommand) Initialize(app *kingpin.Application, cfg *servicecfg. } // TryRun attempts to run subcommands. -func (c *TerraformCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *TerraformCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { switch cmd { case c.envCmd.FullCommand(): + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } err = c.RunEnvCommand(ctx, client, os.Stdout, os.Stderr) + closeFn(ctx) + return true, trace.Wrap(err) default: return false, nil } - - return true, trace.Wrap(err) } // RunEnvCommand contains all the Terraform helper logic. It: diff --git a/tool/tctl/common/token_command.go b/tool/tctl/common/token_command.go index a9d5a5085cd89..5ac5a6225b126 100644 --- a/tool/tctl/common/token_command.go +++ b/tool/tctl/common/token_command.go @@ -44,6 +44,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) var mdmTokenAddTemplate = template.Must( @@ -109,7 +111,7 @@ type TokensCommand struct { } // Initialize allows TokenCommand to plug itself into the CLI parser -func (c *TokensCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *TokensCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config tokens := app.Command("tokens", "List or revoke invitation tokens") @@ -148,17 +150,25 @@ func (c *TokensCommand) Initialize(app *kingpin.Application, config *servicecfg. } // TryRun takes the CLI command as an argument (like "nodes ls") and executes it. -func (c *TokensCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *TokensCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.tokenAdd.FullCommand(): - err = c.Add(ctx, client) + commandFunc = c.Add case c.tokenDel.FullCommand(): - err = c.Del(ctx, client) + commandFunc = c.Del case c.tokenList.FullCommand(): - err = c.List(ctx, client) + commandFunc = c.List default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/top_command.go b/tool/tctl/common/top_command.go index 526463569189f..b19ca6a069de2 100644 --- a/tool/tctl/common/top_command.go +++ b/tool/tctl/common/top_command.go @@ -41,9 +41,10 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // TopCommand implements `tctl top` group of commands. @@ -57,7 +58,7 @@ type TopCommand struct { } // Initialize allows TopCommand to plug itself into the CLI parser. -func (c *TopCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (c *TopCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { c.config = config c.top = app.Command("top", "Report diagnostic information.") c.diagURL = c.top.Arg("diag-addr", "Diagnostic HTTP URL").Default("http://127.0.0.1:3000").String() @@ -65,7 +66,7 @@ func (c *TopCommand) Initialize(app *kingpin.Application, config *servicecfg.Con } // TryRun takes the CLI command as an argument (like "nodes ls") and executes it. -func (c *TopCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (c *TopCommand) TryRun(ctx context.Context, cmd string, _ commonclient.InitFunc) (match bool, err error) { switch cmd { case c.top.FullCommand(): diagClient, err := roundtrip.NewClient(*c.diagURL, "") diff --git a/tool/tctl/common/touchid.go b/tool/tctl/common/touchid.go index fdddb056516e1..121432a2bc358 100644 --- a/tool/tctl/common/touchid.go +++ b/tool/tctl/common/touchid.go @@ -21,9 +21,10 @@ import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/tool/common/touchid" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // touchIDCommand adapts touchid.Command for tclt. @@ -31,10 +32,10 @@ type touchIDCommand struct { impl *touchid.Command } -func (c *touchIDCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) { +func (c *touchIDCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { c.impl = touchid.NewCommand(app) } -func (c *touchIDCommand) TryRun(ctx context.Context, selectedCommand string, _ *authclient.Client) (match bool, err error) { - return c.impl.TryRun(ctx, selectedCommand) +func (c *touchIDCommand) TryRun(ctx context.Context, cmd string, _ commonclient.InitFunc) (match bool, err error) { + return c.impl.TryRun(ctx, cmd) } diff --git a/tool/tctl/common/user_command.go b/tool/tctl/common/user_command.go index f2fc151226d1b..83fe2f7e56643 100644 --- a/tool/tctl/common/user_command.go +++ b/tool/tctl/common/user_command.go @@ -44,6 +44,8 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/gcp" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // UserCommand implements `tctl users` set of commands @@ -80,7 +82,7 @@ type UserCommand struct { } // Initialize allows UserCommand to plug itself into the CLI parser -func (u *UserCommand) Initialize(app *kingpin.Application, config *servicecfg.Config) { +func (u *UserCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) { const helpPrefix string = "[Teleport local users only]" u.config = config @@ -153,21 +155,29 @@ func (u *UserCommand) Initialize(app *kingpin.Application, config *servicecfg.Co } // TryRun takes the CLI command as an argument (like "users add") and executes it. -func (u *UserCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { +func (u *UserCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case u.userAdd.FullCommand(): - err = u.Add(ctx, client) + commandFunc = u.Add case u.userUpdate.FullCommand(): - err = u.Update(ctx, client) + commandFunc = u.Update case u.userList.FullCommand(): - err = u.List(ctx, client) + commandFunc = u.List case u.userDelete.FullCommand(): - err = u.Delete(ctx, client) + commandFunc = u.Delete case u.userResetPassword.FullCommand(): - err = u.ResetPassword(ctx, client) + commandFunc = u.ResetPassword default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/common/version_command.go b/tool/tctl/common/version_command.go new file mode 100644 index 0000000000000..6d250110ca2a7 --- /dev/null +++ b/tool/tctl/common/version_command.go @@ -0,0 +1,56 @@ +/* + * Teleport + * Copyright (C) 2024 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 . + */ + +package common + +import ( + "context" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/modules" + "github.com/gravitational/teleport/lib/service/servicecfg" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" +) + +// VersionCommand implements the `tctl version` +type VersionCommand struct { + app *kingpin.Application + + verCmd *kingpin.CmdClause +} + +// Initialize allows VersionCommand to plug itself into the CLI parser. +func (c *VersionCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { + c.app = app + c.verCmd = app.Command("version", "Print the version of your tctl binary.") +} + +// TryRun takes the CLI command as an argument and executes it. +func (c *VersionCommand) TryRun(_ context.Context, cmd string, _ commonclient.InitFunc) (match bool, err error) { + switch cmd { + case c.verCmd.FullCommand(): + modules.GetModules().PrintVersion() + default: + return false, nil + } + + return true, trace.Wrap(err) +} diff --git a/tool/tctl/common/webauthnwin.go b/tool/tctl/common/webauthnwin.go index 7d428e88946b3..59adf84e1a4ba 100644 --- a/tool/tctl/common/webauthnwin.go +++ b/tool/tctl/common/webauthnwin.go @@ -21,9 +21,10 @@ import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/tool/common/webauthnwin" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // webauthnwinCommand adapts webauthnwin.Command for tctl. @@ -31,10 +32,10 @@ type webauthnwinCommand struct { impl *webauthnwin.Command } -func (c *webauthnwinCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) { +func (c *webauthnwinCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) { c.impl = webauthnwin.NewCommand(app) } -func (c *webauthnwinCommand) TryRun(ctx context.Context, selectedCommand string, _ *authclient.Client) (match bool, err error) { - return c.impl.TryRun(ctx, selectedCommand) +func (c *webauthnwinCommand) TryRun(ctx context.Context, cmd string, _ commonclient.InitFunc) (match bool, err error) { + return c.impl.TryRun(ctx, cmd) } diff --git a/tool/tctl/common/workload_identity_command.go b/tool/tctl/common/workload_identity_command.go index 54ceff23dfdaa..2080366ca24a4 100644 --- a/tool/tctl/common/workload_identity_command.go +++ b/tool/tctl/common/workload_identity_command.go @@ -31,6 +31,8 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // WorkloadIdentityCommand is a group of commands pertaining to Teleport @@ -47,7 +49,7 @@ type WorkloadIdentityCommand struct { // Initialize sets up the "tctl workload-identity" command. func (c *WorkloadIdentityCommand) Initialize( - app *kingpin.Application, config *servicecfg.Config, + app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config, ) { // TODO(noah): Remove the hidden flag once base functionality is released. cmd := app.Command( @@ -84,17 +86,25 @@ func (c *WorkloadIdentityCommand) Initialize( // TryRun attempts to run subcommands. func (c *WorkloadIdentityCommand) TryRun( - ctx context.Context, cmd string, client *authclient.Client, + ctx context.Context, cmd string, clientFunc commonclient.InitFunc, ) (match bool, err error) { + var commandFunc func(ctx context.Context, client *authclient.Client) error switch cmd { case c.listCmd.FullCommand(): - err = c.ListWorkloadIdentities(ctx, client) + commandFunc = c.ListWorkloadIdentities case c.rmCmd.FullCommand(): - err = c.DeleteWorkloadIdentity(ctx, client) + commandFunc = c.DeleteWorkloadIdentity default: return false, nil } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = commandFunc(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } diff --git a/tool/tctl/sso/configure/command.go b/tool/tctl/sso/configure/command.go index 30bd6e752b5de..77e4a75e6aa9e 100644 --- a/tool/tctl/sso/configure/command.go +++ b/tool/tctl/sso/configure/command.go @@ -31,6 +31,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // SSOConfigureCommand implements common.CLICommand interface @@ -48,7 +50,7 @@ type AuthKindCommand struct { // Initialize allows a caller-defined command to plug itself into CLI // argument parsing -func (cmd *SSOConfigureCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (cmd *SSOConfigureCommand) Initialize(app *kingpin.Application, flags *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { cmd.Config = cfg cmd.Logger = cfg.Log.WithField(teleport.ComponentKey, teleport.ComponentClient) @@ -59,7 +61,7 @@ func (cmd *SSOConfigureCommand) Initialize(app *kingpin.Application, cfg *servic // TryRun is executed after the CLI parsing is done. The command must // determine if selectedCommand belongs to it and return match=true -func (cmd *SSOConfigureCommand) TryRun(ctx context.Context, selectedCommand string, clt *authclient.Client) (match bool, err error) { +func (cmd *SSOConfigureCommand) TryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) { for _, subCommand := range cmd.AuthCommands { if subCommand.Parsed { // the default tctl logging behavior is to ignore all logs, unless --debug is present. @@ -70,8 +72,14 @@ func (cmd *SSOConfigureCommand) TryRun(ctx context.Context, selectedCommand stri cmd.Logger.Logger.SetFormatter(formatter) cmd.Logger.Logger.SetOutput(os.Stderr) } + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = subCommand.Run(ctx, client) + closeFn(ctx) - return true, trace.Wrap(subCommand.Run(ctx, clt)) + return true, trace.Wrap(err) } } diff --git a/tool/tctl/sso/tester/command.go b/tool/tctl/sso/tester/command.go index 7dfb0d6803411..3f8a4716fc0d5 100644 --- a/tool/tctl/sso/tester/command.go +++ b/tool/tctl/sso/tester/command.go @@ -38,6 +38,8 @@ import ( "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + commonclient "github.com/gravitational/teleport/tool/tctl/common/client" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) // SSOTestCommand implements common.CLICommand interface @@ -59,7 +61,7 @@ type SSOTestCommand struct { // Initialize allows a caller-defined command to plug itself into CLI // argument parsing -func (cmd *SSOTestCommand) Initialize(app *kingpin.Application, cfg *servicecfg.Config) { +func (cmd *SSOTestCommand) Initialize(app *kingpin.Application, flags *tctlcfg.GlobalCLIFlags, cfg *servicecfg.Config) { cmd.config = cfg sso := app.GetCommand("sso") @@ -154,9 +156,15 @@ func (cmd *SSOTestCommand) ssoTestCommand(ctx context.Context, c *authclient.Cli // TryRun is executed after the CLI parsing is done. The command must // determine if selectedCommand belongs to it and return match=true -func (cmd *SSOTestCommand) TryRun(ctx context.Context, selectedCommand string, c *authclient.Client) (match bool, err error) { +func (cmd *SSOTestCommand) TryRun(ctx context.Context, selectedCommand string, clientFunc commonclient.InitFunc) (match bool, err error) { if selectedCommand == cmd.ssoTestCmd.FullCommand() { - return true, cmd.ssoTestCommand(ctx, c) + client, closeFn, err := clientFunc(ctx) + if err != nil { + return false, trace.Wrap(err) + } + err = cmd.ssoTestCommand(ctx, client) + closeFn(ctx) + return true, trace.Wrap(err) } return false, nil } diff --git a/tool/tsh/common/tctl_test.go b/tool/tsh/common/tctl_test.go index d0cf25df9ef13..94958414f93fb 100644 --- a/tool/tsh/common/tctl_test.go +++ b/tool/tsh/common/tctl_test.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport/lib/utils" toolcommon "github.com/gravitational/teleport/tool/common" "github.com/gravitational/teleport/tool/tctl/common" + tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config" ) func TestLoadConfigFromProfile(t *testing.T) { @@ -60,20 +61,20 @@ func TestLoadConfigFromProfile(t *testing.T) { tests := []struct { name string - ccf *common.GlobalCLIFlags + ccf *tctlcfg.GlobalCLIFlags cfg *servicecfg.Config want error }{ { name: "teleportHome is valid dir", - ccf: &common.GlobalCLIFlags{}, + ccf: &tctlcfg.GlobalCLIFlags{}, cfg: &servicecfg.Config{ TeleportHome: tmpHomePath, }, want: nil, }, { name: "teleportHome is nonexistent dir", - ccf: &common.GlobalCLIFlags{}, + ccf: &tctlcfg.GlobalCLIFlags{}, cfg: &servicecfg.Config{ TeleportHome: "some/dir/that/does/not/exist", }, @@ -82,7 +83,7 @@ func TestLoadConfigFromProfile(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := common.LoadConfigFromProfile(tc.ccf, tc.cfg) + _, err := tctlcfg.LoadConfigFromProfile(tc.ccf, tc.cfg) if tc.want != nil { require.ErrorIs(t, err, tc.want) return @@ -231,7 +232,7 @@ func TestSetAuthServerFlagWhileLoggedIn(t *testing.T) { } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - ccf := &common.GlobalCLIFlags{} + ccf := &tctlcfg.GlobalCLIFlags{} ccf.AuthServerAddr = tt.authServerFlag ccf.ConfigFile = tt.configFileFlag @@ -241,7 +242,7 @@ func TestSetAuthServerFlagWhileLoggedIn(t *testing.T) { // ApplyConfig will try to read local auth server identity if the profile is not found. cfg.DataDir = authProcess.Config.DataDir - _, err = common.ApplyConfig(ccf, cfg) + _, err = tctlcfg.ApplyConfig(ccf, cfg) require.NoError(t, err) require.NotEmpty(t, cfg.AuthServerAddresses(), "auth servers should be set to a non-empty default if not specified")