Skip to content

Commit fd66daf

Browse files
committed
feat: Add built-in PostgreSQL for simple production setup
Fixes #2321.
1 parent 0a949aa commit fd66daf

File tree

19 files changed

+405
-467
lines changed

19 files changed

+405
-467
lines changed

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 PostgreSQL and an external access URL on *.try.coder.app
54+
coder server --postgres-builtin --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](./docs/quickstart.md) 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: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ func init() {
3737
}
3838

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

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-
}
76+
if username == "" {
77+
if !isTTY(cmd) {
78+
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
79+
}
80+
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
81+
Text: "Would you like to create the first user?",
82+
Default: "yes",
83+
IsConfirm: true,
84+
})
85+
if errors.Is(err, cliui.Canceled) {
86+
return nil
87+
}
88+
if err != nil {
10789
return err
108-
},
109-
})
110-
if err != nil {
111-
return xerrors.Errorf("specify email prompt: %w", err)
90+
}
91+
currentUser, err := user.Current()
92+
if err != nil {
93+
return xerrors.Errorf("get current user: %w", err)
94+
}
95+
username, err = cliui.Prompt(cmd, cliui.PromptOptions{
96+
Text: "What " + cliui.Styles.Field.Render("username") + " would you like?",
97+
Default: currentUser.Username,
98+
})
99+
if errors.Is(err, cliui.Canceled) {
100+
return nil
101+
}
102+
if err != nil {
103+
return xerrors.Errorf("pick username prompt: %w", err)
104+
}
112105
}
113106

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

136147
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
@@ -219,6 +230,10 @@ func login() *cobra.Command {
219230
return nil
220231
},
221232
}
233+
cmd.Flags().StringVarP(&email, "email", "e", "", "Specifies an email address to authenticate with.")
234+
cmd.Flags().StringVarP(&username, "username", "u", "", "Specifies a username to authenticate with.")
235+
cmd.Flags().StringVarP(&password, "password", "p", "", "Specifies a password to authenticate with.")
236+
return cmd
222237
}
223238

224239
// 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)