Skip to content

feat: Add "coder projects create" command #246

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add create workspace CLI
  • Loading branch information
kylecarbs committed Feb 11, 2022
commit b5a774a78c6b577ee53d9e03622a5d3f08a2ccf1
31 changes: 13 additions & 18 deletions cli/clitest/clitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ import (
"archive/tar"
"bufio"
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"regexp"
"testing"

"github.com/Netflix/go-expect"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/coder/coder/cli"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
Copy link
Contributor

Choose a reason for hiding this comment

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

Neat 🎉

)
Expand All @@ -39,20 +37,12 @@ func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
return cmd, root
}

// CreateInitialUser creates the initial user and write's the session
// token to the config root provided.
func CreateInitialUser(t *testing.T, client *codersdk.Client, root config.Root) coderd.CreateInitialUserRequest {
user := coderdtest.CreateInitialUser(t, client)
resp, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: user.Password,
})
require.NoError(t, err)
err = root.Session().Write(resp.SessionToken)
// 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)
return user
}

// CreateProjectVersionSource writes the echo provisioner responses into a
Expand All @@ -66,9 +56,9 @@ func CreateProjectVersionSource(t *testing.T, responses *echo.Responses) string
return directory
}

// StdoutLogs provides a writer to t.Log that strips
// all ANSI escape codes.
func StdoutLogs(t *testing.T) io.Writer {
// 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 @@ -83,7 +73,12 @@ func StdoutLogs(t *testing.T) io.Writer {
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(data []byte, directory string) error {
Expand Down
14 changes: 4 additions & 10 deletions cli/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package cli_test
import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"

"github.com/ActiveState/termtest/expect"
"github.com/stretchr/testify/require"
)

func TestLogin(t *testing.T) {
Expand All @@ -23,12 +20,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 @@ -44,12 +38,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)
})
}
23 changes: 9 additions & 14 deletions cli/projectcreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cli_test
import (
"testing"

"github.com/ActiveState/termtest/expect"
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
Expand All @@ -17,18 +16,16 @@ func TestProjectCreate(t *testing.T) {
t.Parallel()
t.Run("NoParameters", func(t *testing.T) {
t.Parallel()
console, err := expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t)))
require.NoError(t, err)
client := coderdtest.New(t)
coderdtest.CreateInitialUser(t, client)
source := clitest.CreateProjectVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
})
cmd, root := clitest.New(t, "projects", "create", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
_ = clitest.CreateInitialUser(t, client, root)
clitest.SetupConfig(t, client, root)
_ = coderdtest.NewProvisionerDaemon(t, client)
cmd.SetIn(console.Tty())
cmd.SetOut(console.Tty())
console := clitest.NewConsole(t, cmd)
closeChan := make(chan struct{})
go func() {
err := cmd.Execute()
Expand All @@ -45,7 +42,7 @@ func TestProjectCreate(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)
Expand All @@ -55,9 +52,8 @@ func TestProjectCreate(t *testing.T) {

t.Run("Parameter", func(t *testing.T) {
t.Parallel()
console, err := expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t)))
require.NoError(t, err)
client := coderdtest.New(t)
coderdtest.CreateInitialUser(t, client)
source := clitest.CreateProjectVersionSource(t, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Expand All @@ -74,10 +70,9 @@ func TestProjectCreate(t *testing.T) {
Provision: echo.ProvisionComplete,
})
cmd, root := clitest.New(t, "projects", "create", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
_ = clitest.CreateInitialUser(t, client, root)
_ = coderdtest.NewProvisionerDaemon(t, client)
cmd.SetIn(console.Tty())
cmd.SetOut(console.Tty())
clitest.SetupConfig(t, client, root)
coderdtest.NewProvisionerDaemon(t, client)
console := clitest.NewConsole(t, cmd)
closeChan := make(chan struct{})
go func() {
err := cmd.Execute()
Expand All @@ -95,7 +90,7 @@ func TestProjectCreate(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)
Expand Down
3 changes: 2 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
return nil, err
}
client := codersdk.New(serverURL)
return client, client.SetSessionToken(token)
client.SessionToken = token
return client, nil
}

// currentOrganization returns the currently active organization for the authenticated user.
Expand Down
3 changes: 3 additions & 0 deletions cli/workspacecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func workspaceCreate() *cobra.Command {
name, err = prompt(cmd, &promptui.Prompt{
Label: "What's your workspace's name?",
Validate: func(s string) error {
if s == "" {
return xerrors.Errorf("You must provide a name!")
}
workspace, _ := client.Workspace(cmd.Context(), "", s)
if workspace.ID.String() != uuid.Nil.String() {
return xerrors.New("A workspace already exists with that name!")
Expand Down
62 changes: 62 additions & 0 deletions cli/workspacecreate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cli_test

import (
"testing"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/stretchr/testify/require"
)

func TestWorkspaceCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportProvisionerJob(t, client, user.Organization, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
}},
},
},
}},
})
coderdtest.AwaitProvisionerJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
cmd, root := clitest.New(t, "workspaces", "create", project.Name)
clitest.SetupConfig(t, client, root)

