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

feat: Add "coder" CLI#221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kylecarbs merged 8 commits intomainfromcli
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions.vscode/settings.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,16 +31,22 @@
"drpcconn",
"drpcmux",
"drpcserver",
"fatih",
"goleak",
"hashicorp",
"httpmw",
"isatty",
"Jobf",
"kirsle",
"manifoldco",
"mattn",
"moby",
"nhooyr",
"nolint",
"nosec",
"oneof",
"parameterscopeid",
"promptui",
"protobuf",
"provisionerd",
"provisionersdk",
Expand Down
7 changes: 6 additions & 1 deletionMakefile
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
bin/coder:
mkdir -p bin
go build -o bin/coder cmd/coder/main.go
.PHONY: bin/coder

bin/coderd:
mkdir -p bin
go build -o bin/coderd cmd/coderd/main.go
.PHONY: bin/coderd

build: site/out bin/coderd
build: site/out bin/coder bin/coderd
.PHONY: build

# Runs migrations to output a dump of the database.
Expand Down
38 changes: 38 additions & 0 deletionscli/clitest/clitest.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
package clitest

import (
"bufio"
"io"
"testing"

"github.com/spf13/cobra"

"github.com/coder/coder/cli"
"github.com/coder/coder/cli/config"
)

funcNew(t*testing.T,args...string) (*cobra.Command, config.Root) {
cmd:=cli.Root()
dir:=t.TempDir()
root:=config.Root(dir)
cmd.SetArgs(append([]string{"--global-config",dir},args...))
returncmd,root
}

funcStdoutLogs(t*testing.T) io.Writer {
reader,writer:=io.Pipe()
scanner:=bufio.NewScanner(reader)
t.Cleanup(func() {
_=reader.Close()
_=writer.Close()
})
gofunc() {
forscanner.Scan() {
ifscanner.Err()!=nil {
return
}
t.Log(scanner.Text())
}
}()
returnwriter
}
71 changes: 71 additions & 0 deletionscli/config/file.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
package config

import (
"io/ioutil"
"os"
"path/filepath"
)

// Root represents the configuration directory.
type Root string

func (r Root) Session() File {
return File(filepath.Join(string(r), "session"))
}

func (r Root) URL() File {
return File(filepath.Join(string(r), "url"))
}

func (r Root) Organization() File {
return File(filepath.Join(string(r), "organization"))
}

// File provides convenience methods for interacting with *os.File.
type File string

// Delete deletes the file.
func (f File) Delete() error {
return os.Remove(string(f))
}

// Write writes the string to the file.
func (f File) Write(s string) error {
return write(string(f), 0600, []byte(s))
}

// Read reads the file to a string.
func (f File) Read() (string, error) {
byt, err := read(string(f))
return string(byt), err
}

// open opens a file in the configuration directory,
// creating all intermediate directories.
func open(path string, flag int, mode os.FileMode) (*os.File, error) {
err := os.MkdirAll(filepath.Dir(path), 0750)
if err != nil {
return nil, err
}

return os.OpenFile(path, flag, mode)
}

func write(path string, mode os.FileMode, dat []byte) error {
fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode)
if err != nil {
return err
}
defer fi.Close()
_, err = fi.Write(dat)
return err
}

func read(path string) ([]byte, error) {
fi, err := open(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer fi.Close()
return ioutil.ReadAll(fi)
}
38 changes: 38 additions & 0 deletionscli/config/file_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
package config_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/config"
)

func TestFile(t *testing.T) {
t.Parallel()

t.Run("Write", func(t *testing.T) {
t.Parallel()
err := config.Root(t.TempDir()).Session().Write("test")
require.NoError(t, err)
})

t.Run("Read", func(t *testing.T) {
t.Parallel()
root := config.Root(t.TempDir())
err := root.Session().Write("test")
require.NoError(t, err)
data, err := root.Session().Read()
require.NoError(t, err)
require.Equal(t, "test", data)
})

t.Run("Delete", func(t *testing.T) {
t.Parallel()
root := config.Root(t.TempDir())
err := root.Session().Write("test")
require.NoError(t, err)
err = root.Session().Delete()
require.NoError(t, err)
})
}
135 changes: 135 additions & 0 deletionscli/login.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
package cli

import (
"fmt"
"net/url"
"os/user"
"strings"

"github.com/fatih/color"
"github.com/go-playground/validator/v10"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd"
"github.com/coder/coder/codersdk"
)

