Skip to content

Commit ccd0616

Browse files
authored
feat: Add built-in PostgreSQL for simple production setup (coder#2345)
* feat: Add built-in PostgreSQL for simple production setup Fixes coder#2321. * Use fork of embedded-postgres for cache path
1 parent bb4ecd7 commit ccd0616

23 files changed

+408
-475
lines changed

.goreleaser.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ nfpms:
9393
type: "config|noreplace"
9494
- src: coder.service
9595
dst: /usr/lib/systemd/system/coder.service
96+
scripts:
97+
preinstall: preinstall.sh
9698

9799
# Image templates are empty on snapshots to avoid lengthy builds for development.
98100
dockers:

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ To install, run:
4747
curl -fsSL https://coder.com/install.sh | sh
4848
```
4949

50-
Once installed, you can run a temporary deployment in dev mode (all data is in-memory and destroyed on exit):
50+
Once installed, you can start a production deployment with a single command:
5151

5252
```sh
53-
coder server --dev
53+
# Automatically sets up an external access URL on *.try.coder.app
54+
coder server --tunnel
55+
56+
# Requires a PostgreSQL instance and external access URL
57+
coder server --postgres-url <url> --access-url <url>
5458
```
5559

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

cli/config/file.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ func (r Root) DotfilesURL() File {
2525
return File(filepath.Join(string(r), "dotfilesurl"))
2626
}
2727

28+
func (r Root) PostgresPath() string {
29+
return filepath.Join(string(r), "postgres")
30+
}
31+
32+
func (r Root) PostgresPassword() File {
33+
return File(filepath.Join(r.PostgresPath(), "password"))
34+
}
35+
36+
func (r Root) PostgresPort() File {
37+
return File(filepath.Join(r.PostgresPath(), "port"))
38+
}
39+
2840
// File provides convenience methods for interacting with *os.File.
2941
type File string
3042

cli/login.go

Lines changed: 76 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/spf13/cobra"
1818
"golang.org/x/xerrors"
1919

20+
"github.com/coder/coder/cli/cliflag"
2021
"github.com/coder/coder/cli/cliui"
2122
"github.com/coder/coder/codersdk"
2223
)
@@ -37,7 +38,12 @@ func init() {
3738
}
3839

3940
func login() *cobra.Command {
40-
return &cobra.Command{
41+
var (
42+
email string
43+
username string
44+
password string
45+
)
46+
cmd := &cobra.Command{
4147
Use: "login <url>",
4248
Short: "Authenticate with a Coder deployment",
4349
Args: cobra.ExactArgs(1),
@@ -66,71 +72,77 @@ func login() *cobra.Command {
6672
return xerrors.Errorf("has initial user: %w", err)
6773
}
6874
if !hasInitialUser {
69-
if !isTTY(cmd) {
70-
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
71-
}
7275
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n")
7376

74-
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
75-
Text: "Would you like to create the first user?",
76-
Default: "yes",
77-
IsConfirm: true,
78-
})
79-
if errors.Is(err, cliui.Canceled) {
80-
return nil
81-
}
82-
if err != nil {
83-
return err
84-
}
85-
currentUser, err := user.Current()
86-
if err != nil {
87-
return xerrors.Errorf("get current user: %w", err)
88-
}
89-
username, err := cliui.Prompt(cmd, cliui.PromptOptions{
90-
Text: "What " + cliui.Styles.Field.Render("username") + " would you like?",
91-
Default: currentUser.Username,
92-
})
93-
if errors.Is(err, cliui.Canceled) {
94-
return nil
95-
}
96-
if err != nil {
97-
return xerrors.Errorf("pick username prompt: %w", err)
98-
}
99-
100-
email, err := cliui.Prompt(cmd, cliui.PromptOptions{
101-
Text: "What's your " + cliui.Styles.Field.Render("email") + "?",
102-
Validate: func(s string) error {
103-
err := validator.New().Var(s, "email")
104-
if err != nil {
105-
return xerrors.New("That's not a valid email address!")
106-
}
77+
if username == "" {
78+
if !isTTY(cmd) {
79+
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
80+
}
81+
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
82+
Text: "Would you like to create the first user?",
83+
Default: "yes",
84+
IsConfirm: true,
85+
})
86+
if errors.Is(err, cliui.Canceled) {
87+
return nil
88+
}
89+
if err != nil {
10790
return err
108-
},
109-
})
110-
if err != nil {
111-
return xerrors.Errorf("specify email prompt: %w", err)
91+
}
92+
currentUser, err := user.Current()
93+
if err != nil {
94+
return xerrors.Errorf("get current user: %w", err)
95+
}
96+
username, err = cliui.Prompt(cmd, cliui.PromptOptions{
97+
Text: "What " + cliui.Styles.Field.Render("username") + " would you like?",
98+
Default: currentUser.Username,
99+
})
100+
if errors.Is(err, cliui.Canceled) {
101+
return nil
102+
}
103+
if err != nil {
104+
return xerrors.Errorf("pick username prompt: %w", err)
105+
}
112106
}
113107

114-
password, err := cliui.Prompt(cmd, cliui.PromptOptions{
115-
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
116-
Secret: true,
117-
Validate: cliui.ValidateNotEmpty,
118-
})
119-
if err != nil {
120-
return xerrors.Errorf("specify password prompt: %w", err)
108+
if email == "" {
109+
email, err = cliui.Prompt(cmd, cliui.PromptOptions{
110+
Text: "What's your " + cliui.Styles.Field.Render("email") + "?",
111+
Validate: func(s string) error {
112+
err := validator.New().Var(s, "email")
113+
if err != nil {
114+
return xerrors.New("That's not a valid email address!")
115+
}
116+
return err
117+
},
118+
})
119+
if err != nil {
120+
return xerrors.Errorf("specify email prompt: %w", err)
121+
}
121122
}
122-
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
123-
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
124-
Secret: true,
125-
Validate: func(s string) error {
126-
if s != password {
127-
return xerrors.Errorf("Passwords do not match")
128-
}
129-
return nil
130-
},
131-
})
132-
if err != nil {
133-
return xerrors.Errorf("confirm password prompt: %w", err)
123+
124+
if password == "" {
125+
password, err = cliui.Prompt(cmd, cliui.PromptOptions{
126+
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
127+
Secret: true,
128+
Validate: cliui.ValidateNotEmpty,
129+
})
130+
if err != nil {
131+
return xerrors.Errorf("specify password prompt: %w", err)
132+
}
133+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
134+
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
135+
Secret: true,
136+
Validate: func(s string) error {
137+
if s != password {
138+
return xerrors.Errorf("Passwords do not match")
139+
}
140+
return nil
141+
},
142+
})
143+
if err != nil {
144+
return xerrors.Errorf("confirm password prompt: %w", err)
145+
}
134146
}
135147

136148
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
@@ -219,6 +231,10 @@ func login() *cobra.Command {
219231
return nil
220232
},
221233
}
234+
cliflag.StringVarP(cmd.Flags(), &email, "email", "e", "CODER_EMAIL", "", "Specifies an email address to authenticate with.")
235+
cliflag.StringVarP(cmd.Flags(), &username, "username", "u", "CODER_USERNAME", "", "Specifies a username to authenticate with.")
236+
cliflag.StringVarP(cmd.Flags(), &password, "password", "p", "CODER_PASSWORD", "", "Specifies a password to authenticate with.")
237+
return cmd
222238
}
223239

224240
// isWSL determines if coder-cli is running within Windows Subsystem for Linux

cli/login_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ func TestLogin(t *testing.T) {
5656
<-doneChan
5757
})
5858

59+
t.Run("InitialUserFlags", func(t *testing.T) {
60+
t.Parallel()
61+
client := coderdtest.New(t, nil)
62+
// The --force-tty flag is required on Windows, because the `isatty` library does not
63+
// accurately detect Windows ptys when they are not attached to a process:
64+
// https://github.com/mattn/go-isatty/issues/59
65+
doneChan := make(chan struct{})
66+
root, _ := clitest.New(t, "login", client.URL.String(), "--username", "testuser", "--email", "user@coder.com", "--password", "password")
67+
pty := ptytest.New(t)
68+
root.SetIn(pty.Input())
69+
root.SetOut(pty.Output())
70+
go func() {
71+
defer close(doneChan)
72+
err := root.Execute()
73+
assert.NoError(t, err)
74+
}()
75+
pty.ExpectMatch("Welcome to Coder")
76+
<-doneChan
77+
})
78+
5979
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
6080
t.Parallel()
6181
ctx, cancel := context.WithCancel(context.Background())

cli/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func Root() *cobra.Command {
5757
SilenceUsage: true,
5858
Long: `Coder — A tool for provisioning self-hosted development environments.
5959
`,
60-
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.
61-
` + cliui.Styles.Code.Render("$ coder server --dev") + `
60+
Example: ` Start a Coder server.
61+
` + cliui.Styles.Code.Render("$ coder server") + `
6262
6363
Get started by creating a template from an example.
6464
` + cliui.Styles.Code.Render("$ coder templates init"),

0 commit comments

Comments
 (0)