Thanks to visit codestin.com
Credit goes to github.com

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)