diff --git a/pkg/agent/install.go b/pkg/agent/install.go index cdf91f89..8320303b 100644 --- a/pkg/agent/install.go +++ b/pkg/agent/install.go @@ -3,7 +3,9 @@ package agent import ( "fmt" "os" + "path" "runtime" + "strings" "github.com/google/uuid" @@ -76,6 +78,13 @@ func install(logger *logging.Logger, transport Transport, prompter string, cmdEx remoteFileName = "." + remoteFileName } fullRemotePath := remotePathFromHome(cmdExe, remoteFileName) + if posix { + if home, homeErr := remoteHomeDirectory(transport); homeErr == nil { + fullRemotePath = path.Join(home, remoteFileName) + } else { + logger.Infof("unable to resolve remote home directory, using ~-relative agent path: %v", homeErr) + } + } if err = transport.Copy(agentExecutable, fullRemotePath); err != nil { return fmt.Errorf("unable to copy agent binary: %w", err) @@ -109,3 +118,15 @@ func install(logger *logging.Logger, transport Transport, prompter string, cmdEx // Success. return nil } + +func remoteHomeDirectory(transport Transport) (string, error) { + out, err := output(transport, `echo "$HOME"`) + if err != nil { + return "", fmt.Errorf("unable to query remote home directory: %w", err) + } + home := strings.TrimSpace(string(out)) + if !strings.HasPrefix(home, "/") { + return "", fmt.Errorf("invalid remote home directory: %q", home) + } + return home, nil +} diff --git a/pkg/agent/install_test.go b/pkg/agent/install_test.go index 55b6cbde..d87eb799 100644 --- a/pkg/agent/install_test.go +++ b/pkg/agent/install_test.go @@ -1,4 +1,65 @@ package agent +import ( + "os" + "os/exec" + "testing" +) + // NOTE: Unfortunately the Install() method can't be tested directly, but it is // tested indirectly by integration tests. + +type echoTransport struct { + stdout string + failCommand bool +} + +func (t *echoTransport) Copy(_, _ string) error { return nil } + +func (t *echoTransport) Command(_ string) (*exec.Cmd, error) { + if t.failCommand { + return exec.Command("false"), nil + } + return exec.Command("printf", "%s", t.stdout), nil +} + +func (t *echoTransport) ClassifyError(_ *os.ProcessState, _ string) (bool, bool, error) { + return false, false, nil +} + +func TestRemoteHomeDirectory(t *testing.T) { + testCases := []struct { + name string + stdout string + failCommand bool + expected string + expectError bool + }{ + {name: "simple", stdout: "/home/ubuntu", expected: "/home/ubuntu"}, + {name: "trailing newline", stdout: "/home/ubuntu\n", expected: "/home/ubuntu"}, + {name: "surrounding whitespace", stdout: " /home/ubuntu \n", expected: "/home/ubuntu"}, + {name: "empty", stdout: "", expectError: true}, + {name: "unexpanded variable", stdout: "$HOME", expectError: true}, + {name: "relative", stdout: "home/ubuntu", expectError: true}, + {name: "command error", failCommand: true, expectError: true}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + transport := &echoTransport{stdout: testCase.stdout, failCommand: testCase.failCommand} + home, err := remoteHomeDirectory(transport) + if testCase.expectError { + if err == nil { + t.Fatalf("expected an error but got home %q", home) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if home != testCase.expected { + t.Fatalf("expected home %q but got %q", testCase.expected, home) + } + }) + } +}