Skip to content

Commit 8f23d91

Browse files
committed
Merge branch 'main' into create-user/presleyp/734
2 parents cf8442f + 8661f92 commit 8f23d91

37 files changed

+718
-266
lines changed

agent/agent.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import (
3333
"golang.org/x/xerrors"
3434
)
3535

36-
type Options struct {
37-
EnvironmentVariables map[string]string
38-
StartupScript string
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"`
3941
}
4042

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

4345
func New(dialer Dialer, logger slog.Logger) io.Closer {
4446
ctx, cancelFunc := context.WithCancel(context.Background())
@@ -62,14 +64,16 @@ type agent struct {
6264
closed chan struct{}
6365

6466
// Environment variables sent by Coder to inject for shell sessions.
65-
// This is atomic because values can change after reconnect.
67+
// These are atomic because values can change after reconnect.
6668
envVars atomic.Value
69+
ownerEmail atomic.String
70+
ownerUsername atomic.String
6771
startupScript atomic.Bool
6872
sshServer *ssh.Server
6973
}
7074

7175
func (a *agent) run(ctx context.Context) {
72-
var options *Options
76+
var options Metadata
7377
var peerListener *peerbroker.Listener
7478
var err error
7579
// An exponential back-off occurs when the connection is failing to dial.
@@ -95,6 +99,8 @@ func (a *agent) run(ctx context.Context) {
9599
default:
96100
}
97101
a.envVars.Store(options.EnvironmentVariables)
102+
a.ownerEmail.Store(options.OwnerEmail)
103+
a.ownerUsername.Store(options.OwnerUsername)
98104

99105
if a.startupScript.CAS(false, true) {
100106
// The startup script has not ran yet!
@@ -303,8 +309,20 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
303309
}
304310
cmd := exec.CommandContext(session.Context(), shell, caller, command)
305311
cmd.Env = append(os.Environ(), session.Environ()...)
312+
executablePath, err := os.Executable()
313+
if err != nil {
314+
return xerrors.Errorf("getting os executable: %w", err)
315+
}
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, "\\", "/")
319+
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()))
306323

307324
// Load environment variables passed via the agent.
325+
// These should override all variables we manually specify.
308326
envVars := a.envVars.Load()
309327
if envVars != nil {
310328
envVarMap, ok := envVars.(map[string]string)
@@ -315,22 +333,17 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
315333
}
316334
}
317335

