Skip to content

Commit

Permalink
feat: Add "coder projects create" command (#246)
Browse files Browse the repository at this point in the history
* Refactor parameter parsing to return nil values if none computed

* Refactor parameter to allow for hiding redisplay

* Refactor parameters to enable schema matching

* Refactor provisionerd to dynamically update parameter schemas

* Refactor job update for provisionerd

* Handle multiple states correctly when provisioning a project

* Add project import job resource table

* Basic creation flow works!

* Create project fully works!!!

* Only show job status if completed

* Add create workspace support

* Replace Netflix/go-expect with ActiveState

* Fix linting errors

* Use forked chzyer/readline

* Add create workspace CLI

* Add CLI test

* Move jobs to their own APIs

* Remove go-expect

* Fix requested changes

* Skip workspacecreate test on windows
  • Loading branch information
kylecarbs committed Feb 12, 2022
1 parent df13fef commit 154b9bc
Show file tree
Hide file tree
Showing 65 changed files with 3,192 additions and 2,218 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"nhooyr",
"nolint",
"nosec",
"ntqry",
"oneof",
"parameterscopeid",
"promptui",
Expand Down
82 changes: 79 additions & 3 deletions cli/clitest/clitest.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
package clitest

import (
"archive/tar"
"bufio"
"bytes"
"errors"
"io"
"os"
"path/filepath"
"regexp"
"testing"

"github.com/Netflix/go-expect"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
)

var (
// Used to ensure terminal output doesn't have anything crazy!
// See: https://stackoverflow.com/a/29497680
stripAnsi = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
)

// New creates a CLI instance with a configuration pointed to a
// temporary testing directory.
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
cmd := cli.Root()
dir := t.TempDir()
Expand All @@ -19,7 +37,27 @@ func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
return cmd, root
}

func StdoutLogs(t *testing.T) io.Writer {
// SetupConfig applies the URL and SessionToken of the client to the config.
func SetupConfig(t *testing.T, client *codersdk.Client, root config.Root) {
err := root.Session().Write(client.SessionToken)
require.NoError(t, err)
err = root.URL().Write(client.URL.String())
require.NoError(t, err)
}

// CreateProjectVersionSource writes the echo provisioner responses into a
// new temporary testing directory.
func CreateProjectVersionSource(t *testing.T, responses *echo.Responses) string {
directory := t.TempDir()
data, err := echo.Tar(responses)
require.NoError(t, err)
extractTar(t, data, directory)
return directory
}

// NewConsole creates a new TTY bound to the command provided.
// All ANSI escape codes are stripped to provide clean output.
func NewConsole(t *testing.T, cmd *cobra.Command) *expect.Console {
reader, writer := io.Pipe()
scanner := bufio.NewScanner(reader)
t.Cleanup(func() {
Expand All @@ -31,8 +69,46 @@ func StdoutLogs(t *testing.T) io.Writer {
if scanner.Err() != nil {
return
}
t.Log(scanner.Text())
t.Log(stripAnsi.ReplaceAllString(scanner.Text(), ""))
}
}()
return writer

console, err := expect.NewConsole(expect.WithStdout(writer))
require.NoError(t, err)
cmd.SetIn(console.Tty())
cmd.SetOut(console.Tty())
return console
}

func extractTar(t *testing.T, data []byte, directory string) {
reader := tar.NewReader(bytes.NewBuffer(data))
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
// #nosec
path := filepath.Join(directory, header.Name)
mode := header.FileInfo().Mode()
if mode == 0 {
mode = 0600
}
switch header.Typeflag {
case tar.TypeDir:
err = os.MkdirAll(path, mode)
require.NoError(t, err)
case tar.TypeReg:
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode)
require.NoError(t, err)
// Max file size of 10MB.
_, err = io.CopyN(file, reader, (1<<20)*10)
if errors.Is(err, io.EOF) {
err = nil
}
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
}
}
}
31 changes: 31 additions & 0 deletions cli/clitest/clitest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build !windows

package clitest_test

import (
"testing"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

func TestCli(t *testing.T) {
t.Parallel()
clitest.CreateProjectVersionSource(t, nil)
client := coderdtest.New(t)
cmd, config := clitest.New(t)
clitest.SetupConfig(t, client, config)
console := clitest.NewConsole(t, cmd)
go func() {
err := cmd.Execute()
require.NoError(t, err)
}()
_, err := console.ExpectString("coder")
require.NoError(t, err)
}
10 changes: 5 additions & 5 deletions cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func login() *cobra.Command {
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">"))

_, err := runPrompt(cmd, &promptui.Prompt{
_, err := prompt(cmd, &promptui.Prompt{
Label: "Would you like to create the first user?",
IsConfirm: true,
Default: "y",
Expand All @@ -61,23 +61,23 @@ func login() *cobra.Command {
if err != nil {
return xerrors.Errorf("get current user: %w", err)
}
username, err := runPrompt(cmd, &promptui.Prompt{
username, err := prompt(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{
organization, err := prompt(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{
email, err := prompt(cmd, &promptui.Prompt{
Label: "What's your email?",
Validate: func(s string) error {
err := validator.New().Var(s, "email")
Expand All @@ -91,7 +91,7 @@ func login() *cobra.Command {
return xerrors.Errorf("specify email prompt: %w", err)
}

password, err := runPrompt(cmd, &promptui.Prompt{
password, err := prompt(cmd, &promptui.Prompt{
Label: "Enter a password:",
Mask: '*',
})
Expand Down
11 changes: 3 additions & 8 deletions cli/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
"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) {
Expand All @@ -24,12 +22,9 @@ func TestLogin(t *testing.T) {

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())
console := clitest.NewConsole(t, root)
go func() {
err := root.Execute()
require.NoError(t, err)
Expand All @@ -45,12 +40,12 @@ func TestLogin(t *testing.T) {
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
_, err = console.ExpectString(match)
_, err := console.ExpectString(match)
require.NoError(t, err)
_, err = console.SendLine(value)
require.NoError(t, err)
}
_, err = console.ExpectString("Welcome to Coder")
_, err := console.ExpectString("Welcome to Coder")
require.NoError(t, err)
})
}

0 comments on commit 154b9bc

Please sign in to comment.