From dc122b650cf4af165fa05a31194974401d5b38ce Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Tue, 14 Jun 2022 16:42:00 +0000 Subject: [PATCH 1/2] feat: Add built-in PostgreSQL for simple production setup Fixes #2321. --- .goreleaser.yaml | 2 + README.md | 8 +- cli/config/file.go | 12 + cli/login.go | 135 +++--- cli/login_test.go | 20 + cli/root.go | 4 +- cli/server.go | 448 ++++++++---------- cli/server_test.go | 139 ++---- coder.env | 5 + coder.service | 3 +- coderd/provisionerdaemons.go | 8 +- docs/install.md | 25 +- .../kubernetes-multi-service/main.tf | 4 +- go.mod | 3 + go.sum | 5 + install.sh | 17 +- preinstall.sh | 12 + provisionerd/provisionerd.go | 4 - scripts/develop.sh | 10 +- site/e2e/globalSetup.ts | 12 +- site/e2e/playwright.config.ts | 6 +- site/src/api/api.ts | 7 + 22 files changed, 421 insertions(+), 468 deletions(-) create mode 100644 preinstall.sh diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7bcedf5087db5..4d6ef72bc4c87 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -93,6 +93,8 @@ nfpms: type: "config|noreplace" - src: coder.service dst: /usr/lib/systemd/system/coder.service + scripts: + preinstall: preinstall.sh # Image templates are empty on snapshots to avoid lengthy builds for development. dockers: diff --git a/README.md b/README.md index 30f11fa570ffc..74f035e98f10a 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,14 @@ To install, run: curl -fsSL https://coder.com/install.sh | sh ``` -Once installed, you can run a temporary deployment in dev mode (all data is in-memory and destroyed on exit): +Once installed, you can start a production deployment with a single command: ```sh -coder server --dev +# Automatically sets up PostgreSQL and an external access URL on *.try.coder.app +coder server --postgres-builtin --tunnel + +# Requires a PostgreSQL instance and external access URL +coder server --postgres-url --access-url ``` Use `coder --help` to get a complete list of flags and environment variables. Use our [quickstart guide](./docs/quickstart.md) for a full walkthrough. diff --git a/cli/config/file.go b/cli/config/file.go index 9af53f8e19f60..a98237afed22b 100644 --- a/cli/config/file.go +++ b/cli/config/file.go @@ -25,6 +25,18 @@ func (r Root) DotfilesURL() File { return File(filepath.Join(string(r), "dotfilesurl")) } +func (r Root) PostgresPath() string { + return filepath.Join(string(r), "postgres") +} + +func (r Root) PostgresPassword() File { + return File(filepath.Join(r.PostgresPath(), "password")) +} + +func (r Root) PostgresPort() File { + return File(filepath.Join(r.PostgresPath(), "port")) +} + // File provides convenience methods for interacting with *os.File. type File string diff --git a/cli/login.go b/cli/login.go index 25c0b4514dc5e..b4bac1a9afdf6 100644 --- a/cli/login.go +++ b/cli/login.go @@ -37,7 +37,12 @@ func init() { } func login() *cobra.Command { - return &cobra.Command{ + var ( + email string + username string + password string + ) + cmd := &cobra.Command{ Use: "login ", Short: "Authenticate with a Coder deployment", Args: cobra.ExactArgs(1), @@ -66,71 +71,77 @@ func login() *cobra.Command { return xerrors.Errorf("has initial user: %w", err) } if !hasInitialUser { - if !isTTY(cmd) { - return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") - } _, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n") - _, err := cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Would you like to create the first user?", - Default: "yes", - IsConfirm: true, - }) - if errors.Is(err, cliui.Canceled) { - return nil - } - if err != nil { - return err - } - currentUser, err := user.Current() - if err != nil { - return xerrors.Errorf("get current user: %w", err) - } - username, err := cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "What " + cliui.Styles.Field.Render("username") + " would you like?", - Default: currentUser.Username, - }) - if errors.Is(err, cliui.Canceled) { - return nil - } - if err != nil { - return xerrors.Errorf("pick username prompt: %w", err) - } - - email, err := cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "What's your " + cliui.Styles.Field.Render("email") + "?", - Validate: func(s string) error { - err := validator.New().Var(s, "email") - if err != nil { - return xerrors.New("That's not a valid email address!") - } + if username == "" { + if !isTTY(cmd) { + return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") + } + _, err := cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Would you like to create the first user?", + Default: "yes", + IsConfirm: true, + }) + if errors.Is(err, cliui.Canceled) { + return nil + } + if err != nil { return err - }, - }) - if err != nil { - return xerrors.Errorf("specify email prompt: %w", err) + } + currentUser, err := user.Current() + if err != nil { + return xerrors.Errorf("get current user: %w", err) + } + username, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "What " + cliui.Styles.Field.Render("username") + " would you like?", + Default: currentUser.Username, + }) + if errors.Is(err, cliui.Canceled) { + return nil + } + if err != nil { + return xerrors.Errorf("pick username prompt: %w", err) + } } - password, err := cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Enter a " + cliui.Styles.Field.Render("password") + ":", - Secret: true, - Validate: cliui.ValidateNotEmpty, - }) - if err != nil { - return xerrors.Errorf("specify password prompt: %w", err) + if email == "" { + email, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "What's your " + cliui.Styles.Field.Render("email") + "?", + Validate: func(s string) error { + err := validator.New().Var(s, "email") + if err != nil { + return xerrors.New("That's not a valid email address!") + } + return err + }, + }) + if err != nil { + return xerrors.Errorf("specify email prompt: %w", err) + } } - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Confirm " + cliui.Styles.Field.Render("password") + ":", - Secret: true, - Validate: func(s string) error { - if s != password { - return xerrors.Errorf("Passwords do not match") - } - return nil - }, - }) - if err != nil { - return xerrors.Errorf("confirm password prompt: %w", err) + + if password == "" { + password, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Enter a " + cliui.Styles.Field.Render("password") + ":", + Secret: true, + Validate: cliui.ValidateNotEmpty, + }) + if err != nil { + return xerrors.Errorf("specify password prompt: %w", err) + } + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Confirm " + cliui.Styles.Field.Render("password") + ":", + Secret: true, + Validate: func(s string) error { + if s != password { + return xerrors.Errorf("Passwords do not match") + } + return nil + }, + }) + if err != nil { + return xerrors.Errorf("confirm password prompt: %w", err) + } } _, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{ @@ -219,6 +230,10 @@ func login() *cobra.Command { return nil }, } + cmd.Flags().StringVarP(&email, "email", "e", "", "Specifies an email address to authenticate with.") + cmd.Flags().StringVarP(&username, "username", "u", "", "Specifies a username to authenticate with.") + cmd.Flags().StringVarP(&password, "password", "p", "", "Specifies a password to authenticate with.") + return cmd } // isWSL determines if coder-cli is running within Windows Subsystem for Linux diff --git a/cli/login_test.go b/cli/login_test.go index a0757c805157a..316f14871e43f 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -56,6 +56,26 @@ func TestLogin(t *testing.T) { <-doneChan }) + t.Run("InitialUserFlags", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + // The --force-tty flag is required on Windows, because the `isatty` library does not + // accurately detect Windows ptys when they are not attached to a process: + // https://github.com/mattn/go-isatty/issues/59 + doneChan := make(chan struct{}) + root, _ := clitest.New(t, "login", client.URL.String(), "--username", "testuser", "--email", "user@coder.com", "--password", "password") + pty := ptytest.New(t) + root.SetIn(pty.Input()) + root.SetOut(pty.Output()) + go func() { + defer close(doneChan) + err := root.Execute() + assert.NoError(t, err) + }() + pty.ExpectMatch("Welcome to Coder") + <-doneChan + }) + t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) diff --git a/cli/root.go b/cli/root.go index cb59816fe10ec..465cb11c613d8 100644 --- a/cli/root.go +++ b/cli/root.go @@ -57,8 +57,8 @@ func Root() *cobra.Command { SilenceUsage: true, Long: `Coder — A tool for provisioning self-hosted development environments. `, - Example: ` Start Coder in "dev" mode. This dev-mode requires no further setup, and your local ` + cliui.Styles.Code.Render("coder") + ` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder. - ` + cliui.Styles.Code.Render("$ coder server --dev") + ` + Example: ` Start a Coder server. + ` + cliui.Styles.Code.Render("$ coder server") + ` Get started by creating a template from an example. ` + cliui.Styles.Code.Render("$ coder templates init"), diff --git a/cli/server.go b/cli/server.go index b69c6c5c83489..baf6c78adc883 100644 --- a/cli/server.go +++ b/cli/server.go @@ -16,21 +16,24 @@ import ( "net/url" "os" "os/signal" + "os/user" "path/filepath" + "strconv" + "strings" "time" "github.com/coder/coder/buildinfo" + "github.com/coder/coder/cryptorand" "github.com/coder/coder/provisioner/echo" - "github.com/briandowns/spinner" "github.com/coreos/go-systemd/daemon" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/google/go-github/v43/github" "github.com/pion/turn/v2" "github.com/pion/webrtc/v3" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" sdktrace "go.opentelemetry.io/otel/sdk/trace" - "golang.org/x/mod/semver" "golang.org/x/oauth2" xgithub "golang.org/x/oauth2/github" "golang.org/x/sync/errgroup" @@ -52,7 +55,6 @@ import ( "github.com/coder/coder/coderd/tracing" "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" @@ -70,12 +72,11 @@ func server() *cobra.Command { pprofEnabled bool pprofAddress string cacheDir string - dev bool - devUserEmail string - devUserPassword string - postgresURL string + inMemoryDatabase bool // provisionerDaemonCount is a uint8 to ensure a number > 0. provisionerDaemonCount uint8 + postgresBuiltin bool + postgresURL string oauth2GithubClientID string oauth2GithubClientSecret string oauth2GithubAllowedOrganizations []string @@ -100,9 +101,9 @@ func server() *cobra.Command { Use: "server", Short: "Start a Coder server", RunE: func(cmd *cobra.Command, args []string) error { + printLogo(cmd, spooky) logger := slog.Make(sloghuman.Sink(os.Stderr)) - buildModeDev := semver.Prerelease(buildinfo.Version()) == "-devel" - if verbose || buildModeDev { + if verbose { logger = logger.Leveled(slog.LevelDebug) } @@ -132,7 +133,37 @@ func server() *cobra.Command { } } - printLogo(cmd, spooky) + config := createConfig(cmd) + // For in-memory, disable embedded PostgreSQL! + // This is primarily for testing and development. + if inMemoryDatabase { + postgresBuiltin = false + postgresURL = "" + } + // Only use built-in if PostgreSQL URL isn't specified! + if postgresURL == "" && postgresBuiltin { + var closeFunc func() error + postgresURL, closeFunc, err = startBuiltinPostgres(cmd.Context(), config, logger) + if err != nil { + return err + } + cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath()) + defer func() { + // Gracefully shut PostgreSQL down! + _ = closeFunc() + }() + } + // If the built-in PostgreSQL isn't used and an external URL isn't specified, + // recommend usage of the built-in! + if !inMemoryDatabase && postgresURL == "" { + cmd.PrintErrln(cliui.Styles.Error.Render("PostgreSQL >=13 is required for running Coder.") + "\n") + cmd.PrintErrln("Automatically install and run PostgreSQL (install and runtime data goes to " + config.PostgresPath() + "):") + cmd.PrintErrln(cliui.Styles.Code.Render(strings.Join(os.Args, " ") + " --postgres-builtin")) + cmd.PrintErrln("\nUse an external PostgreSQL deployment:") + cmd.PrintErrln(" coder server --postgres-url ") + return xerrors.New("") + } + listener, err := net.Listen("tcp", address) if err != nil { return xerrors.Errorf("listen %q: %w", address, err) @@ -154,7 +185,8 @@ func server() *cobra.Command { if tcpAddr.IP.IsUnspecified() { tcpAddr.IP = net.IPv4(127, 0, 0, 1) } - + // If no access URL is specified, fallback to the + // bounds URL. localURL := &url.URL{ Scheme: "http", Host: tcpAddr.String(), @@ -164,9 +196,6 @@ func server() *cobra.Command { } if accessURL == "" { accessURL = localURL.String() - } else { - // If an access URL is specified, always skip tunneling. - tunnel = false } var ( @@ -178,66 +207,38 @@ func server() *cobra.Command { // If we're attempting to tunnel in dev-mode, the access URL // needs to be changed to use the tunnel. - if dev && tunnel { - _, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render( - "Coder requires a URL accessible by workspaces you provision. "+ - "A free tunnel can be created for simple setup. This will "+ - "expose your Coder deployment to a publicly accessible URL. "+ - cliui.Styles.Field.Render("--access-url")+" can be specified instead.\n", - )) - - // This skips the prompt if the flag is explicitly specified. - if !cmd.Flags().Changed("tunnel") { - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Would you like to start a tunnel for simple setup?", - IsConfirm: true, - }) - if errors.Is(err, cliui.Canceled) { - return err - } - } - if err == nil { - devTunnel, devTunnelErrChan, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel")) - if err != nil { - return xerrors.Errorf("create tunnel: %w", err) - } - accessURL = devTunnel.URL + if tunnel { + cmd.Printf("Opening tunnel so workspaces can connect to your deployment\n") + devTunnel, devTunnelErrChan, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel")) + if err != nil { + return xerrors.Errorf("create tunnel: %w", err) } - _, _ = fmt.Fprintln(cmd.ErrOrStderr()) + accessURL = devTunnel.URL } // Warn the user if the access URL appears to be a loopback address. isLocal, err := isLocalURL(cmd.Context(), accessURL) if isLocal || err != nil { - var reason string + reason := "could not be resolved" if isLocal { - reason = "appears to be a loopback address" - } else { - reason = "could not be resolved" + reason = "isn't reachable externally" } - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render( - cliui.Styles.Warn.Render("Warning:")+" The current access URL:")+"\n\n") - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), " "+cliui.Styles.Field.Render(accessURL)+"\n\n") - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render( - reason+". Provisioned workspaces are unlikely to be able to "+ - "connect to Coder. Please consider changing your "+ - "access URL using the --access-url option, or directly "+ - "specifying access URLs on templates.", - )+"\n\n") - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "For more information, see "+ - "https://github.com/coder/coder/issues/1528\n\n") - } - - validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication()) - if err != nil { - return err + cmd.Printf("%s The access URL %s %s. Workspaces must be able to reach Coder from this URL. Optionally, generate a unique *.try.coder.app URL with:\n", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURL), reason) + cmd.Println(cliui.Styles.Code.Render(strings.Join(os.Args, " ") + " --tunnel")) } + cmd.Printf("View the Web UI: %s\n", accessURL) accessURLParsed, err := url.Parse(accessURL) if err != nil { return xerrors.Errorf("parse access url %q: %w", accessURL, err) } + // Used for zero-trust instance identity with Google Cloud. + googleTokenValidator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication()) + if err != nil { + return err + } + sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(sshKeygenAlgorithmRaw) if err != nil { return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err) @@ -263,7 +264,7 @@ func server() *cobra.Command { Logger: logger.Named("coderd"), Database: databasefake.New(), Pubsub: database.NewPubsubInMemory(), - GoogleTokenValidator: validator, + GoogleTokenValidator: googleTokenValidator, SecureAuthCookie: secureAuthCookie, SSHKeygenAlgorithm: sshKeygenAlgorithm, TURNServer: turnServer, @@ -277,11 +278,10 @@ func server() *cobra.Command { } } - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "access-url: %s\n", accessURL) - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "provisioner-daemons: %d\n", provisionerDaemonCount) - _, _ = fmt.Fprintln(cmd.ErrOrStderr()) - - if !dev { + if inMemoryDatabase { + options.Database = databasefake.New() + options.Pubsub = database.NewPubsubInMemory() + } else { sqlDB, err := sql.Open(sqlDriver, postgresURL) if err != nil { return xerrors.Errorf("dial postgres: %w", err) @@ -327,7 +327,7 @@ func server() *cobra.Command { errCh := make(chan error, 1) provisionerDaemons := make([]*provisionerd.Server, 0) for i := 0; uint8(i) < provisionerDaemonCount; i++ { - daemonClose, err := newProvisionerDaemon(cmd.Context(), coderAPI, logger, cacheDir, errCh, dev) + daemonClose, err := newProvisionerDaemon(cmd.Context(), coderAPI, logger, cacheDir, errCh, false) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) } @@ -357,14 +357,14 @@ func server() *cobra.Command { wg.Go(func() error { // Make sure to close the tunnel listener if we exit so the // errgroup doesn't wait forever! - if dev && tunnel { + if tunnel { defer devTunnel.Listener.Close() } return server.Serve(listener) }) - if dev && tunnel { + if tunnel { wg.Go(func() error { defer listener.Close() @@ -375,45 +375,20 @@ func server() *cobra.Command { errCh <- wg.Wait() }() - config := createConfig(cmd) + // This is helpful for tests, but can be silently ignored. + // Coder may be ran as users that don't have permission to write in the homedir, + // such as via the systemd service. + _ = config.URL().Write(client.URL.String()) - if dev { - if devUserPassword == "" { - devUserPassword, err = cryptorand.String(10) - if err != nil { - return xerrors.Errorf("generate random admin password for dev: %w", err) - } - } - restorePreviousSession, err := createFirstUser(logger, cmd, client, config, devUserEmail, devUserPassword) - if err != nil { - return xerrors.Errorf("create first user: %w", err) - } - defer restorePreviousSession() - _, _ = 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 `+ - cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`)+"\n\n") - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Run `+cliui.Styles.Code.Render("coder templates init")+ - " in a new terminal to start creating workspaces.")+"\n") - } else { - // This is helpful for tests, but can be silently ignored. - // Coder may be ran as users that don't have permission to write in the homedir, - // such as via the systemd service. - _ = config.URL().Write(client.URL.String()) - - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+ - cliui.Styles.Field.Render("production")+` mode. All data is stored in the PostgreSQL provided! Press `+cliui.Styles.Field.Render("ctrl+c")+` to gracefully shutdown.`))+"\n") - - hasFirstUser, err := client.HasFirstUser(cmd.Context()) - if !hasFirstUser && err == nil { - // This could fail for a variety of TLS-related reasons. - // This is a helpful starter message, and not critical for user interaction. - _, _ = 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"))) - } + hasFirstUser, err := client.HasFirstUser(cmd.Context()) + if !hasFirstUser && err == nil { + cmd.Println() + cmd.Println("Get started by creating the first user (in a new terminal):") + cmd.Println(cliui.Styles.Code.Render("coder login " + accessURL)) } + cmd.Println("\n==> Logs will stream in below (press ctrl+c to gracefully exit):") + // Updates the systemd status from activating to activated. _, err = daemon.SdNotify(false, daemon.SdNotifyReady) if err != nil { @@ -452,65 +427,54 @@ func server() *cobra.Command { if err != nil { return xerrors.Errorf("notify systemd: %w", err) } - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+ - cliui.Styles.Bold.Render( - "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit")) - - if dev { - workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{ - Owner: codersdk.Me, - }) - if err != nil { - return xerrors.Errorf("get workspaces: %w", err) - } - for _, workspace := range workspaces { - before := time.Now() - build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionDelete, - }) - if err != nil { - return xerrors.Errorf("delete workspace: %w", err) - } - - err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before) - if err != nil { - return xerrors.Errorf("delete workspace %s: %w", workspace.Name, err) - } - } - } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Bold.Render( + "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit")) for _, provisionerDaemon := range provisionerDaemons { - spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) - spin.Writer = cmd.OutOrStdout() - spin.Suffix = cliui.Styles.Keyword.Render(" Shutting down provisioner daemon...") - spin.Start() + if verbose { + cmd.Println("Shutting down provisioner daemon...") + } err = provisionerDaemon.Shutdown(cmd.Context()) if err != nil { - spin.FinalMSG = cliui.Styles.Prompt.String() + "Failed to shutdown provisioner daemon: " + err.Error() - spin.Stop() + cmd.PrintErrf("Failed to shutdown provisioner daemon: %s\n", err) + continue } err = provisionerDaemon.Close() if err != nil { - spin.Stop() return xerrors.Errorf("close provisioner daemon: %w", err) } - spin.FinalMSG = cliui.Styles.Prompt.String() + "Gracefully shut down provisioner daemon!\n" - spin.Stop() + if verbose { + cmd.Println("Gracefully shut down provisioner daemon!") + } } - if dev && tunnel { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for dev tunnel to close...\n") + if tunnel { + cmd.Println("Waiting for tunnel to close...") closeTunnel() <-devTunnelErrChan } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n") + cmd.Println("Waiting for WebSocket connections to close...") shutdownConns() coderAPI.Close() return nil }, } + root.AddCommand(&cobra.Command{ + Use: "postgres-builtin-url", + Short: "Output the connection URL for the built-in PostgreSQL deployment.", + RunE: func(cmd *cobra.Command, args []string) error { + cfg := createConfig(cmd) + url, err := embeddedPostgresURL(cfg) + if err != nil { + return err + } + cmd.Println(cliui.Styles.Code.Render("psql \"" + url + "\"")) + return nil + }, + }) + cliflag.DurationVarP(root.Flags(), &autobuildPollInterval, "autobuild-poll-interval", "", "CODER_AUTOBUILD_POLL_INTERVAL", time.Minute, "Specifies the interval at which to poll for and execute automated workspace build operations.") cliflag.StringVarP(root.Flags(), &accessURL, "access-url", "", "CODER_ACCESS_URL", "", "Specifies the external URL to access Coder.") cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard.") @@ -524,10 +488,12 @@ func server() *cobra.Command { defaultCacheDir = dir } cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CODER_CACHE_DIRECTORY", defaultCacheDir, "Specifies a directory to cache binaries for provision operations. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.") - 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.BoolVarP(root.Flags(), &inMemoryDatabase, "in-memory", "", "CODER_INMEMORY", false, + "Specifies whether data will be stored in an in-memory database.") + _ = root.Flags().MarkHidden("in-memory") cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to") + cliflag.BoolVarP(root.Flags(), &postgresBuiltin, "postgres-builtin", "", "CODER_PG_BUILTIN", false, + "Start and run a PostgreSQL database for the Coder deployment. This will download PostgreSQL binaries from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access this database with \"coder server postgres-builtin-url\"") 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", "", "Specifies a client ID to use for oauth2 with GitHub.") @@ -551,8 +517,8 @@ func server() *cobra.Command { "Specifies the path to the private key for the certificate. It requires a PEM-encoded file") cliflag.StringVarP(root.Flags(), &tlsMinVersion, "tls-min-version", "", "CODER_TLS_MIN_VERSION", "tls12", `Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`) - cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_DEV_TUNNEL", true, - "Specifies whether the dev tunnel will be enabled or not. If specified, the interactive prompt will not display.") + cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_TUNNEL", false, + "Workspaces must be able to reach the `access-url`. This overrides your access URL with a public access URL that tunnels your Coder deployment.") cliflag.StringArrayVarP(root.Flags(), &stunServers, "stun-server", "", "CODER_STUN_SERVERS", []string{ "stun:stun.l.google.com:19302", }, "Specify URLs for STUN servers to enable P2P connections.") @@ -569,81 +535,6 @@ func server() *cobra.Command { return root } -// createFirstUser creates the first user and sets a valid session. -// Caller must call restorePreviousSession on server exit. -func createFirstUser(logger slog.Logger, cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) (func(), error) { - if email == "" { - return nil, xerrors.New("email is empty") - } - if password == "" { - return nil, 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 nil, xerrors.Errorf("create first user: %w", err) - } - token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ - Email: email, - Password: password, - }) - if err != nil { - return nil, xerrors.Errorf("login with first user: %w", err) - } - client.SessionToken = token.SessionToken - - // capture the current session and if exists recover session on server exit - restorePreviousSession := func() {} - oldURL, _ := cfg.URL().Read() - oldSession, _ := cfg.Session().Read() - if oldURL != "" && oldSession != "" { - restorePreviousSession = func() { - currentURL, err := cfg.URL().Read() - if err != nil { - logger.Error(cmd.Context(), "failed to read current session url", slog.Error(err)) - return - } - currentSession, err := cfg.Session().Read() - if err != nil { - logger.Error(cmd.Context(), "failed to read current session token", slog.Error(err)) - return - } - - // if it's changed since we wrote to it don't restore session - if currentURL != client.URL.String() || - currentSession != token.SessionToken { - return - } - - err = cfg.URL().Write(oldURL) - if err != nil { - logger.Error(cmd.Context(), "failed to recover previous session url", slog.Error(err)) - return - } - err = cfg.Session().Write(oldSession) - if err != nil { - logger.Error(cmd.Context(), "failed to recover previous session token", slog.Error(err)) - return - } - } - } - - err = cfg.URL().Write(client.URL.String()) - if err != nil { - return nil, xerrors.Errorf("write local url: %w", err) - } - err = cfg.Session().Write(token.SessionToken) - if err != nil { - return nil, xerrors.Errorf("write session token: %w", err) - } - - return restorePreviousSession, nil -} - // nolint:revive func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API, logger slog.Logger, cacheDir string, errChan chan error, dev bool) (*provisionerd.Server, error) { @@ -697,28 +588,20 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API, // nolint: revive func printLogo(cmd *cobra.Command, spooky bool) { if spooky { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), ` - ▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███ - ▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒ - ▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒ - ▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄ - ▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒ - ░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░ - ░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░ - ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ - ░ ░ ░ ░ ░ ░ ░ ░ - ░ ░ - + _, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███ +▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒ +▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒ +▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄ +▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒ +░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░ + ░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░ +░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ +░ ░ ░ ░ ░ ░ ░ ░ +░ ░ `) return } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄ - ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█ - ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ -█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ - ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ - -`) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s - Remote development on your infrastucture\n", cliui.Styles.Bold.Render("Coder "+buildinfo.Version())) } func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) { @@ -869,3 +752,86 @@ func isLocalURL(ctx context.Context, urlString string) (bool, error) { } return false, nil } + +// embeddedPostgresURL returns the URL for the embedded PostgreSQL deployment. +func embeddedPostgresURL(cfg config.Root) (string, error) { + pgPassword, err := cfg.PostgresPassword().Read() + if errors.Is(err, os.ErrNotExist) { + pgPassword, err = cryptorand.String(16) + if err != nil { + return "", xerrors.Errorf("generate password: %w", err) + } + err = cfg.PostgresPassword().Write(pgPassword) + if err != nil { + return "", xerrors.Errorf("write password: %w", err) + } + } + if err != nil && !errors.Is(err, os.ErrNotExist) { + return "", err + } + pgPort, err := cfg.PostgresPort().Read() + if errors.Is(err, os.ErrNotExist) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + return "", xerrors.Errorf("listen for random port: %w", err) + } + _ = listener.Close() + tcpAddr, valid := listener.Addr().(*net.TCPAddr) + if !valid { + return "", xerrors.Errorf("listener returned non TCP addr: %T", tcpAddr) + } + pgPort = strconv.Itoa(tcpAddr.Port) + err = cfg.PostgresPort().Write(pgPort) + if err != nil { + return "", xerrors.Errorf("write postgres port: %w", err) + } + } + return fmt.Sprintf("postgres://coder@localhost:%s/coder?sslmode=disable&password=%s", pgPort, pgPassword), nil +} + +func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logger) (string, func() error, error) { + usr, err := user.Current() + if err != nil { + return "", nil, err + } + if usr.Uid == "0" { + return "", nil, xerrors.New("The built-in PostgreSQL cannot run as the root user. Create a non-root user and run again!") + } + + // Ensure a password and port have been generated! + connectionURL, err := embeddedPostgresURL(cfg) + if err != nil { + return "", nil, err + } + pgPassword, err := cfg.PostgresPassword().Read() + if err != nil { + return "", nil, xerrors.Errorf("read postgres password: %w", err) + } + pgPortRaw, err := cfg.PostgresPort().Read() + if err != nil { + return "", nil, xerrors.Errorf("read postgres port: %w", err) + } + pgPort, err := strconv.Atoi(pgPortRaw) + if err != nil { + return "", nil, xerrors.Errorf("parse postgres port: %w", err) + } + + stdlibLogger := slog.Stdlib(ctx, logger.Named("postgres"), slog.LevelDebug) + ep := embeddedpostgres.NewDatabase( + embeddedpostgres.DefaultConfig(). + Version(embeddedpostgres.V13). + BinariesPath(filepath.Join(cfg.PostgresPath(), "bin")). + DataPath(filepath.Join(cfg.PostgresPath(), "data")). + RuntimePath(filepath.Join(cfg.PostgresPath(), "runtime")). + Username("coder"). + Password(pgPassword). + Database("coder"). + Port(uint32(pgPort)). + Logger(stdlibLogger.Writer()), + ) + err = ep.Start() + if err != nil { + return "", nil, xerrors.Errorf("Failed to start built-in PostgreSQL: %w", err) + } + return connectionURL, ep.Stop, nil +} diff --git a/cli/server_test.go b/cli/server_test.go index 7af01138e8c2c..6b86e3986e123 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -9,7 +9,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" - "fmt" "math/big" "net" "net/http" @@ -25,7 +24,6 @@ import ( "go.uber.org/goleak" "github.com/coder/coder/cli/clitest" - "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database/postgres" "github.com/coder/coder/codersdk" ) @@ -34,8 +32,6 @@ import ( // nolint:paralleltest func TestServer(t *testing.T) { t.Run("Production", func(t *testing.T) { - // postgres.Open() seems to be creating race conditions when run in parallel. - // t.Parallel() if runtime.GOOS != "linux" || testing.Short() { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() @@ -71,99 +67,40 @@ func TestServer(t *testing.T) { cancelFunc() require.ErrorIs(t, <-errC, context.Canceled) }) - - t.Run("Development", func(t *testing.T) { + t.Run("NoPostgres", func(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - - wantEmail := "admin@coder.com" - - root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0") - var buf strings.Builder + root, _ := clitest.New(t, "server", "--address", ":0") + err := root.ExecuteContext(ctx) + require.Error(t, err) + }) + t.Run("BuiltinPostgres", func(t *testing.T) { + t.Parallel() + if testing.Short() { + t.SkipNow() + } + ctx, cancelFunc := context.WithCancel(context.Background()) + root, cfg := clitest.New(t, "server", "--address", ":0", "--postgres-builtin") errC := make(chan error) - root.SetOutput(&buf) go func() { errC <- root.ExecuteContext(ctx) }() - - var token string require.Eventually(t, func() bool { - var err error - token, err = cfg.Session().Read() - return err == nil && token != "" - }, 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, "token:", token) - + _, err := cfg.URL().Read() + return err == nil + }, time.Minute, 25*time.Millisecond) cancelFunc() require.ErrorIs(t, <-errC, 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) - // 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") - } - - // Verify that we warned the user about the default access URL possibly not being what they want. - assert.Contains(t, buf.String(), "coder/coder/issues/1528") }) - - // 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", "--tunnel=false", "--address", ":0") + t.Run("BuiltinPostgresURL", func(t *testing.T) { + t.Parallel() + root, _ := clitest.New(t, "server", "postgres-builtin-url") var buf strings.Builder root.SetOutput(&buf) - errC := make(chan error) - go func() { - errC <- root.ExecuteContext(ctx) - }() - - 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) + err := root.Execute() require.NoError(t, err) - - cancelFunc() - require.ErrorIs(t, <-errC, 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) + require.Contains(t, buf.String(), "psql") }) t.Run("NoWarningWithRemoteAccessURL", func(t *testing.T) { @@ -171,7 +108,7 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--access-url", "http://1.2.3.4:3000/") + root, cfg := clitest.New(t, "server", "--in-memory", "--address", ":0", "--access-url", "http://1.2.3.4:3000/") var buf strings.Builder errC := make(chan error) root.SetOutput(&buf) @@ -189,14 +126,14 @@ func TestServer(t *testing.T) { cancelFunc() require.ErrorIs(t, <-errC, context.Canceled) - assert.NotContains(t, buf.String(), "coder/coder/issues/1528") + assert.NotContains(t, buf.String(), "Workspaces must be able to reach Coder from this URL") }) t.Run("TLSBadVersion", func(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", + root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--tls-enable", "--tls-min-version", "tls9") err := root.ExecuteContext(ctx) require.Error(t, err) @@ -205,7 +142,7 @@ func TestServer(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", + root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--tls-enable", "--tls-client-auth", "something") err := root.ExecuteContext(ctx) require.Error(t, err) @@ -214,7 +151,7 @@ func TestServer(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", + root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--tls-enable") err := root.ExecuteContext(ctx) require.Error(t, err) @@ -225,7 +162,7 @@ func TestServer(t *testing.T) { defer cancelFunc() certPath, keyPath := generateTLSCertificate(t) - root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", + root, cfg := clitest.New(t, "server", "--in-memory", "--address", ":0", "--tls-enable", "--tls-cert-file", certPath, "--tls-key-file", keyPath) errC := make(chan error) go func() { @@ -266,36 +203,18 @@ func TestServer(t *testing.T) { } ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--provisioner-daemons", "1") + root, cfg := clitest.New(t, "server", "--in-memory", "--address", ":0", "--provisioner-daemons", "1") serverErr := make(chan error) go func() { err := root.ExecuteContext(ctx) serverErr <- err }() - var token string require.Eventually(t, func() bool { var err error - token, err = cfg.Session().Read() + _, err = cfg.URL().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 - orgs, err := client.OrganizationsByUser(ctx, codersdk.Me) - require.NoError(t, err) - - // Create a workspace so the cleanup occurs! - version := coderdtest.CreateTemplateVersion(t, client, orgs[0].ID, nil) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, orgs[0].ID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, orgs[0].ID, template.ID) - coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - require.NoError(t, err) currentProcess, err := os.FindProcess(os.Getpid()) require.NoError(t, err) err = currentProcess.Signal(os.Interrupt) @@ -313,7 +232,7 @@ func TestServer(t *testing.T) { t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--trace=true") + root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--trace=true") errC := make(chan error) go func() { errC <- root.ExecuteContext(ctx) diff --git a/coder.env b/coder.env index 370314b0f4c44..f2f68c5910782 100644 --- a/coder.env +++ b/coder.env @@ -1,6 +1,11 @@ # Run "coder server --help" for flag information. +CODER_ACCESS_URL= CODER_ADDRESS= +# Automatically install and run PostgreSQL by setting this to "true" +CODER_PG_BUILTIN= CODER_PG_CONNECTION_URL= CODER_TLS_CERT_FILE= CODER_TLS_ENABLE= CODER_TLS_KEY_FILE= +# Generate a unique *.try.coder.app access URL by setting this to "true" +CODER_TUNNEL= diff --git a/coder.service b/coder.service index a7ecfe642c758..928df1ff1c28e 100644 --- a/coder.service +++ b/coder.service @@ -10,8 +10,9 @@ StartLimitBurst=3 [Service] Type=notify EnvironmentFile=/etc/coder.d/coder.env +User=coder +Group=coder ProtectSystem=full -ProtectHome=read-only PrivateTmp=yes PrivateDevices=yes SecureBits=keep-caps diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 825e53b6efc65..f54f94e4e76ca 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "math/rand" "net/http" "net/url" "reflect" @@ -62,14 +63,17 @@ func (api *API) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPCP } }() + // Required for randomly generated names to not conflict! + rand.Seed(time.Now().UnixMicro()) + name := namesgenerator.GetRandomName(1) daemon, err := api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ ID: uuid.New(), CreatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), + Name: name, Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform}, }) if err != nil { - return nil, err + return nil, xerrors.Errorf("insert provisioner daemon %q: %w", name, err) } mux := drpcmux.New() diff --git a/docs/install.md b/docs/install.md index 5f055f7224513..13ce598ee10a6 100644 --- a/docs/install.md +++ b/docs/install.md @@ -41,19 +41,11 @@ Coder publishes the following system packages [in GitHub releases](https://githu Once installed, you can run Coder as a system service: ```sh -# Specify a PostgreSQL database -# in the configuration first: +# Setup PostgreSQL and an external access URL sudo vim /etc/coder.d/coder.env sudo service coder restart ``` -Or run a **temporary deployment** with dev mode (all data is in-memory and destroyed on exit): - - -```sh -coder server --dev -``` - ## docker-compose Before proceeding, please ensure that you have both Docker and the [latest version of @@ -111,15 +103,10 @@ We publish self-contained .zip and .tar.gz archives in [GitHub releases](https:/ 1. Start a Coder server - To run a **temporary deployment**, start with dev mode (all data is in-memory and destroyed on exit): - - ```bash - coder server --dev - ``` - - To run a **production deployment** with PostgreSQL: + ```sh + # Automatically sets up PostgreSQL and an external access URL on *.try.coder.app + coder server --postgres-builtin --tunnel - ```bash - CODER_PG_CONNECTION_URL="postgres://@/?password=" \ - coder server + # Requires a PostgreSQL instance and external access URL + coder server --postgres-url --access-url ``` diff --git a/examples/templates/kubernetes-multi-service/main.tf b/examples/templates/kubernetes-multi-service/main.tf index cd5623d790db5..7a87170505484 100644 --- a/examples/templates/kubernetes-multi-service/main.tf +++ b/examples/templates/kubernetes-multi-service/main.tf @@ -21,9 +21,7 @@ variable "use_kubeconfig" { Kubernetes cluster as you are deploying workspaces to. Set this to true if the Coder host is running outside the Kubernetes cluster - for workspaces. A valid "~/.kube/config" must be present on the Coder host. This - is likely not your local machine unless you are using `coder server --dev.` - + for workspaces. A valid "~/.kube/config" must be present on the Coder host. EOF } diff --git a/go.mod b/go.mod index 4d335e96de29c..8694d32cdb777 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/creack/pty v1.1.18 github.com/fatih/color v1.13.0 github.com/fatih/structs v1.1.0 + github.com/fergusstrange/embedded-postgres v1.16.0 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a github.com/gliderlabs/ssh v0.3.4 @@ -129,6 +130,8 @@ require ( storj.io/drpc v0.0.30 ) +require github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect diff --git a/go.sum b/go.sum index 436fb3395242c..1c620cf1a89f1 100644 --- a/go.sum +++ b/go.sum @@ -547,6 +547,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fergusstrange/embedded-postgres v1.16.0 h1:4imxi+KJnoYj9j64OYXoh3c2g51SwPyKZp+AzLfgy7M= +github.com/fergusstrange/embedded-postgres v1.16.0/go.mod h1:0B+3bPsMvcNgR9nN+bdM2x9YaNYDnf3ksUqYp1OAub0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -1096,6 +1098,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= @@ -1596,6 +1599,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= diff --git a/install.sh b/install.sh index 795c1af367bc6..f18c4c1cda5df 100755 --- a/install.sh +++ b/install.sh @@ -97,12 +97,8 @@ PATH="$STANDALONE_INSTALL_PREFIX/bin:\$PATH" EOF fi cath < /dev/null 2>&1; then + useradd \ + --system \ + --user-group \ + --shell /bin/false \ + $USER +fi diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index 4717dd7905c3a..6abbb17ae6059 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -964,10 +964,6 @@ func (p *Server) failActiveJob(failedJob *proto.FailedJob) { p.jobMutex.Lock() defer p.jobMutex.Unlock() if !p.isRunningJob() { - if p.isClosed() { - return - } - p.opts.Logger.Info(context.Background(), "skipping job fail; none running", slog.F("error_message", failedJob.Error)) return } if p.jobFailed.Load() { diff --git a/scripts/develop.sh b/scripts/develop.sh index a46576d121747..3b4cc5c0d5376 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -12,10 +12,6 @@ echo '== Without these binaries, workspaces will fail to start!' # Run yarn install, to make sure node_modules are ready to go "$PROJECT_ROOT/scripts/yarn_install.sh" -# Use static credentials for development -export CODER_DEV_ADMIN_EMAIL=admin@coder.com -export CODER_DEV_ADMIN_PASSWORD=password - # This is a way to run multiple processes in parallel, and have Ctrl-C work correctly # to kill both at the same time. For more details, see: # https://stackoverflow.com/questions/3004811/how-do-you-run-multiple-programs-in-parallel-from-a-bash-script @@ -24,10 +20,14 @@ export CODER_DEV_ADMIN_PASSWORD=password trap 'kill 0' SIGINT CODERV2_HOST=http://127.0.0.1:3000 INSPECT_XSTATE=true yarn --cwd=./site dev & - go run -tags embed cmd/coder/main.go server --dev --tunnel=true & + go run -tags embed cmd/coder/main.go server --in-memory --tunnel & # Just a minor sleep to ensure the first user was created to make the member. sleep 2 + + # create the first user, the admin + go run cmd/coder/main.go login http://127.0.0.1:3000 --username=admin --email=admin@coder.com --password=password || true + # || yes to always exit code 0. If this fails, whelp. go run cmd/coder/main.go users create --email=member@coder.com --username=member --password="${CODER_DEV_ADMIN_PASSWORD}" || true wait diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index 7ea3c62e2a216..2ad1195439aed 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -1,5 +1,15 @@ +import axios from "axios" +import { postFirstUser } from "../src/api/api" +import * as constants from "./constants" + const globalSetup = async (): Promise => { - // Nothing yet! + axios.defaults.baseURL = "http://localhost:3000" + await postFirstUser({ + email: constants.email, + organization: constants.organization, + username: constants.username, + password: constants.password, + }) } export default globalSetup diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index a124b9b86d387..71b18659eba96 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -1,6 +1,5 @@ import { PlaywrightTestConfig } from "@playwright/test" import * as path from "path" -import * as constants from "./constants" const config: PlaywrightTestConfig = { testDir: "tests", @@ -18,10 +17,7 @@ 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 --tunnel=false --dev-admin-email ${constants.email} --dev-admin-password ${constants.password}`, + command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --in-memory`, port: 3000, timeout: 120 * 10000, reuseExistingServer: false, diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 09c14645fbffb..cd46b4f0011d5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -235,6 +235,13 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise => { + const response = await axios.post(`/api/v2/users/first`, req) + return response.data +} + export const updateUserPassword = async ( userId: TypesGen.User["id"], updatePassword: TypesGen.UpdateUserPasswordRequest, From 7fd35604684e7002004a256831f5a31ec82146de Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Wed, 15 Jun 2022 20:30:06 +0000 Subject: [PATCH 2/2] Use fork of embedded-postgres for cache path --- cli/server.go | 1 + go.mod | 3 +++ go.sum | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/server.go b/cli/server.go index e82b45f0a9f7b..d228dcfe8f6b3 100644 --- a/cli/server.go +++ b/cli/server.go @@ -804,6 +804,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg BinariesPath(filepath.Join(cfg.PostgresPath(), "bin")). DataPath(filepath.Join(cfg.PostgresPath(), "data")). RuntimePath(filepath.Join(cfg.PostgresPath(), "runtime")). + CachePath(filepath.Join(cfg.PostgresPath(), "cache")). Username("coder"). Password(pgPassword). Database("coder"). diff --git a/go.mod b/go.mod index bd4cff947b1bf..97f1c4519261f 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,9 @@ replace github.com/briandowns/spinner => github.com/kylecarbs/spinner v1.18.2-0. // Required until https://github.com/storj/drpc/pull/31 is merged. replace storj.io/drpc => github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff +// Required until https://github.com/fergusstrange/embedded-postgres/pull/75 is merged. +replace github.com/fergusstrange/embedded-postgres => github.com/kylecarbs/embedded-postgres v1.17.1-0.20220615202325-461532cecd3a + // opencensus-go leaks a goroutine by default. replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b diff --git a/go.sum b/go.sum index c100611cce06e..27c4921a4cdb0 100644 --- a/go.sum +++ b/go.sum @@ -547,8 +547,6 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fergusstrange/embedded-postgres v1.16.0 h1:4imxi+KJnoYj9j64OYXoh3c2g51SwPyKZp+AzLfgy7M= -github.com/fergusstrange/embedded-postgres v1.16.0/go.mod h1:0B+3bPsMvcNgR9nN+bdM2x9YaNYDnf3ksUqYp1OAub0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -1077,6 +1075,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff h1:7qg425aXdULnZWCCQNPOzHO7c+M6BpbTfOUJLrk5+3w= github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg= +github.com/kylecarbs/embedded-postgres v1.17.1-0.20220615202325-461532cecd3a h1:uOnis+HNE6e6eR17YlqzKk51GDahd7E/FacnZxS8h8w= +github.com/kylecarbs/embedded-postgres v1.17.1-0.20220615202325-461532cecd3a/go.mod h1:0B+3bPsMvcNgR9nN+bdM2x9YaNYDnf3ksUqYp1OAub0= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY=