[FEATURE] Support separate database SSH targets during sync · intercube/cli@3d3508e · GitHub
Skip to content

Commit 3d3508e

Browse files
committed
[FEATURE] Support separate database SSH targets during sync
1 parent c739619 commit 3d3508e

3 files changed

Lines changed: 151 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions

cmd/sync-database.go

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ type mysqlSyncConfig struct {
2121
TargetHost string
2222
TargetPort int
2323
TargetPasswordEnv string
24+
DatabaseSSH syncSSHTarget
2425
DumpFlags []string
2526
}
2627

28+
type syncSSHTarget struct {
29+
Host string
30+
Username string
31+
Port int
32+
}
33+
2734
func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSettings, dryRun bool, autoApprove bool) error {
2835
if isNonInteractiveMode() {
2936
return fmt.Errorf("database sync requires interactive prompts in current implementation; run with an interactive terminal")
@@ -41,13 +48,13 @@ func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSetti
4148
return err
4249
}
4350

44-
if isSameDatabaseTarget(target, databaseConfig) {
51+
if isSameDatabaseTarget(databaseConfig) {
4552
return fmt.Errorf("source and target resolve to the same database destination")
4653
}
4754

4855
fmt.Println("Database sync plan:")
4956
fmt.Printf(" Source: %s@%s:%d/%s\n", databaseConfig.SourceUser, databaseConfig.SourceHost, databaseConfig.SourcePort, databaseConfig.SourceDatabase)
50-
fmt.Printf(" Target: %s@%s:%d/%s (via %s@%s:%d)\n", databaseConfig.TargetUser, databaseConfig.TargetHost, databaseConfig.TargetPort, databaseConfig.TargetDatabase, target.Username, target.Host, target.Port)
57+
fmt.Printf(" Target: %s@%s:%d/%s (via %s@%s:%d)\n", databaseConfig.TargetUser, databaseConfig.TargetHost, databaseConfig.TargetPort, databaseConfig.TargetDatabase, databaseConfig.DatabaseSSH.Username, databaseConfig.DatabaseSSH.Host, databaseConfig.DatabaseSSH.Port)
5158

5259
if !autoApprove {
5360
confirmed, confirmErr := promptYesNo("Continue with MySQL import into target?")
@@ -73,7 +80,7 @@ func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSetti
7380

7481
dumpArgs := buildMySQLDumpArgs(databaseConfig)
7582
remoteCommand := buildRemoteMySQLImportCommand(databaseConfig, targetPassword)
76-
sshArgs := []string{"-p", strconv.Itoa(target.Port), fmt.Sprintf("%s@%s", target.Username, target.Host), remoteCommand}
83+
sshArgs := buildDatabaseSSHArgs(databaseConfig.DatabaseSSH, remoteCommand)
7784

7885
fmt.Printf("Running: MYSQL_PWD=<hidden> mysqldump %s | ssh %s\n", strings.Join(dumpArgs, " "), strings.Join(sshArgs, " "))
7986

@@ -173,6 +180,11 @@ func promptMySQLSyncConfig(target ResolvedSyncTarget) (mysqlSyncConfig, error) {
173180
return mysqlSyncConfig{}, err
174181
}
175182

183+
databaseSSH, err := promptDatabaseSSHTarget(target)
184+
if err != nil {
185+
return mysqlSyncConfig{}, err
186+
}
187+
176188
flagsRaw, err := promptText("Extra mysqldump flags (space-separated, optional)", "--single-transaction --quick", optionalValue, 0)
177189
if err != nil {
178190
return mysqlSyncConfig{}, err
@@ -189,10 +201,47 @@ func promptMySQLSyncConfig(target ResolvedSyncTarget) (mysqlSyncConfig, error) {
189201
TargetHost: strings.TrimSpace(targetHost),
190202
TargetPort: targetPort,
191203
TargetPasswordEnv: strings.TrimSpace(targetPasswordEnv),
204+
DatabaseSSH: databaseSSH,
192205
DumpFlags: strings.Fields(strings.TrimSpace(flagsRaw)),
193206
}, nil
194207
}
195208

209+
func promptDatabaseSSHTarget(target ResolvedSyncTarget) (syncSSHTarget, error) {
210+
defaultTarget := defaultDatabaseSSHTarget(target)
211+
212+
host, err := promptText("Database SSH host override (optional)", "", optionalValue, 0)
213+
if err != nil {
214+
return syncSSHTarget{}, err
215+
}
216+
if strings.TrimSpace(host) == "" {
217+
return defaultTarget, nil
218+
}
219+
220+
username, err := promptText("Database SSH user", defaultTarget.Username, requiredValue, 0)
221+
if err != nil {
222+
return syncSSHTarget{}, err
223+
}
224+
225+
port, err := promptPort("Database SSH port", strconv.Itoa(defaultTarget.Port))
226+
if err != nil {
227+
return syncSSHTarget{}, err
228+
}
229+
230+
return syncSSHTarget{
231+
Host: strings.TrimSpace(host),
232+
Username: strings.TrimSpace(username),
233+
Port: port,
234+
}, nil
235+
}
236+
237+
func defaultDatabaseSSHTarget(target ResolvedSyncTarget) syncSSHTarget {
238+
return syncSSHTarget{
239+
Host: strings.TrimSpace(target.Host),
240+
Username: strings.TrimSpace(target.Username),
241+
Port: target.Port,
242+
}
243+
}
244+
196245
func promptPort(label, defaultValue string) (int, error) {
197246
value, err := promptText(label, defaultValue, requiredValue, 0)
198247
if err != nil {
@@ -230,23 +279,42 @@ func buildRemoteMySQLImportCommand(config mysqlSyncConfig, targetPassword string
230279
)
231280
}
232281

282+
func buildDatabaseSSHArgs(target syncSSHTarget, remoteCommand string) []string {
283+
return []string{"-p", strconv.Itoa(target.Port), fmt.Sprintf("%s@%s", target.Username, target.Host), remoteCommand}
284+
}
285+
233286
func shellQuote(value string) string {
234287
escaped := strings.ReplaceAll(value, "'", "'\\''")
235288
return "'" + escaped + "'"
236289
}
237290

238-
func isSameDatabaseTarget(target ResolvedSyncTarget, config mysqlSyncConfig) bool {
239-
if !strings.EqualFold(strings.TrimSpace(target.Host), strings.TrimSpace(config.SourceHost)) {
291+
func isSameDatabaseTarget(config mysqlSyncConfig) bool {
292+
if !isSameDatabaseHost(config.SourceHost, config.TargetHost, config.DatabaseSSH.Host) {
240293
return false
241294
}
242295

243296
if config.TargetPort != config.SourcePort {
244297
return false
245298
}
246299

247-
if !strings.EqualFold(strings.TrimSpace(config.TargetHost), strings.TrimSpace(config.SourceHost)) {
300+
return strings.EqualFold(strings.TrimSpace(config.SourceDatabase), strings.TrimSpace(config.TargetDatabase))
301+
}
302+
303+
func isSameDatabaseHost(sourceHost string, targetHost string, targetSSHHost string) bool {
304+
source := strings.TrimSpace(sourceHost)
305+
target := strings.TrimSpace(targetHost)
306+
if source == "" || target == "" {
248307
return false
249308
}
250309

251-
return strings.EqualFold(strings.TrimSpace(config.SourceDatabase), strings.TrimSpace(config.TargetDatabase))
310+
if !isLoopbackHost(target) {
311+
return strings.EqualFold(source, target)
312+
}
313+
314+
return strings.EqualFold(source, strings.TrimSpace(targetSSHHost))
315+
}
316+
317+
func isLoopbackHost(host string) bool {
318+
normalized := strings.ToLower(strings.TrimSpace(host))
319+
return normalized == "localhost" || normalized == "127.0.0.1" || normalized == "::1"
252320
}

cmd/sync-database_test.go

Lines changed: 73 additions & 0 deletions

0 commit comments

Comments
 (0)