88 "net"
99 "os"
1010 "os/exec"
11+ "sort"
12+ "strings"
1113 "time"
1214
1315 "github.com/deevus/pixels/internal/retry"
@@ -46,8 +48,9 @@ func WaitReady(ctx context.Context, host string, timeout time.Duration, log io.W
4648}
4749
4850// Exec runs a command on the remote host via SSH and returns its exit code.
49- func Exec (ctx context.Context , host , user , keyPath string , command []string ) (int , error ) {
50- args := append (sshArgs (host , user , keyPath ), command ... )
51+ // If env is non-nil, the entries are forwarded via SSH SetEnv.
52+ func Exec (ctx context.Context , host , user , keyPath string , command []string , env map [string ]string ) (int , error ) {
53+ args := append (sshArgs (host , user , keyPath , env ), command ... )
5154 cmd := exec .CommandContext (ctx , "ssh" , args ... )
5255 cmd .Stdin = os .Stdin
5356 cmd .Stdout = os .Stdout
@@ -65,7 +68,7 @@ func Exec(ctx context.Context, host, user, keyPath string, command []string) (in
6568
6669// Output runs a command on the remote host via SSH and returns its stdout.
6770func Output (ctx context.Context , host , user , keyPath string , command []string ) ([]byte , error ) {
68- args := append (sshArgs (host , user , keyPath ), command ... )
71+ args := append (sshArgs (host , user , keyPath , nil ), command ... )
6972 cmd := exec .CommandContext (ctx , "ssh" , args ... )
7073 cmd .Stderr = os .Stderr
7174 return cmd .Output ()
@@ -74,20 +77,20 @@ func Output(ctx context.Context, host, user, keyPath string, command []string) (
7477// WaitProvisioned polls the remote host until /root/.devtools-provisioned exists.
7578func WaitProvisioned (ctx context.Context , host , user , keyPath string , timeout time.Duration ) error {
7679 return retry .Poll (ctx , 2 * time .Second , timeout , func (ctx context.Context ) (bool , error ) {
77- code , err := Exec (ctx , host , user , keyPath , []string {"sudo" , "test" , "-f" , "/root/.devtools-provisioned" })
80+ code , err := Exec (ctx , host , user , keyPath , []string {"sudo" , "test" , "-f" , "/root/.devtools-provisioned" }, nil )
7881 return err == nil && code == 0 , nil
7982 })
8083}
8184
8285// TestAuth runs a quick SSH connection test (ssh ... true) to verify
8386// key-based authentication works. Returns nil on success.
8487func TestAuth (ctx context.Context , host , user , keyPath string ) error {
85- args := append (sshArgs (host , user , keyPath ), "true" )
88+ args := append (sshArgs (host , user , keyPath , nil ), "true" )
8689 cmd := exec .CommandContext (ctx , "ssh" , args ... )
8790 return cmd .Run ()
8891}
8992
90- func sshArgs (host , user , keyPath string ) []string {
93+ func sshArgs (host , user , keyPath string , env map [ string ] string ) []string {
9194 args := []string {
9295 "-o" , "StrictHostKeyChecking=no" ,
9396 "-o" , "UserKnownHostsFile=" + os .DevNull ,
@@ -97,6 +100,28 @@ func sshArgs(host, user, keyPath string) []string {
97100 if keyPath != "" {
98101 args = append (args , "-i" , keyPath )
99102 }
103+
104+ // Forward env vars via SSH protocol (requires AcceptEnv on server).
105+ // All vars must be in a single SetEnv directive (multiple -o SetEnv
106+ // flags don't stack in OpenSSH — only the first takes effect).
107+ if len (env ) > 0 {
108+ keys := make ([]string , 0 , len (env ))
109+ for k := range env {
110+ keys = append (keys , k )
111+ }
112+ sort .Strings (keys )
113+
114+ var setenv strings.Builder
115+ setenv .WriteString ("SetEnv=" )
116+ for i , k := range keys {
117+ if i > 0 {
118+ setenv .WriteByte (' ' )
119+ }
120+ fmt .Fprintf (& setenv , "%s=%s" , k , env [k ])
121+ }
122+ args = append (args , "-o" , setenv .String ())
123+ }
124+
100125 args = append (args , user + "@" + host )
101126 return args
102127}
0 commit comments