Skip to content

Commit 9889b81

Browse files
committed
Merge branch 'upstream' into feat/custom-port
* upstream: (24 commits) docs: add docs on how to allow public github signups (#5168) feat: add CODER_OIDC_IGNORE_EMAIL_VERIFIED config knob (#5165) fix: Improve debuggability of ptytest cleanup (#5170) Filter query: has-agent connecting, connected, disconnected, timeout (#5145) feat: Change workspace version using the UI (#5158) feat: Add support for MOTD file in coder agents (#5147) fix: Fix flakeyness of TestProvisionerd/ReconnectAndComplete (#5169) Don't override 0 ttl with template default (#5151) refactor: Refactor login page (#5148) feat: tweak timeline design (#5144) Audit build outcomes/kira pilot (#5143) fix: don't use yamux for in-memory provisioner{,d} streams (#5136) fix: Trigger workspace event after agent timeout seconds (#5141) fix: Adjust description for cancel in-progress workspace jobs (#5142) chore: refactor audit page to use window function for count (#5133) refactor: Minor build bar UI improvement (#5132) feat: Allow user to cancel workspace jobs (#5115) refactor: Improve empty views (#5134) refactor: Fix up to date color (#5131) chore: refactor agent stats streaming (#5112) ...
2 parents 1018c12 + 5f31ea3 commit 9889b81

File tree

135 files changed

+3225
-1563
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+3225
-1563
lines changed

.vscode/settings.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"enablements",
3434
"errgroup",
3535
"eventsourcemock",
36+
"Failf",
3637
"fatih",
3738
"Formik",
3839
"gitauth",
@@ -127,6 +128,7 @@
127128
"tfplan",
128129
"tfstate",
129130
"tios",
131+
"tmpdir",
130132
"tparallel",
131133
"trialer",
132134
"trimprefix",
@@ -160,10 +162,7 @@
160162
"xstate",
161163
"yamux"
162164
],
163-
"cSpell.ignorePaths": [
164-
"site/package.json",
165-
".vscode/settings.json"
166-
],
165+
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],
167166
"emeraldwalk.runonsave": {
168167
"commands": [
169168
{
@@ -195,10 +194,7 @@
195194
// To reduce redundancy in tests, it's covered by other packages.
196195
// Since package coverage pairing can't be defined, all packages cover
197196
// all other packages.
198-
"go.testFlags": [
199-
"-short",
200-
"-coverpkg=./..."
201-
],
197+
"go.testFlags": ["-short", "-coverpkg=./..."],
202198
// We often use a version of TypeScript that's ahead of the version shipped
203199
// with VS Code.
204200
"typescript.tsdk": "./site/node_modules/typescript/lib"

agent/agent.go

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"bufio"
45
"context"
56
"crypto/rand"
67
"crypto/rsa"
@@ -32,6 +33,7 @@ import (
3233
"golang.org/x/xerrors"
3334
"tailscale.com/net/speedtest"
3435
"tailscale.com/tailcfg"
36+
"tailscale.com/types/netlogtype"
3537

3638
"cdr.dev/slog"
3739
"github.com/coder/coder/agent/usershell"
@@ -98,7 +100,6 @@ func New(options Options) io.Closer {
98100
exchangeToken: options.ExchangeToken,
99101
filesystem: options.Filesystem,
100102
tempDir: options.TempDir,
101-
stats: &Stats{},
102103
}
103104
server.init(ctx)
104105
return server
@@ -126,7 +127,6 @@ type agent struct {
126127
sshServer *ssh.Server
127128

128129
network *tailnet.Conn
129-
stats *Stats
130130
}
131131

132132
// runLoop attempts to start the agent in a retry loop.
@@ -238,22 +238,16 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
238238
return nil, xerrors.New("closed")
239239
}
240240
network, err := tailnet.NewConn(&tailnet.Options{
241-
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)},
242-
DERPMap: derpMap,
243-
Logger: a.logger.Named("tailnet"),
241+
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)},
242+
DERPMap: derpMap,
243+
Logger: a.logger.Named("tailnet"),
244+
EnableTrafficStats: true,
244245
})
245246
if err != nil {
246247
a.closeMutex.Unlock()
247248
return nil, xerrors.Errorf("create tailnet: %w", err)
248249
}
249250
a.network = network
250-
network.SetForwardTCPCallback(func(conn net.Conn, listenerExists bool) net.Conn {
251-
if listenerExists {
252-
// If a listener already exists, we would double-wrap the conn.
253-
return conn
254-
}
255-
return a.stats.wrapConn(conn)
256-
})
257251
a.connCloseWait.Add(4)
258252
a.closeMutex.Unlock()
259253

@@ -268,7 +262,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
268262
if err != nil {
269263
return
270264
}
271-
go a.sshServer.HandleConn(a.stats.wrapConn(conn))
265+
go a.sshServer.HandleConn(conn)
272266
}
273267
}()
274268

@@ -284,7 +278,6 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
284278
a.logger.Debug(ctx, "accept pty failed", slog.Error(err))
285279
return
286280
}
287-
conn = a.stats.wrapConn(conn)
288281
// This cannot use a JSON decoder, since that can
289282
// buffer additional data that is required for the PTY.
290283
rawLen := make([]byte, 2)
@@ -487,12 +480,11 @@ func (a *agent) init(ctx context.Context) {
487480
var opts []sftp.ServerOption
488481
// Change current working directory to the users home
489482
// directory so that SFTP connections land there.
490-
// https://github.com/coder/coder/issues/3620
491-
u, err := user.Current()
483+
homedir, err := userHomeDir()
492484
if err != nil {
493-
sshLogger.Warn(ctx, "get sftp working directory failed, unable to get current user", slog.Error(err))
485+
sshLogger.Warn(ctx, "get sftp working directory failed, unable to get home dir", slog.Error(err))
494486
} else {
495-
opts = append(opts, sftp.WithServerWorkingDirectory(u.HomeDir))
487+
opts = append(opts, sftp.WithServerWorkingDirectory(homedir))
496488
}
497489

498490
server, err := sftp.NewServer(session, opts...)
@@ -523,7 +515,13 @@ func (a *agent) init(ctx context.Context) {
523515

524516
go a.runLoop(ctx)
525517
cl, err := a.client.AgentReportStats(ctx, a.logger, func() *codersdk.AgentStats {
526-
return a.stats.Copy()
518+
stats := map[netlogtype.Connection]netlogtype.Counts{}
519+
a.closeMutex.Lock()
520+
if a.network != nil {
521+
stats = a.network.ExtractTrafficStats()
522+
}
523+
a.closeMutex.Unlock()
524+
return convertAgentStats(stats)
527525
})
528526
if err != nil {
529527
a.logger.Error(ctx, "report stats", slog.Error(err))
@@ -537,6 +535,23 @@ func (a *agent) init(ctx context.Context) {
537535
}()
538536
}
539537

538+
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *codersdk.AgentStats {
539+
stats := &codersdk.AgentStats{
540+
ConnsByProto: map[string]int64{},
541+
NumConns: int64(len(counts)),
542+
}
543+
544+
for conn, count := range counts {
545+
stats.ConnsByProto[conn.Proto.String()]++
546+
stats.RxPackets += int64(count.RxPackets)
547+
stats.RxBytes += int64(count.RxBytes)
548+
stats.TxPackets += int64(count.TxPackets)
549+
stats.TxBytes += int64(count.TxBytes)
550+
}
551+
552+
return stats
553+
}
554+
540555
// createCommand processes raw command input with OpenSSH-like behavior.
541556
// If the rawCommand provided is empty, it will default to the users shell.
542557
// This injects environment variables specified by the user at launch too.
@@ -583,8 +598,12 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
583598
cmd := exec.CommandContext(ctx, shell, args...)
584599
cmd.Dir = metadata.Directory
585600
if cmd.Dir == "" {
586-
// Default to $HOME if a directory is not set!
587-
cmd.Dir = os.Getenv("HOME")
601+
// Default to user home if a directory is not set.
602+
homedir, err := userHomeDir()
603+
if err != nil {
604+
return nil, xerrors.Errorf("get home dir: %w", err)
605+
}
606+
cmd.Dir = homedir
588607
}
589608
cmd.Env = append(os.Environ(), env...)
590609
executablePath, err := os.Executable()
@@ -660,6 +679,18 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
660679
// See https://github.com/coder/coder/issues/3371.
661680
session.DisablePTYEmulation()
662681

682+
if !isQuietLogin(session.RawCommand()) {
683+
metadata, ok := a.metadata.Load().(codersdk.WorkspaceAgentMetadata)
684+
if ok {
685+
err = showMOTD(session, metadata.MOTDFile)
686+
if err != nil {
687+
a.logger.Error(ctx, "show MOTD", slog.Error(err))
688+
}
689+
} else {
690+
a.logger.Warn(ctx, "metadata lookup failed, unable to show MOTD")
691+
}
692+
}
693+
663694
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
664695

665696
// The pty package sets `SSH_TTY` on supported platforms.
@@ -985,19 +1016,74 @@ func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
9851016
}
9861017
}
9871018

988-
// ExpandRelativeHomePath expands the tilde at the beginning of a path to the
989-
// current user's home directory and returns a full absolute path.
990-
func ExpandRelativeHomePath(in string) (string, error) {
991-
usr, err := user.Current()
1019+
// isQuietLogin checks if the SSH server should perform a quiet login or not.
1020+
//
1021+
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
1022+
func isQuietLogin(rawCommand string) bool {
1023+
// We are always quiet unless this is a login shell.
1024+
if len(rawCommand) != 0 {
1025+
return true
1026+
}
1027+
1028+
// Best effort, if we can't get the home directory,
1029+
// we can't lookup .hushlogin.
1030+
homedir, err := userHomeDir()
9921031
if err != nil {
993-
return "", xerrors.Errorf("get current user details: %w", err)
1032+
return false
1033+
}
1034+
1035+
_, err = os.Stat(filepath.Join(homedir, ".hushlogin"))
1036+
return err == nil
1037+
}
1038+
1039+
// showMOTD will output the message of the day from
1040+
// the given filename to dest, if the file exists.
1041+
//
1042+
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L784
1043+
func showMOTD(dest io.Writer, filename string) error {
1044+
if filename == "" {
1045+
return nil
1046+
}
1047+
1048+
f, err := os.Open(filename)
1049+
if err != nil {
1050+
if xerrors.Is(err, os.ErrNotExist) {
1051+
// This is not an error, there simply isn't a MOTD to show.
1052+
return nil
1053+
}
1054+
return xerrors.Errorf("open MOTD: %w", err)
9941055
}
1056+
defer f.Close()
9951057

996-
if in == "~" {
997-
in = usr.HomeDir
998-
} else if strings.HasPrefix(in, "~/") {
999-
in = filepath.Join(usr.HomeDir, in[2:])
1058+
s := bufio.NewScanner(f)
1059+
for s.Scan() {
1060+
// Carriage return ensures each line starts
1061+
// at the beginning of the terminal.
1062+
_, err = fmt.Fprint(dest, s.Text()+"\r\n")
1063+
if err != nil {
1064+
return xerrors.Errorf("write MOTD: %w", err)
1065+
}
1066+
}
1067+
if err := s.Err(); err != nil {
1068+
return xerrors.Errorf("read MOTD: %w", err)
1069+
}
1070+
1071+
return nil
1072+
}
1073+
1074+
// userHomeDir returns the home directory of the current user, giving
1075+
// priority to the $HOME environment variable.
1076+
func userHomeDir() (string, error) {
1077+
// First we check the environment.
1078+
homedir, err := os.UserHomeDir()
1079+
if err == nil {
1080+
return homedir, nil
10001081
}
10011082

1002-
return filepath.Abs(in)
1083+
// As a fallback, we try the user information.
1084+
u, err := user.Current()
1085+
if err != nil {
1086+
return "", xerrors.Errorf("current user: %w", err)
1087+
}
1088+
return u.HomeDir, nil
10031089
}

0 commit comments

Comments
 (0)