Skip to content

Commit ea326a7

Browse files
mafredrikylecarbs
authored andcommitted
feat: Generate random admin user password in dev mode (#1207)
* feat: Generate random admin user password in dev mode * Add dev mode test with email/pass from env * Set email/pass for playwright e2e test via cli flags
1 parent b97f37b commit ea326a7

File tree

3 files changed

+109
-27
lines changed

3 files changed

+109
-27
lines changed

cli/server.go

+34-19
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,23 @@ import (
4141
"github.com/coder/coder/coderd/gitsshkey"
4242
"github.com/coder/coder/coderd/turnconn"
4343
"github.com/coder/coder/codersdk"
44+
"github.com/coder/coder/cryptorand"
4445
"github.com/coder/coder/provisioner/terraform"
4546
"github.com/coder/coder/provisionerd"
4647
"github.com/coder/coder/provisionersdk"
4748
"github.com/coder/coder/provisionersdk/proto"
4849
)
4950

50-
var defaultDevUser = codersdk.CreateFirstUserRequest{
51-
Email: "admin@coder.com",
52-
Username: "developer",
53-
Password: "password",
54-
OrganizationName: "acme-corp",
55-
}
56-
5751
// nolint:gocyclo
5852
func server() *cobra.Command {
5953
var (
60-
accessURL string
61-
address string
62-
cacheDir string
63-
dev bool
64-
postgresURL string
54+
accessURL string
55+
address string
56+
cacheDir string
57+
dev bool
58+
devUserEmail string
59+
devUserPassword string
60+
postgresURL string
6561
// provisionerDaemonCount is a uint8 to ensure a number > 0.
6662
provisionerDaemonCount uint8
6763
oauth2GithubClientID string
@@ -278,12 +274,18 @@ func server() *cobra.Command {
278274
config := createConfig(cmd)
279275

280276
if dev {
281-
err = createFirstUser(cmd, client, config)
277+
if devUserPassword == "" {
278+
devUserPassword, err = cryptorand.String(10)
279+
if err != nil {
280+
return xerrors.Errorf("generate random admin password for dev: %w", err)
281+
}
282+
}
283+
err = createFirstUser(cmd, client, config, devUserEmail, devUserPassword)
282284
if err != nil {
283285
return xerrors.Errorf("create first user: %w", err)
284286
}
285-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", defaultDevUser.Email)
286-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", defaultDevUser.Password)
287+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail)
288+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword)
287289
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
288290

289291
_, _ = 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 `+
@@ -409,6 +411,8 @@ func server() *cobra.Command {
409411
// systemd uses the CACHE_DIRECTORY environment variable!
410412
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
411413
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
414+
cliflag.StringVarP(root.Flags(), &devUserEmail, "dev-admin-email", "", "CODER_DEV_ADMIN_EMAIL", "admin@coder.com", "Specifies the admin email to be used in dev mode (--dev)")
415+
cliflag.StringVarP(root.Flags(), &devUserPassword, "dev-admin-password", "", "CODER_DEV_ADMIN_PASSWORD", "", "Specifies the admin password to be used in dev mode (--dev) instead of a randomly generated one")
412416
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
413417
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
414418
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
@@ -450,14 +454,25 @@ func server() *cobra.Command {
450454
return root
451455
}
452456

453-
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error {
454-
_, err := client.CreateFirstUser(cmd.Context(), defaultDevUser)
457+
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) error {
458+
if email == "" {
459+
return xerrors.New("email is empty")
460+
}
461+
if password == "" {
462+
return xerrors.New("password is empty")
463+
}
464+
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
465+
Email: email,
466+
Username: "developer",
467+
Password: password,
468+
OrganizationName: "acme-corp",
469+
})
455470
if err != nil {
456471
return xerrors.Errorf("create first user: %w", err)
457472
}
458473
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
459-
Email: defaultDevUser.Email,
460-
Password: defaultDevUser.Password,
474+
Email: email,
475+
Password: password,
461476
})
462477
if err != nil {
463478
return xerrors.Errorf("login with first user: %w", err)

cli/server_test.go

+70-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cli_test
22

33
import (
4-
"bytes"
54
"context"
65
"crypto/ecdsa"
76
"crypto/elliptic"
@@ -10,12 +9,15 @@ import (
109
"crypto/x509"
1110
"crypto/x509/pkix"
1211
"encoding/pem"
12+
"fmt"
1313
"math/big"
1414
"net"
1515
"net/http"
1616
"net/url"
1717
"os"
1818
"runtime"
19+
"strings"
20+
"sync"
1921
"testing"
2022
"time"
2123

@@ -74,18 +76,30 @@ func TestServer(t *testing.T) {
7476
t.Parallel()
7577
ctx, cancelFunc := context.WithCancel(context.Background())
7678
defer cancelFunc()
79+
80+
wantEmail := "admin@coder.com"
81+
7782
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
78-
var stdoutBuf bytes.Buffer
79-
root.SetOutput(&stdoutBuf)
83+
var buf strings.Builder
84+
root.SetOutput(&buf)
85+
var wg sync.WaitGroup
86+
wg.Add(1)
8087
go func() {
88+
defer wg.Done()
89+
8190
err := root.ExecuteContext(ctx)
8291
require.ErrorIs(t, err, context.Canceled)
8392

8493
// 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)
94+
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
95+
// Check that the password line is output and that it's non-empty.
96+
if _, after, found := strings.Cut(buf.String(), "password: "); found {
97+
before, _, _ := strings.Cut(after, "\n")
98+
before = strings.Trim(before, "\r") // Ensure no control character is left.
99+
assert.NotEmpty(t, before, "expected non-empty password; got empty")
100+
} else {
101+
t.Error("expected password line output; got no match")
102+
}
89103
}()
90104
var token string
91105
require.Eventually(t, func() bool {
@@ -102,6 +116,55 @@ func TestServer(t *testing.T) {
102116
client.SessionToken = token
103117
_, err = client.User(ctx, codersdk.Me)
104118
require.NoError(t, err)
119+
120+
cancelFunc()
121+
wg.Wait()
122+
})
123+
// Duplicated test from "Development" above to test setting email/password via env.
124+
// Cannot run parallel due to os.Setenv.
125+
//nolint:paralleltest
126+
t.Run("Development with email and password from env", func(t *testing.T) {
127+
ctx, cancelFunc := context.WithCancel(context.Background())
128+
defer cancelFunc()
129+
130+
wantEmail := "myadmin@coder.com"
131+
wantPassword := "testpass42"
132+
t.Setenv("CODER_DEV_ADMIN_EMAIL", wantEmail)
133+
t.Setenv("CODER_DEV_ADMIN_PASSWORD", wantPassword)
134+
135+
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
136+
var buf strings.Builder
137+
root.SetOutput(&buf)
138+
var wg sync.WaitGroup
139+
wg.Add(1)
140+
go func() {
141+
defer wg.Done()
142+
143+
err := root.ExecuteContext(ctx)
144+
require.ErrorIs(t, err, context.Canceled)
145+
146+
// Verify that credentials were output to the terminal.
147+
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
148+
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
149+
}()
150+
var token string
151+
require.Eventually(t, func() bool {
152+
var err error
153+
token, err = cfg.Session().Read()
154+
return err == nil
155+
}, 15*time.Second, 25*time.Millisecond)
156+
// Verify that authentication was properly set in dev-mode.
157+
accessURL, err := cfg.URL().Read()
158+
require.NoError(t, err)
159+
parsed, err := url.Parse(accessURL)
160+
require.NoError(t, err)
161+
client := codersdk.New(parsed)
162+
client.SessionToken = token
163+
_, err = client.User(ctx, codersdk.Me)
164+
require.NoError(t, err)
165+
166+
cancelFunc()
167+
wg.Wait()
105168
})
106169
t.Run("TLSBadVersion", func(t *testing.T) {
107170
t.Parallel()

site/e2e/playwright.config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PlaywrightTestConfig } from "@playwright/test"
22
import * as path from "path"
3+
import * as constants from "./constants"
34

45
const config: PlaywrightTestConfig = {
56
testDir: "tests",
@@ -17,7 +18,10 @@ const config: PlaywrightTestConfig = {
1718
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
1819
webServer: {
1920
// Run the coder daemon directly.
20-
command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --dev --skip-tunnel`,
21+
command: `go run -tags embed ${path.join(
22+
__dirname,
23+
"../../cmd/coder/main.go",
24+
)} server --dev --skip-tunnel --dev-admin-email ${constants.email} --dev-admin-password ${constants.password}`,
2125
port: 3000,
2226
timeout: 120 * 10000,
2327
reuseExistingServer: false,

0 commit comments

Comments
 (0)