Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 26c6952

Browse files
authored
feat: Validate workspace build parameters (coder#5807)
1 parent 138887d commit 26c6952

23 files changed

+1051
-268
lines changed

cli/cliui/parameter.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,21 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
6767
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.Description, "\n"), "\n "))+"\n")
6868
}
6969

70-
// TODO Implement full validation and show descriptions.
7170
var err error
7271
var value string
7372
if len(templateVersionParameter.Options) > 0 {
7473
// Move the cursor up a single line for nicer display!
7574
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
76-
value, err = Select(cmd, SelectOptions{
77-
Options: templateVersionParameterOptionValues(templateVersionParameter),
75+
var richParameterOption *codersdk.TemplateVersionParameterOption
76+
richParameterOption, err = RichSelect(cmd, RichSelectOptions{
77+
Options: templateVersionParameter.Options,
7878
Default: templateVersionParameter.DefaultValue,
7979
HideSearch: true,
8080
})
8181
if err == nil {
8282
_, _ = fmt.Fprintln(cmd.OutOrStdout())
83-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(value))
83+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
84+
value = richParameterOption.Value
8485
}
8586
} else {
8687
text := "Enter a value"
@@ -91,6 +92,9 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
9192

9293
value, err = Prompt(cmd, PromptOptions{
9394
Text: Styles.Bold.Render(text),
95+
Validate: func(value string) error {
96+
return validateRichPrompt(value, templateVersionParameter)
97+
},
9498
})
9599
value = strings.TrimSpace(value)
96100
}
@@ -106,10 +110,9 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
106110
return value, nil
107111
}
108112

109-
func templateVersionParameterOptionValues(param codersdk.TemplateVersionParameter) []string {
110-
var options []string
111-
for _, opt := range param.Options {
112-
options = append(options, opt.Value)
113-
}
114-
return options
113+
func validateRichPrompt(value string, p codersdk.TemplateVersionParameter) error {
114+
return codersdk.ValidateWorkspaceBuildParameter(p, codersdk.WorkspaceBuildParameter{
115+
Name: p.Name,
116+
Value: value,
117+
})
115118
}

cli/cliui/select.go

+39
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
"github.com/AlecAivazis/survey/v2"
1010
"github.com/AlecAivazis/survey/v2/terminal"
1111
"github.com/spf13/cobra"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/codersdk"
1215
)
1316

1417
func init() {
@@ -42,6 +45,42 @@ type SelectOptions struct {
4245
HideSearch bool
4346
}
4447

48+
type RichSelectOptions struct {
49+
Options []codersdk.TemplateVersionParameterOption
50+
Default string
51+
Size int
52+
HideSearch bool
53+
}
54+
55+
// RichSelect displays a list of user options including name and description.
56+
func RichSelect(cmd *cobra.Command, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
57+
opts := make([]string, len(richOptions.Options))
58+
for i, option := range richOptions.Options {
59+
line := option.Name
60+
if len(option.Description) > 0 {
61+
line += ": " + option.Description
62+
}
63+
opts[i] = line
64+
}
65+
66+
selected, err := Select(cmd, SelectOptions{
67+
Options: opts,
68+
Default: richOptions.Default,
69+
Size: richOptions.Size,
70+
HideSearch: richOptions.HideSearch,
71+
})
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
for i, option := range opts {
77+
if option == selected {
78+
return &richOptions.Options[i], nil
79+
}
80+
}
81+
return nil, xerrors.Errorf("unknown option selected: %s", selected)
82+
}
83+
4584
// Select displays a list of user options.
4685
func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
4786
// The survey library used *always* fails when testing on Windows,

cli/cliui/select_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/stretchr/testify/require"
1010

1111
"github.com/coder/coder/cli/cliui"
12+
"github.com/coder/coder/codersdk"
1213
"github.com/coder/coder/pty/ptytest"
1314
)
1415

@@ -42,3 +43,46 @@ func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
4243
cmd.SetIn(ptty.Input())
4344
return value, cmd.ExecuteContext(context.Background())
4445
}
46+
47+
func TestRichSelect(t *testing.T) {
48+
t.Parallel()
49+
t.Run("RichSelect", func(t *testing.T) {
50+
t.Parallel()
51+
ptty := ptytest.New(t)
52+
msgChan := make(chan string)
53+
go func() {
54+
resp, err := newRichSelect(ptty, cliui.RichSelectOptions{
55+
Options: []codersdk.TemplateVersionParameterOption{
56+
{
57+
Name: "A-Name",
58+
Value: "A-Value",
59+
Description: "A-Description",
60+
}, {
61+
Name: "B-Name",
62+
Value: "B-Value",
63+
Description: "B-Description",
64+
},
65+
},
66+
})
67+
assert.NoError(t, err)
68+
msgChan <- resp
69+
}()
70+
require.Equal(t, "A-Value", <-msgChan)
71+
})
72+
}
73+
74+
func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, error) {
75+
value := ""
76+
cmd := &cobra.Command{
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
richOption, err := cliui.RichSelect(cmd, opts)
79+
if err == nil {
80+
value = richOption.Value
81+
}
82+
return err
83+
},
84+
}
85+
cmd.SetOutput(ptty.Output())
86+
cmd.SetIn(ptty.Input())
87+
return value, cmd.ExecuteContext(context.Background())
88+
}

