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")