Fix node SSH upload by replacing broken scp with stdin cat.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+45
-47
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -72,10 +73,16 @@ func run(client *ssh.Client, cmd string, log LogFunc) (string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.FileMode, log LogFunc) error {
|
||||
func shellQuote(s string) string {
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
|
||||
// uploadViaStdin writes bytes to remotePath using `cat` over SSH stdin.
|
||||
// Avoids legacy `scp -t`, which fails on many modern OpenSSH servers (status 1).
|
||||
func uploadViaStdin(client *ssh.Client, remotePath string, data []byte, mode os.FileMode, log LogFunc) error {
|
||||
dir := filepath.ToSlash(filepath.Dir(remotePath))
|
||||
if _, err := run(client, fmt.Sprintf("mkdir -p %q", dir), log); err != nil {
|
||||
return err
|
||||
if _, err := run(client, "mkdir -p "+shellQuote(dir)+" 2>/dev/null || sudo mkdir -p "+shellQuote(dir), log); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", dir, err)
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
@@ -84,61 +91,52 @@ func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.Fil
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
session.Stderr = &stderr
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := filepath.Base(remotePath)
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "C%04o %d %s\n", mode, len(content), base)
|
||||
_, _ = io.WriteString(stdin, content)
|
||||
fmt.Fprint(stdin, "\x00")
|
||||
}()
|
||||
// Prefer direct write; fall back to sudo tee if permission denied.
|
||||
cmd := fmt.Sprintf(
|
||||
`sh -c 'cat > %s && chmod %o %s' 2>/dev/null || sudo sh -c 'cat > %s && chmod %o %s'`,
|
||||
shellQuote(remotePath), mode, shellQuote(remotePath),
|
||||
shellQuote(remotePath), mode, shellQuote(remotePath),
|
||||
)
|
||||
|
||||
if log != nil {
|
||||
log(fmt.Sprintf("upload %s (%d bytes)", remotePath, len(content)))
|
||||
log(fmt.Sprintf("upload %s (%d bytes) via ssh stdin", remotePath, len(data)))
|
||||
}
|
||||
cmd := fmt.Sprintf("scp -t %q", dir)
|
||||
if err := session.Run(cmd); err != nil {
|
||||
return fmt.Errorf("scp %s: %w", remotePath, err)
|
||||
|
||||
if err := session.Start(cmd); err != nil {
|
||||
return fmt.Errorf("start upload: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(stdin, bytes.NewReader(data)); err != nil {
|
||||
_ = stdin.Close()
|
||||
_ = session.Wait()
|
||||
return fmt.Errorf("write upload: %w (%s)", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
_ = stdin.Close()
|
||||
|
||||
if err := session.Wait(); err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if msg == "" {
|
||||
msg = err.Error()
|
||||
}
|
||||
return fmt.Errorf("upload %s failed: %s", remotePath, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.FileMode, log LogFunc) error {
|
||||
return uploadViaStdin(client, remotePath, []byte(content), mode, log)
|
||||
}
|
||||
|
||||
func uploadBinary(client *ssh.Client, remotePath string, data []byte, log LogFunc) error {
|
||||
dir := filepath.ToSlash(filepath.Dir(remotePath))
|
||||
if _, err := run(client, fmt.Sprintf("mkdir -p %q", dir), log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := filepath.Base(remotePath)
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "C0755 %d %s\n", len(data), base)
|
||||
_, _ = stdin.Write(data)
|
||||
fmt.Fprint(stdin, "\x00")
|
||||
}()
|
||||
|
||||
if log != nil {
|
||||
log(fmt.Sprintf("upload binary %s (%d bytes)", remotePath, len(data)))
|
||||
}
|
||||
if err := session.Run(fmt.Sprintf("scp -t %q", dir)); err != nil {
|
||||
return fmt.Errorf("scp binary: %w", err)
|
||||
}
|
||||
return nil
|
||||
return uploadViaStdin(client, remotePath, data, 0o755, log)
|
||||
}
|
||||
|
||||
// Provision installs Docker (if needed), uploads agent binary + compose, starts the node.
|
||||
@@ -162,14 +160,14 @@ func Provision(n *models.Node, sshSecret string, agentBinary []byte, log LogFunc
|
||||
if _, err := run(client, `command -v docker >/dev/null 2>&1 || (curl -fsSL https://get.docker.com | sh)`, log); err != nil {
|
||||
return fmt.Errorf("install docker: %w", err)
|
||||
}
|
||||
if _, err := run(client, `sudo mkdir -p /opt/vpn-panel-node/data && sudo chown -R $(whoami):$(whoami) /opt/vpn-panel-node || true`, log); err != nil {
|
||||
if _, err := run(client, `sudo mkdir -p /opt/vpn-panel-node/data && (sudo chown -R "$(whoami):$(whoami)" /opt/vpn-panel-node || true)`, log); err != nil {
|
||||
return fmt.Errorf("prepare dir: %w", err)
|
||||
}
|
||||
|
||||
if err := uploadBinary(client, "/opt/vpn-panel-node/vpn-node", agentBinary, log); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeRemoteFile(client, "/opt/vpn-panel-node/docker-compose.yml", ComposeYAML(n), 0644, log); err != nil {
|
||||
if err := writeRemoteFile(client, "/opt/vpn-panel-node/docker-compose.yml", ComposeYAML(n), 0o644, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user