mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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
This commit is contained in:
@ -24,8 +24,21 @@ type PromptOptions struct {
|
|||||||
Validate func(string) error
|
Validate func(string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AllowSkipPrompt(cmd *cobra.Command) {
|
||||||
|
cmd.Flags().BoolP("yes", "y", false, "Bypass prompts")
|
||||||
|
}
|
||||||
|
|
||||||
// Prompt asks the user for input.
|
// Prompt asks the user for input.
|
||||||
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
||||||
|
// If the cmd has a "yes" flag for skipping confirm prompts, honor it.
|
||||||
|
// If it's not a "Confirm" prompt, then don't skip. As the default value of
|
||||||
|
// "yes" makes no sense.
|
||||||
|
if opts.IsConfirm && cmd.Flags().Lookup("yes") != nil {
|
||||||
|
if skip, _ := cmd.Flags().GetBool("yes"); skip {
|
||||||
|
return "yes", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
|
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
|
||||||
if opts.IsConfirm {
|
if opts.IsConfirm {
|
||||||
opts.Default = "yes"
|
opts.Default = "yes"
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package cliui_test
|
package cliui_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
@ -24,7 +26,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
Text: "Example",
|
Text: "Example",
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
msgChan <- resp
|
msgChan <- resp
|
||||||
}()
|
}()
|
||||||
@ -41,7 +43,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
Text: "Example",
|
Text: "Example",
|
||||||
IsConfirm: true,
|
IsConfirm: true,
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
doneChan <- resp
|
doneChan <- resp
|
||||||
}()
|
}()
|
||||||
@ -50,6 +52,47 @@ func TestPrompt(t *testing.T) {
|
|||||||
require.Equal(t, "yes", <-doneChan)
|
require.Equal(t, "yes", <-doneChan)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Skip", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ptty := ptytest.New(t)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// Copy all data written out to a buffer. When we close the ptty, we can
|
||||||
|
// no longer read from the ptty.Output(), but we can read what was
|
||||||
|
// written to the buffer.
|
||||||
|
dataRead, doneReading := context.WithTimeout(context.Background(), time.Second*2)
|
||||||
|
go func() {
|
||||||
|
// This will throw an error sometimes. The underlying ptty
|
||||||
|
// has its own cleanup routines in t.Cleanup. Instead of
|
||||||
|
// trying to control the close perfectly, just let the ptty
|
||||||
|
// double close. This error isn't important, we just
|
||||||
|
// want to know the ptty is done sending output.
|
||||||
|
_, _ = io.Copy(&buf, ptty.Output())
|
||||||
|
doneReading()
|
||||||
|
}()
|
||||||
|
|
||||||
|
doneChan := make(chan string)
|
||||||
|
go func() {
|
||||||
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
|
Text: "ShouldNotSeeThis",
|
||||||
|
IsConfirm: true,
|
||||||
|
}, func(cmd *cobra.Command) {
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
|
cmd.SetArgs([]string{"-y"})
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
doneChan <- resp
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.Equal(t, "yes", <-doneChan)
|
||||||
|
// Close the reader to end the io.Copy
|
||||||
|
require.NoError(t, ptty.Close(), "close eof reader")
|
||||||
|
// Wait for the IO copy to finish
|
||||||
|
<-dataRead.Done()
|
||||||
|
// Timeout error means the output was hanging
|
||||||
|
require.ErrorIs(t, dataRead.Err(), context.Canceled, "should be canceled")
|
||||||
|
require.Len(t, buf.Bytes(), 0, "expect no output")
|
||||||
|
})
|
||||||
t.Run("JSON", func(t *testing.T) {
|
t.Run("JSON", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ptty := ptytest.New(t)
|
ptty := ptytest.New(t)
|
||||||
@ -57,7 +100,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
Text: "Example",
|
Text: "Example",
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
doneChan <- resp
|
doneChan <- resp
|
||||||
}()
|
}()
|
||||||
@ -73,7 +116,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
Text: "Example",
|
Text: "Example",
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
doneChan <- resp
|
doneChan <- resp
|
||||||
}()
|
}()
|
||||||
@ -89,7 +132,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||||
Text: "Example",
|
Text: "Example",
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
doneChan <- resp
|
doneChan <- resp
|
||||||
}()
|
}()
|
||||||
@ -101,7 +144,7 @@ func TestPrompt(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
|
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, cmdOpt func(cmd *cobra.Command)) (string, error) {
|
||||||
value := ""
|
value := ""
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@ -110,7 +153,12 @@ func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
|
|||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.SetOutput(ptty.Output())
|
// Optionally modify the cmd
|
||||||
|
if cmdOpt != nil {
|
||||||
|
cmdOpt(cmd)
|
||||||
|
}
|
||||||
|
cmd.SetOut(ptty.Output())
|
||||||
|
cmd.SetErr(ptty.Output())
|
||||||
cmd.SetIn(ptty.Input())
|
cmd.SetIn(ptty.Input())
|
||||||
return value, cmd.ExecuteContext(context.Background())
|
return value, cmd.ExecuteContext(context.Background())
|
||||||
}
|
}
|
||||||
|
@ -204,6 +204,7 @@ func create() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
|
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
|
||||||
cliflag.StringVarP(cmd.Flags(), ¶meterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
|
cliflag.StringVarP(cmd.Flags(), ¶meterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package cli_test
|
package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -46,34 +48,24 @@ func TestCreate(t *testing.T) {
|
|||||||
<-doneChan
|
<-doneChan
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("CreateFromList", func(t *testing.T) {
|
t.Run("CreateFromListWithSkip", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
cmd, root := clitest.New(t, "create", "my-workspace")
|
cmd, root := clitest.New(t, "create", "my-workspace", "-y")
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
doneChan := make(chan struct{})
|
cmdCtx, done := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
pty := ptytest.New(t)
|
|
||||||
cmd.SetIn(pty.Input())
|
|
||||||
cmd.SetOut(pty.Output())
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(doneChan)
|
defer done()
|
||||||
err := cmd.Execute()
|
err := cmd.ExecuteContext(cmdCtx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
matches := []string{
|
// No pty interaction needed since we use the -y skip prompt flag
|
||||||
"Confirm create", "yes",
|
<-cmdCtx.Done()
|
||||||
}
|
require.ErrorIs(t, cmdCtx.Err(), context.Canceled)
|
||||||
for i := 0; i < len(matches); i += 2 {
|
|
||||||
match := matches[i]
|
|
||||||
value := matches[i+1]
|
|
||||||
pty.ExpectMatch(match)
|
|
||||||
pty.WriteLine(value)
|
|
||||||
}
|
|
||||||
<-doneChan
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("FromNothing", func(t *testing.T) {
|
t.Run("FromNothing", func(t *testing.T) {
|
||||||
|
@ -11,13 +11,21 @@ import (
|
|||||||
|
|
||||||
// nolint
|
// nolint
|
||||||
func delete() *cobra.Command {
|
func delete() *cobra.Command {
|
||||||
return &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Annotations: workspaceCommand,
|
Annotations: workspaceCommand,
|
||||||
Use: "delete <workspace>",
|
Use: "delete <workspace>",
|
||||||
Short: "Delete a workspace",
|
Short: "Delete a workspace",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||||
|
Text: "Confirm delete workspace?",
|
||||||
|
IsConfirm: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := createClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -40,4 +48,6 @@ func delete() *cobra.Command {
|
|||||||
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func TestDelete(t *testing.T) {
|
|||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||||
cmd, root := clitest.New(t, "delete", workspace.Name)
|
cmd, root := clitest.New(t, "delete", workspace.Name, "-y")
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
doneChan := make(chan struct{})
|
doneChan := make(chan struct{})
|
||||||
pty := ptytest.New(t)
|
pty := ptytest.New(t)
|
||||||
|
12
cli/start.go
12
cli/start.go
@ -10,12 +10,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func start() *cobra.Command {
|
func start() *cobra.Command {
|
||||||
return &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Annotations: workspaceCommand,
|
Annotations: workspaceCommand,
|
||||||
Use: "start <workspace>",
|
Use: "start <workspace>",
|
||||||
Short: "Build a workspace with the start state",
|
Short: "Build a workspace with the start state",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||||
|
Text: "Confirm start workspace?",
|
||||||
|
IsConfirm: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := createClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -38,4 +46,6 @@ func start() *cobra.Command {
|
|||||||
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
12
cli/stop.go
12
cli/stop.go
@ -10,12 +10,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func stop() *cobra.Command {
|
func stop() *cobra.Command {
|
||||||
return &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Annotations: workspaceCommand,
|
Annotations: workspaceCommand,
|
||||||
Use: "stop <workspace>",
|
Use: "stop <workspace>",
|
||||||
Short: "Build a workspace with the stop state",
|
Short: "Build a workspace with the stop state",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||||
|
Text: "Confirm stop workspace?",
|
||||||
|
IsConfirm: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := createClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -38,4 +46,6 @@ func stop() *cobra.Command {
|
|||||||
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import (
|
|||||||
|
|
||||||
func templateCreate() *cobra.Command {
|
func templateCreate() *cobra.Command {
|
||||||
var (
|
var (
|
||||||
yes bool
|
|
||||||
directory string
|
directory string
|
||||||
provisioner string
|
provisioner string
|
||||||
parameterFile string
|
parameterFile string
|
||||||
@ -85,14 +84,12 @@ func templateCreate() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !yes {
|
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
Text: "Confirm create?",
|
||||||
Text: "Confirm create?",
|
IsConfirm: true,
|
||||||
IsConfirm: true,
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{
|
_, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{
|
||||||
@ -123,7 +120,7 @@ func templateCreate() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Bypass prompts")
|
cliui.AllowSkipPrompt(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ func templateUpdate() *cobra.Command {
|
|||||||
currentDirectory, _ := os.Getwd()
|
currentDirectory, _ := os.Getwd()
|
||||||
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
|
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
|
||||||
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
|
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
|
||||||
|
cliui.AllowSkipPrompt(cmd)
|
||||||
// This is for testing!
|
// This is for testing!
|
||||||
err := cmd.Flags().MarkHidden("test.provisioner")
|
err := cmd.Flags().MarkHidden("test.provisioner")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Reference in New Issue
Block a user