Skip to content

Commit d6a1eb8

Browse files
committed
feat: Add "coder" CLI
1 parent 7364933 commit d6a1eb8

21 files changed

+798
-5
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@
3131
"drpcconn",
3232
"drpcmux",
3333
"drpcserver",
34+
"fatih",
3435
"goleak",
3536
"hashicorp",
3637
"httpmw",
3738
"Jobf",
39+
"manifoldco",
3840
"moby",
3941
"nhooyr",
4042
"nolint",
4143
"nosec",
4244
"oneof",
4345
"parameterscopeid",
46+
"promptui",
4447
"protobuf",
4548
"provisionerd",
4649
"provisionersdk",

cli/config/file.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package config
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// Root represents the configuration directory.
10+
type Root string
11+
12+
func (r Root) Session() File {
13+
return File(filepath.Join(string(r), "session"))
14+
}
15+
16+
func (r Root) URL() File {
17+
return File(filepath.Join(string(r), "url"))
18+
}
19+
20+
func (r Root) Organization() File {
21+
return File(filepath.Join(string(r), "organization"))
22+
}
23+
24+
// File provides convenience methods for interacting with *os.File.
25+
type File string
26+
27+
// Delete deletes the file.
28+
func (f File) Delete() error {
29+
return os.Remove(string(f))
30+
}
31+
32+
// Write writes the string to the file.
33+
func (f File) Write(s string) error {
34+
return write(string(f), 0600, []byte(s))
35+
}
36+
37+
// Read reads the file to a string.
38+
func (f File) Read() (string, error) {
39+
byt, err := read(string(f))
40+
return string(byt), err
41+
}
42+
43+
// open opens a file in the configuration directory,
44+
// creating all intermediate directories.
45+
func open(path string, flag int, mode os.FileMode) (*os.File, error) {
46+
err := os.MkdirAll(filepath.Dir(path), 0750)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return os.OpenFile(path, flag, mode)
52+
}
53+
54+
func write(path string, mode os.FileMode, dat []byte) error {
55+
fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode)
56+
if err != nil {
57+
return err
58+
}
59+
defer fi.Close()
60+
_, err = fi.Write(dat)
61+
return err
62+
}
63+
64+
func read(path string) ([]byte, error) {
65+
fi, err := open(path, os.O_RDONLY, 0)
66+
if err != nil {
67+
return nil, err
68+
}
69+
defer fi.Close()
70+
return ioutil.ReadAll(fi)
71+
}

cli/config/file_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package config_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/config"
9+
)
10+
11+
func TestFile(t *testing.T) {
12+
t.Parallel()
13+
14+
t.Run("Write", func(t *testing.T) {
15+
t.Parallel()
16+
err := config.Root(t.TempDir()).Session().Write("test")
17+
require.NoError(t, err)
18+
})
19+
20+
t.Run("Read", func(t *testing.T) {
21+
t.Parallel()
22+
root := config.Root(t.TempDir())
23+
err := root.Session().Write("test")
24+
require.NoError(t, err)
25+
data, err := root.Session().Read()
26+
require.NoError(t, err)
27+
require.Equal(t, "test", string(data))
28+
})
29+
30+
t.Run("Delete", func(t *testing.T) {
31+
t.Parallel()
32+
root := config.Root(t.TempDir())
33+
err := root.Session().Write("test")
34+
require.NoError(t, err)
35+
err = root.Session().Delete()
36+
require.NoError(t, err)
37+
})
38+
}

cli/login.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/coder/coder/coderd"
10+
"github.com/coder/coder/codersdk"
11+
"github.com/fatih/color"
12+
"github.com/go-playground/validator/v10"
13+
"github.com/manifoldco/promptui"
14+
"github.com/spf13/cobra"
15+
"golang.org/x/xerrors"
16+
)
17+
18+
func login() *cobra.Command {
19+
return &cobra.Command{
20+
Use: "login <url>",
21+
Args: cobra.ExactArgs(1),
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
rawURL := args[0]
24+
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
25+
scheme := "https"
26+
if strings.HasPrefix(rawURL, "localhost") {
27+
scheme = "http"
28+
}
29+
rawURL = fmt.Sprintf("%s://%s", scheme, rawURL)
30+
}
31+
serverURL, err := url.Parse(rawURL)
32+
if err != nil {
33+
return err
34+
}
35+
// Default to HTTPs. Enables simple URLs like: master.cdr.dev
36+
if serverURL.Scheme == "" {
37+
serverURL.Scheme = "https"
38+
}
39+
40+
client := codersdk.New(serverURL)
41+
hasInitialUser, err := client.HasInitialUser(cmd.Context())
42+
if err != nil {
43+
return err
44+
}
45+
if !hasInitialUser {
46+
if !isTTY(cmd.InOrStdin()) {
47+
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
48+
}
49+
fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been setup!\n", color.HiBlackString(">"))
50+
51+
_, err := runPrompt(cmd, &promptui.Prompt{
52+
Label: "Would you like to create the first user?",
53+
IsConfirm: true,
54+
Default: "y",
55+
})
56+
if err != nil {
57+
return err
58+
}
59+
60+
username, err := runPrompt(cmd, &promptui.Prompt{
61+
Label: "What username would you like?",
62+
Default: "kyle",
63+
})
64+
if err != nil {
65+
return err
66+
}
67+
68+
organization, err := runPrompt(cmd, &promptui.Prompt{
69+
Label: "What is the name of your organization?",
70+
Default: "acme-corp",
71+
})
72+
if err != nil {
73+
return err
74+
}
75+
76+
email, err := runPrompt(cmd, &promptui.Prompt{
77+
Label: "What's your email?",
78+
Validate: func(s string) error {
79+
err := validator.New().Var(s, "email")
80+
if err != nil {
81+
return errors.New("That's not a valid email address!")
82+
}
83+
return err
84+
},
85+
})
86+
if err != nil {
87+
return err
88+
}
89+
90+
password, err := runPrompt(cmd, &promptui.Prompt{
91+
Label: "Enter a password:",
92+
Mask: '*',
93+
})
94+
if err != nil {
95+
return err
96+
}
97+
98+
_, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{
99+
Email: email,
100+
Username: username,
101+
Password: password,
102+
Organization: organization,
103+
})
104+
if err != nil {
105+
return xerrors.Errorf("create initial user: %w", err)
106+
}
107+
resp, err := client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{
108+
Email: email,
109+
Password: password,
110+
})
111+
if err != nil {
112+
return xerrors.Errorf("login with password: %w", err)
113+
}
114+
config := createConfig(cmd)
115+
err = config.Session().Write(resp.SessionToken)
116+
if err != nil {
117+
return xerrors.Errorf("write session token: %w", err)
118+
}
119+
err = config.URL().Write(serverURL.String())
120+
if err != nil {
121+
return xerrors.Errorf("write server url: %w", err)
122+
}
123+
124+
fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're logged in.\n", color.HiBlackString(">"), color.HiCyanString(username))
125+
return nil
126+
}
127+
128+
return nil
129+
},
130+
}
131+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy