Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit07fe5ce

Browse files
authored
feat: Add "coder" CLI (#221)
* feat: Add "coder" CLI* Add CLI test for login* Add "bin/coder" target to Makefile* Update promptui to fix race* Fix error scope* Don't run CLI tests on Windows* Fix requested changes
1 parent277318b commit07fe5ce

24 files changed

+921
-7
lines changed

‎.vscode/settings.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,22 @@
3131
"drpcconn",
3232
"drpcmux",
3333
"drpcserver",
34+
"fatih",
3435
"goleak",
3536
"hashicorp",
3637
"httpmw",
38+
"isatty",
3739
"Jobf",
40+
"kirsle",
41+
"manifoldco",
42+
"mattn",
3843
"moby",
3944
"nhooyr",
4045
"nolint",
4146
"nosec",
4247
"oneof",
4348
"parameterscopeid",
49+
"promptui",
4450
"protobuf",
4551
"provisionerd",
4652
"provisionersdk",

‎Makefile‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
bin/coder:
2+
mkdir -p bin
3+
go build -o bin/coder cmd/coder/main.go
4+
.PHONY: bin/coder
5+
16
bin/coderd:
27
mkdir -p bin
38
go build -o bin/coderd cmd/coderd/main.go
49
.PHONY: bin/coderd
510

6-
build: site/out bin/coderd
11+
build: site/out bin/coder bin/coderd
712
.PHONY: build
813

914
# Runs migrations to output a dump of the database.

‎cli/clitest/clitest.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package clitest
2+
3+
import (
4+
"bufio"
5+
"io"
6+
"testing"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/coder/coder/cli"
11+
"github.com/coder/coder/cli/config"
12+
)
13+
14+
funcNew(t*testing.T,args...string) (*cobra.Command, config.Root) {
15+
cmd:=cli.Root()
16+
dir:=t.TempDir()
17+
root:=config.Root(dir)
18+
cmd.SetArgs(append([]string{"--global-config",dir},args...))
19+
returncmd,root
20+
}
21+
22+
funcStdoutLogs(t*testing.T) io.Writer {
23+
reader,writer:=io.Pipe()
24+
scanner:=bufio.NewScanner(reader)
25+
t.Cleanup(func() {
26+
_=reader.Close()
27+
_=writer.Close()
28+
})
29+
gofunc() {
30+
forscanner.Scan() {
31+
ifscanner.Err()!=nil {
32+
return
33+
}
34+
t.Log(scanner.Text())
35+
}
36+
}()
37+
returnwriter
38+
}

‎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+
typeRootstring
11+
12+
func (rRoot)Session()File {
13+
returnFile(filepath.Join(string(r),"session"))
14+
}
15+
16+
func (rRoot)URL()File {
17+
returnFile(filepath.Join(string(r),"url"))
18+
}
19+
20+
func (rRoot)Organization()File {
21+
returnFile(filepath.Join(string(r),"organization"))
22+
}
23+
24+
// File provides convenience methods for interacting with *os.File.
25+
typeFilestring
26+
27+
// Delete deletes the file.
28+
func (fFile)Delete()error {
29+
returnos.Remove(string(f))
30+
}
31+
32+
// Write writes the string to the file.
33+
func (fFile)Write(sstring)error {
34+
returnwrite(string(f),0600, []byte(s))
35+
}
36+
37+
// Read reads the file to a string.
38+
func (fFile)Read() (string,error) {
39+
byt,err:=read(string(f))
40+
returnstring(byt),err
41+
}
42+
43+
// open opens a file in the configuration directory,
44+
// creating all intermediate directories.
45+
funcopen(pathstring,flagint,mode os.FileMode) (*os.File,error) {
46+
err:=os.MkdirAll(filepath.Dir(path),0750)
47+
iferr!=nil {
48+
returnnil,err
49+
}
50+
51+
returnos.OpenFile(path,flag,mode)
52+
}
53+
54+
funcwrite(pathstring,mode os.FileMode,dat []byte)error {
55+
fi,err:=open(path,os.O_WRONLY|os.O_TRUNC|os.O_CREATE,mode)
56+
iferr!=nil {
57+
returnerr
58+
}
59+
deferfi.Close()
60+
_,err=fi.Write(dat)
61+
returnerr
62+
}
63+
64+
funcread(pathstring) ([]byte,error) {
65+
fi,err:=open(path,os.O_RDONLY,0)
66+
iferr!=nil {
67+
returnnil,err
68+
}
69+
deferfi.Close()
70+
returnioutil.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+
funcTestFile(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",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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"os/user"
7+
"strings"
8+
9+
"github.com/fatih/color"
10+
"github.com/go-playground/validator/v10"
11+
"github.com/manifoldco/promptui"
12+
"github.com/spf13/cobra"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/coder/coder/coderd"
16+
"github.com/coder/coder/codersdk"
17+
)
18+
19+
funclogin()*cobra.Command {
20+
return&cobra.Command{
21+
Use:"login <url>",
22+
Args:cobra.ExactArgs(1),
23+
RunE:func(cmd*cobra.Command,args []string)error {
24+
rawURL:=args[0]
25+
if!strings.HasPrefix(rawURL,"http://")&&!strings.HasPrefix(rawURL,"https://") {
26+
scheme:="https"
27+
ifstrings.HasPrefix(rawURL,"localhost") {
28+
scheme="http"
29+
}
30+
rawURL=fmt.Sprintf("%s://%s",scheme,rawURL)
31+
}
32+
serverURL,err:=url.Parse(rawURL)
33+
iferr!=nil {
34+
returnxerrors.Errorf("parse raw url %q: %w",rawURL,err)
35+
}
36+
// Default to HTTPs. Enables simple URLs like: master.cdr.dev
37+
ifserverURL.Scheme=="" {
38+
serverURL.Scheme="https"
39+
}
40+
41+
client:=codersdk.New(serverURL)
42+
hasInitialUser,err:=client.HasInitialUser(cmd.Context())
43+
iferr!=nil {
44+
returnxerrors.Errorf("has initial user: %w",err)
45+
}
46+
if!hasInitialUser {
47+
if!isTTY(cmd.InOrStdin()) {
48+
returnxerrors.New("the initial user cannot be created in non-interactive mode. use the API")
49+
}
50+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s Your Coder deployment hasn't been set up!\n",color.HiBlackString(">"))
51+
52+
_,err:=runPrompt(cmd,&promptui.Prompt{
53+
Label:"Would you like to create the first user?",
54+
IsConfirm:true,
55+
Default:"y",
56+
})
57+
iferr!=nil {
58+
returnxerrors.Errorf("create user prompt: %w",err)
59+
}
60+
currentUser,err:=user.Current()
61+
iferr!=nil {
62+
returnxerrors.Errorf("get current user: %w",err)
63+
}
64+
username,err:=runPrompt(cmd,&promptui.Prompt{
65+
Label:"What username would you like?",
66+
Default:currentUser.Username,
67+
})
68+
iferr!=nil {
69+
returnxerrors.Errorf("pick username prompt: %w",err)
70+
}
71+
72+
organization,err:=runPrompt(cmd,&promptui.Prompt{
73+
Label:"What is the name of your organization?",
74+
Default:"acme-corp",
75+
})
76+
iferr!=nil {
77+
returnxerrors.Errorf("pick organization prompt: %w",err)
78+
}
79+
80+
email,err:=runPrompt(cmd,&promptui.Prompt{
81+
Label:"What's your email?",
82+
Validate:func(sstring)error {
83+
err:=validator.New().Var(s,"email")
84+
iferr!=nil {
85+
returnxerrors.New("That's not a valid email address!")
86+
}
87+
returnerr
88+
},
89+
})
90+
iferr!=nil {
91+
returnxerrors.Errorf("specify email prompt: %w",err)
92+
}
93+
94+
password,err:=runPrompt(cmd,&promptui.Prompt{
95+
Label:"Enter a password:",
96+
Mask:'*',
97+
})
98+
iferr!=nil {
99+
returnxerrors.Errorf("specify password prompt: %w",err)
100+
}
101+
102+
_,err=client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{
103+
Email:email,
104+
Username:username,
105+
Password:password,
106+
Organization:organization,
107+
})
108+
iferr!=nil {
109+
returnxerrors.Errorf("create initial user: %w",err)
110+
}
111+
resp,err:=client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{
112+
Email:email,
113+
Password:password,
114+
})
115+
iferr!=nil {
116+
returnxerrors.Errorf("login with password: %w",err)
117+
}
118+
config:=createConfig(cmd)
119+
err=config.Session().Write(resp.SessionToken)
120+
iferr!=nil {
121+
returnxerrors.Errorf("write session token: %w",err)
122+
}
123+
err=config.URL().Write(serverURL.String())
124+
iferr!=nil {
125+
returnxerrors.Errorf("write server url: %w",err)
126+
}
127+
128+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s Welcome to Coder, %s! You're authenticated.\n",color.HiBlackString(">"),color.HiCyanString(username))
129+
returnnil
130+
}
131+
132+
returnnil
133+
},
134+
}
135+
}

‎cli/login_test.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//go:build !windows
2+
3+
package cli_test
4+
5+
import (
6+
"testing"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/Netflix/go-expect"
13+
)
14+
15+
funcTestLogin(t*testing.T) {
16+
t.Parallel()
17+
t.Run("InitialUserNoTTY",func(t*testing.T) {
18+
t.Parallel()
19+
client:=coderdtest.New(t)
20+
root,_:=clitest.New(t,"login",client.URL.String())
21+
err:=root.Execute()
22+
require.Error(t,err)
23+
})
24+
25+
t.Run("InitialUserTTY",func(t*testing.T) {
26+
t.Parallel()
27+
console,err:=expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t)))
28+
require.NoError(t,err)
29+
client:=coderdtest.New(t)
30+
root,_:=clitest.New(t,"login",client.URL.String())
31+
root.SetIn(console.Tty())
32+
root.SetOut(console.Tty())
33+
gofunc() {
34+
err:=root.Execute()
35+
require.NoError(t,err)
36+
}()
37+
38+
matches:= []string{
39+
"first user?","y",
40+
"username","testuser",
41+
"organization","testorg",
42+
"email","user@coder.com",
43+
"password","password",
44+
}
45+
fori:=0;i<len(matches);i+=2 {
46+
match:=matches[i]
47+
value:=matches[i+1]
48+
_,err=console.ExpectString(match)
49+
require.NoError(t,err)
50+
_,err=console.SendLine(value)
51+
require.NoError(t,err)
52+
}
53+
_,err=console.ExpectString("Welcome to Coder")
54+
require.NoError(t,err)
55+
})
56+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp