Skip to content
Open
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
9 changes: 4 additions & 5 deletions cmd/xsql/command_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ func TestRunProxy_SSHConnectError(t *testing.T) {
}
}

func TestSetupSSH_NoConfig(t *testing.T) {
client, err := setupSSH(nil, configProfile(""), false, false)
func TestResolveSSH_NoConfig(t *testing.T) {
client, err := app.ResolveSSH(nil, config.Profile{}, false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -306,9 +306,8 @@ func TestSetupSSH_NoConfig(t *testing.T) {
}
}

func TestSetupSSH_PassphraseResolveError(t *testing.T) {
func TestResolveSSH_PassphraseResolveError(t *testing.T) {
profile := config.Profile{
DB: "mysql",
SSHConfig: &config.SSHProxy{
Host: "example.com",
Port: 22,
Expand All @@ -317,7 +316,7 @@ func TestSetupSSH_PassphraseResolveError(t *testing.T) {
},
}

_, err := setupSSH(context.Background(), profile, false, false)
_, err := app.ResolveSSH(context.Background(), profile, false, false)
if err == nil {
t.Fatal("expected error for passphrase resolve")
}
Expand Down
34 changes: 2 additions & 32 deletions cmd/xsql/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import (

"github.com/spf13/cobra"

"github.com/zx06/xsql/internal/app"
"github.com/zx06/xsql/internal/errors"
"github.com/zx06/xsql/internal/output"
"github.com/zx06/xsql/internal/proxy"
"github.com/zx06/xsql/internal/secret"
"github.com/zx06/xsql/internal/ssh"
)

// ProxyFlags holds the flags for the proxy command
Expand Down Expand Up @@ -60,45 +59,21 @@ func runProxy(cmd *cobra.Command, flags *ProxyFlags, w *output.Writer) error {
return errors.New(errors.CodeCfgInvalid, "db type is required (mysql|pg)", nil)
}

// Check if SSH proxy is configured
if p.SSHConfig == nil {
return errors.New(errors.CodeCfgInvalid, "profile must have ssh_proxy configured for port forwarding", nil)
}

// Allow plaintext passwords (CLI > Config)
allowPlaintext := flags.AllowPlaintext || p.AllowPlaintext

// Resolve SSH passphrase
passphrase := p.SSHConfig.Passphrase
if passphrase != "" {
pp, xe := secret.Resolve(passphrase, secret.Options{AllowPlaintext: allowPlaintext})
if xe != nil {
return xe
}
passphrase = pp
}

// Setup SSH connection
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sshOpts := ssh.Options{
Host: p.SSHConfig.Host,
Port: p.SSHConfig.Port,
User: p.SSHConfig.User,
IdentityFile: p.SSHConfig.IdentityFile,
Passphrase: passphrase,
KnownHostsFile: p.SSHConfig.KnownHostsFile,
SkipKnownHostsCheck: flags.SSHSkipHostKey || p.SSHConfig.SkipHostKey,
}

sshClient, xe := ssh.Connect(ctx, sshOpts)
sshClient, xe := app.ResolveSSH(ctx, p, allowPlaintext, flags.SSHSkipHostKey)
if xe != nil {
return xe
}
defer func() { _ = sshClient.Close() }()

// Start proxy
proxyOpts := proxy.Options{
LocalHost: flags.LocalHost,
LocalPort: flags.LocalPort,
Expand All @@ -113,16 +88,13 @@ func runProxy(cmd *cobra.Command, flags *ProxyFlags, w *output.Writer) error {
}
defer func() { _ = px.Stop() }()

// Print result based on format
if format == output.FormatTable {
// Custom table output for proxy
fmt.Fprintf(os.Stderr, "✓ Proxy started successfully\n")
fmt.Fprintf(os.Stderr, " Local: %s\n", result.LocalAddress)
fmt.Fprintf(os.Stderr, " Remote: %s (via %s)\n", result.RemoteAddress, p.SSHConfig.Host)
fmt.Fprintf(os.Stderr, " Profile: %s\n", profileName)
fmt.Fprintf(os.Stderr, "\nPress Ctrl+C to stop\n")
} else {
// JSON/YAML output
data := map[string]any{
"local_address": result.LocalAddress,
"remote_address": result.RemoteAddress,
Expand All @@ -132,11 +104,9 @@ func runProxy(cmd *cobra.Command, flags *ProxyFlags, w *output.Writer) error {
_ = w.WriteOK(format, data)
}

// Setup signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

// Wait for interrupt signal
<-sigChan
fmt.Fprintf(os.Stderr, "\nShutting down proxy...\n")

Expand Down
89 changes: 8 additions & 81 deletions cmd/xsql/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ import (

"github.com/spf13/cobra"

"github.com/zx06/xsql/internal/config"
"github.com/zx06/xsql/internal/app"
"github.com/zx06/xsql/internal/db"
_ "github.com/zx06/xsql/internal/db/mysql"
_ "github.com/zx06/xsql/internal/db/pg"
"github.com/zx06/xsql/internal/errors"
"github.com/zx06/xsql/internal/output"
"github.com/zx06/xsql/internal/secret"
"github.com/zx06/xsql/internal/ssh"
)

// QueryFlags holds the flags for the query command
Expand Down Expand Up @@ -56,57 +52,21 @@ func runQuery(cmd *cobra.Command, args []string, flags *QueryFlags, w *output.Wr
return errors.New(errors.CodeCfgInvalid, "db type is required (mysql|pg)", nil)
}

// Allow plaintext passwords (CLI > Config)
allowPlaintext := flags.AllowPlaintext || p.AllowPlaintext

// Resolve password (supports keyring)
password := p.Password
if password != "" {
pw, xe := secret.Resolve(password, secret.Options{AllowPlaintext: allowPlaintext})
if xe != nil {
return xe
}
password = pw
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// SSH proxy (if configured)
sshClient, err := setupSSH(ctx, p, allowPlaintext, flags.SSHSkipHostKey)
if err != nil {
return err
}
if sshClient != nil {
defer sshClient.Close()
}

// Get driver
drv, ok := db.Get(p.DB)
if !ok {
return errors.New(errors.CodeDBDriverUnsupported, "unsupported db driver", map[string]any{"db": p.DB})
}

connOpts := db.ConnOptions{
DSN: p.DSN,
Host: p.Host,
Port: p.Port,
User: p.User,
Password: password,
Database: p.Database,
}
if sshClient != nil {
connOpts.Dialer = sshClient
}

conn, xe := drv.Open(ctx, connOpts)
conn, xe := app.ResolveConnection(ctx, app.ConnectionOptions{
Profile: p,
AllowPlaintext: flags.AllowPlaintext,
SkipHostKeyCheck: flags.SSHSkipHostKey,
})
if xe != nil {
return xe
}
defer conn.Close()
defer func() { _ = conn.Close() }()

unsafeAllowWrite := flags.UnsafeAllowWrite || p.UnsafeAllowWrite
result, xe := db.Query(ctx, conn, sql, db.QueryOptions{
result, xe := db.Query(ctx, conn.DB, sql, db.QueryOptions{
UnsafeAllowWrite: unsafeAllowWrite,
DBType: p.DB,
})
Expand All @@ -116,36 +76,3 @@ func runQuery(cmd *cobra.Command, args []string, flags *QueryFlags, w *output.Wr

return w.WriteOK(format, result)
}

// setupSSH sets up SSH proxy connection
func setupSSH(ctx context.Context, p config.Profile, allowPlaintext, skipHostKeyCheck bool) (*ssh.Client, error) {
if p.SSHConfig == nil {
return nil, nil
}

passphrase := p.SSHConfig.Passphrase
if passphrase != "" {
pp, xe := secret.Resolve(passphrase, secret.Options{AllowPlaintext: allowPlaintext})
if xe != nil {
return nil, xe
}
passphrase = pp
}

sshOpts := ssh.Options{
Host: p.SSHConfig.Host,
Port: p.SSHConfig.Port,
User: p.SSHConfig.User,
IdentityFile: p.SSHConfig.IdentityFile,
Passphrase: passphrase,
KnownHostsFile: p.SSHConfig.KnownHostsFile,
SkipKnownHostsCheck: skipHostKeyCheck || p.SSHConfig.SkipHostKey,
}

sc, xe := ssh.Connect(ctx, sshOpts)
if xe != nil {
return nil, xe
}

return sc, nil
}
55 changes: 8 additions & 47 deletions cmd/xsql/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import (

"github.com/spf13/cobra"

"github.com/zx06/xsql/internal/app"
"github.com/zx06/xsql/internal/db"
_ "github.com/zx06/xsql/internal/db/mysql"
_ "github.com/zx06/xsql/internal/db/pg"
"github.com/zx06/xsql/internal/errors"
"github.com/zx06/xsql/internal/output"
"github.com/zx06/xsql/internal/secret"
)

// SchemaFlags holds the flags for the schema command
Expand Down Expand Up @@ -67,62 +65,25 @@ func runSchemaDump(cmd *cobra.Command, args []string, flags *SchemaFlags, w *out
return errors.New(errors.CodeCfgInvalid, "db type is required (mysql|pg)", nil)
}

// Allow plaintext passwords (CLI > Config)
allowPlaintext := flags.AllowPlaintext || p.AllowPlaintext

// Resolve password (supports keyring)
password := p.Password
if password != "" {
pw, xe := secret.Resolve(password, secret.Options{AllowPlaintext: allowPlaintext})
if xe != nil {
return xe
}
password = pw
}

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

// SSH proxy (if configured)
sshClient, err := setupSSH(ctx, p, allowPlaintext, flags.SSHSkipHostKey)
if err != nil {
return err
}
if sshClient != nil {
defer sshClient.Close()
}

// Get driver
drv, ok := db.Get(p.DB)
if !ok {
return errors.New(errors.CodeDBDriverUnsupported, "unsupported db driver", map[string]any{"db": p.DB})
}

connOpts := db.ConnOptions{
DSN: p.DSN,
Host: p.Host,
Port: p.Port,
User: p.User,
Password: password,
Database: p.Database,
}
if sshClient != nil {
connOpts.Dialer = sshClient
}

conn, xe := drv.Open(ctx, connOpts)
conn, xe := app.ResolveConnection(ctx, app.ConnectionOptions{
Profile: p,
AllowPlaintext: flags.AllowPlaintext,
SkipHostKeyCheck: flags.SSHSkipHostKey,
})
if xe != nil {
return xe
}
defer conn.Close()
defer func() { _ = conn.Close() }()

// Dump schema
schemaOpts := db.SchemaOptions{
TablePattern: flags.TablePattern,
IncludeSystem: flags.IncludeSystem,
}

result, xe := db.DumpSchema(ctx, p.DB, conn, schemaOpts)
result, xe := db.DumpSchema(ctx, p.DB, conn.DB, schemaOpts)
if xe != nil {
return xe
}
Expand Down
Loading