Skip to content

feat: validate user input values the same way as "default" #381

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
211 changes: 136 additions & 75 deletions provider/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import (
"golang.org/x/xerrors"
)

var (
defaultValuePath = cty.Path{cty.GetAttrStep{Name: "default"}}
)

type Option struct {
Name string
Description string
Expand All @@ -46,7 +50,6 @@ const (
)

type Parameter struct {
Value string
Name string
DisplayName string `mapstructure:"display_name"`
Description string
Expand Down Expand Up @@ -82,7 +85,6 @@ func parameterDataSource() *schema.Resource {

var parameter Parameter
err = mapstructure.Decode(struct {
Value interface{}
Name interface{}
DisplayName interface{}
Description interface{}
Expand All @@ -97,7 +99,6 @@ func parameterDataSource() *schema.Resource {
Order interface{}
Ephemeral interface{}
}{
Value: rd.Get("value"),
Name: rd.Get("name"),
DisplayName: rd.Get("display_name"),
Description: rd.Get("description"),
Expand All @@ -122,14 +123,7 @@ func parameterDataSource() *schema.Resource {
if err != nil {
return diag.Errorf("decode parameter: %s", err)
}
var value string
if parameter.Default != "" {
err := valueIsType(parameter.Type, parameter.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
if err != nil {
return err
}
value = parameter.Default
}
value := parameter.Default
envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
if ok {
value = envValue
Expand All @@ -144,34 +138,22 @@ func parameterDataSource() *schema.Resource {
return diag.Errorf("ephemeral parameter requires the default property")
}

// TODO: Should we move this into the Valid() function on
// Parameter?
if len(parameter.Validation) == 1 {
validation := &parameter.Validation[0]
err = validation.Valid(parameter.Type, value)
if err != nil {
return diag.FromErr(err)
}
}

// Validate options
_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: "Invalid form_type for parameter",
Detail: err.Error(),
AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
},
}
// Do ValidateFormType up front. If there is no error, update the
// 'parameter.FormType' value to the new value. This is to handle default cases,
// since the default logic is more advanced than the sdk provider schema
// supports.
_, newFT, err := ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
if err == nil {
// If there is an error, parameter.Valid will catch it.
parameter.FormType = newFT

// Set the form_type back in case the value was changed.
// Eg via a default. If a user does not specify, a default value
// is used and saved.
rd.Set("form_type", parameter.FormType)
}
// Set the form_type back in case the value was changed.
// Eg via a default. If a user does not specify, a default value
// is used and saved.
rd.Set("form_type", parameter.FormType)

diags := parameter.Valid()
diags := parameter.Valid(value)
if diags.HasError() {
return diags
}
Expand Down Expand Up @@ -389,35 +371,38 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
return vArr, nil
}

func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
func valueIsType(typ OptionType, value string) error {
switch typ {
case OptionTypeNumber:
_, err := strconv.ParseFloat(value, 64)
if err != nil {
return diag.Errorf("%q is not a number", value)
return fmt.Errorf("%q is not a number", value)
}
case OptionTypeBoolean:
_, err := strconv.ParseBool(value)
if err != nil {
return diag.Errorf("%q is not a bool", value)
return fmt.Errorf("%q is not a bool", value)
}
case OptionTypeListString:
_, diags := valueIsListString(value, attrPath)
if diags.HasError() {
return diags
_, err := valueIsListString(value)
if err != nil {
return err
}
case OptionTypeString:
// Anything is a string!
default:
return diag.Errorf("invalid type %q", typ)
return fmt.Errorf("invalid type %q", typ)
}
return nil
}

func (v *Parameter) Valid() diag.Diagnostics {
func (v *Parameter) Valid(value string) diag.Diagnostics {
var err error
var optionType OptionType

// optionType might differ from parameter.Type. This is ok, and parameter.Type
// should be used for the value type, and optionType for options.
optionType, _, err := ValidateFormType(v.Type, len(v.Option), v.FormType)
optionType, v.FormType, err = ValidateFormType(v.Type, len(v.Option), v.FormType)
if err != nil {
return diag.Diagnostics{
{
Expand Down Expand Up @@ -452,61 +437,144 @@ func (v *Parameter) Valid() diag.Diagnostics {
},
}
}
diags := valueIsType(optionType, option.Value, cty.Path{})
if diags.HasError() {
return diags
err = valueIsType(optionType, option.Value)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
Detail: err.Error(),
},
}
}
optionValues[option.Value] = nil
optionNames[option.Name] = nil

// TODO: Option values should also be validated.
// v.validValue(option.Value, optionType, nil, cty.Path{})
}
}

// Validate the default value
if v.Default != "" {
err := valueIsType(v.Type, v.Default)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Default value is not of type %q", v.Type),
Detail: err.Error(),
AttributePath: defaultValuePath,
},
}
}

d := v.validValue(v.Default, optionType, optionValues, defaultValuePath)
if d.HasError() {
return d
}
}

if v.Default != "" && len(v.Option) > 0 {
// Value must always be validated
d := v.validValue(value, optionType, optionValues, cty.Path{})
if d.HasError() {
return d
}

err = valueIsType(v.Type, value)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Parameter value is not of type %q", v.Type),
Detail: err.Error(),
},
}
}

return nil
}