func login() *cobra.Command {
return &cobra.Command{
Use: "login <url>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should have tests in place that run the CLI on Windows / Linux / Mac - maybe that would be a good-first-issue for getting someone on-boarded in the repo?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I actually just made some! I wasn't sure how to scaffold it, but I think it's OK for now.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I guess having support for a fake db should make these tests easier now!

kylecarbs reacted with heart emoji
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rawURL := args[0]
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
scheme := "https"
if strings.HasPrefix(rawURL, "localhost") {
scheme = "http"
}
rawURL = fmt.Sprintf("%s://%s", scheme, rawURL)
}
serverURL, err := url.Parse(rawURL)
if err != nil {
return xerrors.Errorf("parse raw url %q: %w", rawURL, err)
}
// Default to HTTPs. Enables simple URLs like: master.cdr.dev
if serverURL.Scheme == "" {
serverURL.Scheme = "https"
}

client := codersdk.New(serverURL)
hasInitialUser, err := client.HasInitialUser(cmd.Context())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That's cool that it guides you through set-up the first time you login, if there isn't a user available yet.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Agreed! I wanted the full flow to be available. I added tests now too!

if err != nil {
return xerrors.Errorf("has initial user: %w", err)
}
if !hasInitialUser {
if !isTTY(cmd.InOrStdin()) {
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">"))

_, err := runPrompt(cmd, &promptui.Prompt{
Label: "Would you like to create the first user?",
IsConfirm: true,
Default: "y",
})
if err != nil {
return xerrors.Errorf("create user prompt: %w", err)
}
currentUser, err := user.Current()
if err != nil {
return xerrors.Errorf("get current user: %w", err)
}
username, err := runPrompt(cmd, &promptui.Prompt{
Label: "What username would you like?",
Default: currentUser.Username,
})
if err != nil {
return xerrors.Errorf("pick username prompt: %w", err)
}

organization, err := runPrompt(cmd, &promptui.Prompt{
Label: "What is the name of your organization?",
Default: "acme-corp",
})
if err != nil {
return xerrors.Errorf("pick organization prompt: %w", err)
}

email, err := runPrompt(cmd, &promptui.Prompt{
Label: "What's your email?",
Validate: func(s string) error {
err := validator.New().Var(s, "email")
if err != nil {
return xerrors.New("That's not a valid email address!")
}
return err
},
})
if err != nil {
return xerrors.Errorf("specify email prompt: %w", err)
}

password, err := runPrompt(cmd, &promptui.Prompt{
Label: "Enter a password:",
Mask: '*',
})
if err != nil {
return xerrors.Errorf("specify password prompt: %w", err)
}

_, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{
Email: email,
Username: username,
Password: password,
Organization: organization,
})
if err != nil {
return xerrors.Errorf("create initial user: %w", err)
}
resp, err := client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{
Email: email,
Password: password,
})
if err != nil {
return xerrors.Errorf("login with password: %w", err)
}
config := createConfig(cmd)
err = config.Session().Write(resp.SessionToken)
if err != nil {
return xerrors.Errorf("write session token: %w", err)
}
err = config.URL().Write(serverURL.String())
if err != nil {
return xerrors.Errorf("write server url: %w", err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username))
return nil
}

return nil
},
}
}
56 changes: 56 additions & 0 deletionscli/login_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
//go:build !windows

package cli_test

import (
"testing"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/stretchr/testify/require"

"github.com/Netflix/go-expect"
)

func TestLogin(t *testing.T) {
t.Parallel()
t.Run("InitialUserNoTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
root, _ := clitest.New(t, "login", client.URL.String())
err := root.Execute()
require.Error(t, err)
})

t.Run("InitialUserTTY", func(t *testing.T) {
t.Parallel()
console, err := expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t)))
require.NoError(t, err)
client := coderdtest.New(t)
root, _ := clitest.New(t, "login", client.URL.String())
root.SetIn(console.Tty())
root.SetOut(console.Tty())
go func() {
err := root.Execute()
require.NoError(t, err)
}()

matches := []string{
"first user?", "y",
"username", "testuser",
"organization", "testorg",
"email", "user@coder.com",
"password", "password",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
_, err = console.ExpectString(match)
require.NoError(t, err)
_, err = console.SendLine(value)
require.NoError(t, err)
}
_, err = console.ExpectString("Welcome to Coder")
require.NoError(t, err)
})
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp