Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Add support for MOTD file in coder agents #5147

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 24, 2022
Merged
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"tfplan",
"tfstate",
"tios",
"tmpdir",
"tparallel",
"trialer",
"trimprefix",
Expand Down
103 changes: 87 additions & 16 deletions agent/agent.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package agent

import (
"bufio"
"context"
"crypto/rand"
"crypto/rsa"
Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 false
}

_, 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 {
return "", xerrors.Errorf("get current user details: %w", err)
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()

if in == "~" {
in = usr.HomeDir
} else if strings.HasPrefix(in, "~/") {
in = filepath.Join(usr.HomeDir, in[2:])
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 filepath.Abs(in)
return nil
}

// 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
}
87 changes: 87 additions & 0 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agent_test

import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,7 @@ func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
ResourceMetadata: arg.ResourceMetadata,
ConnectionTimeoutSeconds: arg.ConnectionTimeoutSeconds,
TroubleshootingURL: arg.TroubleshootingURL,
MOTDFile: arg.MOTDFile,
}

q.workspaceAgents = append(q.workspaceAgents, agent)
Expand Down Expand Up @@ -2895,6 +2896,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U
}
return database.WorkspaceBuild{}, sql.ErrNoRows
}

func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
Expand Down
5 changes: 4 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workspace_agents
DROP COLUMN motd_file;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE workspace_agents
ADD COLUMN motd_file text NOT NULL DEFAULT '';

COMMENT ON COLUMN workspace_agents.motd_file IS 'Path to file inside workspace containing the message of the day (MOTD) to show to the user when logging in via SSH.';
2 changes: 2 additions & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading