mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add support for MOTD file in coder agents (#5147)
This commit is contained in:
+87
-16
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
@@ -479,12 +480,11 @@ func (a *agent) init(ctx context.Context) {
|
||||
var opts []sftp.ServerOption
|
||||
// Change current working directory to the users home
|
||||
// directory so that SFTP connections land there.
|
||||
// https://github.com/coder/coder/issues/3620
|
||||
u, err := user.Current()
|
||||
homedir, err := userHomeDir()
|
||||
if err != nil {
|
||||
sshLogger.Warn(ctx, "get sftp working directory failed, unable to get current user", slog.Error(err))
|
||||
sshLogger.Warn(ctx, "get sftp working directory failed, unable to get home dir", slog.Error(err))
|
||||
} else {
|
||||
opts = append(opts, sftp.WithServerWorkingDirectory(u.HomeDir))
|
||||
opts = append(opts, sftp.WithServerWorkingDirectory(homedir))
|
||||
}
|
||||
|
||||
server, err := sftp.NewServer(session, opts...)
|
||||
@@ -598,8 +598,12 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
|
||||
cmd := exec.CommandContext(ctx, shell, args...)
|
||||
cmd.Dir = metadata.Directory
|
||||
if cmd.Dir == "" {
|
||||
// Default to $HOME if a directory is not set!
|
||||
cmd.Dir = os.Getenv("HOME")
|
||||
// Default to user home if a directory is not set.
|
||||
homedir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get home dir: %w", err)
|
||||
}
|
||||
cmd.Dir = homedir
|
||||
}
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
executablePath, err := os.Executable()
|
||||
@@ -675,6 +679,18 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
||||
// See https://github.com/coder/coder/issues/3371.
|
||||
session.DisablePTYEmulation()
|
||||
|
||||
if !isQuietLogin(session.RawCommand()) {
|
||||
metadata, ok := a.metadata.Load().(codersdk.WorkspaceAgentMetadata)
|
||||
if ok {
|
||||
err = showMOTD(session, metadata.MOTDFile)
|
||||
if err != nil {
|
||||
a.logger.Error(ctx, "show MOTD", slog.Error(err))
|
||||
}
|
||||
} else {
|
||||
a.logger.Warn(ctx, "metadata lookup failed, unable to show MOTD")
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
|
||||
|
||||
// The pty package sets `SSH_TTY` on supported platforms.
|
||||
@@ -1000,19 +1016,74 @@ func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandRelativeHomePath expands the tilde at the beginning of a path to the
|
||||
// current user's home directory and returns a full absolute path.
|
||||
func ExpandRelativeHomePath(in string) (string, error) {
|
||||
usr, err := user.Current()
|
||||
// isQuietLogin checks if the SSH server should perform a quiet login or not.
|
||||
//
|
||||
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
|
||||
func isQuietLogin(rawCommand string) bool {
|
||||
// We are always quiet unless this is a login shell.
|
||||
if len(rawCommand) != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Best effort, if we can't get the home directory,
|
||||
// we can't lookup .hushlogin.
|
||||
homedir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("get current user details: %w", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if in == "~" {
|
||||
in = usr.HomeDir
|
||||
} else if strings.HasPrefix(in, "~/") {
|
||||
in = filepath.Join(usr.HomeDir, in[2:])
|
||||
_, err = os.Stat(filepath.Join(homedir, ".hushlogin"))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// showMOTD will output the message of the day from
|
||||
// the given filename to dest, if the file exists.
|
||||
//
|
||||
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L784
|
||||
func showMOTD(dest io.Writer, filename string) error {
|
||||
if filename == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
if xerrors.Is(err, os.ErrNotExist) {
|
||||
// This is not an error, there simply isn't a MOTD to show.
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("open MOTD: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
// Carriage return ensures each line starts
|
||||
// at the beginning of the terminal.
|
||||
_, err = fmt.Fprint(dest, s.Text()+"\r\n")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("write MOTD: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return xerrors.Errorf("read MOTD: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return filepath.Abs(in)
|
||||
// userHomeDir returns the home directory of the current user, giving
|
||||
// priority to the $HOME environment variable.
|
||||
func userHomeDir() (string, error) {
|
||||
// First we check the environment.
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
return homedir, nil
|
||||
}
|
||||
|
||||
// As a fallback, we try the user information.
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("current user: %w", err)
|
||||
}
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package agent_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -193,6 +194,92 @@ func TestAgent(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
//nolint:paralleltest // This test sets an environment variable.
|
||||
t.Run("Session TTY MOTD", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// This might be our implementation, or ConPTY itself.
|
||||
// It's difficult to find extensive tests for it, so
|
||||
// it seems like it could be either.
|
||||
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
||||
}
|
||||
|
||||
wantMOTD := "Welcome to your Coder workspace!"
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
name := filepath.Join(tmpdir, "motd")
|
||||
err := os.WriteFile(name, []byte(wantMOTD), 0o600)
|
||||
require.NoError(t, err, "write motd file")
|
||||
|
||||
// Set HOME so we can ensure no ~/.hushlogin is present.
|
||||
t.Setenv("HOME", tmpdir)
|
||||
|
||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
||||
MOTDFile: name,
|
||||
})
|
||||
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
||||
require.NoError(t, err)
|
||||
|
||||
ptty := ptytest.New(t)
|
||||
var stdout bytes.Buffer
|
||||
session.Stdout = &stdout
|
||||
session.Stderr = ptty.Output()
|
||||
session.Stdin = ptty.Input()
|
||||
err = session.Shell()
|
||||
require.NoError(t, err)
|
||||
|
||||
ptty.WriteLine("exit 0")
|
||||
err = session.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, stdout.String(), wantMOTD, "should show motd")
|
||||
})
|
||||
|
||||
//nolint:paralleltest // This test sets an environment variable.
|
||||
t.Run("Session TTY Hushlogin", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// This might be our implementation, or ConPTY itself.
|
||||
// It's difficult to find extensive tests for it, so
|
||||
// it seems like it could be either.
|
||||
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
||||
}
|
||||
|
||||
wantNotMOTD := "Welcome to your Coder workspace!"
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
name := filepath.Join(tmpdir, "motd")
|
||||
err := os.WriteFile(name, []byte(wantNotMOTD), 0o600)
|
||||
require.NoError(t, err, "write motd file")
|
||||
|
||||
// Create hushlogin to silence motd.
|
||||
f, err := os.Create(filepath.Join(tmpdir, ".hushlogin"))
|
||||
require.NoError(t, err, "create .hushlogin file")
|
||||
err = f.Close()
|
||||
require.NoError(t, err, "close .hushlogin file")
|
||||
|
||||
// Set HOME so we can ensure ~/.hushlogin is present.
|
||||
t.Setenv("HOME", tmpdir)
|
||||
|
||||
session := setupSSHSession(t, codersdk.WorkspaceAgentMetadata{
|
||||
MOTDFile: name,
|
||||
})
|
||||
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
||||
require.NoError(t, err)
|
||||
|
||||
ptty := ptytest.New(t)
|
||||
var stdout bytes.Buffer
|
||||
session.Stdout = &stdout
|
||||
session.Stderr = ptty.Output()
|
||||
session.Stdin = ptty.Input()
|
||||
err = session.Shell()
|
||||
require.NoError(t, err)
|
||||
|
||||
ptty.WriteLine("exit 0")
|
||||
err = session.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotContains(t, stdout.String(), wantNotMOTD, "should not show motd")
|
||||
})
|
||||
|
||||
t.Run("LocalForwarding", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
random, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
Reference in New Issue
Block a user