cli/create_test.go

+162
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,168 @@ func TestCreateWithRichParameters(t *testing.T) {
439439
})
440440
}
441441

442+
func TestCreateValidateRichParameters(t *testing.T) {
443+
t.Parallel()
444+
445+
const (
446+
stringParameterName = "string_parameter"
447+
stringParameterValue = "abc"
448+
449+
numberParameterName = "number_parameter"
450+
numberParameterValue = "7"
451+
452+
boolParameterName = "bool_parameter"
453+
boolParameterValue = "true"
454+
)
455+
456+
numberRichParameters := []*proto.RichParameter{
457+
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
458+
}
459+
460+
stringRichParameters := []*proto.RichParameter{
461+
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
462+
}
463+
464+
boolRichParameters := []*proto.RichParameter{
465+
{Name: boolParameterName, Type: "bool", Mutable: true},
466+
}
467+
468+
prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
469+
return &echo.Responses{
470+
Parse: echo.ParseComplete,
471+
ProvisionPlan: []*proto.Provision_Response{
472+
{
473+
Type: &proto.Provision_Response_Complete{
474+
Complete: &proto.Provision_Complete{
475+
Parameters: richParameters,
476+
},
477+
},
478+
}},
479+
ProvisionApply: []*proto.Provision_Response{
480+
{
481+
Type: &proto.Provision_Response_Complete{
482+
Complete: &proto.Provision_Complete{},
483+
},
484+
},
485+
},
486+
}
487+
}
488+
489+
t.Run("ValidateString", func(t *testing.T) {
490+
t.Parallel()
491+
492+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
493+
user := coderdtest.CreateFirstUser(t, client)
494+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(stringRichParameters))
495+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
496+
497+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
498+
499+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
500+
clitest.SetupConfig(t, client, root)
501+
doneChan := make(chan struct{})
502+
pty := ptytest.New(t)
503+
cmd.SetIn(pty.Input())
504+
cmd.SetOut(pty.Output())
505+
go func() {
506+
defer close(doneChan)
507+
err := cmd.Execute()
508+
assert.NoError(t, err)
509+
}()
510+
511+
matches := []string{
512+
stringParameterName, "$$",
513+
"does not match", "",
514+
"Enter a value", "abc",
515+
"Confirm create?", "yes",
516+
}
517+
for i := 0; i < len(matches); i += 2 {
518+
match := matches[i]
519+
value := matches[i+1]
520+
pty.ExpectMatch(match)
521+
pty.WriteLine(value)
522+
}
523+
<-doneChan
524+
})
525+
526+
t.Run("ValidateNumber", func(t *testing.T) {
527+
t.Parallel()
528+
529+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
530+
user := coderdtest.CreateFirstUser(t, client)
531+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(numberRichParameters))
532+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
533+
534+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
535+
536+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
537+
clitest.SetupConfig(t, client, root)
538+
doneChan := make(chan struct{})
539+
pty := ptytest.New(t)
540+
cmd.SetIn(pty.Input())
541+
cmd.SetOut(pty.Output())
542+
go func() {
543+
defer close(doneChan)
544+
err := cmd.Execute()
545+
assert.NoError(t, err)
546+
}()
547+
548+
matches := []string{
549+
numberParameterName, "12",
550+
"is more than the maximum", "",
551+
"Enter a value", "8",
552+
"Confirm create?", "yes",
553+
}
554+
for i := 0; i < len(matches); i += 2 {
555+
match := matches[i]
556+
value := matches[i+1]
557+
pty.ExpectMatch(match)
558+
559+
if value != "" {
560+
pty.WriteLine(value)
561+
}
562+
}
563+
<-doneChan
564+
})
565+
566+
t.Run("ValidateBool", func(t *testing.T) {
567+
t.Parallel()
568+
569+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
570+
user := coderdtest.CreateFirstUser(t, client)
571+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(boolRichParameters))
572+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
573+
574+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
575+
576+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
577+
clitest.SetupConfig(t, client, root)
578+
doneChan := make(chan struct{})
579+
pty := ptytest.New(t)
580+
cmd.SetIn(pty.Input())
581+
cmd.SetOut(pty.Output())
582+
go func() {
583+
defer close(doneChan)
584+
err := cmd.Execute()
585+
assert.NoError(t, err)
586+
}()
587+
588+
matches := []string{
589+
boolParameterName, "cat",
590+
"boolean value can be either", "",
591+
"Enter a value", "true",
592+
"Confirm create?", "yes",
593+
}
594+
for i := 0; i < len(matches); i += 2 {
595+
match := matches[i]
596+
value := matches[i+1]
597+
pty.ExpectMatch(match)
598+
pty.WriteLine(value)
599+
}
600+
<-doneChan
601+
})
602+
}
603+
442604
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
443605
return []*proto.Parse_Response{{
444606
Type: &proto.Parse_Response_Complete{

0 commit comments

Comments
 (0)