318-
executablePath, err := os.Executable()
319-
if err != nil {
320-
return xerrors.Errorf("getting os executable: %w", err)
321-
}
322-
// Git on Windows resolves with UNIX-style paths.
323-
// If using backslashes, it's unable to find the executable.
324-
executablePath = strings.ReplaceAll(executablePath, "\\", "/")
325-
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND=%s gitssh --`, executablePath))
326-
327336
sshPty, windowSize, isPty := session.Pty()
328337
if isPty {
329338
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
330339
ptty, process, err := pty.Start(cmd)
331340
if err != nil {
332341
return xerrors.Errorf("start command: %w", err)
333342
}
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+
}
334347
go func() {
335348
for win := range windowSize {
336349
err = ptty.Resize(uint16(win.Height), uint16(win.Width))

agent/agent_test.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func TestAgent(t *testing.T) {
4040
t.Parallel()
4141
t.Run("SessionExec", func(t *testing.T) {
4242
t.Parallel()
43-
session := setupSSHSession(t, nil)
43+
session := setupSSHSession(t, agent.Metadata{})
4444

4545
command := "echo test"
4646
if runtime.GOOS == "windows" {
@@ -53,7 +53,7 @@ func TestAgent(t *testing.T) {
5353

5454
t.Run("GitSSH", func(t *testing.T) {
5555
t.Parallel()
56-
session := setupSSHSession(t, nil)
56+
session := setupSSHSession(t, agent.Metadata{})
5757
command := "sh -c 'echo $GIT_SSH_COMMAND'"
5858
if runtime.GOOS == "windows" {
5959
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
@@ -71,7 +71,7 @@ func TestAgent(t *testing.T) {
7171
// it seems like it could be either.
7272
t.Skip("ConPTY appears to be inconsistent on Windows.")
7373
}
74-
session := setupSSHSession(t, nil)
74+
session := setupSSHSession(t, agent.Metadata{})
7575
command := "bash"
7676
if runtime.GOOS == "windows" {
7777
command = "cmd.exe"
@@ -131,7 +131,7 @@ func TestAgent(t *testing.T) {
131131

132132
t.Run("SFTP", func(t *testing.T) {
133133
t.Parallel()
134-
sshClient, err := setupAgent(t, nil).SSHClient()
134+
sshClient, err := setupAgent(t, agent.Metadata{}).SSHClient()
135135
require.NoError(t, err)
136136
client, err := sftp.NewClient(sshClient)
137137
require.NoError(t, err)
@@ -148,7 +148,7 @@ func TestAgent(t *testing.T) {
148148
t.Parallel()
149149
key := "EXAMPLE"
150150
value := "value"
151-
session := setupSSHSession(t, &agent.Options{
151+
session := setupSSHSession(t, agent.Metadata{
152152
EnvironmentVariables: map[string]string{
153153
key: value,
154154
},
@@ -166,7 +166,7 @@ func TestAgent(t *testing.T) {
166166
t.Parallel()
167167
tempPath := filepath.Join(os.TempDir(), "content.txt")
168168
content := "somethingnice"
169-
setupAgent(t, &agent.Options{
169+
setupAgent(t, agent.Metadata{
170170
StartupScript: "echo " + content + " > " + tempPath,
171171
})
172172
var gotContent string
@@ -191,7 +191,7 @@ func TestAgent(t *testing.T) {
191191
}
192192

193193
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
194-
agentConn := setupAgent(t, nil)
194+
agentConn := setupAgent(t, agent.Metadata{})
195195
listener, err := net.Listen("tcp", "127.0.0.1:0")
196196
require.NoError(t, err)
197197
go func() {
@@ -219,20 +219,17 @@ func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exe
219219
return exec.Command("ssh", args...)
220220
}
221221

222-
func setupSSHSession(t *testing.T, options *agent.Options) *ssh.Session {
222+
func setupSSHSession(t *testing.T, options agent.Metadata) *ssh.Session {
223223
sshClient, err := setupAgent(t, options).SSHClient()
224224
require.NoError(t, err)
225225
session, err := sshClient.NewSession()
226226
require.NoError(t, err)
227227
return session
228228
}
229229

230-
func setupAgent(t *testing.T, options *agent.Options) *agent.Conn {
231-
if options == nil {
232-
options = &agent.Options{}
233-
}
230+
func setupAgent(t *testing.T, options agent.Metadata) *agent.Conn {
234231
client, server := provisionersdk.TransportPipe()
235-
closer := agent.New(func(ctx context.Context, logger slog.Logger) (*agent.Options, *peerbroker.Listener, error) {
232+
closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) {
236233
listener, err := peerbroker.Listen(server, nil)
237234
return options, listener, err
238235
}, slogtest.Make(t, nil).Leveled(slog.LevelDebug))

cli/server.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ import (
4747
"github.com/coder/coder/provisionersdk/proto"
4848
)
4949

50+
var defaultDevUser = codersdk.CreateFirstUserRequest{
51+
Email: "admin@coder.com",
52+
Username: "developer",
53+
Password: "password",
54+
OrganizationName: "acme-corp",
55+
}
56+
5057
// nolint:gocyclo
5158
func server() *cobra.Command {
5259
var (
@@ -275,6 +282,9 @@ func server() *cobra.Command {
275282
if err != nil {
276283
return xerrors.Errorf("create first user: %w", err)
277284
}
285+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", defaultDevUser.Email)
286+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", defaultDevUser.Password)
287+
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
278288

279289
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Started in dev mode. All data is in-memory! `+cliui.Styles.Bold.Render("Do not use in production")+`. Press `+
280290
cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`)+"\n\n")
@@ -293,7 +303,7 @@ func server() *cobra.Command {
293303
if !hasFirstUser && err == nil {
294304
// This could fail for a variety of TLS-related reasons.
295305
// This is a helpful starter message, and not critical for user interaction.
296-
_, _ = fmt.Fprint(cmd.ErrOrStderr(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder login "+client.URL.String())+" in a new terminal to get started.\n")))
306+
_, _ = fmt.Fprint(cmd.ErrOrStderr(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder login "+accessURL)+" in a new terminal to get started.\n")))
297307
}
298308
}
299309

@@ -400,7 +410,7 @@ func server() *cobra.Command {
400410
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
401411
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
402412
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
403-
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 1, "The amount of provisioner daemons to create on start.")
413+
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
404414
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
405415
"Specifies a client ID to use for oauth2 with GitHub.")
406416
cliflag.StringVarP(root.Flags(), &oauth2GithubClientSecret, "oauth2-github-client-secret", "", "CODER_OAUTH2_GITHUB_CLIENT_SECRET", "",
@@ -441,18 +451,13 @@ func server() *cobra.Command {
441451
}
442452

443453
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error {
444-
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
445-
Email: "admin@coder.com",
446-
Username: "developer",
447-
Password: "password",
448-
OrganizationName: "acme-corp",
449-
})
454+
_, err := client.CreateFirstUser(cmd.Context(), defaultDevUser)
450455
if err != nil {
451456
return xerrors.Errorf("create first user: %w", err)
452457
}
453458
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
454-
Email: "admin@coder.com",
455-
Password: "password",
459+
Email: defaultDevUser.Email,
460+
Password: defaultDevUser.Password,
456461
})
457462
if err != nil {
458463
return xerrors.Errorf("login with first user: %w", err)

cli/server_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli_test
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/ecdsa"
67
"crypto/elliptic"
@@ -18,6 +19,7 @@ import (
1819
"testing"
1920
"time"
2021

22+
"github.com/stretchr/testify/assert"
2123
"github.com/stretchr/testify/require"
2224
"go.uber.org/goleak"
2325

@@ -73,9 +75,17 @@ func TestServer(t *testing.T) {
7375
ctx, cancelFunc := context.WithCancel(context.Background())
7476
defer cancelFunc()
7577
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
78+
var stdoutBuf bytes.Buffer
79+
root.SetOutput(&stdoutBuf)
7680
go func() {
7781
err := root.ExecuteContext(ctx)
7882
require.ErrorIs(t, err, context.Canceled)
83+
84+
// Verify that credentials were output to the terminal.
85+
wantEmail := "email: admin@coder.com"
86+
wantPassword := "password: password"
87+
assert.Contains(t, stdoutBuf.String(), wantEmail, "expected output %q; got no match", wantEmail)
88+
assert.Contains(t, stdoutBuf.String(), wantPassword, "expected output %q; got no match", wantPassword)
7989
}()
8090
var token string
8191
require.Eventually(t, func() bool {

coder.service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ SecureBits=keep-caps
1818
AmbientCapabilities=CAP_IPC_LOCK
1919
CacheDirectory=coder
2020
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE
21+
KillSignal=SIGINT
2122
NoNewPrivileges=yes
2223
ExecStart=/usr/bin/coder server
2324
Restart=on-failure

coderd/audit/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
4141
"hashed_password": ActionSecret, // A user can change their own password.
4242
"created_at": ActionIgnore, // Never changes.
4343
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
44+
"status": ActionTrack, // A user can update another user status
4445
},
4546
&database.Workspace{}: {
4647
"id": ActionIgnore, // Never changes.

coderd/coderd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func New(options *Options) (http.Handler, func()) {
182182
r.Use(httpmw.ExtractUserParam(options.Database))
183183
r.Get("/", api.userByName)
184184
r.Put("/profile", api.putUserProfile)
185+
r.Put("/suspend", api.putUserSuspend)
185186
r.Get("/organizations", api.organizationsByUser)
186187
r.Post("/organizations", api.postOrganizationsByUser)
187188
r.Post("/keys", api.postAPIKey)
@@ -201,7 +202,7 @@ func New(options *Options) (http.Handler, func()) {
201202
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
202203
r.Route("/me", func(r chi.Router) {
203204
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
204-
r.Get("/", api.workspaceAgentMe)
205+
r.Get("/metadata", api.workspaceAgentMetadata)
205206
r.Get("/listen", api.workspaceAgentListen)
206207
r.Get("/gitsshkey", api.agentGitSSHKey)
207208
r.Get("/turn", api.workspaceAgentTurn)

coderd/database/databasefake/databasefake.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
11381138
CreatedAt: arg.CreatedAt,
11391139
UpdatedAt: arg.UpdatedAt,
11401140
Username: arg.Username,
1141+
Status: database.UserStatusActive,
11411142
}
11421143
q.users = append(q.users, user)
11431144
return user, nil
@@ -1159,6 +1160,22 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs
11591160
return database.User{}, sql.ErrNoRows
11601161
}
11611162

1163+
func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
1164+
q.mutex.Lock()
1165+
defer q.mutex.Unlock()
1166+
1167+
for index, user := range q.users {
1168+
if user.ID != arg.ID {
1169+
continue
1170+
}
1171+
user.Status = arg.Status
1172+
user.UpdatedAt = arg.UpdatedAt
1173+
q.users[index] = user
1174+
return user, nil
1175+
}
1176+
return database.User{}, sql.ErrNoRows
1177+
}
1178+
11621179
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
11631180
q.mutex.Lock()
11641181
defer q.mutex.Unlock()

coderd/database/dump.sql

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ALTER TABLE ONLY users
2+
DROP COLUMN IF EXISTS status;
3+
4+
DROP TYPE user_status;

0 commit comments

Comments
 (0)