func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]any, path cty.Path) diag.Diagnostics {
// name is used for constructing more precise error messages.
name := "Value"
if path.Equals(defaultValuePath) {
name = "Default value"
}

// First validate if the value is a valid option
if len(optionValues) > 0 {
if v.Type == OptionTypeListString && optionType == OptionTypeString {
// If the type is list(string) and optionType is string, we have
// to ensure all elements of the default exist as options.
defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
if diags.HasError() {
return diags
listValues, err := valueIsListString(value)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: "When using list(string) type, value must be a json encoded list of strings",
Detail: err.Error(),
AttributePath: defaultValuePath,
},
}
}

// missing is used to construct a more helpful error message
var missing []string
for _, defaultValue := range defaultValues {
_, defaultIsValid := optionValues[defaultValue]
if !defaultIsValid {
missing = append(missing, defaultValue)
for _, listValue := range listValues {
_, isValid := optionValues[listValue]
if !isValid {
missing = append(missing, listValue)
}
}

if len(missing) > 0 {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: "Default values must be a valid option",
Summary: fmt.Sprintf("%ss must be a valid option", name),
Detail: fmt.Sprintf(
"default value %q is not a valid option, values %q are missing from the options",
v.Default, strings.Join(missing, ", "),
"%s %q is not a valid option, values %q are missing from the options",
name, value, strings.Join(missing, ", "),
),
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
AttributePath: defaultValuePath,
},
}
}
} else {
_, defaultIsValid := optionValues[v.Default]
if !defaultIsValid {
_, isValid := optionValues[value]
if !isValid {
extra := ""
if value == "" {
extra = ". The value is empty, did you forget to set it with a default or from user input?"
}
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: "Default value must be a valid option",
Detail: fmt.Sprintf("the value %q must be defined as one of options", v.Default),
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
Summary: fmt.Sprintf("%s must be a valid option%s", name, extra),
Detail: fmt.Sprintf("the value %q must be defined as one of options", value),
AttributePath: path,
},
}
}
}
}

if len(v.Validation) == 1 {
validCheck := &v.Validation[0]
err := validCheck.Valid(v.Type, value)
if err != nil {
return diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Invalid parameter %s according to 'validation' block", strings.ToLower(name)),
Detail: err.Error(),
AttributePath: path,
},
}
}
}

return nil
}

Expand Down Expand Up @@ -570,18 +638,11 @@ func (v *Validation) Valid(typ OptionType, value string) error {
return nil
}

func valueIsListString(value string, path cty.Path) ([]string, diag.Diagnostics) {
func valueIsListString(value string) ([]string, error) {
var items []string
err := json.Unmarshal([]byte(value), &items)
if err != nil {
return nil, diag.Diagnostics{
{
Severity: diag.Error,
Summary: "When using list(string) type, value must be a json encoded list of strings",
Detail: fmt.Sprintf("value %q is not a valid list of strings", value),
AttributePath: path,
},
}
return nil, fmt.Errorf("value %q is not a valid list of strings", value)
}
return items, nil
}
Expand Down
Loading
Loading