Skip to content

Commit c5c13bc

Browse files
Emyrkkylecarbs
authored andcommitted
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 c459928 commit c5c13bc

File tree

10 files changed

+121
-39
lines changed

10 files changed

+121
-39
lines changed

cli/cliui/prompt.go

Lines changed: 13 additions & 0 deletions
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

Lines changed: 55 additions & 7 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 10 additions & 18 deletions
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 7 additions & 10 deletions
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

Lines changed: 1 addition & 0 deletions
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)
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