- Notifications
You must be signed in to change notification settings - Fork925
chore(agent/agentssh): extract EnvInfoer#16603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -409,7 +409,7 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv | ||
magicTypeLabel := magicTypeMetricLabel(magicType) | ||
sshPty, windowSize, isPty := session.Pty() | ||
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env, nil) | ||
if err != nil { | ||
ptyLabel := "no" | ||
if isPty { | ||
@@ -670,17 +670,63 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) { | ||
_ = session.Exit(1) | ||
} | ||
// EnvInfoer encapsulates external information required by CreateCommand. | ||
type EnvInfoer interface { | ||
// CurrentUser returns the current user. | ||
CurrentUser() (*user.User, error) | ||
// Environ returns the environment variables of the current process. | ||
Environ() []string | ||
// UserHomeDir returns the home directory of the current user. | ||
UserHomeDir() (string, error) | ||
// UserShell returns the shell of the given user. | ||
UserShell(username string) (string, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I feel like all these methods are related to the environment, figuring out the user, env, home and shell, so EnvInfo sounds like a pretty good name to me. Deps is a bit vague IMO and makes me think of dependency injection. 😄 We could also simplify this interface somewhat. All we really need are:
We'd just make sure the returned user has the correct home directory and shell populated. Not sure if that'd be worth the lift, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more.
I'd prefer to do the bare minimum here, but we can consolidate + simplify down the line! | ||
} | ||
type systemEnvInfoer struct{} | ||
var defaultEnvInfoer EnvInfoer = &systemEnvInfoer{} | ||
// DefaultEnvInfoer returns a default implementation of | ||
// EnvInfoer. This reads information using the default Go | ||
// implementations. | ||
func DefaultEnvInfoer() EnvInfoer { | ||
return defaultEnvInfoer | ||
} | ||
func (systemEnvInfoer) CurrentUser() (*user.User, error) { | ||
return user.Current() | ||
} | ||
func (systemEnvInfoer) Environ() []string { | ||
return os.Environ() | ||
} | ||
func (systemEnvInfoer) UserHomeDir() (string, error) { | ||
return userHomeDir() | ||
} | ||
func (systemEnvInfoer) UserShell(username string) (string, error) { | ||
return usershell.Get(username) | ||
} | ||
// CreateCommand processes raw command input with OpenSSH-like behavior. | ||
// If the script provided is empty, it will default to the users shell. | ||
// This injects environment variables specified by the user at launch too. | ||
// The final argument is an interface that allows the caller to provide | ||
// alternative implementations for the dependencies of CreateCommand. | ||
// This is useful when creating a command to be run in a separate environment | ||
// (for example, a Docker container). Pass in nil to use the default. | ||
func (s *Server) CreateCommand(ctx context.Context, script string, env []string, deps EnvInfoer) (*pty.Cmd, error) { | ||
if deps == nil { | ||
deps = DefaultEnvInfoer() | ||
} | ||
currentUser, err := deps.CurrentUser() | ||
if err != nil { | ||
return nil, xerrors.Errorf("get current user: %w", err) | ||
} | ||
username := currentUser.Username | ||
shell, err :=deps.UserShell(username) | ||
if err != nil { | ||
return nil, xerrors.Errorf("get user shell: %w", err) | ||
} | ||
@@ -736,13 +782,13 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string) | ||
_, err = os.Stat(cmd.Dir) | ||
if cmd.Dir == "" || err != nil { | ||
// Default to user home if a directory is not set. | ||
homedir, err :=deps.UserHomeDir() | ||
if err != nil { | ||
return nil, xerrors.Errorf("get home dir: %w", err) | ||
} | ||
cmd.Dir = homedir | ||
} | ||
cmd.Env = append(deps.Environ(), env...) | ||
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username)) | ||
// Set SSH connection environment variables (these are also set by OpenSSH | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -8,6 +8,7 @@ import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"os/user" | ||
"runtime" | ||
"strings" | ||
"sync" | ||
@@ -87,7 +88,7 @@ func TestNewServer_ExecuteShebang(t *testing.T) { | ||
t.Run("Basic", func(t *testing.T) { | ||
t.Parallel() | ||
cmd, err := s.CreateCommand(ctx, `#!/bin/bash | ||
echo test`, nil, nil) | ||
require.NoError(t, err) | ||
output, err := cmd.AsExec().CombinedOutput() | ||
require.NoError(t, err) | ||
@@ -96,12 +97,45 @@ func TestNewServer_ExecuteShebang(t *testing.T) { | ||
t.Run("Args", func(t *testing.T) { | ||
t.Parallel() | ||
cmd, err := s.CreateCommand(ctx, `#!/usr/bin/env bash | ||
echo test`, nil, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Perhaps we should add a test for a custom implementation as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Can do! | ||
require.NoError(t, err) | ||
output, err := cmd.AsExec().CombinedOutput() | ||
require.NoError(t, err) | ||
require.Equal(t, "test\n", string(output)) | ||
}) | ||
t.Run("CustomEnvInfoer", func(t *testing.T) { | ||
t.Parallel() | ||
ei := &fakeEnvInfoer{ | ||
CurrentUserFn: func() (u *user.User, err error) { | ||
return nil, assert.AnError | ||
}, | ||
} | ||
_, err := s.CreateCommand(ctx, `whatever`, nil, ei) | ||
require.ErrorIs(t, err, assert.AnError) | ||
}) | ||
} | ||
type fakeEnvInfoer struct { | ||
CurrentUserFn func() (*user.User, error) | ||
EnvironFn func() []string | ||
UserHomeDirFn func() (string, error) | ||
UserShellFn func(string) (string, error) | ||
} | ||
func (f *fakeEnvInfoer) CurrentUser() (u *user.User, err error) { | ||
return f.CurrentUserFn() | ||
} | ||
func (f *fakeEnvInfoer) Environ() []string { | ||
return f.EnvironFn() | ||
} | ||
func (f *fakeEnvInfoer) UserHomeDir() (string, error) { | ||
return f.UserHomeDirFn() | ||
} | ||
func (f *fakeEnvInfoer) UserShell(u string) (string, error) { | ||
return f.UserShellFn(u) | ||
} | ||
func TestNewServer_CloseActiveConnections(t *testing.T) { | ||
Uh oh!
There was an error while loading.Please reload this page.