Skip to content

Commit ad946c3

Browse files
authored
feat: Add confirm prompts to some cli actions (#1591)
* feat: Add confirm prompts to some cli actions - Add optional -y skip. Standardize -y flag across commands
1 parent 4f70f84 commit ad946c3

10 files changed

+121
-39
lines changed

cli/cliui/prompt.go

+13
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,21 @@ type PromptOptions struct {
2424
Validate func(string) error
2525
}
2626

27+
func AllowSkipPrompt(cmd *cobra.Command) {
28+
cmd.Flags().BoolP("yes", "y", false, "Bypass prompts")
29+
}
30+
2731
// Prompt asks the user for input.
2832
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
33+
// If the cmd has a "yes" flag for skipping confirm prompts, honor it.
34+
// If it's not a "Confirm" prompt, then don't skip. As the default value of
35+
// "yes" makes no sense.
36+
if opts.IsConfirm && cmd.Flags().Lookup("yes") != nil {
37+
if skip, _ := cmd.Flags().GetBool("yes"); skip {
38+
return "yes", nil
39+
}
40+
}
41+
2942
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
3043
if opts.IsConfirm {
3144
opts.Default = "yes"

cli/cliui/prompt_test.go

+55-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package cliui_test
22

33
import (
4+
"bytes"
45
"context"
6+
"io"
57
"os"
68
"os/exec"
79
"testing"
@@ -24,7 +26,7 @@ func TestPrompt(t *testing.T) {
2426
go func() {
2527
resp, err := newPrompt(ptty, cliui.PromptOptions{
2628
Text: "Example",
27-
})
29+
}, nil)
2830
require.NoError(t, err)
2931
msgChan <- resp
3032
}()
@@ -41,7 +43,7 @@ func TestPrompt(t *testing.T) {
4143
resp, err := newPrompt(ptty, cliui.PromptOptions{
4244
Text: "Example",
4345
IsConfirm: true,
44-
})
46+
}, nil)
4547
require.NoError(t, err)
4648
doneChan <- resp
4749
}()
@@ -50,14 +52,55 @@ func TestPrompt(t *testing.T) {
5052
require.Equal(t, "yes", <-doneChan)
5153
})
5254

55+
t.Run("Skip", func(t *testing.T) {
56+
t.Parallel()
57+
ptty := ptytest.New(t)
58+
var buf bytes.Buffer
59+
60+
// Copy all data written out to a buffer. When we close the ptty, we can
61+
// no longer read from the ptty.Output(), but we can read what was
62+
// written to the buffer.
63+
dataRead, doneReading := context.WithTimeout(context.Background(), time.Second*2)
64+
go func() {
65+
// This will throw an error sometimes. The underlying ptty
66+
// has its own cleanup routines in t.Cleanup. Instead of
67+
// trying to control the close perfectly, just let the ptty
68+
// double close. This error isn't important, we just
69+
// want to know the ptty is done sending output.
70+
_, _ = io.Copy(&buf, ptty.Output())
71+
doneReading()
72+
}()
73+
74+
doneChan := make(chan string)
75+
go func() {
76+
resp, err := newPrompt(ptty, cliui.PromptOptions{
77+
Text: "ShouldNotSeeThis",
78+
IsConfirm: true,
79+
}, func(cmd *cobra.Command) {
80+
cliui.AllowSkipPrompt(cmd)
81+
cmd.SetArgs([]string{"-y"})
82+
})
83+
require.NoError(t, err)
84+
doneChan <- resp
85+
}()
86+
87+
require.Equal(t, "yes", <-doneChan)
88+
// Close the reader to end the io.Copy
89+
require.NoError(t, ptty.Close(), "close eof reader")
90+
// Wait for the IO copy to finish
91+
<-dataRead.Done()
92+
// Timeout error means the output was hanging
93+
require.ErrorIs(t, dataRead.Err(), context.Canceled, "should be canceled")
94+
require.Len(t, buf.Bytes(), 0, "expect no output")
95+
})
5396
t.Run("JSON", func(t *testing.T) {
5497
t.Parallel()
5598
ptty := ptytest.New(t)
5699
doneChan := make(chan string)
57100
go func() {
58101
resp, err := newPrompt(ptty, cliui.PromptOptions{
59102
Text: "Example",
60-
})
103+
}, nil)
61104
require.NoError(t, err)
62105
doneChan <- resp
63106
}()
@@ -73,7 +116,7 @@ func TestPrompt(t *testing.T) {
73116
go func() {
74117
resp, err := newPrompt(ptty, cliui.PromptOptions{
75118
Text: "Example",
76-
})
119+
}, nil)
77120
require.NoError(t, err)
78121
doneChan <- resp
79122
}()
@@ -89,7 +132,7 @@ func TestPrompt(t *testing.T) {
89132
go func() {
90133
resp, err := newPrompt(ptty, cliui.PromptOptions{
91134
Text: "Example",
92-
})
135+
}, nil)
93136
require.NoError(t, err)
94137
doneChan <- resp
95138
}()
@@ -101,7 +144,7 @@ func TestPrompt(t *testing.T) {
101144
})
102145
}
103146

104-
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
147+
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, cmdOpt func(cmd *cobra.Command)) (string, error) {
105148
value := ""
106149
cmd := &cobra.Command{
107150
RunE: func(cmd *cobra.Command, args []string) error {
@@ -110,7 +153,12 @@ func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
110153
return err
111154
},
112155
}
113-
cmd.SetOutput(ptty.Output())
156+
// Optionally modify the cmd
157+
if cmdOpt != nil {
158+
cmdOpt(cmd)
159+
}
160+
cmd.SetOut(ptty.Output())
161+
cmd.SetErr(ptty.Output())
114162
cmd.SetIn(ptty.Input())
115163
return value, cmd.ExecuteContext(context.Background())
116164
}

cli/create.go

+1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ func create() *cobra.Command {
204204
},
205205
}
206206

207+
cliui.AllowSkipPrompt(cmd)
207208
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
208209
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
209210
return cmd

cli/create_test.go

+10-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package cli_test
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"testing"
8+
"time"
79

810
"github.com/stretchr/testify/require"
911

@@ -46,34 +48,24 @@ func TestCreate(t *testing.T) {
4648
<-doneChan
4749
})
4850

49-
t.Run("CreateFromList", func(t *testing.T) {
51+
t.Run("CreateFromListWithSkip", func(t *testing.T) {
5052
t.Parallel()
5153
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
5254
user := coderdtest.CreateFirstUser(t, client)
5355
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
5456
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
5557
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
56-
cmd, root := clitest.New(t, "create", "my-workspace")
58+
cmd, root := clitest.New(t, "create", "my-workspace", "-y")
5759
clitest.SetupConfig(t, client, root)
58-
doneChan := make(chan struct{})
59-
pty := ptytest.New(t)
60-
cmd.SetIn(pty.Input())
61-
cmd.SetOut(pty.Output())
60+
cmdCtx, done := context.WithTimeout(context.Background(), time.Second*3)
6261
go func() {
63-
defer close(doneChan)
64-
err := cmd.Execute()
62+
defer done()
63+
err := cmd.ExecuteContext(cmdCtx)
6564
require.NoError(t, err)
6665
}()
67-
matches := []string{
68-
"Confirm create", "yes",
69-
}
70-
for i := 0; i < len(matches); i += 2 {
71-
match := matches[i]
72-
value := matches[i+1]
73-
pty.ExpectMatch(match)
74-
pty.WriteLine(value)
75-
}
76-
<-doneChan
66+
// No pty interaction needed since we use the -y skip prompt flag
67+
<-cmdCtx.Done()
68+
require.ErrorIs(t, cmdCtx.Err(), context.Canceled)
7769
})
7870

7971
t.Run("FromNothing", func(t *testing.T) {

cli/delete.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ import (
1111

1212
// nolint
1313
func delete() *cobra.Command {
14-
return &cobra.Command{
14+
cmd := &cobra.Command{
1515
Annotations: workspaceCommand,
1616
Use: "delete <workspace>",
1717
Short: "Delete a workspace",
1818
Aliases: []string{"rm"},
1919
Args: cobra.ExactArgs(1),
2020
RunE: func(cmd *cobra.Command, args []string) error {
21+
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
22+
Text: "Confirm delete workspace?",
23+
IsConfirm: true,
24+
})
25+
if err != nil {
26+
return err
27+
}
28+
2129
client, err := createClient(cmd)
2230
if err != nil {
2331
return err
@@ -40,4 +48,6 @@ func delete() *cobra.Command {
4048
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
4149
},
4250
}
51+
cliui.AllowSkipPrompt(cmd)
52+
return cmd
4353
}

cli/delete_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestDelete(t *testing.T) {
2020
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
2121
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
2222
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
23-
cmd, root := clitest.New(t, "delete", workspace.Name)
23+
cmd, root := clitest.New(t, "delete", workspace.Name, "-y")
2424
clitest.SetupConfig(t, client, root)
2525
doneChan := make(chan struct{})
2626
pty := ptytest.New(t)

cli/start.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import (
1010
)
1111

1212
func start() *cobra.Command {
13-
return &cobra.Command{
13+
cmd := &cobra.Command{
1414
Annotations: workspaceCommand,
1515
Use: "start <workspace>",
1616
Short: "Build a workspace with the start state",
1717
Args: cobra.ExactArgs(1),
1818
RunE: func(cmd *cobra.Command, args []string) error {
19+
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
20+
Text: "Confirm start workspace?",
21+
IsConfirm: true,
22+
})
23+
if err != nil {
24+
return err
25+
}
26+
1927
client, err := createClient(cmd)
2028
if err != nil {
2129
return err
@@ -38,4 +46,6 @@ func start() *cobra.Command {
3846
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
3947
},
4048
}
49+
cliui.AllowSkipPrompt(cmd)
50+
return cmd
4151
}

cli/stop.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import (
1010
)
1111

1212
func stop() *cobra.Command {
13-
return &cobra.Command{
13+
cmd := &cobra.Command{
1414
Annotations: workspaceCommand,
1515
Use: "stop <workspace>",
1616
Short: "Build a workspace with the stop state",
1717
Args: cobra.ExactArgs(1),
1818
RunE: func(cmd *cobra.Command, args []string) error {
19+
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
20+
Text: "Confirm stop workspace?",
21+
IsConfirm: true,
22+
})
23+
if err != nil {
24+
return err
25+
}
26+
1927
client, err := createClient(cmd)
2028
if err != nil {
2129
return err
@@ -38,4 +46,6 @@ func stop() *cobra.Command {
3846
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
3947
},
4048
}
49+
cliui.AllowSkipPrompt(cmd)
50+
return cmd
4151
}

cli/templatecreate.go

+7-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121

2222
func templateCreate() *cobra.Command {
2323
var (
24-
yes bool
2524
directory string
2625
provisioner string
2726
parameterFile string
@@ -85,14 +84,12 @@ func templateCreate() *cobra.Command {
8584
return err
8685
}
8786

88-
if !yes {
89-
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
90-
Text: "Confirm create?",
91-
IsConfirm: true,
92-
})
93-
if err != nil {
94-
return err
95-
}
87+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
88+
Text: "Confirm create?",
89+
IsConfirm: true,
90+
})
91+
if err != nil {
92+
return err
9693
}
9794

9895
_, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{
@@ -123,7 +120,7 @@ func templateCreate() *cobra.Command {
123120
if err != nil {
124121
panic(err)
125122
}
126-
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Bypass prompts")
123+
cliui.AllowSkipPrompt(cmd)
127124
return cmd
128125
}
129126

cli/templateupdate.go

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func templateUpdate() *cobra.Command {
108108
currentDirectory, _ := os.Getwd()
109109
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
110110
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
111+
cliui.AllowSkipPrompt(cmd)
111112
// This is for testing!
112113
err := cmd.Flags().MarkHidden("test.provisioner")
113114
if err != nil {

0 commit comments

Comments
 (0)