diff --git a/cli/server.go b/cli/server.go index 95deb2dcad413..8becc46153ffe 100644 --- a/cli/server.go +++ b/cli/server.go @@ -41,27 +41,23 @@ import ( "github.com/coder/coder/coderd/gitsshkey" "github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/codersdk" + "github.com/coder/coder/cryptorand" "github.com/coder/coder/provisioner/terraform" "github.com/coder/coder/provisionerd" "github.com/coder/coder/provisionersdk" "github.com/coder/coder/provisionersdk/proto" ) -var defaultDevUser = codersdk.CreateFirstUserRequest{ - Email: "admin@coder.com", - Username: "developer", - Password: "password", - OrganizationName: "acme-corp", -} - // nolint:gocyclo func server() *cobra.Command { var ( - accessURL string - address string - cacheDir string - dev bool - postgresURL string + accessURL string + address string + cacheDir string + dev bool + devUserEmail string + devUserPassword string + postgresURL string // provisionerDaemonCount is a uint8 to ensure a number > 0. provisionerDaemonCount uint8 oauth2GithubClientID string @@ -278,12 +274,18 @@ func server() *cobra.Command { config := createConfig(cmd) if dev { - err = createFirstUser(cmd, client, config) + if devUserPassword == "" { + devUserPassword, err = cryptorand.String(10) + if err != nil { + return xerrors.Errorf("generate random admin password for dev: %w", err) + } + } + err = createFirstUser(cmd, client, config, devUserEmail, devUserPassword) if err != nil { return xerrors.Errorf("create first user: %w", err) } - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", defaultDevUser.Email) - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", defaultDevUser.Password) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword) _, _ = fmt.Fprintln(cmd.ErrOrStderr()) _, _ = 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 { // systemd uses the CACHE_DIRECTORY environment variable! cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.") cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering") + 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)") + 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") cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to") cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.") cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "", @@ -450,14 +454,25 @@ func server() *cobra.Command { return root } -func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error { - _, err := client.CreateFirstUser(cmd.Context(), defaultDevUser) +func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) error { + if email == "" { + return xerrors.New("email is empty") + } + if password == "" { + return xerrors.New("password is empty") + } + _, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{ + Email: email, + Username: "developer", + Password: password, + OrganizationName: "acme-corp", + }) if err != nil { return xerrors.Errorf("create first user: %w", err) } token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ - Email: defaultDevUser.Email, - Password: defaultDevUser.Password, + Email: email, + Password: password, }) if err != nil { return xerrors.Errorf("login with first user: %w", err) diff --git a/cli/server_test.go b/cli/server_test.go index 100f80f75febf..369334c48ec14 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1,7 +1,6 @@ package cli_test import ( - "bytes" "context" "crypto/ecdsa" "crypto/elliptic" @@ -10,12 +9,15 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "fmt" "math/big" "net" "net/http" "net/url" "os" "runtime" + "strings" + "sync" "testing" "time" @@ -74,18 +76,30 @@ func TestServer(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() + + wantEmail := "admin@coder.com" + root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0") - var stdoutBuf bytes.Buffer - root.SetOutput(&stdoutBuf) + var buf strings.Builder + root.SetOutput(&buf) + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() + err := root.ExecuteContext(ctx) require.ErrorIs(t, err, context.Canceled) // Verify that credentials were output to the terminal. - wantEmail := "email: admin@coder.com" - wantPassword := "password: password" - assert.Contains(t, stdoutBuf.String(), wantEmail, "expected output %q; got no match", wantEmail) - assert.Contains(t, stdoutBuf.String(), wantPassword, "expected output %q; got no match", wantPassword) + assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail) + // Check that the password line is output and that it's non-empty. + if _, after, found := strings.Cut(buf.String(), "password: "); found { + before, _, _ := strings.Cut(after, "\n") + before = strings.Trim(before, "\r") // Ensure no control character is left. + assert.NotEmpty(t, before, "expected non-empty password; got empty") + } else { + t.Error("expected password line output; got no match") + } }() var token string require.Eventually(t, func() bool { @@ -102,6 +116,55 @@ func TestServer(t *testing.T) { client.SessionToken = token _, err = client.User(ctx, codersdk.Me) require.NoError(t, err) + + cancelFunc() + wg.Wait() + }) + // Duplicated test from "Development" above to test setting email/password via env. + // Cannot run parallel due to os.Setenv. + //nolint:paralleltest + t.Run("Development with email and password from env", func(t *testing.T) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + wantEmail := "myadmin@coder.com" + wantPassword := "testpass42" + t.Setenv("CODER_DEV_ADMIN_EMAIL", wantEmail) + t.Setenv("CODER_DEV_ADMIN_PASSWORD", wantPassword) + + root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0") + var buf strings.Builder + root.SetOutput(&buf) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + err := root.ExecuteContext(ctx) + require.ErrorIs(t, err, context.Canceled) + + // Verify that credentials were output to the terminal. + assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail) + assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword) + }() + var token string + require.Eventually(t, func() bool { + var err error + token, err = cfg.Session().Read() + return err == nil + }, 15*time.Second, 25*time.Millisecond) + // Verify that authentication was properly set in dev-mode. + accessURL, err := cfg.URL().Read() + require.NoError(t, err) + parsed, err := url.Parse(accessURL) + require.NoError(t, err) + client := codersdk.New(parsed) + client.SessionToken = token + _, err = client.User(ctx, codersdk.Me) + require.NoError(t, err) + + cancelFunc() + wg.Wait() }) t.Run("TLSBadVersion", func(t *testing.T) { t.Parallel() diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index b1d36ee5c60ef..365e023d95f3e 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -1,5 +1,6 @@ import { PlaywrightTestConfig } from "@playwright/test" import * as path from "path" +import * as constants from "./constants" const config: PlaywrightTestConfig = { testDir: "tests", @@ -17,7 +18,10 @@ const config: PlaywrightTestConfig = { // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests webServer: { // Run the coder daemon directly. - command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --dev --skip-tunnel`, + command: `go run -tags embed ${path.join( + __dirname, + "../../cmd/coder/main.go", + )} server --dev --skip-tunnel --dev-admin-email ${constants.email} --dev-admin-password ${constants.password}`, port: 3000, timeout: 120 * 10000, reuseExistingServer: false,