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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions lib/client/db/dbcmd/dbcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"

"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/db"
"github.com/gravitational/teleport/lib/client/db/mysql"
Expand Down Expand Up @@ -78,6 +79,8 @@ const (
awsBin = "aws"
// oracleBin is the Oracle CLI program name.
oracleBin = "sql"
// spannerBin is a Google Spanner interactive CLI program name.
spannerBin = "spanner-cli"
)

// Execer is an abstraction of Go's exec module, as this one doesn't specify any interfaces.
Expand Down Expand Up @@ -206,6 +209,8 @@ func (c *CLICommandBuilder) GetConnectCommand() (*exec.Cmd, error) {
case defaults.ProtocolClickHouse:
return c.getClickhouseNativeCommand()

case defaults.ProtocolSpanner:
return c.getSpannerCommand()
}

return nil, trace.BadParameter("unsupported database protocol: %v", c.db)
Expand Down Expand Up @@ -668,15 +673,23 @@ func (c *CLICommandBuilder) getOpenSearchCLICommand() (*exec.Cmd, error) {
return exec.Command(openSearchCLIBin, args...), nil
}

func (c *CLICommandBuilder) getDynamoDBCommand() (*exec.Cmd, error) {
// we can't guess at what the user wants to do, so this command is for print purposes only,
// and it only works with a local proxy tunnel.
if !c.options.printFormat || !c.options.noTLS || c.options.localProxyHost == "" || c.options.localProxyPort == 0 {
func (c *CLICommandBuilder) checkLocalProxyTunnelOnly(requirePrint bool) error {
if (requirePrint && !c.options.printFormat) || !c.options.noTLS || c.options.localProxyHost == "" || c.options.localProxyPort == 0 {
svc := "<db>"
if c.db != nil && c.db.ServiceName != "" {
svc = c.db.ServiceName
}
return nil, trace.BadParameter("DynamoDB requires a local proxy tunnel. Use `tsh proxy db --tunnel %v`", svc)
protocol := defaults.ReadableDatabaseProtocol(c.db.Protocol)
return trace.BadParameter("%s requires a local proxy tunnel. Use `tsh proxy db --tunnel %v`", protocol, svc)
}
return nil
}

func (c *CLICommandBuilder) getDynamoDBCommand() (*exec.Cmd, error) {
// we can't guess at what the user wants to do, so this command is for print
// purposes only, and it only works with a local proxy tunnel.
if err := c.checkLocalProxyTunnelOnly(true); err != nil {
return nil, trace.Wrap(err)
}
args := []string{
"--endpoint", fmt.Sprintf("http://%v:%v/", c.options.localProxyHost, c.options.localProxyPort),
Expand All @@ -686,6 +699,52 @@ func (c *CLICommandBuilder) getDynamoDBCommand() (*exec.Cmd, error) {
return exec.Command(awsBin, args...), nil
}

func (c *CLICommandBuilder) getSpannerCommand() (*exec.Cmd, error) {
if err := c.checkLocalProxyTunnelOnly(false); err != nil {
return nil, trace.Wrap(err)
}
var (
project,
instance,
database string
)
if c.options.printFormat {
// default placeholders for a print command if not all info is available
project, instance, database = "<project>", "<instance>", "<database>"
}

if c.options.gcp.ProjectID != "" {
project = c.options.gcp.ProjectID
}
if c.options.gcp.InstanceID != "" {
instance = c.options.gcp.InstanceID
}
if c.db.Database != "" {
database = c.db.Database
}

protocol := defaults.ReadableDatabaseProtocol(c.db.Protocol)
switch {
case project == "":
return nil, trace.BadParameter("missing GCP project ID for %s command (this is a bug)", protocol)
case instance == "":
return nil, trace.BadParameter("missing GCP instance ID for %s command (this is a bug)", protocol)
case database == "":
return nil, trace.BadParameter("missing database name for %s command (this is a bug)", protocol)
}

args := []string{
"-p", project,
"-i", instance,
"-d", database,
}
cmd := exec.Command(spannerBin, args...)
cmd.Env = append(cmd.Env,
fmt.Sprintf("SPANNER_EMULATOR_HOST=%s:%d", c.host, c.port),
)
return cmd, nil
}

type jdbcOracleThinConnection struct {
host string
port int
Expand Down Expand Up @@ -814,6 +873,7 @@ type connectionCommandOpts struct {
log *logrus.Entry
exe Execer
password string
gcp types.GCPCloudSQL
}

// ConnectCommandFunc is a type for functions returned by the "With*" functions in this package.
Expand Down Expand Up @@ -903,6 +963,13 @@ func WithExecer(exe Execer) ConnectCommandFunc {
}
}

// WithGCP adds GCP metadata for the database command to access.
func WithGCP(gcp types.GCPCloudSQL) ConnectCommandFunc {
return func(opts *connectionCommandOpts) {
opts.gcp = gcp
}
}

const (
// envVarMongoServerSelectionTimeoutMS is the environment variable that
// controls the server selection timeout used for MongoDB clients.
Expand Down
97 changes: 97 additions & 0 deletions lib/client/db/dbcmd/dbcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
Expand Down Expand Up @@ -630,6 +631,102 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) {
cmd: []string{"sql", "-L", "'jdbc:oracle:thin:@tcps://localhost:12345/oracle01?TNS_ADMIN=/tmp/keys/example.com/bob-db/mysql-wallet'"},
wantErr: false,
},
{
name: "Spanner for exec is ok",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{ProjectID: "foo-proj", InstanceID: "bar-instance"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
cmd: []string{"spanner-cli", "-p", "foo-proj", "-i", "bar-instance", "-d", "googlesql-db"},
wantErr: false,
},
{
name: "Spanner with print format is ok",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithPrintFormat(),
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{ProjectID: "foo-proj", InstanceID: "bar-instance"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
cmd: []string{"spanner-cli", "-p", "foo-proj", "-i", "bar-instance", "-d", "googlesql-db"},
wantErr: false,
},
{
name: "Spanner with print format and placeholders is ok",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithPrintFormat(),
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{}),
},
execer: &fakeExec{},
databaseName: "",
cmd: []string{"spanner-cli", "-p", "<project>", "-i", "<instance>", "-d", "<database>"},
wantErr: false,
},
{
name: "Spanner for exec without GCP project is an error",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{InstanceID: "bar-instance"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
wantErr: true,
},
{
name: "Spanner for exec without GCP instance is an error",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{ProjectID: "foo-proj"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
wantErr: true,
},
{
name: "Spanner for exec without database name is an error",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithLocalProxy("localhost", 12345, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{ProjectID: "foo-proj"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
wantErr: true,
},
{
name: "Spanner without a local proxy is an error",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{
WithLocalProxy("", 0, ""),
WithNoTLS(),
WithGCP(types.GCPCloudSQL{ProjectID: "foo-proj", InstanceID: "bar-instance"}),
},
execer: &fakeExec{},
databaseName: "googlesql-db",
wantErr: true,
},
{
name: "Spanner with TLS local proxy is an error",
dbProtocol: defaults.ProtocolSpanner,
opts: []ConnectCommandFunc{WithPrintFormat(), WithLocalProxy("localhost", 12345, "")},
execer: &fakeExec{},
wantErr: true,
},
}

for _, tt := range tests {
Expand Down
8 changes: 6 additions & 2 deletions lib/teleterm/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ func newDBCLICommandWithExecer(cluster *clusters.Cluster, gateway gateway.Gatewa
dbcmd.WithExecer(execer),
}

// DynamoDB doesn't support non-print-format use.
if gateway.Protocol() == defaults.ProtocolDynamoDB {
switch gateway.Protocol() {
case defaults.ProtocolDynamoDB, defaults.ProtocolSpanner:
// DynamoDB doesn't support non-print-format use.
// Spanner does, but it's not supported in Teleterm yet.
// TODO(gavin): get the database GCP metadata to enable spanner-cli in
// Teleterm.
opts = append(opts, dbcmd.WithPrintFormat())
}

Expand Down
6 changes: 6 additions & 0 deletions lib/teleterm/cmd/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ func TestNewDBCLICommand(t *testing.T) {
protocol: defaults.ProtocolDynamoDB,
checkCmds: checkArgsNotEmpty,
},
{
name: "custom handling of Spanner does not blow up",
targetSubresourceName: "bar",
protocol: defaults.ProtocolSpanner,
checkCmds: checkArgsNotEmpty,
},
}

for _, tc := range testCases {
Expand Down
13 changes: 6 additions & 7 deletions tool/tsh/common/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ func onDatabaseConnect(cf *CLIConf) error {
if opts, err = maybeAddDBUserPassword(cf, tc, dbInfo, opts); err != nil {
return trace.Wrap(err)
}
if opts, err = maybeAddGCPMetadata(cf.Context, tc, dbInfo, opts); err != nil {
return trace.Wrap(err)
}

bb := dbcmd.NewCmdBuilder(tc, profile, dbInfo.RouteToDatabase, rootClusterName, opts...)
cmd, err := bb.GetConnectCommand()
Expand Down Expand Up @@ -1642,7 +1645,8 @@ func getDBLocalProxyRequirement(tc *client.TeleportClient, route tlsca.RouteToDa
defaults.ProtocolSQLServer,
defaults.ProtocolCassandra,
defaults.ProtocolOracle,
defaults.ProtocolClickHouse:
defaults.ProtocolClickHouse,
defaults.ProtocolSpanner:

// Some protocols only work in the local tunnel mode.
out.addLocalProxyWithTunnel(formatDBProtocolReason(route.Protocol))
Expand Down Expand Up @@ -1736,12 +1740,7 @@ func formatDbCmdUnsupportedDBProtocol(cf *CLIConf, route tlsca.RouteToDatabase)
// getDbCmdAlternatives is a helper func that returns alternative tsh commands for connecting to a database.
func getDbCmdAlternatives(clusterFlag string, route tlsca.RouteToDatabase) []string {
var alts []string
switch route.Protocol {
case defaults.ProtocolDynamoDB:
// DynamoDB only works with a local proxy tunnel and there is no "shell-like" cli, so `tsh db connect` doesn't make sense.
case defaults.ProtocolClickHouseHTTP:
// ClickHouse HTTP protocol don't support interactive mode
default:
if protocolSupportsInteractiveMode(route.Protocol) {
// prefer displaying the connect command as the first suggested command alternative.
alts = append(alts, formatDatabaseConnectCommand(clusterFlag, route))
}
Expand Down
Loading