@@ -21,10 +21,17 @@ type mysqlSyncConfig struct {
2121 TargetHost string
2222 TargetPort int
2323 TargetPasswordEnv string
24+ DatabaseSSH syncSSHTarget
2425 DumpFlags []string
2526}
2627
27- func runDatabaseSync (cmd * cobra.Command , target ResolvedSyncTarget , _ * SyncSettings , dryRun bool , autoApprove bool ) error {
28+ type syncSSHTarget struct {
29+ Host string
30+ Username string
31+ Port int
32+ }
33+
34+ func runDatabaseSync (cmd * cobra.Command , target ResolvedSyncTarget , settings * 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" )
3037 }
@@ -36,18 +43,18 @@ func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSetti
3643 return err
3744 }
3845
39- databaseConfig , err := promptMySQLSyncConfig (target )
46+ databaseConfig , err := promptMySQLSyncConfig (target , settings )
4047 if err != nil {
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
@@ -122,7 +129,7 @@ func ensureCommandAvailable(name string) error {
122129 return nil
123130}
124131
125- func promptMySQLSyncConfig (target ResolvedSyncTarget ) (mysqlSyncConfig , error ) {
132+ func promptMySQLSyncConfig (target ResolvedSyncTarget , settings * SyncSettings ) (mysqlSyncConfig , error ) {
126133 sourceDatabase , err := promptText ("Source MySQL database" , "" , requiredValue , 0 )
127134 if err != nil {
128135 return mysqlSyncConfig {}, err
@@ -173,6 +180,11 @@ func promptMySQLSyncConfig(target ResolvedSyncTarget) (mysqlSyncConfig, error) {
173180 return mysqlSyncConfig {}, err
174181 }
175182
183+ databaseSSH , err := promptDatabaseSSHTarget (target , settings )
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,89 @@ 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 , settings * SyncSettings ) (syncSSHTarget , error ) {
210+ defaultTarget := defaultDatabaseSSHTarget (target )
211+ configuredTarget , hasConfiguredTarget := configuredDatabaseSSHTarget (target , settings )
212+
213+ hostDefault := ""
214+ if hasConfiguredTarget {
215+ hostDefault = configuredTarget .Host
216+ }
217+
218+ host , err := promptText ("Database SSH host override (blank = selected target)" , hostDefault , optionalValue , 0 )
219+ if err != nil {
220+ return syncSSHTarget {}, err
221+ }
222+ if strings .TrimSpace (host ) == "" {
223+ return defaultTarget , nil
224+ }
225+
226+ usernameDefault := defaultTarget .Username
227+ portDefault := defaultTarget .Port
228+ if hasConfiguredTarget {
229+ usernameDefault = configuredTarget .Username
230+ portDefault = configuredTarget .Port
231+ }
232+
233+ username , err := promptText ("Database SSH user" , usernameDefault , requiredValue , 0 )
234+ if err != nil {
235+ return syncSSHTarget {}, err
236+ }
237+
238+ port , err := promptPort ("Database SSH port" , strconv .Itoa (portDefault ))
239+ if err != nil {
240+ return syncSSHTarget {}, err
241+ }
242+
243+ return syncSSHTarget {
244+ Host : strings .TrimSpace (host ),
245+ Username : strings .TrimSpace (username ),
246+ Port : port ,
247+ }, nil
248+ }
249+
250+ func configuredDatabaseSSHTarget (target ResolvedSyncTarget , settings * SyncSettings ) (syncSSHTarget , bool ) {
251+ defaultTarget := defaultDatabaseSSHTarget (target )
252+ if settings == nil {
253+ return defaultTarget , false
254+ }
255+
256+ configured := settings .Database .TargetSSH
257+ host := strings .TrimSpace (configured .Host )
258+ if host == "" {
259+ return defaultTarget , false
260+ }
261+
262+ username := strings .TrimSpace (configured .User )
263+ if username == "" {
264+ username = defaultTarget .Username
265+ }
266+
267+ port := configured .Port
268+ if port <= 0 {
269+ port = defaultTarget .Port
270+ }
271+
272+ return syncSSHTarget {
273+ Host : host ,
274+ Username : username ,
275+ Port : port ,
276+ }, true
277+ }
278+
279+ func defaultDatabaseSSHTarget (target ResolvedSyncTarget ) syncSSHTarget {
280+ return syncSSHTarget {
281+ Host : strings .TrimSpace (target .Host ),
282+ Username : strings .TrimSpace (target .Username ),
283+ Port : target .Port ,
284+ }
285+ }
286+
196287func promptPort (label , defaultValue string ) (int , error ) {
197288 value , err := promptText (label , defaultValue , requiredValue , 0 )
198289 if err != nil {
@@ -230,23 +321,42 @@ func buildRemoteMySQLImportCommand(config mysqlSyncConfig, targetPassword string
230321 )
231322}
232323
324+ func buildDatabaseSSHArgs (target syncSSHTarget , remoteCommand string ) []string {
325+ return []string {"-p" , strconv .Itoa (target .Port ), fmt .Sprintf ("%s@%s" , target .Username , target .Host ), remoteCommand }
326+ }
327+
233328func shellQuote (value string ) string {
234329 escaped := strings .ReplaceAll (value , "'" , "'\\ ''" )
235330 return "'" + escaped + "'"
236331}
237332
238- func isSameDatabaseTarget (target ResolvedSyncTarget , config mysqlSyncConfig ) bool {
239- if ! strings . EqualFold ( strings . TrimSpace ( target . Host ), strings . TrimSpace ( config .SourceHost ) ) {
333+ func isSameDatabaseTarget (config mysqlSyncConfig ) bool {
334+ if ! isSameDatabaseHost ( config . SourceHost , config . TargetHost , config .DatabaseSSH . Host ) {
240335 return false
241336 }
242337
243338 if config .TargetPort != config .SourcePort {
244339 return false
245340 }
246341
247- if ! strings .EqualFold (strings .TrimSpace (config .TargetHost ), strings .TrimSpace (config .SourceHost )) {
342+ return strings .EqualFold (strings .TrimSpace (config .SourceDatabase ), strings .TrimSpace (config .TargetDatabase ))
343+ }
344+
345+ func isSameDatabaseHost (sourceHost string , targetHost string , targetSSHHost string ) bool {
346+ source := strings .TrimSpace (sourceHost )
347+ target := strings .TrimSpace (targetHost )
348+ if source == "" || target == "" {
248349 return false
249350 }
250351
251- return strings .EqualFold (strings .TrimSpace (config .SourceDatabase ), strings .TrimSpace (config .TargetDatabase ))
352+ if ! isLoopbackHost (target ) {
353+ return strings .EqualFold (source , target )
354+ }
355+
356+ return strings .EqualFold (source , strings .TrimSpace (targetSSHHost ))
357+ }
358+
359+ func isLoopbackHost (host string ) bool {
360+ normalized := strings .ToLower (strings .TrimSpace (host ))
361+ return normalized == "localhost" || normalized == "127.0.0.1" || normalized == "::1"
252362}
0 commit comments