Skip to content

Commit 8766a33

Browse files
committed
Add create workspace support
1 parent bff96b6 commit 8766a33

File tree

9 files changed

+366
-69
lines changed

9 files changed

+366
-69
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"nhooyr",
4545
"nolint",
4646
"nosec",
47+
"ntqry",
4748
"oneof",
4849
"parameterscopeid",
4950
"promptui",

cli/clitest/clitest.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
package clitest
22

33
import (
4+
"archive/tar"
45
"bufio"
6+
"bytes"
57
"context"
8+
"errors"
69
"io"
10+
"os"
11+
"path/filepath"
12+
"regexp"
713
"testing"
814

915
"github.com/spf13/cobra"
1016
"github.com/stretchr/testify/require"
17+
"golang.org/x/xerrors"
1118

1219
"github.com/coder/coder/cli"
1320
"github.com/coder/coder/cli/config"
1421
"github.com/coder/coder/coderd"
1522
"github.com/coder/coder/coderd/coderdtest"
1623
"github.com/coder/coder/codersdk"
24+
"github.com/coder/coder/provisioner/echo"
1725
)
1826

27+
var (
28+
// Used to ensure terminal output doesn't have anything crazy!
29+
stripAnsi = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
30+
)
31+
32+
// New creates a CLI instance with a configuration pointed to a
33+
// temporary testing directory.
1934
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
2035
cmd := cli.Root()
2136
dir := t.TempDir()
@@ -24,6 +39,8 @@ func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
2439
return cmd, root
2540
}
2641

42+
// CreateInitialUser creates the initial user and write's the session
43+
// token to the config root provided.
2744
func CreateInitialUser(t *testing.T, client *codersdk.Client, root config.Root) coderd.CreateInitialUserRequest {
2845
user := coderdtest.CreateInitialUser(t, client)
2946
resp, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
@@ -38,6 +55,19 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client, root config.Root)
3855
return user
3956
}
4057

58+
// CreateProjectVersionSource writes the echo provisioner responses into a
59+
// new temporary testing directory.
60+
func CreateProjectVersionSource(t *testing.T, responses *echo.Responses) string {
61+
directory := t.TempDir()
62+
data, err := echo.Tar(responses)
63+
require.NoError(t, err)
64+
err = extractTar(data, directory)
65+
require.NoError(t, err)
66+
return directory
67+
}
68+
69+
// StdoutLogs provides a writer to t.Log that strips
70+
// all ANSI escape codes.
4171
func StdoutLogs(t *testing.T) io.Writer {
4272
reader, writer := io.Pipe()
4373
scanner := bufio.NewScanner(reader)
@@ -50,8 +80,52 @@ func StdoutLogs(t *testing.T) io.Writer {
5080
if scanner.Err() != nil {
5181
return
5282
}
53-
t.Log(scanner.Text())
83+
t.Log(stripAnsi.ReplaceAllString(scanner.Text(), ""))
5484
}
5585
}()
5686
return writer
5787
}
88+
89+
func extractTar(data []byte, directory string) error {
90+
reader := tar.NewReader(bytes.NewBuffer(data))
91+
for {
92+
header, err := reader.Next()
93+
if errors.Is(err, io.EOF) {
94+
break
95+
}
96+
if err != nil {
97+
return xerrors.Errorf("read project source archive: %w", err)
98+
}
99+
path := filepath.Join(directory, header.Name)
100+
mode := header.FileInfo().Mode()
101+
if mode == 0 {
102+
mode = 0600
103+
}
104+
switch header.Typeflag {
105+
case tar.TypeDir:
106+
err = os.MkdirAll(path, mode)
107+
if err != nil {
108+
return xerrors.Errorf("mkdir: %w", err)
109+
}
110+
case tar.TypeReg:
111+
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode)
112+
if err != nil {
113+
return xerrors.Errorf("create file %q: %w", path, err)
114+
}
115+
// Max file size of 10MB.
116+
_, err = io.CopyN(file, reader, (1<<20)*10)
117+
if errors.Is(err, io.EOF) {
118+
err = nil
119+
}
120+
if err != nil {
121+
_ = file.Close()
122+
return err
123+
}
124+
err = file.Close()
125+
if err != nil {
126+
return err
127+
}
128+
}
129+
}
130+
return nil
131+
}

cli/login.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func login() *cobra.Command {
4949
}
5050
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">"))
5151

