70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// shellQuote wraps s in single quotes so it survives the remote shell that
|
|
// OpenSSH hands its non-option arguments to (ssh joins them itself; it does
|
|
// not exec the remote command with a distinct argv the way os/exec does).
|
|
func shellQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
|
}
|
|
|
|
func shellJoin(args []string) string {
|
|
parts := make([]string, len(args))
|
|
for i, a := range args {
|
|
parts[i] = shellQuote(a)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// sshCommand builds an *exec.Cmd that runs remoteArgs on host via ssh.
|
|
// batchMode disables interactive prompts (used for the push/pull direction
|
|
// probe, so an unreachable/unauthenticated attempt fails fast instead of
|
|
// hanging); it is left off for the manager's own control connections so a
|
|
// password prompt still works when the user is present at a terminal.
|
|
func sshCommand(sshBin string, extraOpts []string, batchMode bool, connectTimeoutSec int, user, host string, remoteArgs []string) *exec.Cmd {
|
|
args := []string{"-o", "StrictHostKeyChecking=accept-new"}
|
|
if batchMode {
|
|
args = append(args, "-o", "BatchMode=yes")
|
|
}
|
|
if connectTimeoutSec > 0 {
|
|
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", connectTimeoutSec))
|
|
}
|
|
for _, o := range extraOpts {
|
|
args = append(args, "-o", o)
|
|
}
|
|
userHost := host
|
|
if user != "" {
|
|
userHost = user + "@" + host
|
|
}
|
|
args = append(args, userHost, shellJoin(remoteArgs))
|
|
return exec.Command(sshBin, args...)
|
|
}
|
|
|
|
// selfExe is the path to the running clonetool binary — used to re-exec it
|
|
// as a local agent and to stream it to a remote host for deployment.
|
|
func selfExe() string {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return os.Args[0]
|
|
}
|
|
return self
|
|
}
|
|
|
|
// localAgentCommand re-execs this same binary as an agent, for a spec with
|
|
// no host part.
|
|
func localAgentCommand(agentArgs []string) *exec.Cmd {
|
|
return exec.Command(selfExe(), agentArgs...)
|
|
}
|
|
|
|
// sudoLocalCommand is localAgentCommand wrapped in an interactive sudo: the
|
|
// manager still owns the terminal, so a local password prompt works.
|
|
func sudoLocalCommand(agentArgs []string) *exec.Cmd {
|
|
return exec.Command("sudo", append([]string{"--", selfExe()}, agentArgs...)...)
|
|
}
|