Skip to content

Commit 1afaa5d

Browse files
committed
Merge remote-tracking branch 'origin/main' into stevenmasley/redo_typescript_gen
2 parents 9a3d7e3 + 816441e commit 1afaa5d

File tree

102 files changed

+3105
-1249
lines changed

Some content is hidden

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

102 files changed

+3105
-1249
lines changed

.github/workflows/coder.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ jobs:
172172
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
173173

174174
- name: Install goreleaser
175-
uses: jaxxstorm/action-install-gh-release@v1.4.0
175+
uses: jaxxstorm/action-install-gh-release@v1.5.0
176176
env:
177177
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178178
with:
179179
repo: gotestyourself/gotestsum
180180
tag: v1.7.0
181181

182-
- uses: hashicorp/setup-terraform@v1
182+
- uses: hashicorp/setup-terraform@v2
183183
with:
184184
terraform_version: 1.1.2
185185
terraform_wrapper: false
@@ -241,14 +241,14 @@ jobs:
241241
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
242242

243243
- name: Install goreleaser
244-
uses: jaxxstorm/action-install-gh-release@v1.4.0
244+
uses: jaxxstorm/action-install-gh-release@v1.5.0
245245
env:
246246
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
247247
with:
248248
repo: gotestyourself/gotestsum
249249
tag: v1.7.0
250250

251-
- uses: hashicorp/setup-terraform@v1
251+
- uses: hashicorp/setup-terraform@v2
252252
with:
253253
terraform_version: 1.1.2
254254
terraform_wrapper: false
@@ -449,7 +449,7 @@ jobs:
449449
with:
450450
go-version: "~1.18"
451451

452-
- uses: hashicorp/setup-terraform@v1
452+
- uses: hashicorp/setup-terraform@v2
453453
with:
454454
terraform_version: 1.1.2
455455
terraform_wrapper: false

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"gographviz",
1717
"goleak",
1818
"gossh",
19+
"gsyslog",
1920
"hashicorp",
2021
"hclsyntax",
2122
"httpmw",

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ fmt: fmt/prettier fmt/terraform
4343
gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto apitypings/generate
4444
.PHONY: gen
4545