52-
_, err := runPrompt(cmd, &promptui.Prompt{
52+
_, err := prompt(cmd, &promptui.Prompt{
5353
Label: "Would you like to create the first user?",
5454
IsConfirm: true,
5555
Default: "y",
@@ -61,23 +61,23 @@ func login() *cobra.Command {
6161
if err != nil {
6262
return xerrors.Errorf("get current user: %w", err)
6363
}
64-
username, err := runPrompt(cmd, &promptui.Prompt{
64+
username, err := prompt(cmd, &promptui.Prompt{
6565
Label: "What username would you like?",
6666
Default: currentUser.Username,
6767
})
6868
if err != nil {
6969
return xerrors.Errorf("pick username prompt: %w", err)
7070
}
7171

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

80-
email, err := runPrompt(cmd, &promptui.Prompt{
80+
email, err := prompt(cmd, &promptui.Prompt{
8181
Label: "What's your email?",
8282
Validate: func(s string) error {
8383
err := validator.New().Var(s, "email")
@@ -91,7 +91,7 @@ func login() *cobra.Command {
9191
return xerrors.Errorf("specify email prompt: %w", err)
9292
}
9393

94-
password, err := runPrompt(cmd, &promptui.Prompt{
94+
password, err := prompt(cmd, &promptui.Prompt{
9595
Label: "Enter a password:",
9696
Mask: '*',
9797
})

cli/projectcreate.go

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@ package cli
33
import (
44
"archive/tar"
55
"bytes"
6+
"errors"
67
"fmt"
78
"io"
89
"os"
910
"path/filepath"
10-
"strings"
1111
"time"
1212

1313
"github.com/briandowns/spinner"
1414
"github.com/fatih/color"
1515
"github.com/google/uuid"
1616
"github.com/manifoldco/promptui"
1717
"github.com/spf13/cobra"
18-
"github.com/xlab/treeprint"
1918
"golang.org/x/xerrors"
2019

2120
"github.com/coder/coder/coderd"
@@ -27,7 +26,8 @@ import (
2726

2827
func projectCreate() *cobra.Command {
2928
var (
30-
directory string
29+
directory string
30+
provisioner string
3131
)
3232
cmd := &cobra.Command{
3333
Use: "create",
@@ -41,16 +41,19 @@ func projectCreate() *cobra.Command {
4141
if err != nil {
4242
return err
4343
}
44-
_, err = runPrompt(cmd, &promptui.Prompt{
44+
_, err = prompt(cmd, &promptui.Prompt{
4545
Default: "y",
4646
IsConfirm: true,
4747
Label: fmt.Sprintf("Set up %s in your organization?", color.New(color.FgHiCyan).Sprintf("%q", directory)),
4848
})
4949
if err != nil {
50+
if errors.Is(err, promptui.ErrAbort) {
51+
return nil
52+
}
5053
return err
5154
}
5255

53-
name, err := runPrompt(cmd, &promptui.Prompt{
56+
name, err := prompt(cmd, &promptui.Prompt{
5457
Default: filepath.Base(directory),
5558
Label: "What's your project's name?",
5659
Validate: func(s string) error {
@@ -65,7 +68,7 @@ func projectCreate() *cobra.Command {
6568
return err
6669
}
6770

68-
job, err := doProjectLoop(cmd, client, organization, directory, []coderd.CreateParameterValueRequest{})
71+
job, err := validateProjectVersionSource(cmd, client, organization, database.ProvisionerType(provisioner), directory)
6972
if err != nil {
7073
return err
7174
}
@@ -77,27 +80,44 @@ func projectCreate() *cobra.Command {
7780
return err
7881
}
7982

83+
_, err = prompt(cmd, &promptui.Prompt{
84+
Label: "Create project?",
85+
IsConfirm: true,
86+
Default: "y",
87+
})
88+
if err != nil {
89+
if errors.Is(err, promptui.ErrAbort) {
90+
return nil
91+
}
92+
return err
93+
}
94+
8095
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", color.HiBlackString(">"), color.HiCyanString(project.Name))
81-
_, err = runPrompt(cmd, &promptui.Prompt{
96+
_, err = prompt(cmd, &promptui.Prompt{
8297
Label: "Create a new workspace?",
8398
IsConfirm: true,
8499
Default: "y",
85100
})
86101
if err != nil {
102+
if errors.Is(err, promptui.ErrAbort) {
103+
return nil
104+
}
87105
return err
88106
}
89107

90-
fmt.Printf("Create a new workspace now!\n")
91108
return nil
92109
},
93110
}
94111
currentDirectory, _ := os.Getwd()
95112
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
113+
cmd.Flags().StringVarP(&provisioner, "provisioner", "p", "terraform", "Customize the provisioner backend")
114+
// This is for testing! There's only 1 provisioner type right now.
115+
cmd.Flags().MarkHidden("provisioner")
96116

97117
return cmd
98118
}
99119

100-
func doProjectLoop(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, directory string, params []coderd.CreateParameterValueRequest) (*coderd.ProvisionerJob, error) {
120+
func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterValueRequest) (*coderd.ProvisionerJob, error) {
101121
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
102122
spin.Writer = cmd.OutOrStdout()
103123
spin.Suffix = " Uploading current directory..."
@@ -118,8 +138,8 @@ func doProjectLoop(cmd *cobra.Command, client *codersdk.Client, organization cod
118138
job, err := client.CreateProjectVersionImportProvisionerJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{
119139
StorageMethod: database.ProvisionerStorageMethodFile,
120140
StorageSource: resp.Hash,
121-
Provisioner: database.ProvisionerTypeTerraform,
122-
ParameterValues: params,
141+
Provisioner: provisioner,
142+
ParameterValues: parameters,
123143
})
124144
if err != nil {
125145
return nil, err
@@ -168,20 +188,20 @@ func doProjectLoop(cmd *cobra.Command, client *codersdk.Client, organization cod
168188
if parameterSchema.Name == parameter.CoderWorkspaceTransition {
169189
continue
170190
}
171-
value, err := runPrompt(cmd, &promptui.Prompt{
191+
value, err := prompt(cmd, &promptui.Prompt{
172192
Label: fmt.Sprintf("Enter value for %s:", color.HiCyanString(parameterSchema.Name)),
173193
})
174194
if err != nil {
175195
return nil, err
176196
}
177-
params = append(params, coderd.CreateParameterValueRequest{
197+
parameters = append(parameters, coderd.CreateParameterValueRequest{
178198
Name: parameterSchema.Name,
179199
SourceValue: value,
180200
SourceScheme: database.ParameterSourceSchemeData,
181201
DestinationScheme: parameterSchema.DefaultDestinationScheme,
182202
})
183203
}
184-
return doProjectLoop(cmd, client, organization, directory, params)
204+
return validateProjectVersionSource(cmd, client, organization, provisioner, directory, parameters...)
185205
}
186206

187207
if job.Status != coderd.ProvisionerJobStatusSucceeded {
@@ -198,50 +218,7 @@ func doProjectLoop(cmd *cobra.Command, client *codersdk.Client, organization cod
198218
if err != nil {
199219
return nil, err
200220
}
201-
return &job, outputProjectInformation(cmd, parameterSchemas, parameterValues, resources)
202-
}
203-
204-
func outputProjectInformation(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.ProjectImportJobResource) error {
205-
schemaByID := map[string]coderd.ParameterSchema{}
206-
for _, schema := range parameterSchemas {
207-
schemaByID[schema.ID.String()] = schema
208-
}
209-
210-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n %s\n\n", color.HiBlackString("Parameters"))
211-
for _, value := range parameterValues {
212-
schema, ok := schemaByID[value.SchemaID.String()]
213-
if !ok {
214-
return xerrors.Errorf("schema not found: %s", value.Name)
215-
}
216-
displayValue := value.SourceValue
217-
if !schema.RedisplayValue {
218-
displayValue = "<redacted>"
219-
}
220-
output := fmt.Sprintf("%s %s %s", color.HiCyanString(value.Name), color.HiBlackString("="), displayValue)
221-
if value.DefaultSourceValue {
222-
output += " (default value)"
223-
} else if value.Scope != database.ParameterScopeImportJob {
224-
output += fmt.Sprintf(" (inherited from %s)", value.Scope)
225-
}
226-
227-
root := treeprint.NewWithRoot(output)
228-
if schema.Description != "" {
229-
root.AddBranch(fmt.Sprintf("%s\n%s\n", color.HiBlackString("Description"), schema.Description))
230-
}
231-
if schema.AllowOverrideSource {
232-
root.AddBranch(fmt.Sprintf("%s Users can customize this value!", color.HiYellowString("+")))
233-
}
234-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.Join(strings.Split(root.String(), "\n"), "\n "))
235-
}
236-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s\n\n", color.HiBlackString("Resources"))
237-
for _, resource := range resources {
238-
transition := color.HiGreenString("start")
239-
if resource.Transition == database.WorkspaceTransitionStop {
240-
transition = color.HiRedString("stop")
241-
}
242-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s %s on %s\n\n", color.HiCyanString(resource.Type), color.HiCyanString(resource.Name), transition)
243-
}
244-
return nil
221+
return &job, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources)
245222
}
246223

247224
func tarDirectory(directory string) ([]byte, error) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy