mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(agent): unify working directory resolution (#26102)
agentssh's CommandEnv, sftpHandler, and agentproc each resolved the session working directory on their own and had drifted: sftpHandler used the configured directory without checking it exists and bypassed the injected EnvInfoer, while the others stat-checked and fell back to home. Home and shell lookups could also skip the EnvInfoer seam through the exported usershell.HomeDir and Get. Resolve through a single usershell.ResolveWorkingDirectory and confine host home and shell lookups to usershell, so SSH sessions, the process API, and tests can no longer diverge and the injected environment is always honored. This also fixes SFTP landing in a configured directory that no longer exists. Refs coder/coder#26099
This commit is contained in:
@@ -1,26 +1,32 @@
|
||||
package usershell_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
)
|
||||
|
||||
//nolint:paralleltest,tparallel // This test sets an environment variable.
|
||||
func TestGet(t *testing.T) {
|
||||
func TestShell(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
ei := usershell.SystemEnvInfo{}
|
||||
|
||||
t.Run("Fallback", func(t *testing.T) {
|
||||
t.Setenv("SHELL", "/bin/sh")
|
||||
|
||||
t.Run("NonExistentUser", func(t *testing.T) {
|
||||
shell, err := usershell.Get("notauser")
|
||||
shell, err := ei.Shell("notauser")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/bin/sh", shell)
|
||||
})
|
||||
@@ -31,14 +37,14 @@ func TestGet(t *testing.T) {
|
||||
t.Setenv("SHELL", "")
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
_, err := usershell.Get("notauser")
|
||||
_, err := ei.Shell("notauser")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("User", func(t *testing.T) {
|
||||
u, err := user.Current()
|
||||
require.NoError(t, err)
|
||||
shell, err := usershell.Get(u.Username)
|
||||
shell, err := ei.Shell(u.Username)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, shell)
|
||||
})
|
||||
@@ -46,10 +52,102 @@ func TestGet(t *testing.T) {
|
||||
|
||||
t.Run("Remove GOTRACEBACK=none", func(t *testing.T) {
|
||||
t.Setenv("GOTRACEBACK", "none")
|
||||
ei := usershell.SystemEnvInfo{}
|
||||
env := ei.Environ()
|
||||
for _, e := range env {
|
||||
require.NotEqual(t, "GOTRACEBACK=none", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// homeEnvInfo reports a fixed home directory and otherwise delegates to
|
||||
// SystemEnvInfo, isolating ResolveWorkingDirectory tests from the host's real
|
||||
// home directory.
|
||||
type homeEnvInfo struct {
|
||||
usershell.SystemEnvInfo
|
||||
home string
|
||||
}
|
||||
|
||||
func (e homeEnvInfo) HomeDir() (string, error) { return e.home, nil }
|
||||
|
||||
// errorEnvInfo reports an error from HomeDir to exercise the fallback
|
||||
// error path.
|
||||
type errorEnvInfo struct {
|
||||
usershell.SystemEnvInfo
|
||||
err error
|
||||
}
|
||||
|
||||
func (e errorEnvInfo) HomeDir() (string, error) { return "", e.err }
|
||||
|
||||
func TestResolveWorkingDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const home = "/home/coder"
|
||||
ei := homeEnvInfo{home: home}
|
||||
|
||||
t.Run("Exists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, fs.MkdirAll("/work", 0o700))
|
||||
dir, err := usershell.ResolveWorkingDirectory(fs, ei, "/work")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/work", dir)
|
||||
})
|
||||
|
||||
t.Run("Missing", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "/work")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, home, dir)
|
||||
})
|
||||
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, home, dir)
|
||||
})
|
||||
|
||||
t.Run("NotADirectory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, "/work", []byte("file"), 0o600))
|
||||
dir, err := usershell.ResolveWorkingDirectory(fs, ei, "/work")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, home, dir)
|
||||
})
|
||||
|
||||
t.Run("HomeDirError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ei := errorEnvInfo{err: xerrors.New("no home")}
|
||||
_, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "")
|
||||
require.ErrorContains(t, err, "no home")
|
||||
})
|
||||
|
||||
t.Run("Symlink", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation requires privileges on Windows")
|
||||
}
|
||||
// MemMapFs cannot model symlinks. Use the real filesystem to
|
||||
// confirm Stat follows symlinks: a link to a directory is honored,
|
||||
// a link to a non-directory falls back to home.
|
||||
fs := afero.NewOsFs()
|
||||
base := t.TempDir()
|
||||
|
||||
realDir := filepath.Join(base, "real")
|
||||
require.NoError(t, os.Mkdir(realDir, 0o700))
|
||||
linkToDir := filepath.Join(base, "link-dir")
|
||||
require.NoError(t, os.Symlink(realDir, linkToDir))
|
||||
dir, err := usershell.ResolveWorkingDirectory(fs, ei, linkToDir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, linkToDir, dir, "symlink to a directory should be honored")
|
||||
|
||||
realFile := filepath.Join(base, "file")
|
||||
require.NoError(t, os.WriteFile(realFile, []byte("x"), 0o600))
|
||||
linkToFile := filepath.Join(base, "link-file")
|
||||
require.NoError(t, os.Symlink(realFile, linkToFile))
|
||||
dir, err = usershell.ResolveWorkingDirectory(fs, ei, linkToFile)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, home, dir, "symlink to a non-directory should fall back to home")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user