diff --git a/cli/cliui/prompt.go b/cli/cliui/prompt.go index ac39404e27d3f..16f0c438f4007 100644 --- a/cli/cliui/prompt.go +++ b/cli/cliui/prompt.go @@ -24,8 +24,21 @@ type PromptOptions struct { Validate func(string) error } +func AllowSkipPrompt(cmd *cobra.Command) { + cmd.Flags().BoolP("yes", "y", false, "Bypass prompts") +} + // Prompt asks the user for input. 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+" ") if opts.IsConfirm { opts.Default = "yes" diff --git a/cli/cliui/prompt_test.go b/cli/cliui/prompt_test.go index 1926349c2d1fc..9c9167ad09708 100644 --- a/cli/cliui/prompt_test.go +++ b/cli/cliui/prompt_test.go @@ -1,7 +1,9 @@ package cliui_test import ( + "bytes" "context" + "io" "os" "os/exec" "testing" @@ -24,7 +26,7 @@ func TestPrompt(t *testing.T) { go func() { resp, err := newPrompt(ptty, cliui.PromptOptions{ Text: "Example", - }) + }, nil) require.NoError(t, err) msgChan <- resp }() @@ -41,7 +43,7 @@ func TestPrompt(t *testing.T) { resp, err := newPrompt(ptty, cliui.PromptOptions{ Text: "Example", IsConfirm: true, - }) + }, nil) require.NoError(t, err) doneChan <- resp }() @@ -50,6 +52,47 @@ func TestPrompt(t *testing.T) { 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.Parallel() ptty := ptytest.New(t) @@ -57,7 +100,7 @@ func TestPrompt(t *testing.T) { go func() { resp, err := newPrompt(ptty, cliui.PromptOptions{ Text: "Example", - }) + }, nil) require.NoError(t, err) doneChan <- resp }() @@ -73,7 +116,7 @@ func TestPrompt(t *testing.T) { go func() { resp, err := newPrompt(ptty, cliui.PromptOptions{ Text: "Example", - }) + }, nil) require.NoError(t, err) doneChan <- resp }() @@ -89,7 +132,7 @@ func TestPrompt(t *testing.T) { go func() { resp, err := newPrompt(ptty, cliui.PromptOptions{ Text: "Example", - }) + }, nil) require.NoError(t, err) 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 := "" cmd := &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { @@ -110,7 +153,12 @@ func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) { 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()) return value, cmd.ExecuteContext(context.Background()) } diff --git a/cli/create.go b/cli/create.go index 4f09f71bc7eb3..1f762a55cca45 100644 --- a/cli/create.go +++ b/cli/create.go @@ -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(), ¶meterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.") return cmd diff --git a/cli/create_test.go b/cli/create_test.go index 955a001fa1245..3e29e3bdabad7 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -1,9 +1,11 @@ package cli_test import ( + "context" "fmt" "os" "testing" + "time" "github.com/stretchr/testify/require" @@ -46,34 +48,24 @@ func TestCreate(t *testing.T) { <-doneChan }) - t.Run("CreateFromList", func(t *testing.T) { + t.Run("CreateFromListWithSkip", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, 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) - doneChan := make(chan struct{}) - pty := ptytest.New(t) - cmd.SetIn(pty.Input()) - cmd.SetOut(pty.Output()) + cmdCtx, done := context.WithTimeout(context.Background(), time.Second*3) go func() { - defer close(doneChan) - err := cmd.Execute() + defer done() + err := cmd.ExecuteContext(cmdCtx) require.NoError(t, err) }() - matches := []string{ - "Confirm create", "yes", - } - for i := 0; i < len(matches); i += 2 { - match := matches[i] - value := matches[i+1] - pty.ExpectMatch(match) - pty.WriteLine(value) - } - <-doneChan + // No pty interaction needed since we use the -y skip prompt flag + <-cmdCtx.Done() + require.ErrorIs(t, cmdCtx.Err(), context.Canceled) }) t.Run("FromNothing", func(t *testing.T) { diff --git a/cli/delete.go b/cli/delete.go index 8d1de59c2e653..42be08965fed4 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -11,13 +11,21 @@ import ( // nolint func delete() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "delete ", Short: "Delete a workspace", Aliases: []string{"rm"}, Args: cobra.ExactArgs(1), 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) if err != nil { return err @@ -40,4 +48,6 @@ func delete() *cobra.Command { return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before) }, } + cliui.AllowSkipPrompt(cmd) + return cmd } diff --git a/cli/delete_test.go b/cli/delete_test.go index f9e1102a0eb39..e7af61b40cd82 100644 --- a/cli/delete_test.go +++ b/cli/delete_test.go @@ -20,7 +20,7 @@ func TestDelete(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.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) doneChan := make(chan struct{}) pty := ptytest.New(t) diff --git a/cli/start.go b/cli/start.go index 1c562ddd9d093..7e35b22aa915a 100644 --- a/cli/start.go +++ b/cli/start.go @@ -10,12 +10,20 @@ import ( ) func start() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "start ", Short: "Build a workspace with the start state", Args: cobra.ExactArgs(1), 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) if err != nil { return err @@ -38,4 +46,6 @@ func start() *cobra.Command { return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before) }, } + cliui.AllowSkipPrompt(cmd) + return cmd } diff --git a/cli/stop.go b/cli/stop.go index af1f44da58af0..f2455458bd1f7 100644 --- a/cli/stop.go +++ b/cli/stop.go @@ -10,12 +10,20 @@ import ( ) func stop() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "stop ", Short: "Build a workspace with the stop state", Args: cobra.ExactArgs(1), 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) if err != nil { return err @@ -38,4 +46,6 @@ func stop() *cobra.Command { return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before) }, } + cliui.AllowSkipPrompt(cmd) + return cmd } diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 6c98afd66cfc8..06205aa008eb2 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -21,7 +21,6 @@ import ( func templateCreate() *cobra.Command { var ( - yes bool directory string provisioner string parameterFile string @@ -85,14 +84,12 @@ func templateCreate() *cobra.Command { return err } - if !yes { - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Confirm create?", - IsConfirm: true, - }) - if err != nil { - return err - } + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Confirm create?", + IsConfirm: true, + }) + if err != nil { + return err } _, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{ @@ -123,7 +120,7 @@ func templateCreate() *cobra.Command { if err != nil { panic(err) } - cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Bypass prompts") + cliui.AllowSkipPrompt(cmd) return cmd } diff --git a/cli/templateupdate.go b/cli/templateupdate.go index 70c2611cea687..32902aab3b518 100644 --- a/cli/templateupdate.go +++ b/cli/templateupdate.go @@ -108,6 +108,7 @@ func templateUpdate() *cobra.Command { currentDirectory, _ := os.Getwd() cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from") cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend") + cliui.AllowSkipPrompt(cmd) // This is for testing! err := cmd.Flags().MarkHidden("test.provisioner") if err != nil { 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