diff --git a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go index 1a3df3b69b6a2..e9bbc41192bf9 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go @@ -171,8 +171,9 @@ func (x *Gateway) GetGatewayCliCommand() *GatewayCLICommand { return nil } -// GatewayCLICommand represents a command that the user can execute to connect to the gateway -// resource. It is a direct translation of os.exec.Cmd. +// GatewayCLICommand represents a command that the user can execute to connect to a gateway +// resource. It is a combination of two different os/exec.Cmd structs, where path, args and env are +// directly taken from one Cmd and the preview field is constructed from another Cmd. type GatewayCLICommand struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -188,12 +189,16 @@ type GatewayCLICommand struct { // invocation. The elements of the list are in the format of NAME=value. Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"` // preview is used to show the user what command will be executed before they decide to run it. - // It's like os.exec.Cmd.String with two exceptions: + // It can also be copied and then pasted into a terminal. + // It's like os/exec.Cmd.String with two exceptions: // // 1) It is prepended with Cmd.Env. // 2) The command name is relative and not absolute. + // 3) It is taken from a different Cmd than the other fields in this message. This Cmd uses a + // special print format which makes the args suitable to be entered into a terminal, but not to + // directly spawn a process. // - // Should not be used to execute anything in the shell. + // Should not be used to execute the command in the shell. Instead, use path, args, and env. Preview string `protobuf:"bytes,4,opt,name=preview,proto3" json:"preview,omitempty"` } diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts index d30d69c14e3a7..834af7d04458a 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts @@ -105,8 +105,9 @@ export interface Gateway { gatewayCliCommand?: GatewayCLICommand; } /** - * GatewayCLICommand represents a command that the user can execute to connect to the gateway - * resource. It is a direct translation of os.exec.Cmd. + * GatewayCLICommand represents a command that the user can execute to connect to a gateway + * resource. It is a combination of two different os/exec.Cmd structs, where path, args and env are + * directly taken from one Cmd and the preview field is constructed from another Cmd. * * @generated from protobuf message teleport.lib.teleterm.v1.GatewayCLICommand */ @@ -134,12 +135,16 @@ export interface GatewayCLICommand { env: string[]; /** * preview is used to show the user what command will be executed before they decide to run it. - * It's like os.exec.Cmd.String with two exceptions: + * It can also be copied and then pasted into a terminal. + * It's like os/exec.Cmd.String with two exceptions: * * 1) It is prepended with Cmd.Env. * 2) The command name is relative and not absolute. + * 3) It is taken from a different Cmd than the other fields in this message. This Cmd uses a + * special print format which makes the args suitable to be entered into a terminal, but not to + * directly spawn a process. * - * Should not be used to execute anything in the shell. + * Should not be used to execute the command in the shell. Instead, use path, args, and env. * * @generated from protobuf field: string preview = 4; */ diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 7d5cae3bdc964..2d80ae9bf6aba 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -515,9 +515,10 @@ func testKubeGatewayCertRenewal(ctx context.Context, t *testing.T, params kubeGa func checkKubeconfigPathInCommandEnv(t *testing.T, daemonService *daemon.Service, gw gateway.Gateway, wantKubeconfigPath string) { t.Helper() - cmd, err := daemonService.GetGatewayCLICommand(gw) + cmds, err := daemonService.GetGatewayCLICommand(gw) require.NoError(t, err) - require.Equal(t, []string{"KUBECONFIG=" + wantKubeconfigPath}, cmd.Env) + require.Equal(t, []string{"KUBECONFIG=" + wantKubeconfigPath}, cmds.Exec.Env) + require.Equal(t, []string{"KUBECONFIG=" + wantKubeconfigPath}, cmds.Preview.Env) } // setupUserMFA upserts role so that it requires per-session MFA and configures the user account to diff --git a/lib/client/db/dbcmd/dbcmd.go b/lib/client/db/dbcmd/dbcmd.go index 647af2a43c75f..ed52858c4d721 100644 --- a/lib/client/db/dbcmd/dbcmd.go +++ b/lib/client/db/dbcmd/dbcmd.go @@ -861,7 +861,7 @@ func WithPassword(pass string) ConnectCommandFunc { // WithPrintFormat is known to be used for the following situations: // - tsh db config --format cmd // - tsh proxy db --tunnel -// - Teleport Connect where the command is put into a terminal. +// - Teleport Connect where the gateway command is shown in the UI. // // WithPrintFormat should NOT be used when the exec.Cmd gets executed by the // client application. diff --git a/lib/client/db/dbcmd/dbcmd_test.go b/lib/client/db/dbcmd/dbcmd_test.go index eb8a7a630e3a7..73e9e830ecefb 100644 --- a/lib/client/db/dbcmd/dbcmd_test.go +++ b/lib/client/db/dbcmd/dbcmd_test.go @@ -574,6 +574,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { cmd: nil, wantErr: true, }, + // If you find yourself changing this test so that generating a command for DynamoDB _doesn't_ + // fail if WithPrintFormat() is not provided, please remember to update lib/teleterm/cmd/db.go. { name: "dynamodb for exec is an error", dbProtocol: defaults.ProtocolDynamoDB, diff --git a/lib/teleterm/apiserver/handler/handler_gateways.go b/lib/teleterm/apiserver/handler/handler_gateways.go index 2718dace6e9aa..22ee1d5695306 100644 --- a/lib/teleterm/apiserver/handler/handler_gateways.go +++ b/lib/teleterm/apiserver/handler/handler_gateways.go @@ -21,12 +21,12 @@ package handler import ( "context" "fmt" - "os/exec" "strings" "github.com/gravitational/trace" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" + "github.com/gravitational/teleport/lib/teleterm/cmd" "github.com/gravitational/teleport/lib/teleterm/daemon" "github.com/gravitational/teleport/lib/teleterm/gateway" ) @@ -82,7 +82,7 @@ func (s *Handler) RemoveGateway(ctx context.Context, req *api.RemoveGatewayReque } func (s *Handler) newAPIGateway(gateway gateway.Gateway) (*api.Gateway, error) { - command, err := s.DaemonService.GetGatewayCLICommand(gateway) + cmds, err := s.DaemonService.GetGatewayCLICommand(gateway) if err != nil { return nil, trace.Wrap(err) } @@ -96,21 +96,21 @@ func (s *Handler) newAPIGateway(gateway gateway.Gateway) (*api.Gateway, error) { Protocol: gateway.Protocol(), LocalAddress: gateway.LocalAddress(), LocalPort: gateway.LocalPort(), - GatewayCliCommand: makeGatewayCLICommand(command), + GatewayCliCommand: makeGatewayCLICommand(cmds), }, nil } -func makeGatewayCLICommand(cmd *exec.Cmd) *api.GatewayCLICommand { - cmdString := strings.TrimSpace( +func makeGatewayCLICommand(cmds cmd.Cmds) *api.GatewayCLICommand { + preview := strings.TrimSpace( fmt.Sprintf("%s %s", - strings.Join(cmd.Env, " "), - strings.Join(cmd.Args, " "))) + strings.Join(cmds.Preview.Env, " "), + strings.Join(cmds.Preview.Args, " "))) return &api.GatewayCLICommand{ - Path: cmd.Path, - Args: cmd.Args, - Env: cmd.Env, - Preview: cmdString, + Path: cmds.Exec.Path, + Args: cmds.Exec.Args, + Env: cmds.Exec.Env, + Preview: preview, } } diff --git a/lib/teleterm/apiserver/handler/handler_gateways_test.go b/lib/teleterm/apiserver/handler/handler_gateways_test.go index 32ebb0e5025c9..bf93a1c5beddc 100644 --- a/lib/teleterm/apiserver/handler/handler_gateways_test.go +++ b/lib/teleterm/apiserver/handler/handler_gateways_test.go @@ -26,29 +26,30 @@ import ( "github.com/stretchr/testify/require" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" + "github.com/gravitational/teleport/lib/teleterm/cmd" ) func Test_makeGatewayCLICommand(t *testing.T) { absPath, err := filepath.Abs("test-binary") require.NoError(t, err) - // Call exec.Command with a relative path so that cmd.Args[0] is a relative path. - // Then replace cmd.Path with an absolute path to simulate binary being resolved to + // Call exec.Command with a relative path so that command.Args[0] is a relative path. + // Then replace command.Path with an absolute path to simulate binary being resolved to // an absolute path. This way we can later verify that gateway.CLICommand doesn't use the absolute // path. // // This also ensures that exec.Command behaves the same way on different devices, no matter // whether a command like postgres is installed on the system or not. - cmd := exec.Command("test-binary", "arg1", "arg2") - cmd.Path = absPath - cmd.Env = []string{"FOO=bar"} + command := exec.Command("test-binary", "arg1", "arg2") + command.Path = absPath + command.Env = []string{"FOO=bar"} - command := makeGatewayCLICommand(cmd) + gatewayCmd := makeGatewayCLICommand(cmd.Cmds{Exec: command, Preview: command}) require.Equal(t, &api.GatewayCLICommand{ Path: absPath, Args: []string{"test-binary", "arg1", "arg2"}, Env: []string{"FOO=bar"}, Preview: "FOO=bar test-binary arg1 arg2", - }, command) + }, gatewayCmd) } diff --git a/lib/teleterm/cmd/db.go b/lib/teleterm/cmd/db.go index 87579233c9719..b5f4ff2f63fa3 100644 --- a/lib/teleterm/cmd/db.go +++ b/lib/teleterm/cmd/db.go @@ -24,18 +24,31 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/client/db/dbcmd" + "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/teleterm/clusters" "github.com/gravitational/teleport/lib/teleterm/gateway" "github.com/gravitational/teleport/lib/tlsca" ) +// Cmds represents a single command in two variants – one that can be used to spawn a process and +// one that can be copied and pasted into a terminal. +type Cmds struct { + // Exec is the command that should be used when directly executing a command for the given + // gateway. + Exec *exec.Cmd + // Preview is the command that should be used to display the command in the UI. Typically this + // means that Preview includes quotes around special characters, so that the command gets executed + // properly when the user copies and then pastes it into a terminal. + Preview *exec.Cmd +} + // NewDBCLICommand creates CLI commands for database gateway. -func NewDBCLICommand(cluster *clusters.Cluster, gateway gateway.Gateway) (*exec.Cmd, error) { - cmd, err := newDBCLICommandWithExecer(cluster, gateway, dbcmd.SystemExecer{}) - return cmd, trace.Wrap(err) +func NewDBCLICommand(cluster *clusters.Cluster, gateway gateway.Gateway) (Cmds, error) { + cmds, err := newDBCLICommandWithExecer(cluster, gateway, dbcmd.SystemExecer{}) + return cmds, trace.Wrap(err) } -func newDBCLICommandWithExecer(cluster *clusters.Cluster, gateway gateway.Gateway, execer dbcmd.Execer) (*exec.Cmd, error) { +func newDBCLICommandWithExecer(cluster *clusters.Cluster, gateway gateway.Gateway, execer dbcmd.Execer) (Cmds, error) { routeToDb := tlsca.RouteToDatabase{ ServiceName: gateway.TargetName(), Protocol: gateway.Protocol(), @@ -43,17 +56,35 @@ func newDBCLICommandWithExecer(cluster *clusters.Cluster, gateway gateway.Gatewa Database: gateway.TargetSubresourceName(), } - cmd, err := clusters.NewDBCLICmdBuilder(cluster, routeToDb, + opts := []dbcmd.ConnectCommandFunc{ dbcmd.WithLogger(gateway.Log()), dbcmd.WithLocalProxy(gateway.LocalAddress(), gateway.LocalPortInt(), ""), dbcmd.WithNoTLS(), - dbcmd.WithPrintFormat(), dbcmd.WithTolerateMissingCLIClient(), dbcmd.WithExecer(execer), - ).GetConnectCommand() + } + + // DynamoDB doesn't support non-print-format use. + if gateway.Protocol() == defaults.ProtocolDynamoDB { + opts = append(opts, dbcmd.WithPrintFormat()) + } + + previewOpts := append(opts, dbcmd.WithPrintFormat()) + + execCmd, err := clusters.NewDBCLICmdBuilder(cluster, routeToDb, opts...).GetConnectCommand() + if err != nil { + return Cmds{}, trace.Wrap(err) + } + + previewCmd, err := clusters.NewDBCLICmdBuilder(cluster, routeToDb, previewOpts...).GetConnectCommand() if err != nil { - return nil, trace.Wrap(err) + return Cmds{}, trace.Wrap(err) + } + + cmds := Cmds{ + Exec: execCmd, + Preview: previewCmd, } - return cmd, nil + return cmds, nil } diff --git a/lib/teleterm/cmd/db_test.go b/lib/teleterm/cmd/db_test.go index 4f892a87c2857..e080a0cff6cc9 100644 --- a/lib/teleterm/cmd/db_test.go +++ b/lib/teleterm/cmd/db_test.go @@ -19,6 +19,7 @@ package cmd import ( + "fmt" "os/exec" "path/filepath" "testing" @@ -52,13 +53,14 @@ type fakeDatabaseGateway struct { gateway.Database targetURI uri.ResourceURI subresourceName string + protocol string } func (m fakeDatabaseGateway) TargetURI() uri.ResourceURI { return m.targetURI } func (m fakeDatabaseGateway) TargetName() string { return m.targetURI.GetDbName() } func (m fakeDatabaseGateway) TargetUser() string { return "alice" } func (m fakeDatabaseGateway) TargetSubresourceName() string { return m.subresourceName } -func (m fakeDatabaseGateway) Protocol() string { return defaults.ProtocolMongoDB } +func (m fakeDatabaseGateway) Protocol() string { return m.protocol } func (m fakeDatabaseGateway) Log() *logrus.Entry { return nil } func (m fakeDatabaseGateway) LocalAddress() string { return "localhost" } func (m fakeDatabaseGateway) LocalPortInt() int { return 8888 } @@ -68,14 +70,27 @@ func TestNewDBCLICommand(t *testing.T) { testCases := []struct { name string targetSubresourceName string + argsCount int + protocol string + checkCmds func(*testing.T, fakeDatabaseGateway, Cmds) }{ { name: "empty name", + protocol: defaults.ProtocolMongoDB, targetSubresourceName: "", + checkCmds: checkMongoCmds, }, { name: "with name", + protocol: defaults.ProtocolMongoDB, targetSubresourceName: "bar", + checkCmds: checkMongoCmds, + }, + { + name: "custom handling of DynamoDB does not blow up", + targetSubresourceName: "bar", + protocol: defaults.ProtocolDynamoDB, + checkCmds: checkArgsNotEmpty, }, } @@ -88,14 +103,38 @@ func TestNewDBCLICommand(t *testing.T) { mockGateway := fakeDatabaseGateway{ targetURI: cluster.URI.AppendDB("foo"), subresourceName: tc.targetSubresourceName, + protocol: tc.protocol, } - command, err := newDBCLICommandWithExecer(&cluster, mockGateway, fakeExec{}) - + cmds, err := newDBCLICommandWithExecer(&cluster, mockGateway, fakeExec{}) require.NoError(t, err) - require.Len(t, command.Args, 2) - require.Contains(t, command.Args[1], tc.targetSubresourceName) - require.Contains(t, command.Args[1], mockGateway.LocalPort()) + + tc.checkCmds(t, mockGateway, cmds) }) } } + +func checkMongoCmds(t *testing.T, gw fakeDatabaseGateway, cmds Cmds) { + t.Helper() + require.Len(t, cmds.Exec.Args, 2) + require.Len(t, cmds.Preview.Args, 2) + + execConnString := cmds.Exec.Args[1] + previewConnString := cmds.Preview.Args[1] + + require.Contains(t, execConnString, gw.TargetSubresourceName()) + require.Contains(t, previewConnString, gw.TargetSubresourceName()) + require.Contains(t, execConnString, gw.LocalPort()) + require.Contains(t, previewConnString, gw.LocalPort()) + + // Verify that the preview cmd has exec cmd conn string wrapped in quotes. + require.NotContains(t, execConnString, "\"") + expectedPreviewConnString := fmt.Sprintf("%q", execConnString) + require.Equal(t, expectedPreviewConnString, previewConnString) +} + +func checkArgsNotEmpty(t *testing.T, gw fakeDatabaseGateway, cmds Cmds) { + t.Helper() + require.NotEmpty(t, cmds.Exec.Args) + require.NotEmpty(t, cmds.Preview.Args) +} diff --git a/lib/teleterm/cmd/kube.go b/lib/teleterm/cmd/kube.go index 59fa67b6402c7..e72aeb167341a 100644 --- a/lib/teleterm/cmd/kube.go +++ b/lib/teleterm/cmd/kube.go @@ -29,14 +29,14 @@ import ( ) // NewKubeCLICommand creates CLI commands for kube gateways. -func NewKubeCLICommand(g gateway.Gateway) (*exec.Cmd, error) { +func NewKubeCLICommand(g gateway.Gateway) (Cmds, error) { kube, err := gateway.AsKube(g) if err != nil { - return nil, trace.Wrap(err) + return Cmds{}, trace.Wrap(err) } // Use kubectl version as placeholders. Only env should be used. cmd := exec.Command("kubectl", "version") cmd.Env = []string{fmt.Sprintf("%v=%v", teleport.EnvKubeConfig, kube.KubeconfigPath())} - return cmd, nil + return Cmds{Exec: cmd, Preview: cmd}, nil } diff --git a/lib/teleterm/cmd/kube_test.go b/lib/teleterm/cmd/kube_test.go index 34403f85ef6c0..27f012da192ff 100644 --- a/lib/teleterm/cmd/kube_test.go +++ b/lib/teleterm/cmd/kube_test.go @@ -35,5 +35,5 @@ func (m fakeKubeGateway) KubeconfigPath() string { return "test.kubeconfig" } func TestNewKubeCLICommand(t *testing.T) { cmd, err := NewKubeCLICommand(fakeKubeGateway{}) require.NoError(t, err) - require.Equal(t, []string{"KUBECONFIG=test.kubeconfig"}, cmd.Env) + require.Equal(t, []string{"KUBECONFIG=test.kubeconfig"}, cmd.Exec.Env) } diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index f3ce7cf92c5a8..25b17df33f850 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -462,27 +462,28 @@ func (s *Service) ListGateways() []gateway.Gateway { } // GetGatewayCLICommand creates the CLI command used for the provided gateway. -func (s *Service) GetGatewayCLICommand(gateway gateway.Gateway) (*exec.Cmd, error) { +func (s *Service) GetGatewayCLICommand(gateway gateway.Gateway) (cmd.Cmds, error) { targetURI := gateway.TargetURI() switch { case targetURI.IsDB(): cluster, _, err := s.cfg.Storage.GetByResourceURI(targetURI) if err != nil { - return nil, trace.Wrap(err) + return cmd.Cmds{}, trace.Wrap(err) } - cmd, err := cmd.NewDBCLICommand(cluster, gateway) - return cmd, trace.Wrap(err) + cmds, err := cmd.NewDBCLICommand(cluster, gateway) + return cmds, trace.Wrap(err) case targetURI.IsKube(): - cmd, err := cmd.NewKubeCLICommand(gateway) - return cmd, trace.Wrap(err) + cmds, err := cmd.NewKubeCLICommand(gateway) + return cmds, trace.Wrap(err) case targetURI.IsApp(): - return exec.Command(""), nil + blankCmd := exec.Command("") + return cmd.Cmds{Exec: blankCmd, Preview: blankCmd}, nil default: - return nil, trace.NotImplemented("gateway not supported for %v", targetURI) + return cmd.Cmds{}, trace.NotImplemented("gateway not supported for %v", targetURI) } } diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 07bb6afa18013..a09a05e684be0 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -23,7 +23,6 @@ import ( "net" "net/http" "net/http/httptest" - "os/exec" "sync/atomic" "testing" "time" @@ -43,6 +42,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/clusters" + "github.com/gravitational/teleport/lib/teleterm/cmd" "github.com/gravitational/teleport/lib/teleterm/gateway" "github.com/gravitational/teleport/lib/teleterm/gatewaytest" "github.com/gravitational/teleport/lib/tlsca" @@ -666,7 +666,7 @@ func TestGetGatewayCLICommand(t *testing.T) { name string inputGateway gateway.Gateway checkError require.ErrorAssertionFunc - checkCmd func(*testing.T, *exec.Cmd) + checkCmds func(*testing.T, cmd.Cmds) }{ { name: "unsupported gateway", @@ -674,7 +674,7 @@ func TestGetGatewayCLICommand(t *testing.T) { targetURI: uri.NewClusterURI("profile").AppendServer("server"), }, checkError: require.Error, - checkCmd: func(*testing.T, *exec.Cmd) {}, + checkCmds: func(*testing.T, cmd.Cmds) {}, }, { name: "database gateway", @@ -683,10 +683,12 @@ func TestGetGatewayCLICommand(t *testing.T) { subresourceName: "subresource-name", }, checkError: require.NoError, - checkCmd: func(t *testing.T, cmd *exec.Cmd) { + checkCmds: func(t *testing.T, cmds cmd.Cmds) { t.Helper() - require.Len(t, cmd.Args, 2) - require.Contains(t, cmd.Args[1], "subresource-name") + require.Len(t, cmds.Exec.Args, 2) + require.Contains(t, cmds.Exec.Args[1], "subresource-name") + require.Len(t, cmds.Preview.Args, 2) + require.Contains(t, cmds.Preview.Args[1], "subresource-name") }, }, { @@ -695,18 +697,19 @@ func TestGetGatewayCLICommand(t *testing.T) { targetURI: uri.NewClusterURI("profile").AppendKube("kube"), }, checkError: require.NoError, - checkCmd: func(t *testing.T, cmd *exec.Cmd) { + checkCmds: func(t *testing.T, cmds cmd.Cmds) { t.Helper() - require.Equal(t, []string{"KUBECONFIG=test.kubeconfig"}, cmd.Env) + require.Equal(t, []string{"KUBECONFIG=test.kubeconfig"}, cmds.Exec.Env) + require.Equal(t, []string{"KUBECONFIG=test.kubeconfig"}, cmds.Preview.Env) }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - cmd, err := daemon.GetGatewayCLICommand(test.inputGateway) + cmds, err := daemon.GetGatewayCLICommand(test.inputGateway) test.checkError(t, err) - test.checkCmd(t, cmd) + test.checkCmds(t, cmds) }) } } diff --git a/proto/teleport/lib/teleterm/v1/gateway.proto b/proto/teleport/lib/teleterm/v1/gateway.proto index d416c9e83fe75..7661a6bf31f4a 100644 --- a/proto/teleport/lib/teleterm/v1/gateway.proto +++ b/proto/teleport/lib/teleterm/v1/gateway.proto @@ -59,8 +59,9 @@ message Gateway { GatewayCLICommand gateway_cli_command = 10; } -// GatewayCLICommand represents a command that the user can execute to connect to the gateway -// resource. It is a direct translation of os.exec.Cmd. +// GatewayCLICommand represents a command that the user can execute to connect to a gateway +// resource. It is a combination of two different os/exec.Cmd structs, where path, args and env are +// directly taken from one Cmd and the preview field is constructed from another Cmd. message GatewayCLICommand { // path is the absolute path to the CLI client of a gateway if the client is // in PATH. Otherwise, the name of the program we were trying to find. @@ -72,11 +73,15 @@ message GatewayCLICommand { // invocation. The elements of the list are in the format of NAME=value. repeated string env = 3; // preview is used to show the user what command will be executed before they decide to run it. - // It's like os.exec.Cmd.String with two exceptions: + // It can also be copied and then pasted into a terminal. + // It's like os/exec.Cmd.String with two exceptions: // // 1) It is prepended with Cmd.Env. // 2) The command name is relative and not absolute. + // 3) It is taken from a different Cmd than the other fields in this message. This Cmd uses a + // special print format which makes the args suitable to be entered into a terminal, but not to + // directly spawn a process. // - // Should not be used to execute anything in the shell. + // Should not be used to execute the command in the shell. Instead, use path, args, and env. string preview = 4; }