console := clitest.NewConsole(t, cmd)
closeChan := make(chan struct{})
go func() {
err := cmd.Execute()
require.NoError(t, err)
close(closeChan)
}()

matches := []string{
"name?", "workspace-name",
"Create workspace", "y",
}
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("Create")
require.NoError(t, err)
<-closeChan
})
}
3 changes: 1 addition & 2 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateIniti
Password: req.Password,
})
require.NoError(t, err)
err = client.SetSessionToken(login.SessionToken)
require.NoError(t, err)
client.SessionToken = login.SessionToken
return req
}

Expand Down
11 changes: 6 additions & 5 deletions coderd/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,11 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
if err != nil {
return nil, failJob(fmt.Sprintf("get project: %s", err))
}
organization, err := server.Database.GetOrganizationByID(ctx, project.OrganizationID)
if err != nil {
return nil, failJob(fmt.Sprintf("get organization: %s", err))
}

// Compute parameters for the workspace to consume.
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
ProjectImportJobID: projectVersion.ImportJobID,
OrganizationID: organization.ID,
OrganizationID: job.OrganizationID,
ProjectID: uuid.NullUUID{
UUID: project.ID,
Valid: true,
Expand All @@ -226,6 +222,11 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
}
protoParameters = append(protoParameters, converted)
}
protoParameters = append(protoParameters, &sdkproto.ParameterValue{
DestinationScheme: sdkproto.ParameterDestination_PROVISIONER_VARIABLE,
Name: parameter.CoderWorkspaceTransition,
Value: string(workspaceHistory.Transition),
})

protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{
WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{
Expand Down
1 change: 1 addition & 0 deletions coderd/provisionerjobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func (api *api) postProvisionerImportJobByOrganization(rw http.ResponseWriter, r
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: file.Hash,
Type: database.ProvisionerJobTypeProjectVersionImport,
Input: []byte{'{', '}'},
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down
2 changes: 1 addition & 1 deletion coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestPostWorkspaceByUser(t *testing.T) {
Password: anotherUser.Password,
})
require.NoError(t, err)
err = client.SetSessionToken(token.SessionToken)
client.SessionToken = token.SessionToken
require.NoError(t, err)

_, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Expand Down
24 changes: 6 additions & 18 deletions codersdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"

Expand All @@ -28,27 +27,12 @@ func New(serverURL *url.URL) *Client {

// Client is an HTTP caller for methods to the Coder API.
type Client struct {
URL *url.URL
URL *url.URL
SessionToken string

httpClient *http.Client
}

// SetSessionToken applies the provided token to the current client.
func (c *Client) SetSessionToken(token string) error {
if c.httpClient.Jar == nil {
var err error
c.httpClient.Jar, err = cookiejar.New(nil)
if err != nil {
return err
}
}
c.httpClient.Jar.SetCookies(c.URL, []*http.Cookie{{
Name: httpmw.AuthCookie,
Value: token,
}})
return nil
}

// request performs an HTTP request with the body provided.
// The caller is responsible for closing the response body.
func (c *Client) request(ctx context.Context, method, path string, body interface{}, opts ...func(r *http.Request)) (*http.Response, error) {
Expand Down Expand Up @@ -76,6 +60,10 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac
if err != nil {
return nil, xerrors.Errorf("create request: %w", err)
}
req.AddCookie(&http.Cookie{
Name: httpmw.AuthCookie,
Value: c.SessionToken,
})
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
Expand Down