Skip to content

feat: Validate workspace build parameters #5807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
df9ddf3
WIP
mtojek Jan 18, 2023
e8e2e53
WIP
mtojek Jan 18, 2023
cfe5521
CLI: handle workspace build parameters
mtojek Jan 18, 2023
74315b2
fix: golintci
mtojek Jan 18, 2023
90b5e35
Fix: dry run
mtojek Jan 19, 2023
34db76e
fix
mtojek Jan 19, 2023
a78f798
CLI: is mutable
mtojek Jan 19, 2023
7e6ddca
coderd: mutable
mtojek Jan 19, 2023
b55e402
fix: golanci
mtojek Jan 19, 2023
f6dee95
fix: richParameterFile
mtojek Jan 19, 2023
f46cf48
CLI: create unit tests
mtojek Jan 19, 2023
40a4aa0
CLI: update test
mtojek Jan 19, 2023
1af7b9a
Fix
mtojek Jan 19, 2023
d20626a
Merge branch 'main' into 5574-cli-add-template-version-parameters
mtojek Jan 20, 2023
8c989ee
fix: order
mtojek Jan 20, 2023
6dbd044
fix
mtojek Jan 20, 2023
125d861
feat: Validate workspace build parameters
mtojek Jan 20, 2023
e278cc0
Validator tests
mtojek Jan 20, 2023
b6c3cde
WIP validation
mtojek Jan 20, 2023
5d6c547
Column: validation error
mtojek Jan 23, 2023
f655603
fix
mtojek Jan 23, 2023
1784771
Merge branch 'main' into 5574-validation-rules
mtojek Jan 23, 2023
f35ec02
merge
mtojek Jan 23, 2023
cfb1d4b
Fix
mtojek Jan 23, 2023
fc3e039
fix
mtojek Jan 23, 2023
ebddd3c
fix: default null
mtojek Jan 23, 2023
967edfc
Merge branch 'main' into 5574-validation-rules
mtojek Jan 23, 2023
cfd0330
CLI validation
mtojek Jan 23, 2023
83ddace
fmt
mtojek Jan 23, 2023
2e7c5f4
fix: default
mtojek Jan 23, 2023
f317891
Fix
mtojek Jan 23, 2023
695680e
Pull new version of terraform-provider-coder
mtojek Jan 23, 2023
1e5b776
go mod
mtojek Jan 23, 2023
fb0ee73
CLI
mtojek Jan 24, 2023
0b426b8
Fix
mtojek Jan 24, 2023
d347b68
TestCreateValidateRichParameters
mtojek Jan 24, 2023
228d806
CLI tests
mtojek Jan 24, 2023
8c5bc6b
Merge branch 'main' into 5574-validation-rules
mtojek Jan 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions cli/cliui/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,21 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.Description, "\n"), "\n "))+"\n")
}

// TODO Implement full validation and show descriptions.
var err error
var value string
if len(templateVersionParameter.Options) > 0 {
// Move the cursor up a single line for nicer display!
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
value, err = Select(cmd, SelectOptions{
Options: templateVersionParameterOptionValues(templateVersionParameter),
var richParameterOption *codersdk.TemplateVersionParameterOption
richParameterOption, err = RichSelect(cmd, RichSelectOptions{
Options: templateVersionParameter.Options,
Default: templateVersionParameter.DefaultValue,
HideSearch: true,
})
if err == nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(value))
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
value = richParameterOption.Value
}
} else {
text := "Enter a value"
Expand All @@ -91,6 +92,9 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat

value, err = Prompt(cmd, PromptOptions{
Text: Styles.Bold.Render(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
},
})
value = strings.TrimSpace(value)
}
Expand All @@ -106,10 +110,9 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
return value, nil
}

func templateVersionParameterOptionValues(param codersdk.TemplateVersionParameter) []string {
var options []string
for _, opt := range param.Options {
options = append(options, opt.Value)
}
return options
func validateRichPrompt(value string, p codersdk.TemplateVersionParameter) error {
return codersdk.ValidateWorkspaceBuildParameter(p, codersdk.WorkspaceBuildParameter{
Name: p.Name,
Value: value,
})
}
39 changes: 39 additions & 0 deletions cli/cliui/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/codersdk"
)

func init() {
Expand Down Expand Up @@ -42,6 +45,42 @@ type SelectOptions struct {
HideSearch bool
}

type RichSelectOptions struct {
Options []codersdk.TemplateVersionParameterOption
Default string
Size int
HideSearch bool
}

// RichSelect displays a list of user options including name and description.
func RichSelect(cmd *cobra.Command, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
opts := make([]string, len(richOptions.Options))
for i, option := range richOptions.Options {
line := option.Name
if len(option.Description) > 0 {
line += ": " + option.Description
}
opts[i] = line
}

selected, err := Select(cmd, SelectOptions{
Options: opts,
Default: richOptions.Default,
Size: richOptions.Size,
HideSearch: richOptions.HideSearch,
})
if err != nil {
return nil, err
}

for i, option := range opts {
if option == selected {
return &richOptions.Options[i], nil
}
}
return nil, xerrors.Errorf("unknown option selected: %s", selected)
}

// Select displays a list of user options.
func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
// The survey library used *always* fails when testing on Windows,
Expand Down
44 changes: 44 additions & 0 deletions cli/cliui/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)

Expand Down Expand Up @@ -42,3 +43,46 @@ func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
}

func TestRichSelect(t *testing.T) {
t.Parallel()
t.Run("RichSelect", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
msgChan := make(chan string)
go func() {
resp, err := newRichSelect(ptty, cliui.RichSelectOptions{
Options: []codersdk.TemplateVersionParameterOption{
{
Name: "A-Name",
Value: "A-Value",
Description: "A-Description",
}, {
Name: "B-Name",
Value: "B-Value",
Description: "B-Description",
},
},
})
assert.NoError(t, err)
msgChan <- resp
}()
require.Equal(t, "A-Value", <-msgChan)
})
}

func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, error) {
value := ""
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
richOption, err := cliui.RichSelect(cmd, opts)
if err == nil {
value = richOption.Value
}
return err
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
}
162 changes: 162 additions & 0 deletions cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,168 @@ func TestCreateWithRichParameters(t *testing.T) {
})
}

func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()

const (
stringParameterName = "string_parameter"
stringParameterValue = "abc"

numberParameterName = "number_parameter"
numberParameterValue = "7"

boolParameterName = "bool_parameter"
boolParameterValue = "true"
)

numberRichParameters := []*proto.RichParameter{
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
}

stringRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
}

boolRichParameters := []*proto.RichParameter{
{Name: boolParameterName, Type: "bool", Mutable: true},
}

prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: richParameters,
},
},
}},
ProvisionApply: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
},
}
}

t.Run("ValidateString", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(stringRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)

template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)

cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()

matches := []string{
stringParameterName, "$$",
"does not match", "",
"Enter a value", "abc",
"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
})

t.Run("ValidateNumber", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(numberRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)

template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)

cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()

matches := []string{
numberParameterName, "12",
"is more than the maximum", "",
"Enter a value", "8",
"Confirm create?", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)

if value != "" {
pty.WriteLine(value)
}
}
<-doneChan
})

t.Run("ValidateBool", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(boolRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)

template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)

cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()

matches := []string{
boolParameterName, "cat",
"boolean value can be either", "",
"Enter a value", "true",
"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
})
}

func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
return []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Expand Down
Loading