46-
install: bin
46+
install: build
4747
@echo "--- Copying from bin to $(INSTALL_DIR)"
4848
cp -r ./dist/coder-$(GOOS)_$(GOOS)_$(GOARCH)*/* $(INSTALL_DIR)
4949
@echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)"

agent/agent.go

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import (
1111
"os"
1212
"os/exec"
1313
"os/user"
14+
"runtime"
15+
"strings"
1416
"sync"
1517
"time"
1618

19+
gsyslog "github.com/hashicorp/go-syslog"
20+
"go.uber.org/atomic"
21+
1722
"cdr.dev/slog"
1823
"github.com/coder/coder/agent/usershell"
1924
"github.com/coder/coder/peer"
@@ -28,11 +33,14 @@ import (
2833
"golang.org/x/xerrors"
2934
)
3035

31-
type Options struct {
32-
Logger slog.Logger
36+
type Metadata struct {
37+
OwnerEmail string `json:"owner_email"`
38+
OwnerUsername string `json:"owner_username"`
39+
EnvironmentVariables map[string]string `json:"environment_variables"`
40+
StartupScript string `json:"startup_script"`
3341
}
3442

35-
type Dialer func(ctx context.Context, logger slog.Logger) (*peerbroker.Listener, error)
43+
type Dialer func(ctx context.Context, logger slog.Logger) (Metadata, *peerbroker.Listener, error)
3644

3745
func New(dialer Dialer, logger slog.Logger) io.Closer {
3846
ctx, cancelFunc := context.WithCancel(context.Background())
@@ -55,16 +63,23 @@ type agent struct {
5563
closeMutex sync.Mutex
5664
closed chan struct{}
5765

58-
sshServer *ssh.Server
66+
// Environment variables sent by Coder to inject for shell sessions.
67+
// These are atomic because values can change after reconnect.
68+
envVars atomic.Value
69+
ownerEmail atomic.String
70+
ownerUsername atomic.String
71+
startupScript atomic.Bool
72+
sshServer *ssh.Server
5973
}
6074

6175
func (a *agent) run(ctx context.Context) {
76+
var options Metadata
6277
var peerListener *peerbroker.Listener
6378
var err error
6479
// An exponential back-off occurs when the connection is failing to dial.
6580
// This is to prevent server spam in case of a coderd outage.
6681
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
67-
peerListener, err = a.dialer(ctx, a.logger)
82+
options, peerListener, err = a.dialer(ctx, a.logger)
6883
if err != nil {
6984
if errors.Is(err, context.Canceled) {
7085
return
@@ -83,6 +98,22 @@ func (a *agent) run(ctx context.Context) {
8398
return
8499
default:
85100
}
101+
a.envVars.Store(options.EnvironmentVariables)
102+
a.ownerEmail.Store(options.OwnerEmail)
103+
a.ownerUsername.Store(options.OwnerUsername)
104+
105+
if a.startupScript.CAS(false, true) {
106+
// The startup script has not ran yet!
107+
go func() {
108+
err := a.runStartupScript(ctx, options.StartupScript)
109+
if errors.Is(err, context.Canceled) {
110+
return
111+
}
112+
if err != nil {
113+
a.logger.Warn(ctx, "agent script failed", slog.Error(err))
114+
}
115+
}()
116+
}
86117

87118
for {
88119
conn, err := peerListener.Accept()
@@ -101,6 +132,48 @@ func (a *agent) run(ctx context.Context) {
101132
}
102133
}
103134

135+
func (*agent) runStartupScript(ctx context.Context, script string) error {
136+
if script == "" {
137+
return nil
138+
}
139+
currentUser, err := user.Current()
140+
if err != nil {
141+
return xerrors.Errorf("get current user: %w", err)
142+
}
143+
username := currentUser.Username
144+
145+
shell, err := usershell.Get(username)
146+
if err != nil {
147+
return xerrors.Errorf("get user shell: %w", err)
148+
}
149+
150+
var writer io.WriteCloser
151+
// Attempt to use the syslog to write startup information.
152+
writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "USER", "coder-startup-script")
153+
if err != nil {
154+
// If the syslog isn't supported or cannot be created, use a text file in temp.
155+
writer, err = os.CreateTemp("", "coder-startup-script.txt")
156+
if err != nil {
157+
return xerrors.Errorf("open startup script log file: %w", err)
158+
}
159+
}
160+
defer func() {
161+
_ = writer.Close()
162+
}()
163+
caller := "-c"
164+
if runtime.GOOS == "windows" {
165+
caller = "/c"
166+
}
167+
cmd := exec.CommandContext(ctx, shell, caller, script)
168+
cmd.Stdout = writer
169+
cmd.Stderr = writer
170+
err = cmd.Run()
171+
if err != nil {
172+
return xerrors.Errorf("run: %w", err)
173+
}
174+
return nil
175+
}
176+
104177
func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
105178
go func() {
106179
select {
@@ -230,13 +303,35 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
230303

231304
// OpenSSH executes all commands with the users current shell.
232305
// We replicate that behavior for IDE support.
233-
cmd := exec.CommandContext(session.Context(), shell, "-c", command)
306+
caller := "-c"
307+
if runtime.GOOS == "windows" {
308+
caller = "/c"
309+
}
310+
cmd := exec.CommandContext(session.Context(), shell, caller, command)
234311
cmd.Env = append(os.Environ(), session.Environ()...)
235312
executablePath, err := os.Executable()
236313
if err != nil {
237314
return xerrors.Errorf("getting os executable: %w", err)
238315
}
316+
// Git on Windows resolves with UNIX-style paths.
317+
// If using backslashes, it's unable to find the executable.
318+
executablePath = strings.ReplaceAll(executablePath, "\\", "/")
239319
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND=%s gitssh --`, executablePath))
320+
// These prevent the user from having to specify _anything_ to successfully commit.
321+
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_AUTHOR_EMAIL=%s`, a.ownerEmail.Load()))
322+
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_AUTHOR_NAME=%s`, a.ownerUsername.Load()))
323+
324+
// Load environment variables passed via the agent.
325+
// These should override all variables we manually specify.
326+
envVars := a.envVars.Load()
327+
if envVars != nil {
328+
envVarMap, ok := envVars.(map[string]string)
329+
if ok {
330+
for key, value := range envVarMap {
331+
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
332+
}
333+
}
334+
}
240335

241336
sshPty, windowSize, isPty := session.Pty()
242337
if isPty {
@@ -245,9 +340,13 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
245340
if err != nil {
246341
return xerrors.Errorf("start command: %w", err)
247342
}
343+
err = ptty.Resize(uint16(sshPty.Window.Height), uint16(sshPty.Window.Width))
344+
if err != nil {
345+
return xerrors.Errorf("resize ptty: %w", err)
346+
}
248347
go func() {
249348
for win := range windowSize {
250-
err = ptty.Resize(uint16(win.Width), uint16(win.Height))
349+
err = ptty.Resize(uint16(win.Height), uint16(win.Width))
251350
if err != nil {
252351
a.logger.Warn(context.Background(), "failed to resize tty", slog.Error(err))
253352
}

agent/agent_test.go

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import (
1212
"strconv"
1313
"strings"
1414
"testing"
15+
"time"
1516

1617
"github.com/pion/webrtc/v3"
1718
"github.com/pkg/sftp"
1819
"github.com/stretchr/testify/require"
1920
"go.uber.org/goleak"
2021
"golang.org/x/crypto/ssh"
22+
"golang.org/x/text/encoding/unicode"
23+
"golang.org/x/text/transform"
2124

2225
"cdr.dev/slog"
2326
"cdr.dev/slog/sloggers/slogtest"
@@ -37,7 +40,7 @@ func TestAgent(t *testing.T) {
3740
t.Parallel()
3841
t.Run("SessionExec", func(t *testing.T) {
3942
t.Parallel()
40-
session := setupSSHSession(t)
43+
session := setupSSHSession(t, agent.Metadata{})
4144

4245
command := "echo test"
4346
if runtime.GOOS == "windows" {
@@ -50,7 +53,7 @@ func TestAgent(t *testing.T) {
5053

5154
t.Run("GitSSH", func(t *testing.T) {
5255
t.Parallel()
53-
session := setupSSHSession(t)
56+
session := setupSSHSession(t, agent.Metadata{})
5457
command := "sh -c 'echo $GIT_SSH_COMMAND'"
5558
if runtime.GOOS == "windows" {
5659
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
@@ -62,7 +65,13 @@ func TestAgent(t *testing.T) {
6265

6366
t.Run("SessionTTY", func(t *testing.T) {
6467
t.Parallel()
65-
session := setupSSHSession(t)
68+
if runtime.GOOS == "windows" {
69+
// This might be our implementation, or ConPTY itself.
70+
// It's difficult to find extensive tests for it, so
71+
// it seems like it could be either.
72+
t.Skip("ConPTY appears to be inconsistent on Windows.")
73+
}
74+
session := setupSSHSession(t, agent.Metadata{})
6675
command := "bash"
6776
if runtime.GOOS == "windows" {
6877
command = "cmd.exe"
@@ -76,6 +85,11 @@ func TestAgent(t *testing.T) {
7685
session.Stdin = ptty.Input()
7786
err = session.Start(command)
7887
require.NoError(t, err)
88+
caret := "$"
89+
if runtime.GOOS == "windows" {
90+
caret = ">"
91+
}
92+
ptty.ExpectMatch(caret)
7993
ptty.WriteLine("echo test")
8094
ptty.ExpectMatch("test")
8195
ptty.WriteLine("exit")
@@ -117,7 +131,7 @@ func TestAgent(t *testing.T) {
117131

118132
t.Run("SFTP", func(t *testing.T) {
119133
t.Parallel()
120-
sshClient, err := setupAgent(t).SSHClient()
134+
sshClient, err := setupAgent(t, agent.Metadata{}).SSHClient()
121135
require.NoError(t, err)
122136
client, err := sftp.NewClient(sshClient)
123137
require.NoError(t, err)
@@ -129,10 +143,55 @@ func TestAgent(t *testing.T) {
129143
_, err = os.Stat(tempFile)
130144
require.NoError(t, err)
131145
})
146+
147+
t.Run("EnvironmentVariables", func(t *testing.T) {
148+
t.Parallel()
149+
key := "EXAMPLE"
150+
value := "value"
151+
session := setupSSHSession(t, agent.Metadata{
152+
EnvironmentVariables: map[string]string{
153+
key: value,
154+
},
155+
})
156+
command := "sh -c 'echo $" + key + "'"
157+
if runtime.GOOS == "windows" {
158+
command = "cmd.exe /c echo %" + key + "%"
159+
}
160+
output, err := session.Output(command)
161+
require.NoError(t, err)
162+
require.Equal(t, value, strings.TrimSpace(string(output)))
163+
})
164+
165+
t.Run("StartupScript", func(t *testing.T) {
166+
t.Parallel()
167+
tempPath := filepath.Join(os.TempDir(), "content.txt")
168+
content := "somethingnice"
169+
setupAgent(t, agent.Metadata{
170+
StartupScript: "echo " + content + " > " + tempPath,
171+
})
172+
var gotContent string
173+
require.Eventually(t, func() bool {
174+
content, err := os.ReadFile(tempPath)
175+
if err != nil {
176+
return false
177+
}
178+
if len(content) == 0 {
179+
return false
180+
}
181+
if runtime.GOOS == "windows" {
182+
// Windows uses UTF16! 🪟🪟🪟
183+
content, _, err = transform.Bytes(unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder(), content)
184+
require.NoError(t, err)
185+
}
186+
gotContent = string(content)
187+
return true
188+
}, 15*time.Second, 100*time.Millisecond)
189+
require.Equal(t, content, strings.TrimSpace(gotContent))
190+
})
132191
}
133192

134193
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
135-
agentConn := setupAgent(t)
194+
agentConn := setupAgent(t, agent.Metadata{})
136195
listener, err := net.Listen("tcp", "127.0.0.1:0")
137196
require.NoError(t, err)
138197
go func() {
@@ -160,18 +219,19 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe
160219
return exec.Command("ssh", args...)
161220
}
162221

163-
func setupSSHSession(t *testing.T) *ssh.Session {
164-
sshClient, err := setupAgent(t).SSHClient()
222+
func setupSSHSession(t *testing.T, options agent.Metadata) *ssh.Session {
223+
sshClient, err := setupAgent(t, options).SSHClient()
165224
require.NoError(t, err)
166225
session, err := sshClient.NewSession()
167226
require.NoError(t, err)
168227
return session
169228
}
170229

171-
func setupAgent(t *testing.T) *agent.Conn {
230+
func setupAgent(t *testing.T, options agent.Metadata) *agent.Conn {
172231
client, server := provisionersdk.TransportPipe()
173-
closer := agent.New(func(ctx context.Context, logger slog.Logger) (*peerbroker.Listener, error) {
174-
return peerbroker.Listen(server, nil)
232+
closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) {
233+
listener, err := peerbroker.Listen(server, nil)
234+
return options, listener, err
175235
}, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
176236
t.Cleanup(func() {
177237
_ = client.Close()

0 commit comments

Comments
 (0)