feat: Add built-in PostgreSQL for simple production setup (#2345)

* feat: Add built-in PostgreSQL for simple production setup

Fixes #2321.

* Use fork of embedded-postgres for cache path
This commit is contained in:
Kyle Carberry
2022-06-15 16:02:18 -05:00
committed by GitHub
parent bb4ecd72c5
commit ccd061652b
23 changed files with 413 additions and 480 deletions

View File

@ -93,6 +93,8 @@ nfpms:
type: "config|noreplace" type: "config|noreplace"
- src: coder.service - src: coder.service
dst: /usr/lib/systemd/system/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. # Image templates are empty on snapshots to avoid lengthy builds for development.
dockers: dockers:

View File

@ -47,10 +47,14 @@ To install, run:
curl -fsSL https://coder.com/install.sh | sh 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 ```sh
coder server --dev # Automatically sets up an external access URL on *.try.coder.app
coder server --tunnel
# Requires a PostgreSQL instance and external access URL
coder server --postgres-url <url> --access-url <url>
``` ```
Use `coder --help` to get a complete list of flags and environment variables. Use our [quickstart guide](https://coder.com/docs/coder-oss/latest/quickstart) for a full walkthrough. Use `coder --help` to get a complete list of flags and environment variables. Use our [quickstart guide](https://coder.com/docs/coder-oss/latest/quickstart) for a full walkthrough.

View File

@ -25,6 +25,18 @@ func (r Root) DotfilesURL() File {
return File(filepath.Join(string(r), "dotfilesurl")) 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. // File provides convenience methods for interacting with *os.File.
type File string type File string

View File

@ -17,6 +17,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui" "github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
) )
@ -37,7 +38,12 @@ func init() {
} }
func login() *cobra.Command { func login() *cobra.Command {
return &cobra.Command{ var (
email string
username string
password string
)
cmd := &cobra.Command{
Use: "login <url>", Use: "login <url>",
Short: "Authenticate with a Coder deployment", Short: "Authenticate with a Coder deployment",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
@ -66,11 +72,12 @@ func login() *cobra.Command {
return xerrors.Errorf("has initial user: %w", err) return xerrors.Errorf("has initial user: %w", err)
} }
if !hasInitialUser { if !hasInitialUser {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n")
if username == "" {
if !isTTY(cmd) { if !isTTY(cmd) {
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") 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{ _, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Would you like to create the first user?", Text: "Would you like to create the first user?",
Default: "yes", Default: "yes",
@ -86,7 +93,7 @@ func login() *cobra.Command {
if err != nil { if err != nil {
return xerrors.Errorf("get current user: %w", err) return xerrors.Errorf("get current user: %w", err)
} }
username, err := cliui.Prompt(cmd, cliui.PromptOptions{ username, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "What " + cliui.Styles.Field.Render("username") + " would you like?", Text: "What " + cliui.Styles.Field.Render("username") + " would you like?",
Default: currentUser.Username, Default: currentUser.Username,
}) })
@ -96,8 +103,10 @@ func login() *cobra.Command {
if err != nil { if err != nil {
return xerrors.Errorf("pick username prompt: %w", err) return xerrors.Errorf("pick username prompt: %w", err)
} }
}
email, err := cliui.Prompt(cmd, cliui.PromptOptions{ if email == "" {
email, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "What's your " + cliui.Styles.Field.Render("email") + "?", Text: "What's your " + cliui.Styles.Field.Render("email") + "?",
Validate: func(s string) error { Validate: func(s string) error {
err := validator.New().Var(s, "email") err := validator.New().Var(s, "email")
@ -110,8 +119,10 @@ func login() *cobra.Command {
if err != nil { if err != nil {
return xerrors.Errorf("specify email prompt: %w", err) return xerrors.Errorf("specify email prompt: %w", err)
} }
}
password, err := cliui.Prompt(cmd, cliui.PromptOptions{ if password == "" {
password, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":", Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
Secret: true, Secret: true,
Validate: cliui.ValidateNotEmpty, Validate: cliui.ValidateNotEmpty,
@ -132,6 +143,7 @@ func login() *cobra.Command {
if err != nil { if err != nil {
return xerrors.Errorf("confirm password prompt: %w", err) return xerrors.Errorf("confirm password prompt: %w", err)
} }
}
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{ _, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email, Email: email,
@ -219,6 +231,10 @@ func login() *cobra.Command {
return nil return nil
}, },
} }
cliflag.StringVarP(cmd.Flags(), &email, "email", "e", "CODER_EMAIL", "", "Specifies an email address to authenticate with.")
cliflag.StringVarP(cmd.Flags(), &username, "username", "u", "CODER_USERNAME", "", "Specifies a username to authenticate with.")
cliflag.StringVarP(cmd.Flags(), &password, "password", "p", "CODER_PASSWORD", "", "Specifies a password to authenticate with.")
return cmd
} }
// isWSL determines if coder-cli is running within Windows Subsystem for Linux // isWSL determines if coder-cli is running within Windows Subsystem for Linux

View File

@ -56,6 +56,26 @@ func TestLogin(t *testing.T) {
<-doneChan <-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.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())

View File

@ -57,8 +57,8 @@ func Root() *cobra.Command {
SilenceUsage: true, SilenceUsage: true,
Long: `Coder — A tool for provisioning self-hosted development environments. 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. Example: ` Start a Coder server.
` + cliui.Styles.Code.Render("$ coder server --dev") + ` ` + cliui.Styles.Code.Render("$ coder server") + `
Get started by creating a template from an example. Get started by creating a template from an example.
` + cliui.Styles.Code.Render("$ coder templates init"), ` + cliui.Styles.Code.Render("$ coder templates init"),

View File

@ -16,21 +16,24 @@ import (
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
"os/user"
"path/filepath" "path/filepath"
"strconv"
"strings"
"time" "time"
"github.com/coder/coder/buildinfo" "github.com/coder/coder/buildinfo"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisioner/echo"
"github.com/briandowns/spinner"
"github.com/coreos/go-systemd/daemon" "github.com/coreos/go-systemd/daemon"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
"github.com/google/go-github/v43/github" "github.com/google/go-github/v43/github"
"github.com/pion/turn/v2" "github.com/pion/turn/v2"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"golang.org/x/mod/semver"
"golang.org/x/oauth2" "golang.org/x/oauth2"
xgithub "golang.org/x/oauth2/github" xgithub "golang.org/x/oauth2/github"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -52,7 +55,6 @@ import (
"github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/coderd/turnconn"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/terraform" "github.com/coder/coder/provisioner/terraform"
"github.com/coder/coder/provisionerd" "github.com/coder/coder/provisionerd"
"github.com/coder/coder/provisionersdk" "github.com/coder/coder/provisionersdk"
@ -70,12 +72,10 @@ func server() *cobra.Command {
pprofEnabled bool pprofEnabled bool
pprofAddress string pprofAddress string
cacheDir string cacheDir string
dev bool inMemoryDatabase bool
devUserEmail string
devUserPassword string
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0. // provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8 provisionerDaemonCount uint8
postgresURL string
oauth2GithubClientID string oauth2GithubClientID string
oauth2GithubClientSecret string oauth2GithubClientSecret string
oauth2GithubAllowedOrganizations []string oauth2GithubAllowedOrganizations []string
@ -100,9 +100,9 @@ func server() *cobra.Command {
Use: "server", Use: "server",
Short: "Start a Coder server", Short: "Start a Coder server",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
printLogo(cmd, spooky)
logger := slog.Make(sloghuman.Sink(os.Stderr)) logger := slog.Make(sloghuman.Sink(os.Stderr))
buildModeDev := semver.Prerelease(buildinfo.Version()) == "-devel" if verbose {
if verbose || buildModeDev {
logger = logger.Leveled(slog.LevelDebug) logger = logger.Leveled(slog.LevelDebug)
} }
@ -132,7 +132,21 @@ func server() *cobra.Command {
} }
} }
printLogo(cmd, spooky) config := createConfig(cmd)
// Only use built-in if PostgreSQL URL isn't specified!
if !inMemoryDatabase && postgresURL == "" {
var closeFunc func() error
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
postgresURL, closeFunc, err = startBuiltinPostgres(cmd.Context(), config, logger)
if err != nil {
return err
}
defer func() {
// Gracefully shut PostgreSQL down!
_ = closeFunc()
}()
}
listener, err := net.Listen("tcp", address) listener, err := net.Listen("tcp", address)
if err != nil { if err != nil {
return xerrors.Errorf("listen %q: %w", address, err) return xerrors.Errorf("listen %q: %w", address, err)
@ -154,7 +168,8 @@ func server() *cobra.Command {
if tcpAddr.IP.IsUnspecified() { if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1) tcpAddr.IP = net.IPv4(127, 0, 0, 1)
} }
// If no access URL is specified, fallback to the
// bounds URL.
localURL := &url.URL{ localURL := &url.URL{
Scheme: "http", Scheme: "http",
Host: tcpAddr.String(), Host: tcpAddr.String(),
@ -164,9 +179,6 @@ func server() *cobra.Command {
} }
if accessURL == "" { if accessURL == "" {
accessURL = localURL.String() accessURL = localURL.String()
} else {
// If an access URL is specified, always skip tunneling.
tunnel = false
} }
var ( var (
@ -178,70 +190,38 @@ func server() *cobra.Command {
// If we're attempting to tunnel in dev-mode, the access URL // If we're attempting to tunnel in dev-mode, the access URL
// needs to be changed to use the tunnel. // needs to be changed to use the tunnel.
if dev && tunnel { if tunnel {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render( cmd.Printf("Opening tunnel so workspaces can connect to your deployment\n")
"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 err != nil && !errors.Is(err, cliui.Canceled) {
return err
}
// Don't tunnel if the user specifies no tunnel.
tunnel = !errors.Is(err, cliui.Canceled)
}
if err == nil {
devTunnel, devTunnelErrChan, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel")) devTunnel, devTunnelErrChan, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel"))
if err != nil { if err != nil {
return xerrors.Errorf("create tunnel: %w", err) return xerrors.Errorf("create tunnel: %w", err)
} }
accessURL = devTunnel.URL accessURL = devTunnel.URL
} }
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
}
// Warn the user if the access URL appears to be a loopback address. // Warn the user if the access URL appears to be a loopback address.
isLocal, err := isLocalURL(cmd.Context(), accessURL) isLocal, err := isLocalURL(cmd.Context(), accessURL)
if isLocal || err != nil { if isLocal || err != nil {
var reason string reason := "could not be resolved"
if isLocal { if isLocal {
reason = "appears to be a loopback address" reason = "isn't externally reachable"
} else {
reason = "could not be resolved"
} }
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render( cmd.Printf("%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL with:\n", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURL), reason)
cliui.Styles.Warn.Render("Warning:")+" The current access URL:")+"\n\n") cmd.Println(cliui.Styles.Code.Render(strings.Join(os.Args, " ") + " --tunnel"))
_, _ = 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("View the Web UI: %s\n", accessURL)
accessURLParsed, err := url.Parse(accessURL) accessURLParsed, err := url.Parse(accessURL)
if err != nil { if err != nil {
return xerrors.Errorf("parse access url %q: %w", accessURL, err) 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) sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(sshKeygenAlgorithmRaw)
if err != nil { if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err) return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err)
@ -267,7 +247,7 @@ func server() *cobra.Command {
Logger: logger.Named("coderd"), Logger: logger.Named("coderd"),
Database: databasefake.New(), Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(), Pubsub: database.NewPubsubInMemory(),
GoogleTokenValidator: validator, GoogleTokenValidator: googleTokenValidator,
SecureAuthCookie: secureAuthCookie, SecureAuthCookie: secureAuthCookie,
SSHKeygenAlgorithm: sshKeygenAlgorithm, SSHKeygenAlgorithm: sshKeygenAlgorithm,
TURNServer: turnServer, TURNServer: turnServer,
@ -281,11 +261,10 @@ func server() *cobra.Command {
} }
} }
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "access-url: %s\n", accessURL) if inMemoryDatabase {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "provisioner-daemons: %d\n", provisionerDaemonCount) options.Database = databasefake.New()
_, _ = fmt.Fprintln(cmd.ErrOrStderr()) options.Pubsub = database.NewPubsubInMemory()
} else {
if !dev {
sqlDB, err := sql.Open(sqlDriver, postgresURL) sqlDB, err := sql.Open(sqlDriver, postgresURL)
if err != nil { if err != nil {
return xerrors.Errorf("dial postgres: %w", err) return xerrors.Errorf("dial postgres: %w", err)
@ -331,7 +310,7 @@ func server() *cobra.Command {
errCh := make(chan error, 1) errCh := make(chan error, 1)
provisionerDaemons := make([]*provisionerd.Server, 0) provisionerDaemons := make([]*provisionerd.Server, 0)
for i := 0; uint8(i) < provisionerDaemonCount; i++ { 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 { if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err) return xerrors.Errorf("create provisioner daemon: %w", err)
} }
@ -361,14 +340,14 @@ func server() *cobra.Command {
wg.Go(func() error { wg.Go(func() error {
// Make sure to close the tunnel listener if we exit so the // Make sure to close the tunnel listener if we exit so the
// errgroup doesn't wait forever! // errgroup doesn't wait forever!
if dev && tunnel { if tunnel {
defer devTunnel.Listener.Close() defer devTunnel.Listener.Close()
} }
return server.Serve(listener) return server.Serve(listener)
}) })
if dev && tunnel { if tunnel {
wg.Go(func() error { wg.Go(func() error {
defer listener.Close() defer listener.Close()
@ -379,45 +358,20 @@ func server() *cobra.Command {
errCh <- wg.Wait() errCh <- wg.Wait()
}() }()
config := createConfig(cmd)
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. // 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, // Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service. // such as via the systemd service.
_ = config.URL().Write(client.URL.String()) _ = 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()) hasFirstUser, err := client.HasFirstUser(cmd.Context())
if !hasFirstUser && err == nil { if !hasFirstUser && err == nil {
// This could fail for a variety of TLS-related reasons. cmd.Println()
// This is a helpful starter message, and not critical for user interaction. cmd.Println("Get started by creating the first user (in a new terminal):")
_, _ = 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"))) 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. // Updates the systemd status from activating to activated.
_, err = daemon.SdNotify(false, daemon.SdNotifyReady) _, err = daemon.SdNotify(false, daemon.SdNotifyReady)
if err != nil { if err != nil {
@ -456,65 +410,54 @@ func server() *cobra.Command {
if err != nil { if err != nil {
return xerrors.Errorf("notify systemd: %w", err) return xerrors.Errorf("notify systemd: %w", err)
} }
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Bold.Render(
cliui.Styles.Bold.Render(
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit")) "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)
}
}
}
for _, provisionerDaemon := range provisionerDaemons { for _, provisionerDaemon := range provisionerDaemons {
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) if verbose {
spin.Writer = cmd.OutOrStdout() cmd.Println("Shutting down provisioner daemon...")
spin.Suffix = cliui.Styles.Keyword.Render(" Shutting down provisioner daemon...") }
spin.Start()
err = provisionerDaemon.Shutdown(cmd.Context()) err = provisionerDaemon.Shutdown(cmd.Context())
if err != nil { if err != nil {
spin.FinalMSG = cliui.Styles.Prompt.String() + "Failed to shutdown provisioner daemon: " + err.Error() cmd.PrintErrf("Failed to shutdown provisioner daemon: %s\n", err)
spin.Stop() continue
} }
err = provisionerDaemon.Close() err = provisionerDaemon.Close()
if err != nil { if err != nil {
spin.Stop()
return xerrors.Errorf("close provisioner daemon: %w", err) return xerrors.Errorf("close provisioner daemon: %w", err)
} }
spin.FinalMSG = cliui.Styles.Prompt.String() + "Gracefully shut down provisioner daemon!\n" if verbose {
spin.Stop() cmd.Println("Gracefully shut down provisioner daemon!")
}
} }
if dev && tunnel { if tunnel {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for dev tunnel to close...\n") cmd.Println("Waiting for tunnel to close...")
closeTunnel() closeTunnel()
<-devTunnelErrChan <-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() shutdownConns()
coderAPI.Close() coderAPI.Close()
return nil 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.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(), &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.") cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard.")
@ -528,10 +471,10 @@ func server() *cobra.Command {
defaultCacheDir = dir 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.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.BoolVarP(root.Flags(), &inMemoryDatabase, "in-memory", "", "CODER_INMEMORY", false,
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)") "Specifies whether data will be stored in an in-memory database.")
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") _ = 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.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "The URL of a PostgreSQL database to connect to. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in 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.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", "", cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
"Specifies a client ID to use for oauth2 with GitHub.") "Specifies a client ID to use for oauth2 with GitHub.")
@ -555,8 +498,8 @@ func server() *cobra.Command {
"Specifies the path to the private key for the certificate. It requires a PEM-encoded file") "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", 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"`) `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, cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_TUNNEL", false,
"Specifies whether the dev tunnel will be enabled or not. If specified, the interactive prompt will not display.") "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{ cliflag.StringArrayVarP(root.Flags(), &stunServers, "stun-server", "", "CODER_STUN_SERVERS", []string{
"stun:stun.l.google.com:19302", "stun:stun.l.google.com:19302",
}, "Specify URLs for STUN servers to enable P2P connections.") }, "Specify URLs for STUN servers to enable P2P connections.")
@ -573,81 +516,6 @@ func server() *cobra.Command {
return root 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 // nolint:revive
func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API, func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
logger slog.Logger, cacheDir string, errChan chan error, dev bool) (*provisionerd.Server, error) { logger slog.Logger, cacheDir string, errChan chan error, dev bool) (*provisionerd.Server, error) {
@ -701,28 +569,20 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
// nolint: revive // nolint: revive
func printLogo(cmd *cobra.Command, spooky bool) { func printLogo(cmd *cobra.Command, spooky bool) {
if spooky { if spooky {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), ` _, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
▄████▄ ▒█████ █████▄ ▓█████ ██▀███ ▒██▀ ▀█ ▒██ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒
▒██▀ ▀█ ▒██ ██▒██▀ ██▌▓ ▓██ ▒ ██ ▒▓█ ▄ ▒██ ██▒██ █▌▒███ ▓██ ░▄█
▒▓█ ▄ ▒██ ██▒░██ ▌▒██ ▓██ ░▄█ ▒ ▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▄ ▒██▀▀█▄
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄ ▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒
▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒ ░ ░▒ ▒ ░░ ▒░░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░ ░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░
`) `)
return 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) { func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) {
@ -873,3 +733,87 @@ func isLocalURL(ctx context.Context, urlString string) (bool, error) {
} }
return false, nil 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")).
CachePath(filepath.Join(cfg.PostgresPath(), "cache")).
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. Optionally, specify an external deployment with `--postgres-url`: %w", err)
}
return connectionURL, ep.Stop, nil
}

View File

@ -9,7 +9,6 @@ import (
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"fmt"
"math/big" "math/big"
"net" "net"
"net/http" "net/http"
@ -25,7 +24,6 @@ import (
"go.uber.org/goleak" "go.uber.org/goleak"
"github.com/coder/coder/cli/clitest" "github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database/postgres" "github.com/coder/coder/coderd/database/postgres"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
) )
@ -34,8 +32,6 @@ import (
// nolint:paralleltest // nolint:paralleltest
func TestServer(t *testing.T) { func TestServer(t *testing.T) {
t.Run("Production", func(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() { if runtime.GOOS != "linux" || testing.Short() {
// Skip on non-Linux because it spawns a PostgreSQL instance. // Skip on non-Linux because it spawns a PostgreSQL instance.
t.SkipNow() t.SkipNow()
@ -71,99 +67,32 @@ func TestServer(t *testing.T) {
cancelFunc() cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled) require.ErrorIs(t, <-errC, context.Canceled)
}) })
t.Run("BuiltinPostgres", func(t *testing.T) {
t.Run("Development", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) if testing.Short() {
defer cancelFunc() t.SkipNow()
wantEmail := "admin@coder.com"
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0")
var buf strings.Builder
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)
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()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() root, cfg := clitest.New(t, "server", "--address", ":0")
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")
var buf strings.Builder
root.SetOutput(&buf)
errC := make(chan error) errC := make(chan error)
go func() { go func() {
errC <- root.ExecuteContext(ctx) errC <- root.ExecuteContext(ctx)
}() }()
var token string
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
var err error _, err := cfg.URL().Read()
token, err = cfg.Session().Read()
return err == nil return err == nil
}, 15*time.Second, 25*time.Millisecond) }, time.Minute, 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() cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled) 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) t.Run("BuiltinPostgresURL", func(t *testing.T) {
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword) t.Parallel()
root, _ := clitest.New(t, "server", "postgres-builtin-url")
var buf strings.Builder
root.SetOutput(&buf)
err := root.Execute()
require.NoError(t, err)
require.Contains(t, buf.String(), "psql")
}) })
t.Run("NoWarningWithRemoteAccessURL", func(t *testing.T) { t.Run("NoWarningWithRemoteAccessURL", func(t *testing.T) {
@ -171,7 +100,7 @@ func TestServer(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() 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 var buf strings.Builder
errC := make(chan error) errC := make(chan error)
root.SetOutput(&buf) root.SetOutput(&buf)
@ -189,14 +118,14 @@ func TestServer(t *testing.T) {
cancelFunc() cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled) 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.Run("TLSBadVersion", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() 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") "--tls-enable", "--tls-min-version", "tls9")
err := root.ExecuteContext(ctx) err := root.ExecuteContext(ctx)
require.Error(t, err) require.Error(t, err)
@ -205,7 +134,7 @@ func TestServer(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() 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") "--tls-enable", "--tls-client-auth", "something")
err := root.ExecuteContext(ctx) err := root.ExecuteContext(ctx)
require.Error(t, err) require.Error(t, err)
@ -214,7 +143,7 @@ func TestServer(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() defer cancelFunc()
root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0",
"--tls-enable") "--tls-enable")
err := root.ExecuteContext(ctx) err := root.ExecuteContext(ctx)
require.Error(t, err) require.Error(t, err)
@ -225,7 +154,7 @@ func TestServer(t *testing.T) {
defer cancelFunc() defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t) 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) "--tls-enable", "--tls-cert-file", certPath, "--tls-key-file", keyPath)
errC := make(chan error) errC := make(chan error)
go func() { go func() {
@ -266,36 +195,18 @@ func TestServer(t *testing.T) {
} }
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() 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) serverErr := make(chan error)
go func() { go func() {
err := root.ExecuteContext(ctx) err := root.ExecuteContext(ctx)
serverErr <- err serverErr <- err
}() }()
var token string
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
var err error var err error
token, err = cfg.Session().Read() _, err = cfg.URL().Read()
return err == nil return err == nil
}, 15*time.Second, 25*time.Millisecond) }, 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()) currentProcess, err := os.FindProcess(os.Getpid())
require.NoError(t, err) require.NoError(t, err)
err = currentProcess.Signal(os.Interrupt) err = currentProcess.Signal(os.Interrupt)
@ -313,7 +224,7 @@ func TestServer(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() 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) errC := make(chan error)
go func() { go func() {
errC <- root.ExecuteContext(ctx) errC <- root.ExecuteContext(ctx)

View File

@ -3,7 +3,9 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/rand"
"os" "os"
"time"
_ "time/tzdata" _ "time/tzdata"
"github.com/coder/coder/cli" "github.com/coder/coder/cli"
@ -11,6 +13,8 @@ import (
) )
func main() { func main() {
rand.Seed(time.Now().UnixMicro())
cmd, err := cli.Root().ExecuteC() cmd, err := cli.Root().ExecuteC()
if err != nil { if err != nil {
if errors.Is(err, cliui.Canceled) { if errors.Is(err, cliui.Canceled) {

View File

@ -1,6 +1,9 @@
# Run "coder server --help" for flag information. # Run "coder server --help" for flag information.
CODER_ACCESS_URL=
CODER_ADDRESS= CODER_ADDRESS=
CODER_PG_CONNECTION_URL= CODER_PG_CONNECTION_URL=
CODER_TLS_CERT_FILE= CODER_TLS_CERT_FILE=
CODER_TLS_ENABLE= CODER_TLS_ENABLE=
CODER_TLS_KEY_FILE= CODER_TLS_KEY_FILE=
# Generate a unique *.try.coder.app access URL
CODER_TUNNEL=false

View File

@ -10,8 +10,9 @@ StartLimitBurst=3
[Service] [Service]
Type=notify Type=notify
EnvironmentFile=/etc/coder.d/coder.env EnvironmentFile=/etc/coder.d/coder.env
User=coder
Group=coder
ProtectSystem=full ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes PrivateTmp=yes
PrivateDevices=yes PrivateDevices=yes
SecureBits=keep-caps SecureBits=keep-caps

View File

@ -62,14 +62,15 @@ func (api *API) ListenProvisionerDaemon(ctx context.Context) (client proto.DRPCP
} }
}() }()
name := namesgenerator.GetRandomName(1)
daemon, err := api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{ daemon, err := api.Database.InsertProvisionerDaemon(ctx, database.InsertProvisionerDaemonParams{
ID: uuid.New(), ID: uuid.New(),
CreatedAt: database.Now(), CreatedAt: database.Now(),
Name: namesgenerator.GetRandomName(1), Name: name,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform}, Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
}) })
if err != nil { if err != nil {
return nil, err return nil, xerrors.Errorf("insert provisioner daemon %q: %w", name, err)
} }
mux := drpcmux.New() mux := drpcmux.New()

View File

@ -41,16 +41,12 @@ Coder publishes the following system packages [in GitHub releases](https://githu
Once installed, you can run Coder as a system service: Once installed, you can run Coder as a system service:
```sh ```sh
# Specify a PostgreSQL database # Set up an external access URL or enable CODER_TUNNEL
# in the configuration first:
sudo vim /etc/coder.d/coder.env sudo vim /etc/coder.d/coder.env
sudo service coder restart # Use systemd to start Coder now and on reboot
``` sudo systemctl enable --now coder
# View the logs to ensure a successful start
Or run a **temporary deployment** with dev mode (all data is in-memory and destroyed on exit): journalctl -u coder.service -b
```sh
coder server --dev
``` ```
## docker-compose ## docker-compose
@ -110,17 +106,12 @@ We publish self-contained .zip and .tar.gz archives in [GitHub releases](https:/
1. Start a Coder server 1. Start a Coder server
To run a **temporary deployment**, start with dev mode (all data is in-memory and destroyed on exit): ```sh
# Automatically sets up an external access URL on *.try.coder.app
coder server --tunnel
```bash # Requires a PostgreSQL instance and external access URL
coder server --dev coder server --postgres-url <url> --access-url <url>
```
To run a **production deployment** with PostgreSQL:
```bash
CODER_PG_CONNECTION_URL="postgres://<username>@<host>/<database>?password=<password>" \
coder server
``` ```
## Next steps ## Next steps

View File

@ -21,9 +21,7 @@ variable "use_kubeconfig" {
Kubernetes cluster as you are deploying workspaces to. Kubernetes cluster as you are deploying workspaces to.
Set this to true if the Coder host is running outside the Kubernetes cluster 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 for workspaces. A valid "~/.kube/config" must be present on the Coder host.
is likely not your local machine unless you are using `coder server --dev.`
EOF EOF
} }

6
go.mod
View File

@ -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. // 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 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. // opencensus-go leaks a goroutine by default.
replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b
@ -56,6 +59,7 @@ require (
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/fatih/structs v1.1.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/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a
github.com/gliderlabs/ssh v0.3.4 github.com/gliderlabs/ssh v0.3.4
@ -130,6 +134,8 @@ require (
storj.io/drpc v0.0.30 storj.io/drpc v0.0.30
) )
require github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect

5
go.sum
View File

@ -1075,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/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 h1:7qg425aXdULnZWCCQNPOzHO7c+M6BpbTfOUJLrk5+3w=
github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg= 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 h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 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= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY=
@ -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.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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.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 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= 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 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 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 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/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/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= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg=

View File

@ -98,11 +98,7 @@ PATH="$STANDALONE_INSTALL_PREFIX/bin:\$PATH"
EOF EOF
fi fi
cath <<EOF cath <<EOF
Run Coder (temporary): Run Coder:
$STANDALONE_BINARY_NAME server --dev
Or run a production deployment with PostgreSQL:
CODER_PG_CONNECTION_URL="postgres://<username>@<host>/<database>?password=<password>" \\
$STANDALONE_BINARY_NAME server $STANDALONE_BINARY_NAME server
EOF EOF
@ -115,15 +111,12 @@ $1 package has been installed.
To run Coder as a system service: To run Coder as a system service:
# Configure the PostgreSQL database for Coder # Set up an external access URL or enable CODER_TUNNEL
sudo vim /etc/coder.d/coder.env sudo vim /etc/coder.d/coder.env
# Have systemd start Coder now and restart on boot # Use systemd to start Coder now and on reboot
sudo systemctl enable --now coder sudo systemctl enable --now coder
# View the logs to ensure a successful start
Or, run a temporary deployment (all data is in-memory journalctl -u coder.service -b
and destroyed on exit):
coder server --dev
EOF EOF
} }

13
preinstall.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/sh
set -eu
USER="coder"
# Add a Coder user to run as in systemd.
if ! id -u $USER >/dev/null 2>&1; then
useradd \
--system \
--user-group \
--shell /bin/false \
$USER
fi

View File

@ -967,10 +967,6 @@ func (p *Server) failActiveJob(failedJob *proto.FailedJob) {
p.jobMutex.Lock() p.jobMutex.Lock()
defer p.jobMutex.Unlock() defer p.jobMutex.Unlock()
if !p.isRunningJob() { 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 return
} }
if p.jobFailed.Load() { if p.jobFailed.Load() {

View File

@ -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 # Run yarn install, to make sure node_modules are ready to go
"$PROJECT_ROOT/scripts/yarn_install.sh" "$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 # 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: # 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 # 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 trap 'kill 0' SIGINT
CODERV2_HOST=http://127.0.0.1:3000 INSPECT_XSTATE=true yarn --cwd=./site dev & 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. # Just a minor sleep to ensure the first user was created to make the member.
sleep 2 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. # || 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 go run cmd/coder/main.go users create --email=member@coder.com --username=member --password="${CODER_DEV_ADMIN_PASSWORD}" || true
wait wait

View File

@ -1,5 +1,15 @@
import axios from "axios"
import { postFirstUser } from "../src/api/api"
import * as constants from "./constants"
const globalSetup = async (): Promise<void> => { const globalSetup = async (): Promise<void> => {
// 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 export default globalSetup

View File

@ -1,6 +1,5 @@
import { PlaywrightTestConfig } from "@playwright/test" import { PlaywrightTestConfig } from "@playwright/test"
import * as path from "path" import * as path from "path"
import * as constants from "./constants"
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
testDir: "tests", testDir: "tests",
@ -18,10 +17,7 @@ const config: PlaywrightTestConfig = {
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: { webServer: {
// Run the coder daemon directly. // Run the coder daemon directly.
command: `go run -tags embed ${path.join( command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --in-memory`,
__dirname,
"../../cmd/coder/main.go",
)} server --dev --tunnel=false --dev-admin-email ${constants.email} --dev-admin-password ${constants.password}`,
port: 3000, port: 3000,
timeout: 120 * 10000, timeout: 120 * 10000,
reuseExistingServer: false, reuseExistingServer: false,

View File

@ -229,6 +229,13 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
return response.data return response.data
} }
export const postFirstUser = async (
req: TypesGen.CreateFirstUserRequest,
): Promise<TypesGen.CreateFirstUserResponse> => {
const response = await axios.post(`/api/v2/users/first`, req)
return response.data
}
export const updateUserPassword = async ( export const updateUserPassword = async (
userId: TypesGen.User["id"], userId: TypesGen.User["id"],
updatePassword: TypesGen.UpdateUserPasswordRequest, updatePassword: TypesGen.UpdateUserPasswordRequest,