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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 413 additions and 480 deletions

View File

@ -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:

View File

@ -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 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.

View File

@ -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

View File

@ -17,6 +17,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
@ -37,7 +38,12 @@ func init() {
}
func login() *cobra.Command {
return &cobra.Command{
var (
email string
username string
password string
)
cmd := &cobra.Command{
Use: "login <url>",
Short: "Authenticate with a Coder deployment",
Args: cobra.ExactArgs(1),
@ -66,71 +72,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!")
}
return err
},
})
if err != nil {
return xerrors.Errorf("specify email 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)
}
_, 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")
}
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 xerrors.Errorf("confirm password prompt: %w", err)
}
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)
}
}
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)
}
}
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 +231,10 @@ func login() *cobra.Command {
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

View File

@ -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())

View File

@ -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"),

View File

@ -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,10 @@ 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
postgresURL string
oauth2GithubClientID string
oauth2GithubClientSecret string
oauth2GithubAllowedOrganizations []string
@ -100,9 +100,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 +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)
if err != nil {
return xerrors.Errorf("listen %q: %w", address, err)
@ -154,7 +168,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 +179,6 @@ func server() *cobra.Command {
}
if accessURL == "" {
accessURL = localURL.String()
} else {
// If an access URL is specified, always skip tunneling.
tunnel = false
}
var (
@ -178,70 +190,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 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 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)
}
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
}
_, _ = 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 externally reachable"
}
_, _ = 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, 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)
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)
@ -267,7 +247,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,
@ -281,11 +261,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)
@ -331,7 +310,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)
}
@ -361,14 +340,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()
@ -379,45 +358,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 {
@ -456,65 +410,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.")
@ -528,10 +471,10 @@ 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.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
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", "", "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.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
"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")
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.")
@ -573,81 +516,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) {
@ -701,28 +569,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) {
@ -873,3 +733,87 @@ 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")).
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/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,32 @@ func TestServer(t *testing.T) {
cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled)
})
t.Run("Development", func(t *testing.T) {
t.Run("BuiltinPostgres", 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
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")
if testing.Short() {
t.SkipNow()
}
// 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")
var buf strings.Builder
root.SetOutput(&buf)
root, cfg := clitest.New(t, "server", "--address", ":0")
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()
_, 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
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err)
}, 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)
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
})
t.Run("BuiltinPostgresURL", func(t *testing.T) {
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) {
@ -171,7 +100,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 +118,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 +134,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 +143,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 +154,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 +195,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 +224,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)

View File

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

View File

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

View File

@ -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

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{
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()

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:
```sh
# Specify a PostgreSQL database
# in the configuration first:
# Set up an external access URL or enable CODER_TUNNEL
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
# Use systemd to start Coder now and on reboot
sudo systemctl enable --now coder
# View the logs to ensure a successful start
journalctl -u coder.service -b
```
## docker-compose
@ -110,18 +106,13 @@ 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):
```sh
# Automatically sets up an external access URL on *.try.coder.app
coder server --tunnel
```bash
coder server --dev
```
To run a **production deployment** with PostgreSQL:
```bash
CODER_PG_CONNECTION_URL="postgres://<username>@<host>/<database>?password=<password>" \
coder server
```
# Requires a PostgreSQL instance and external access URL
coder server --postgres-url <url> --access-url <url>
```
## Next steps

View File

@ -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
}

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.
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
@ -56,6 +59,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
@ -130,6 +134,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

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/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=
@ -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=

View File

@ -98,12 +98,8 @@ PATH="$STANDALONE_INSTALL_PREFIX/bin:\$PATH"
EOF
fi
cath <<EOF
Run Coder (temporary):
$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
Run Coder:
$STANDALONE_BINARY_NAME server
EOF
}
@ -115,15 +111,12 @@ $1 package has been installed.
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
# Have systemd start Coder now and restart on boot
# Use systemd to start Coder now and on reboot
sudo systemctl enable --now coder
Or, run a temporary deployment (all data is in-memory
and destroyed on exit):
coder server --dev
# View the logs to ensure a successful start
journalctl -u coder.service -b
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()
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() {

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
"$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

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> => {
// 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

View File

@ -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,

View File

@ -229,6 +229,13 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
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 (
userId: TypesGen.User["id"],
updatePassword: TypesGen.UpdateUserPasswordRequest,