mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
1086 lines
38 KiB
Go
1086 lines
38 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package dbcmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gravitational/trace"
|
|
"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"
|
|
"github.com/gravitational/teleport/lib/client/db/opensearch"
|
|
"github.com/gravitational/teleport/lib/client/db/postgres"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
const (
|
|
// postgresBin is the Postgres client binary name.
|
|
postgresBin = "psql"
|
|
// cockroachBin is the Cockroach client binary name.
|
|
cockroachBin = "cockroach"
|
|
// mysqlBin is the MySQL client binary name.
|
|
mysqlBin = "mysql"
|
|
// mariadbBin is the MariaDB client binary name.
|
|
mariadbBin = "mariadb"
|
|
// mongoshBin is the Mongo Shell client binary name.
|
|
mongoshBin = "mongosh"
|
|
// mongoBin is the Mongo client binary name.
|
|
mongoBin = "mongo"
|
|
// redisBin is the Redis client binary name.
|
|
redisBin = "redis-cli"
|
|
// mssqlBin is the SQL Server client program name.
|
|
mssqlBin = "mssql-cli"
|
|
// sqlcmd is the SQL Server client program name.
|
|
sqlcmdBin = "sqlcmd"
|
|
// snowsqlBin is the Snowflake client program name.
|
|
snowsqlBin = "snowsql"
|
|
// cqlshBin is the Cassandra client program name.
|
|
cqlshBin = "cqlsh"
|
|
// curlBin is the program name for `curl`, which is used as Elasticsearch client if other options are unavailable.
|
|
curlBin = "curl"
|
|
// elasticsearchSQLBin is the Elasticsearch SQL client program name.
|
|
elasticsearchSQLBin = "elasticsearch-sql-cli"
|
|
// openSearchCLIBin is the OpenSearch CLI client program name.
|
|
openSearchCLIBin = "opensearch-cli"
|
|
// openSearchSQLBin is the OpenSearch SQL client program name.
|
|
openSearchSQLBin = "opensearchsql"
|
|
// awsBin is the aws CLI program name.
|
|
awsBin = "aws"
|
|
// sqlclBin is the SQLcl program name (Oracle client).
|
|
sqlclBin = "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.
|
|
// This interface exists only to enable mocking.
|
|
type Execer interface {
|
|
// RunCommand runs a system command.
|
|
RunCommand(name string, arg ...string) ([]byte, error)
|
|
// LookPath returns a full path to a binary if this one is found in system PATH,
|
|
// error otherwise.
|
|
LookPath(file string) (string, error)
|
|
}
|
|
|
|
// SystemExecer implements execer interface by using Go exec module.
|
|
type SystemExecer struct{}
|
|
|
|
// RunCommand is a wrapper for exec.Command(...).Output()
|
|
func (s SystemExecer) RunCommand(name string, arg ...string) ([]byte, error) {
|
|
return exec.Command(name, arg...).Output()
|
|
}
|
|
|
|
// LookPath is a wrapper for exec.LookPath(...)
|
|
func (s SystemExecer) LookPath(file string) (string, error) {
|
|
return exec.LookPath(file)
|
|
}
|
|
|
|
// CLICommandBuilder holds data needed to build a CLI command from args passed to NewCmdBuilder.
|
|
// Any calls to the exec package within CLICommandBuilder methods that need to be mocked should
|
|
// use the exe field rather than calling the package directly.
|
|
type CLICommandBuilder struct {
|
|
tc *client.TeleportClient
|
|
rootCluster string
|
|
// profile is the currently selected tsh profile.
|
|
//
|
|
// Note that profile.Cluster indicates the cluster selected with `tsh login
|
|
// <root/leaf>`. However, the target cluster can be overwritten with
|
|
// --cluster flag. Therefore profile.Cluster is not suitable for
|
|
// determining the target cluster or the root cluster. Use tc.SiteName for
|
|
// the target cluster and rootCluster for root cluster.
|
|
profile *client.ProfileStatus
|
|
db *tlsca.RouteToDatabase
|
|
host string
|
|
port int
|
|
options connectionCommandOpts
|
|
uid utils.UID
|
|
getDatabaseFunc GetDatabaseFunc
|
|
}
|
|
|
|
func NewCmdBuilder(
|
|
tc *client.TeleportClient,
|
|
profile *client.ProfileStatus,
|
|
db tlsca.RouteToDatabase,
|
|
rootClusterName string,
|
|
getDatabaseFunc GetDatabaseFunc,
|
|
opts ...ConnectCommandFunc,
|
|
) (*CLICommandBuilder, error) {
|
|
var options connectionCommandOpts
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
// In TLS routing mode a local proxy is started on demand so connect to it.
|
|
host := options.localProxyHost
|
|
port := options.localProxyPort
|
|
if host == "" || port == 0 {
|
|
host, port = tc.DatabaseProxyHostPort(db)
|
|
}
|
|
|
|
if options.logger == nil {
|
|
options.logger = slog.Default()
|
|
}
|
|
|
|
if options.exe == nil {
|
|
options.exe = &SystemExecer{}
|
|
}
|
|
|
|
if getDatabaseFunc == nil {
|
|
return nil, trace.BadParameter("GetDatabaseFunc is required and cannot be nil")
|
|
}
|
|
|
|
return &CLICommandBuilder{
|
|
tc: tc,
|
|
profile: profile,
|
|
db: &db,
|
|
host: host,
|
|
port: port,
|
|
options: options,
|
|
rootCluster: rootClusterName,
|
|
getDatabaseFunc: getDatabaseFunc,
|
|
uid: utils.NewRealUID(),
|
|
}, nil
|
|
}
|
|
|
|
// GetConnectCommand returns a command that can connect the user directly to the given database
|
|
// using an appropriate CLI database client. It takes into account cluster configuration, binaries
|
|
// available on the system and in some cases it even connects to the database to check which exact
|
|
// version of the database the user is running.
|
|
//
|
|
// Underneath it uses exec.Command, so the resulting command will always be expanded to its absolute
|
|
// path if exec.LookPath was able to find the given binary on user's system.
|
|
//
|
|
// If CLICommandBuilder's options.tolerateMissingCLIClient is set to true, GetConnectCommand
|
|
// shouldn't return an error if it cannot locate a client binary. Check WithTolerateMissingCLIClient
|
|
// docs for more details.
|
|
func (c *CLICommandBuilder) GetConnectCommand(ctx context.Context) (*exec.Cmd, error) {
|
|
switch c.db.Protocol {
|
|
case defaults.ProtocolPostgres:
|
|
return c.getPostgresCommand(), nil
|
|
|
|
case defaults.ProtocolCockroachDB:
|
|
return c.getCockroachCommand(), nil
|
|
|
|
case defaults.ProtocolMySQL:
|
|
return c.getMySQLCommand()
|
|
|
|
case defaults.ProtocolMongoDB:
|
|
return c.getMongoCommand(ctx)
|
|
|
|
case defaults.ProtocolRedis:
|
|
return c.getRedisCommand(), nil
|
|
|
|
case defaults.ProtocolSQLServer:
|
|
return c.getSQLServerCommand(), nil
|
|
|
|
case defaults.ProtocolSnowflake:
|
|
return c.getSnowflakeCommand(), nil
|
|
|
|
case defaults.ProtocolCassandra:
|
|
return c.getCassandraCommand()
|
|
|
|
case defaults.ProtocolElasticsearch:
|
|
return c.getElasticsearchCommand()
|
|
|
|
case defaults.ProtocolOpenSearch:
|
|
return c.getOpenSearchCommand()
|
|
|
|
case defaults.ProtocolDynamoDB:
|
|
return c.getDynamoDBCommand()
|
|
|
|
case defaults.ProtocolOracle:
|
|
return c.getOracleCommand()
|
|
|
|
case defaults.ProtocolClickHouseHTTP:
|
|
return c.getClickhouseHTTPCommand()
|
|
|
|
case defaults.ProtocolClickHouse:
|
|
return c.getClickhouseNativeCommand()
|
|
|
|
case defaults.ProtocolSpanner:
|
|
return c.getSpannerCommand(ctx)
|
|
}
|
|
|
|
return nil, trace.BadParameter("unsupported database protocol: %v", c.db)
|
|
}
|
|
|
|
// CommandAlternative represents alternative command along with description.
|
|
type CommandAlternative struct {
|
|
Description string
|
|
Command *exec.Cmd
|
|
}
|
|
|
|
// GetConnectCommandAlternatives returns optional connection commands for protocols that offer multiple options.
|
|
// Otherwise, it falls back to GetConnectCommand.
|
|
// The keys in the returned map are command descriptions suitable for display to the end user.
|
|
func (c *CLICommandBuilder) GetConnectCommandAlternatives(ctx context.Context) ([]CommandAlternative, error) {
|
|
switch c.db.Protocol {
|
|
case defaults.ProtocolElasticsearch:
|
|
return c.getElasticsearchAlternativeCommands(), nil
|
|
case defaults.ProtocolOpenSearch:
|
|
return c.getOpenSearchAlternativeCommands(), nil
|
|
case defaults.ProtocolOracle:
|
|
return c.getOracleAlternativeCommands(), nil
|
|
}
|
|
|
|
cmd, err := c.GetConnectCommand(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return []CommandAlternative{{Description: "default command", Command: cmd}}, nil
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getPostgresCommand() *exec.Cmd {
|
|
return exec.Command(postgresBin, c.getPostgresConnString())
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getCockroachCommand() *exec.Cmd {
|
|
// If cockroach CLI client is not available, fallback to psql.
|
|
if _, err := c.options.exe.LookPath(cockroachBin); err != nil {
|
|
c.options.logger.DebugContext(context.Background(), "Couldn't find cockroach client in PATH, falling back to postgres client",
|
|
"cockroach_client", cockroachBin,
|
|
"postgres_client", postgresBin,
|
|
"error", err,
|
|
)
|
|
return c.getPostgresCommand()
|
|
}
|
|
return exec.Command(cockroachBin, "sql", "--url", c.getPostgresConnString())
|
|
}
|
|
|
|
// getPostgresConnString returns the connection string for postgres.
|
|
func (c *CLICommandBuilder) getPostgresConnString() string {
|
|
return postgres.GetConnString(
|
|
db.New(c.tc, *c.db, *c.profile, c.rootCluster, c.host, c.port),
|
|
c.options.noTLS,
|
|
c.options.printFormat,
|
|
)
|
|
}
|
|
|
|
// getMySQLCommonCmdOpts returns common command line arguments for mysql and mariadb.
|
|
// Currently, the common options are: user, database, host, port and protocol.
|
|
func (c *CLICommandBuilder) getMySQLCommonCmdOpts() []string {
|
|
args := []string{"--skip-password"}
|
|
if c.db.Username != "" {
|
|
args = append(args, "--user", c.db.Username)
|
|
}
|
|
if c.db.Database != "" {
|
|
args = append(args, "--database", c.db.Database)
|
|
}
|
|
|
|
if c.options.localProxyPort != 0 {
|
|
args = append(args, "--port", strconv.Itoa(c.options.localProxyPort))
|
|
args = append(args, "--host", c.options.localProxyHost)
|
|
// MySQL CLI treats localhost as a special value and tries to use Unix Domain Socket for connection
|
|
// To enforce TCP connection protocol needs to be explicitly specified.
|
|
if c.options.localProxyHost == "localhost" {
|
|
args = append(args, "--protocol", "TCP")
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// getMariaDBArgs returns arguments unique for mysql cmd shipped by MariaDB and mariadb cmd. Common options for mysql
|
|
// between Oracle and MariaDB version are covered by getMySQLCommonCmdOpts().
|
|
func (c *CLICommandBuilder) getMariaDBArgs() []string {
|
|
args := c.getMySQLCommonCmdOpts()
|
|
|
|
if c.options.noTLS {
|
|
return args
|
|
}
|
|
|
|
// Some options used in the MySQL options file are not compatible with the
|
|
// "mariadb" client. Thus instead of using `--defaults-group-suffix=`,
|
|
// specify the proxy host and port directly as parameters. When
|
|
// localProxyPort is specified, the --port and --host flags are set by
|
|
// getMySQLCommonCmdOpts.
|
|
if c.options.localProxyPort == 0 {
|
|
host, port := c.tc.MySQLProxyHostPort()
|
|
args = append(args, "--port", strconv.Itoa(port))
|
|
args = append(args, "--host", host)
|
|
}
|
|
|
|
sslCertPath := c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName)
|
|
sslKeyPath := c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName)
|
|
|
|
args = append(args, []string{"--ssl-key", sslKeyPath}...)
|
|
args = append(args, []string{"--ssl-ca", c.profile.CACertPathForCluster(c.rootCluster)}...)
|
|
args = append(args, []string{"--ssl-cert", sslCertPath}...)
|
|
|
|
// Flag below verifies "Common Name" check on the certificate provided by the server.
|
|
// This option is disabled by default.
|
|
if !c.tc.InsecureSkipVerify {
|
|
args = append(args, "--ssl-verify-server-cert")
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// getMySQLOracleCommand returns arguments unique for mysql cmd shipped by Oracle. Common options between
|
|
// Oracle and MariaDB version are covered by getMySQLCommonCmdOpts().
|
|
func (c *CLICommandBuilder) getMySQLOracleCommand() (*exec.Cmd, error) {
|
|
args := c.getMySQLCommonCmdOpts()
|
|
|
|
if c.options.noTLS {
|
|
return exec.Command(mysqlBin, args...), nil
|
|
}
|
|
|
|
// defaults-group-suffix must be first.
|
|
groupSuffix := []string{fmt.Sprintf("--defaults-group-suffix=_%v-%v", c.tc.SiteName, c.db.ServiceName)}
|
|
args = append(groupSuffix, args...)
|
|
|
|
if runtime.GOOS == constants.WindowsOS {
|
|
// We save configuration to ~/.my.cnf, but on Windows that file is not read,
|
|
// see tables 4.1 and 4.2 on https://dev.mysql.com/doc/refman/8.0/en/option-files.html.
|
|
// We instruct mysql client to use use that file with --defaults-extra-file.
|
|
configPath, err := c.getMySQLOptionFilePath()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
extraFile := []string{fmt.Sprintf("--defaults-extra-file=%v", configPath)}
|
|
args = append(extraFile, args...)
|
|
}
|
|
|
|
// override the ssl-mode from a config file is --insecure flag is provided to 'tsh db connect'.
|
|
if c.tc.InsecureSkipVerify {
|
|
args = append(args, fmt.Sprintf("--ssl-mode=%s", mysql.MySQLSSLModeVerifyCA))
|
|
}
|
|
|
|
return exec.Command(mysqlBin, args...), nil
|
|
}
|
|
|
|
// getMySQLOptionFilePath gets the filepath to .my.cnf from the default location
|
|
// in ~/.my.cnf, unless overridden by config.
|
|
func (c *CLICommandBuilder) getMySQLOptionFilePath() (string, error) {
|
|
if c.tc.OverrideMySQLOptionFilePath != "" {
|
|
return c.tc.OverrideMySQLOptionFilePath, nil
|
|
}
|
|
return mysql.DefaultConfigPath()
|
|
}
|
|
|
|
// getMySQLCommand returns mariadb command if the binary is on the path. Otherwise,
|
|
// mysql command is returned. Both mysql versions (MariaDB and Oracle) are supported.
|
|
func (c *CLICommandBuilder) getMySQLCommand() (*exec.Cmd, error) {
|
|
// Check if mariadb client is available. Prefer it over mysql client even if connecting to MySQL server.
|
|
if c.isMariaDBBinAvailable() {
|
|
args := c.getMariaDBArgs()
|
|
return exec.Command(mariadbBin, args...), nil
|
|
}
|
|
|
|
// Check for mysql binary. In case the caller doesn't tolerate a missing CLI client, return with
|
|
// error as mysql and mariadb are missing. There is nothing else we can do here.
|
|
if !c.isMySQLBinAvailable() {
|
|
if c.options.tolerateMissingCLIClient {
|
|
return c.getMySQLOracleCommand()
|
|
}
|
|
|
|
return nil, trace.NotFound("neither %q nor %q CLI clients were found, please make sure an appropriate CLI client is available in $PATH", mysqlBin, mariadbBin)
|
|
}
|
|
|
|
// Check which flavor is installed. Otherwise, we don't know which ssl flag to use.
|
|
// At the moment of writing mysql binary shipped by Oracle and MariaDB accept different ssl parameters and have the same name.
|
|
mySQLMariaDBFlavor, err := c.isMySQLBinMariaDBFlavor()
|
|
if mySQLMariaDBFlavor && err == nil {
|
|
args := c.getMariaDBArgs()
|
|
return exec.Command(mysqlBin, args...), nil
|
|
}
|
|
|
|
// Either we failed to check the flavor or binary comes from Oracle. Regardless return mysql/Oracle command.
|
|
return c.getMySQLOracleCommand()
|
|
}
|
|
|
|
func (c *CLICommandBuilder) isBinAvailable(file string) bool {
|
|
_, err := c.options.exe.LookPath(file)
|
|
return err == nil
|
|
}
|
|
|
|
// isMariaDBBinAvailable returns true if "mariadb" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isMariaDBBinAvailable() bool {
|
|
return c.isBinAvailable(mariadbBin)
|
|
}
|
|
|
|
// isMySQLBinAvailable returns true if "mysql" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isMySQLBinAvailable() bool {
|
|
return c.isBinAvailable(mysqlBin)
|
|
}
|
|
|
|
// isMongoshBinAvailable returns true if "mongosh" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isMongoshBinAvailable() bool {
|
|
return c.isBinAvailable(mongoshBin)
|
|
}
|
|
|
|
// isElasticsearchSQLBinAvailable returns true if "elasticsearch-sql-cli" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isElasticsearchSQLBinAvailable() bool {
|
|
return c.isBinAvailable(elasticsearchSQLBin)
|
|
}
|
|
|
|
// isOpenSearchCLIBinAvailable returns true if "opensearch-cli" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isOpenSearchCLIBinAvailable() bool {
|
|
return c.isBinAvailable(openSearchCLIBin)
|
|
}
|
|
|
|
// isOpenSearchCLIBinAvailable returns true if "opensearchsql" binary is found in the system PATH.
|
|
func (c *CLICommandBuilder) isOpenSearchSQLBinAvailable() bool {
|
|
return c.isBinAvailable(openSearchSQLBin)
|
|
}
|
|
|
|
// isMySQLBinMariaDBFlavor checks if mysql binary comes from Oracle or MariaDB.
|
|
// true is returned when binary comes from MariaDB, false when from Oracle.
|
|
func (c *CLICommandBuilder) isMySQLBinMariaDBFlavor() (bool, error) {
|
|
// Check if mysql comes from Oracle or MariaDB
|
|
mysqlVer, err := c.options.exe.RunCommand(mysqlBin, "--version")
|
|
if err != nil {
|
|
// Looks like incorrect mysql installation.
|
|
return false, trace.Wrap(err)
|
|
}
|
|
|
|
// Check which flavor is installed. Otherwise, we don't know which ssl flag to use.
|
|
// Example output:
|
|
// Oracle:
|
|
// mysql Ver 8.0.27-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))
|
|
// MariaDB:
|
|
// mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
|
|
return strings.Contains(strings.ToLower(string(mysqlVer)), "mariadb"), nil
|
|
}
|
|
|
|
func (c *CLICommandBuilder) shouldUseMongoshBin(db types.Database) bool {
|
|
// DocumentDB prefers the legacy "mongo" client.
|
|
if db.GetType() == types.DatabaseTypeDocumentDB {
|
|
return c.isMongoshBinAvailable() && !c.isBinAvailable(mongoBin)
|
|
}
|
|
// Use "mongosh" if available.
|
|
// If not, use legacy "mongo" if available.
|
|
// If both are not available, pick "mongosh" in print out.
|
|
return c.isMongoshBinAvailable() || !c.isBinAvailable(mongoBin)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getMongoCommand(ctx context.Context) (*exec.Cmd, error) {
|
|
db, err := c.getDatabase(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// look for `mongosh`
|
|
useMongosh := c.shouldUseMongoshBin(db)
|
|
|
|
var args []string
|
|
|
|
if !c.options.noTLS {
|
|
// Starting with Mongo 4.2 there is an updated set of flags.
|
|
// We are using them with `mongosh` as otherwise warnings will get displayed.
|
|
type tlsFlags struct {
|
|
tls string
|
|
tlsCertKeyFile string
|
|
tlsCAFile string
|
|
}
|
|
|
|
var flags tlsFlags
|
|
|
|
if useMongosh {
|
|
flags = tlsFlags{tls: "--tls", tlsCertKeyFile: "--tlsCertificateKeyFile", tlsCAFile: "--tlsCAFile"}
|
|
} else {
|
|
flags = tlsFlags{tls: "--ssl", tlsCertKeyFile: "--sslPEMKeyFile", tlsCAFile: "--sslCAFile"}
|
|
}
|
|
|
|
args = append(args,
|
|
flags.tls,
|
|
flags.tlsCertKeyFile,
|
|
c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName))
|
|
|
|
if c.options.caPath != "" {
|
|
// caPath is set only if mongo connects to the Teleport Proxy via ALPN SNI Local Proxy
|
|
// and connection is terminated by proxy identity certificate.
|
|
args = append(args, []string{flags.tlsCAFile, c.options.caPath}...)
|
|
} else {
|
|
// mongosh does not load system CAs by default which will cause issues if
|
|
// the proxy presents a certificate signed by a non-recognized authority
|
|
// which your system trusts (e.g. mkcert).
|
|
if useMongosh {
|
|
args = append(args, "--tlsUseSystemCA")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the address at the end. Address contains host, port, database name,
|
|
// and other options like server selection timeout.
|
|
args = append(args, c.getMongoAddress())
|
|
|
|
// use `mongosh` if available
|
|
if useMongosh {
|
|
// DocumentDB does not support retryable writes.
|
|
// https://docs.aws.amazon.com/documentdb/latest/developerguide/functional-differences.html#functional-differences.retryable-writes
|
|
if db.GetType() == types.DatabaseTypeDocumentDB {
|
|
args = append(args, "--retryWrites=false")
|
|
}
|
|
|
|
return exec.Command(mongoshBin, args...), nil
|
|
}
|
|
|
|
// fall back to `mongo` if `mongosh` isn't found
|
|
return exec.Command(mongoBin, args...), nil
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getDatabase(ctx context.Context) (types.Database, error) {
|
|
db, err := c.getDatabaseFunc(ctx, c.tc, c.db.ServiceName)
|
|
return db, trace.Wrap(err)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getMongoAddress() string {
|
|
query := make(url.Values)
|
|
|
|
// Use the same default server selection timeout (5s) that the backend
|
|
// engine is using. The environment variable serves as a hidden option to
|
|
// force a different timeout for debugging purpose or extreme situations.
|
|
serverSelectionTimeoutMS := "5000"
|
|
if envValue := os.Getenv(envVarMongoServerSelectionTimeoutMS); envValue != "" {
|
|
c.options.logger.InfoContext(context.Background(), "Using server selection timeout value from environment variable",
|
|
"environment_variable", envVarMongoServerSelectionTimeoutMS,
|
|
"server_selection_timeout", envValue,
|
|
)
|
|
serverSelectionTimeoutMS = envValue
|
|
}
|
|
query.Set("serverSelectionTimeoutMS", serverSelectionTimeoutMS)
|
|
// If directConnection is false (default for many clients), the client
|
|
// attempts to discover all servers in the replica set, and sends operations
|
|
// to the primary member.
|
|
// https://www.mongodb.com/docs/manual/reference/connection-string-options/#mongodb-urioption-urioption.directConnection
|
|
//
|
|
// Since Teleport is a proxy that appears only as a single server,
|
|
// directConnection should always be used.
|
|
//
|
|
// mongosh automatically adds the directConnection=true parameter. However,
|
|
// here we explicitly set it for other clients like MongoDB compass.
|
|
// https://www.mongodb.com/docs/mongodb-shell/connect/
|
|
query.Set("directConnection", "true")
|
|
|
|
address := url.URL{
|
|
Scheme: connstring.SchemeMongoDB,
|
|
Host: net.JoinHostPort(c.host, strconv.Itoa(c.port)),
|
|
RawQuery: query.Encode(),
|
|
Path: fmt.Sprintf("/%s", c.db.Database),
|
|
}
|
|
|
|
// Quote the address for printing as the address contains "?".
|
|
if c.options.printFormat {
|
|
return fmt.Sprintf(`"%s"`, address.String())
|
|
}
|
|
return address.String()
|
|
}
|
|
|
|
// getRedisCommand returns redis-cli commands used by 'tsh db connect' when connecting to a Redis instance.
|
|
func (c *CLICommandBuilder) getRedisCommand() *exec.Cmd {
|
|
// TODO(jakub): Add "-3" when Teleport adds support for Redis RESP3 protocol.
|
|
args := []string{
|
|
"-h", c.host,
|
|
"-p", strconv.Itoa(c.port),
|
|
}
|
|
|
|
if !c.options.noTLS {
|
|
args = append(args,
|
|
"--tls",
|
|
"--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
"--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName))
|
|
|
|
if c.tc.InsecureSkipVerify {
|
|
args = append(args, "--insecure")
|
|
}
|
|
|
|
if c.options.caPath != "" {
|
|
args = append(args, []string{"--cacert", c.options.caPath}...)
|
|
}
|
|
|
|
// Set SNI when sending to remote web proxy.
|
|
if c.options.localProxyHost == "" {
|
|
args = append(args, []string{"--sni", c.tc.WebProxyHost()}...)
|
|
}
|
|
}
|
|
|
|
// append database number if provided
|
|
if c.db.Database != "" {
|
|
args = append(args, []string{"-n", c.db.Database}...)
|
|
}
|
|
|
|
return exec.Command(redisBin, args...)
|
|
}
|
|
|
|
// getSQLServerCommand returns a command to connect to SQL Server.
|
|
// mssql-cli and sqlcmd commands have the same argument names.
|
|
//
|
|
// sqlcmd is preferred and used by default. mssql-cli is only returned when
|
|
// sqlcmd is not in PATH but mssql-cli is, so users who only have mssql-cli
|
|
// installed are still given a working command. When neither binary is in PATH
|
|
// (e.g., Teleport Connect launched from a desktop environment where $PATH is
|
|
// not set), the printed command falls back to sqlcmd, which is the actively
|
|
// maintained client.
|
|
func (c *CLICommandBuilder) getSQLServerCommand() *exec.Cmd {
|
|
args := []string{
|
|
// Host and port must be comma-separated.
|
|
"-S", fmt.Sprintf("%v,%v", c.host, c.port),
|
|
"-U", c.db.Username,
|
|
// Password is required by the client but doesn't matter as we're
|
|
// connecting to local proxy.
|
|
"-P", c.uid.New(),
|
|
}
|
|
|
|
if c.db.Database != "" {
|
|
args = append(args, "-d", c.db.Database)
|
|
}
|
|
|
|
if !c.isBinAvailable(sqlcmdBin) && c.isBinAvailable(mssqlBin) {
|
|
return exec.Command(mssqlBin, args...)
|
|
}
|
|
|
|
return exec.Command(sqlcmdBin, args...)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getSnowflakeCommand() *exec.Cmd {
|
|
args := []string{
|
|
"-a", "teleport", // Account name doesn't matter as it will be overridden in the backend anyway.
|
|
"-u", c.db.Username,
|
|
"-h", c.host,
|
|
"-p", strconv.Itoa(c.port),
|
|
}
|
|
|
|
if c.db.Database != "" {
|
|
args = append(args, "-w", c.db.Database)
|
|
}
|
|
|
|
cmd := exec.Command(snowsqlBin, args...)
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("SNOWSQL_PWD=%s", c.uid.New()))
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getCassandraCommand() (*exec.Cmd, error) {
|
|
args := []string{
|
|
"-u", c.db.Username,
|
|
c.host, strconv.Itoa(c.port),
|
|
}
|
|
if c.options.password != "" {
|
|
args = append(args, []string{"-p", c.options.password}...)
|
|
}
|
|
return exec.Command(cqlshBin, args...), nil
|
|
}
|
|
|
|
// getElasticsearchCommand returns a command to connect to Elasticsearch. We support `elasticsearch-sql-cli`, but only in non-TLS scenario.
|
|
func (c *CLICommandBuilder) getElasticsearchCommand() (*exec.Cmd, error) {
|
|
if c.options.noTLS {
|
|
return exec.Command(elasticsearchSQLBin, fmt.Sprintf("http://%v:%v/", c.host, c.port)), nil
|
|
}
|
|
return nil, trace.BadParameter("%v interactive command is only supported in --tunnel mode.", elasticsearchSQLBin)
|
|
}
|
|
|
|
// getOpenSearchCommand returns a command to connect to OpenSearch.
|
|
func (c *CLICommandBuilder) getOpenSearchCommand() (*exec.Cmd, error) {
|
|
if c.options.tolerateMissingCLIClient == false && c.isOpenSearchSQLBinAvailable() == false {
|
|
return nil, trace.NotFound("%q not found, please make sure it is available in $PATH", openSearchSQLBin)
|
|
}
|
|
|
|
if c.options.noTLS {
|
|
args := []string{fmt.Sprintf("http://%v:%v", c.host, c.port)}
|
|
return exec.Command(openSearchSQLBin, args...), nil
|
|
}
|
|
|
|
return nil, trace.BadParameter("%v interactive command is only supported in --tunnel mode.", openSearchSQLBin)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOpenSearchCLICommand() (*exec.Cmd, error) {
|
|
cfg := opensearch.ConfigNoTLS(c.host, c.port)
|
|
if !c.options.noTLS {
|
|
cfg = opensearch.ConfigTLS(c.host, c.port, c.options.caPath,
|
|
c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName))
|
|
}
|
|
|
|
baseDir := filepath.Join(c.profile.Dir, c.profile.Cluster, c.db.ServiceName)
|
|
tempCfg, err := opensearch.WriteConfig(baseDir, cfg)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
args := []string{"--profile", opensearch.ProfileName, "--config", tempCfg, "curl", "get", "--path", "/"}
|
|
|
|
return exec.Command(openSearchCLIBin, args...), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
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),
|
|
"[dynamodb|dynamodbstreams|dax]",
|
|
"<command>",
|
|
}
|
|
return exec.Command(awsBin, args...), nil
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getSpannerCommand(ctx context.Context) (*exec.Cmd, error) {
|
|
if err := c.checkLocalProxyTunnelOnly(false); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var (
|
|
gcp types.GCPCloudSQL
|
|
project,
|
|
instance,
|
|
database string
|
|
)
|
|
|
|
db, err := c.getDatabase(ctx)
|
|
switch {
|
|
case err != nil && c.options.printFormat:
|
|
// OK to continue in this case, we'll print the placeholders instead.
|
|
project, instance, database = "<project>", "<instance>", "<database>"
|
|
case err != nil:
|
|
return nil, trace.Wrap(err)
|
|
default:
|
|
gcp = db.GetGCP()
|
|
}
|
|
|
|
if gcp.ProjectID != "" {
|
|
project = gcp.ProjectID
|
|
}
|
|
if gcp.InstanceID != "" {
|
|
instance = 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
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOracleTNSDescriptorString() string {
|
|
return fmt.Sprintf("/@(DESCRIPTION=(SDU=8000)(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=%s)(PORT=%d)))(CONNECT_DATA=(SERVICE_NAME=%s)))", c.host, c.port, c.db.Database)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOracleDirectConnectionString() string {
|
|
return fmt.Sprintf("/@%s:%d/%s", c.host, c.port, c.db.Database)
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOracleJDBCConnectionString() string {
|
|
tnsAdminPath := c.profile.OracleWalletDir(c.tc.SiteName, c.db.ServiceName)
|
|
if runtime.GOOS == constants.WindowsOS {
|
|
tnsAdminPath = strings.ReplaceAll(tnsAdminPath, `\`, `\\`)
|
|
}
|
|
// Quote the address for printing as the address contains "?".
|
|
connString := fmt.Sprintf(`jdbc:oracle:thin:@tcps://%s:%d/%s?TNS_ADMIN=%s`, c.host, c.port, c.db.Database, tnsAdminPath)
|
|
if c.options.printFormat {
|
|
connString = fmt.Sprintf(`'%s'`, connString)
|
|
}
|
|
return connString
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOracleCommand() (*exec.Cmd, error) {
|
|
alternatives := c.getOracleAlternativeCommands()
|
|
if len(alternatives) == 0 {
|
|
return nil, trace.BadParameter("no alternative commands found")
|
|
}
|
|
return alternatives[0].Command, nil
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOracleAlternativeCommands() []CommandAlternative {
|
|
var commands []CommandAlternative
|
|
|
|
ctx := context.Background()
|
|
|
|
c.options.logger.DebugContext(ctx, "Building Oracle commands.")
|
|
c.options.logger.DebugContext(ctx, "Found servers with TCP support", "count", c.options.oracle.hasTCPServers)
|
|
c.options.logger.DebugContext(ctx, "All servers support TCP", "all_servers_support_tcp", c.options.oracle.canUseTCP)
|
|
|
|
c.options.logger.DebugContext(ctx, "Connection strings:")
|
|
c.options.logger.DebugContext(ctx, "JDBC", "connection_string", c.getOracleJDBCConnectionString())
|
|
if c.options.oracle.hasTCPServers {
|
|
c.options.logger.DebugContext(ctx, "TNS", "connection_string", c.getOracleTNSDescriptorString())
|
|
c.options.logger.DebugContext(ctx, "Direct", "connection_string", c.getOracleDirectConnectionString())
|
|
}
|
|
|
|
const oneShotLogin = "-L"
|
|
|
|
commandTCP := exec.Command(sqlclBin, oneShotLogin, c.getOracleDirectConnectionString())
|
|
commandTCPS := exec.Command(sqlclBin, oneShotLogin, c.getOracleJDBCConnectionString())
|
|
|
|
if c.options.oracle.canUseTCP {
|
|
commands = append(commands, CommandAlternative{Description: "SQLcl", Command: commandTCP})
|
|
commands = append(commands, CommandAlternative{Description: "SQLcl (JDBC)", Command: commandTCPS})
|
|
} else {
|
|
commands = append(commands, CommandAlternative{Description: "SQLcl", Command: commandTCPS})
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getElasticsearchAlternativeCommands() []CommandAlternative {
|
|
var commands []CommandAlternative
|
|
if c.isElasticsearchSQLBinAvailable() {
|
|
if cmd, err := c.getElasticsearchCommand(); err == nil {
|
|
commands = append(commands, CommandAlternative{Description: "interactive SQL connection", Command: cmd})
|
|
}
|
|
}
|
|
|
|
var curlCommand *exec.Cmd
|
|
if c.options.noTLS {
|
|
curlCommand = exec.Command(curlBin, fmt.Sprintf("http://%v:%v/", c.host, c.port))
|
|
} else {
|
|
args := []string{
|
|
fmt.Sprintf("https://%v:%v/", c.host, c.port),
|
|
"--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
"--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
}
|
|
|
|
if c.tc.InsecureSkipVerify {
|
|
args = append(args, "--insecure")
|
|
}
|
|
|
|
if c.options.caPath != "" {
|
|
args = append(args, []string{"--cacert", c.options.caPath}...)
|
|
}
|
|
|
|
// Force HTTP 1.1 when connecting to remote web proxy. Otherwise, HTTP2 can
|
|
// be negotiated which breaks the engine.
|
|
if c.options.localProxyHost == "" {
|
|
args = append(args, "--http1.1")
|
|
}
|
|
|
|
curlCommand = exec.Command(curlBin, args...)
|
|
}
|
|
commands = append(commands, CommandAlternative{Description: "run single request with curl", Command: curlCommand})
|
|
|
|
return commands
|
|
}
|
|
|
|
func (c *CLICommandBuilder) getOpenSearchAlternativeCommands() []CommandAlternative {
|
|
var commands []CommandAlternative
|
|
if c.isOpenSearchSQLBinAvailable() {
|
|
if cmd, err := c.getOpenSearchCommand(); err == nil {
|
|
commands = append(commands, CommandAlternative{Description: "start interactive session with opensearchsql", Command: cmd})
|
|
}
|
|
}
|
|
|
|
if c.isOpenSearchCLIBinAvailable() {
|
|
if cmd, err := c.getOpenSearchCLICommand(); err == nil {
|
|
commands = append(commands, CommandAlternative{Description: "run request with opensearch-cli", Command: cmd})
|
|
}
|
|
}
|
|
|
|
var curlCommand *exec.Cmd
|
|
if c.options.noTLS {
|
|
curlCommand = exec.Command(curlBin, fmt.Sprintf("http://%v:%v/", c.host, c.port))
|
|
} else {
|
|
args := []string{
|
|
fmt.Sprintf("https://%v:%v/", c.host, c.port),
|
|
"--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
"--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName),
|
|
}
|
|
|
|
if c.tc.InsecureSkipVerify {
|
|
args = append(args, "--insecure")
|
|
}
|
|
|
|
if c.options.caPath != "" {
|
|
args = append(args, []string{"--cacert", c.options.caPath}...)
|
|
}
|
|
|
|
// Force HTTP 1.1 when connecting to remote web proxy. Otherwise, HTTP2 can
|
|
// be negotiated which breaks the engine.
|
|
if c.options.localProxyHost == "" {
|
|
args = append(args, "--http1.1")
|
|
}
|
|
|
|
curlCommand = exec.Command(curlBin, args...)
|
|
}
|
|
commands = append(commands, CommandAlternative{Description: "run request with curl", Command: curlCommand})
|
|
|
|
return commands
|
|
}
|
|
|
|
type connectionCommandOpts struct {
|
|
localProxyPort int
|
|
localProxyHost string
|
|
caPath string
|
|
noTLS bool
|
|
printFormat bool
|
|
tolerateMissingCLIClient bool
|
|
logger *slog.Logger
|
|
exe Execer
|
|
password string
|
|
oracle oracleOpts
|
|
}
|
|
|
|
// ConnectCommandFunc is a type for functions returned by the "With*" functions in this package.
|
|
// A function of type ConnectCommandFunc changes connectionCommandOpts of CLICommandBuilder based on
|
|
// the arguments passed to a "With*" function.
|
|
type ConnectCommandFunc func(*connectionCommandOpts)
|
|
|
|
// WithLocalProxy makes CLICommandBuilder pass appropriate args to the CLI database clients that
|
|
// will let them connect to a database through a local proxy.
|
|
// In most cases it means using the passed host and port as the address, but some database clients
|
|
// require additional flags in those scenarios.
|
|
func WithLocalProxy(host string, port int, caPath string) ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.localProxyPort = port
|
|
opts.localProxyHost = host
|
|
opts.caPath = caPath
|
|
}
|
|
}
|
|
|
|
// WithNoTLS is the connect command option that makes the command connect
|
|
// without TLS.
|
|
//
|
|
// It is used when connecting through the local proxy that was started in
|
|
// mutual TLS mode (i.e. with a client certificate).
|
|
func WithNoTLS() ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.noTLS = true
|
|
}
|
|
}
|
|
|
|
// WithPassword is the command option that allows to set the database password
|
|
// that will be used for database CLI.
|
|
func WithPassword(pass string) ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.password = pass
|
|
}
|
|
}
|
|
|
|
// WithPrintFormat is the connect command option that hints the command will be
|
|
// printed instead of being executed.
|
|
//
|
|
// For example, when enabled, a quote will be used for Postgres and MongoDB
|
|
// connection strings to avoid "&" getting interpreted by the shell.
|
|
//
|
|
// WithPrintFormat is known to be used for the following situations:
|
|
// - tsh db config --format cmd <database>
|
|
// - tsh proxy db --tunnel <database>
|
|
// - 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.
|
|
func WithPrintFormat() ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.printFormat = true
|
|
}
|
|
}
|
|
|
|
// WithLogger is the connect command option that allows the caller to pass a logger that will be
|
|
// used by CLICommandBuilder.
|
|
func WithLogger(log *slog.Logger) ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.logger = log
|
|
}
|
|
}
|
|
|
|
// WithTolerateMissingCLIClient is the connect command option that makes CLICommandBuilder not
|
|
// return an error in case a specific binary couldn't be found in the system. Instead, it should
|
|
// return the command with just a base version of the binary name, without an absolute path.
|
|
//
|
|
// In general CLICommandBuilder doesn't return an error in that scenario as it uses exec.Command
|
|
// underneath. However, there are some specific situations where we need to execute some
|
|
// binaries before returning the final command.
|
|
//
|
|
// The flag is mostly for scenarios where the caller doesn't care that the final command might not
|
|
// work.
|
|
func WithTolerateMissingCLIClient() ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.tolerateMissingCLIClient = true
|
|
}
|
|
}
|
|
|
|
// WithExecer allows to provide a different Execer than the default SystemExecer. Useful in contexts
|
|
// where there's a place that wants to use dbcmd with the ability to mock out SystemExecer in tests.
|
|
func WithExecer(exe Execer) ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.exe = exe
|
|
}
|
|
}
|
|
|
|
type oracleOpts struct {
|
|
canUseTCP bool
|
|
hasTCPServers bool
|
|
}
|
|
|
|
// WithOracleOpts configures Oracle-specific options.
|
|
func WithOracleOpts(canUseTCP bool, hasTCPServers bool) ConnectCommandFunc {
|
|
return func(opts *connectionCommandOpts) {
|
|
opts.oracle.canUseTCP = canUseTCP
|
|
opts.oracle.hasTCPServers = hasTCPServers
|
|
}
|
|
}
|
|
|
|
// GetDatabaseFunc is a callback to retrieve types.Database.
|
|
type GetDatabaseFunc func(context.Context, *client.TeleportClient, string) (types.Database, error)
|
|
|
|
const (
|
|
// envVarMongoServerSelectionTimeoutMS is the environment variable that
|
|
// controls the server selection timeout used for MongoDB clients.
|
|
envVarMongoServerSelectionTimeoutMS = "TELEPORT_MONGO_SERVER_SELECTION_TIMEOUT_MS"
|
|
)
|