From f9d70ae35050188b9062464112db0ea984174ea8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 6 May 2025 11:59:36 -0500 Subject: [PATCH 1/3] feat: enforce monotonicity in terraform provider Previous value must come from env var. To read tfstate requires changing param from a `data` block to a `resource` block --- provider/parameter.go | 49 ++++++++++++++++++++++--- provider/parameter_test.go | 75 ++++++++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index fd48457..3e52c47 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -144,7 +144,13 @@ func parameterDataSource() *schema.Resource { input = &envValue } - value, diags := parameter.ValidateInput(input) + var previous *string + envPreviousValue, ok := os.LookupEnv(ParameterEnvironmentVariablePrevious(parameter.Name)) + if ok { + previous = &envPreviousValue + } + + value, diags := parameter.ValidateInput(input, previous) if diags.HasError() { return diags } @@ -395,7 +401,7 @@ func valueIsType(typ OptionType, value string) error { return nil } -func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) { +func (v *Parameter) ValidateInput(input *string, previous *string) (string, diag.Diagnostics) { var err error var optionType OptionType @@ -442,7 +448,7 @@ func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) { forcedValue = *value } - d := v.validValue(forcedValue, optionType, optionValues, valuePath) + d := v.validValue(forcedValue, previous, optionType, optionValues, valuePath) if d.HasError() { return "", d } @@ -506,7 +512,7 @@ func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, di return optionValues, nil } -func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics { +func (v *Parameter) validValue(value string, previous *string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics { // name is used for constructing more precise error messages. name := "Value" if path.Equals(defaultValuePath) { @@ -573,7 +579,7 @@ func (v *Parameter) validValue(value string, optionType OptionType, optionValues if len(v.Validation) == 1 { validCheck := &v.Validation[0] - err := validCheck.Valid(v.Type, value) + err := validCheck.Valid(v.Type, value, previous) if err != nil { return diag.Diagnostics{ { @@ -589,7 +595,7 @@ func (v *Parameter) validValue(value string, optionType OptionType, optionValues return nil } -func (v *Validation) Valid(typ OptionType, value string) error { +func (v *Validation) Valid(typ OptionType, value string, previous *string) error { if typ != OptionTypeNumber { if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) @@ -639,6 +645,28 @@ func (v *Validation) Valid(typ OptionType, value string) error { if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing { return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing) } + + switch v.Monotonic { + case "": + // No monotonicity check + case ValidationMonotonicIncreasing, ValidationMonotonicDecreasing: + if previous != nil { // Only check if previous value exists + previousNum, err := strconv.Atoi(*previous) + if err != nil { + return fmt.Errorf("previous value %q is not a number", *previous) + } + + if v.Monotonic == ValidationMonotonicIncreasing && !(num >= previousNum) { + return fmt.Errorf("parameter value '%d' must be equal or greater than previous value: %d", num, previousNum) + } + + if v.Monotonic == ValidationMonotonicDecreasing && !(num <= previousNum) { + return fmt.Errorf("parameter value '%d' must be equal or lower than previous value: %d", num, previousNum) + } + } + default: + return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing) + } case OptionTypeListString: var listOfStrings []string err := json.Unmarshal([]byte(value), &listOfStrings) @@ -666,6 +694,15 @@ func ParameterEnvironmentVariable(name string) string { return "CODER_PARAMETER_" + hex.EncodeToString(sum[:]) } +// ParameterEnvironmentVariablePrevious returns the environment variable to +// specify for a parameter's previous value. This is used for workspace +// subsequent builds after the first. Primarily to validate monotonicity in the +// `validation` block. +func ParameterEnvironmentVariablePrevious(name string) string { + sum := sha256.Sum256([]byte(name)) + return "CODER_PARAMETER_PREVIOUS_" + hex.EncodeToString(sum[:]) +} + func takeFirstError(errs ...error) error { for _, err := range errs { if err != nil { diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 21842b6..7fe81e8 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -839,7 +839,7 @@ func TestParameterValidation(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { t.Parallel() value := &tc.Value - _, diags := tc.Parameter.ValidateInput(value) + _, diags := tc.Parameter.ValidateInput(value, nil) if tc.ExpectError != nil { require.True(t, diags.HasError()) errMsg := fmt.Sprintf("%+v", diags[0]) // close enough @@ -919,11 +919,19 @@ func TestParameterValidationEnforcement(t *testing.T) { var validation *provider.Validation if columns[5] != "" { - // Min-Max validation should look like: - // 1-10 :: min=1, max=10 - // -10 :: max=10 - // 1- :: min=1 - if validMinMax.MatchString(columns[5]) { + switch { + case columns[5] == provider.ValidationMonotonicIncreasing || columns[5] == provider.ValidationMonotonicDecreasing: + validation = &provider.Validation{ + MinDisabled: true, + MaxDisabled: true, + Monotonic: columns[5], + Error: "monotonicity", + } + case validMinMax.MatchString(columns[5]): + // Min-Max validation should look like: + // 1-10 :: min=1, max=10 + // -10 :: max=10 + // 1- :: min=1 parts := strings.Split(columns[5], "-") min, _ := strconv.ParseInt(parts[0], 10, 64) max, _ := strconv.ParseInt(parts[1], 10, 64) @@ -936,7 +944,7 @@ func TestParameterValidationEnforcement(t *testing.T) { Regex: "", Error: "{min} < {value} < {max}", } - } else { + default: validation = &provider.Validation{ Min: 0, MinDisabled: true, @@ -1067,6 +1075,7 @@ func TestValueValidatesType(t *testing.T) { Name string Type provider.OptionType Value string + Previous *string Regex string RegexError string Min int @@ -1154,6 +1163,56 @@ func TestValueValidatesType(t *testing.T) { Min: 0, Max: 2, Monotonic: "decreasing", + }, { + Name: "IncreasingMonotonicityEqual", + Type: "number", + Previous: ptr("1"), + Value: "1", + Monotonic: "increasing", + MinDisabled: true, + MaxDisabled: true, + }, { + Name: "DecreasingMonotonicityEqual", + Type: "number", + Value: "1", + Previous: ptr("1"), + Monotonic: "decreasing", + MinDisabled: true, + MaxDisabled: true, + }, { + Name: "IncreasingMonotonicityGreater", + Type: "number", + Previous: ptr("0"), + Value: "1", + Monotonic: "increasing", + MinDisabled: true, + MaxDisabled: true, + }, { + Name: "DecreasingMonotonicityGreater", + Type: "number", + Value: "1", + Previous: ptr("0"), + Monotonic: "decreasing", + MinDisabled: true, + MaxDisabled: true, + Error: regexp.MustCompile("must be equal or"), + }, { + Name: "IncreasingMonotonicityLesser", + Type: "number", + Previous: ptr("2"), + Value: "1", + Monotonic: "increasing", + MinDisabled: true, + MaxDisabled: true, + Error: regexp.MustCompile("must be equal or"), + }, { + Name: "DecreasingMonotonicityLesser", + Type: "number", + Value: "1", + Previous: ptr("2"), + Monotonic: "decreasing", + MinDisabled: true, + MaxDisabled: true, }, { Name: "ValidListOfStrings", Type: "list(string)", @@ -1205,7 +1264,7 @@ func TestValueValidatesType(t *testing.T) { Regex: tc.Regex, Error: tc.RegexError, } - err := v.Valid(tc.Type, tc.Value) + err := v.Valid(tc.Type, tc.Value, tc.Previous) if tc.Error != nil { require.Error(t, err) require.True(t, tc.Error.MatchString(err.Error()), "got: %s", err.Error()) From 6f94a4beec39e158b36f544cdd37bd16b5869be6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 6 May 2025 13:47:15 -0500 Subject: [PATCH 2/3] add unit test cases for previous values --- provider/parameter_test.go | 51 +++++--- provider/testdata/parameter_table.md | 173 ++++++++++++++------------- 2 files changed, 126 insertions(+), 98 deletions(-) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 7fe81e8..9b5e76f 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -881,6 +881,7 @@ func TestParameterValidationEnforcement(t *testing.T) { OutputValue string Optional bool CreateError *regexp.Regexp + Previous *string } rows := make([]row, 0) @@ -898,41 +899,44 @@ func TestParameterValidationEnforcement(t *testing.T) { continue // Skip rows with empty names } - optional, err := strconv.ParseBool(columns[8]) - if columns[8] != "" { + cname, ctype, cprev, cinput, cdefault, coptions, cvalidation, _, coutput, coptional, cerr := + columns[0], columns[1], columns[2], columns[3], columns[4], columns[5], columns[6], columns[7], columns[8], columns[9], columns[10] + + optional, err := strconv.ParseBool(coptional) + if coptional != "" { // Value does not matter if not specified require.NoError(t, err) } var rerr *regexp.Regexp - if columns[9] != "" { - rerr, err = regexp.Compile(columns[9]) + if cerr != "" { + rerr, err = regexp.Compile(cerr) if err != nil { - t.Fatalf("failed to parse error column %q: %v", columns[9], err) + t.Fatalf("failed to parse error column %q: %v", cerr, err) } } var options []string - if columns[4] != "" { - options = strings.Split(columns[4], ",") + if coptions != "" { + options = strings.Split(coptions, ",") } var validation *provider.Validation - if columns[5] != "" { + if cvalidation != "" { switch { - case columns[5] == provider.ValidationMonotonicIncreasing || columns[5] == provider.ValidationMonotonicDecreasing: + case cvalidation == provider.ValidationMonotonicIncreasing || cvalidation == provider.ValidationMonotonicDecreasing: validation = &provider.Validation{ MinDisabled: true, MaxDisabled: true, - Monotonic: columns[5], + Monotonic: cvalidation, Error: "monotonicity", } - case validMinMax.MatchString(columns[5]): + case validMinMax.MatchString(cvalidation): // Min-Max validation should look like: // 1-10 :: min=1, max=10 // -10 :: max=10 // 1- :: min=1 - parts := strings.Split(columns[5], "-") + parts := strings.Split(cvalidation, "-") min, _ := strconv.ParseInt(parts[0], 10, 64) max, _ := strconv.ParseInt(parts[1], 10, 64) validation = &provider.Validation{ @@ -951,22 +955,30 @@ func TestParameterValidationEnforcement(t *testing.T) { Max: 0, MaxDisabled: true, Monotonic: "", - Regex: columns[5], + Regex: cvalidation, Error: "regex error", } } } + var prev *string + if cprev != "" { + prev = ptr(cprev) + if cprev == `""` { + prev = ptr("") + } + } rows = append(rows, row{ - Name: columns[0], - Types: strings.Split(columns[1], ","), - InputValue: columns[2], - Default: columns[3], + Name: cname, + Types: strings.Split(ctype, ","), + InputValue: cinput, + Default: cdefault, Options: options, Validation: validation, - OutputValue: columns[7], + OutputValue: coutput, Optional: optional, CreateError: rerr, + Previous: prev, }) } @@ -984,6 +996,9 @@ func TestParameterValidationEnforcement(t *testing.T) { if row.InputValue != "" { t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue) } + if row.Previous != nil { + t.Setenv(provider.ParameterEnvironmentVariablePrevious("parameter"), *row.Previous) + } if row.CreateError != nil && row.OutputValue != "" { t.Errorf("output value %q should not be set if both errors are set", row.OutputValue) diff --git a/provider/testdata/parameter_table.md b/provider/testdata/parameter_table.md index 3df16f0..e14c08b 100644 --- a/provider/testdata/parameter_table.md +++ b/provider/testdata/parameter_table.md @@ -1,80 +1,93 @@ -| Name | Type | Input | Default | Options | Validation | -> | Output | Optional | ErrorCreate | -|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|-----------------| -| | Empty Vals | | | | | | | | | -| Empty | string,number | | | | | | "" | false | | -| EmptyDupeOps | string,number | | | 1,1,1 | | | | | unique | -| EmptyList | list(string) | | | | | | "" | false | | -| EmptyListDupeOpts | list(string) | | | ["a"],["a"] | | | | | unique | -| EmptyMulti | tag-select | | | | | | "" | false | | -| EmptyOpts | string,number | | | 1,2,3 | | | "" | false | | -| EmptyRegex | string | | | | world | | | | regex error | -| EmptyMin | number | | | | 1-10 | | | | 1 < < 10 | -| EmptyMinOpt | number | | | 1,2,3 | 2-5 | | | | valid option | -| EmptyRegexOpt | string | | | "hello","goodbye" | goodbye | | | | valid option | -| EmptyRegexOk | string | | | | .* | | "" | false | | -| | | | | | | | | | | -| | Default Set | No inputs | | | | | | | | -| NumDef | number | | 5 | | | | 5 | true | | -| NumDefVal | number | | 5 | | 3-7 | | 5 | true | | -| NumDefInv | number | | 5 | | 10- | | | | 10 < 5 < 0 | -| NumDefOpts | number | | 5 | 1,3,5,7 | 2-6 | | 5 | true | | -| NumDefNotOpts | number | | 5 | 1,3,7,9 | 2-6 | | | | valid option | -| NumDefInvOpt | number | | 5 | 1,3,5,7 | 6-10 | | | | 6 < 5 < 10 | -| NumDefNotNum | number | | a | | | | | | type "number" | -| NumDefOptsNotNum | number | | 1 | 1,a,2 | | | | | type "number" | -| | | | | | | | | | | -| StrDef | string | | hello | | | | hello | true | | -| StrDefInv | string | | hello | | world | | | | regex error | -| StrDefOpts | string | | a | a,b,c | | | a | true | | -| StrDefNotOpts | string | | a | b,c,d | | | | | valid option | -| StrDefValOpts | string | | a | a,b,c,d,e,f | [a-c] | | a | true | | -| StrDefInvOpt | string | | d | a,b,c,d,e,f | [a-c] | | | | regex error | -| | | | | | | | | | | -| LStrDef | list(string) | | ["a"] | | | | ["a"] | true | | -| LStrDefOpts | list(string) | | ["a"] | ["a"], ["b"] | | | ["a"] | true | | -| LStrDefNotOpts | list(string) | | ["a"] | ["b"], ["c"] | | | | | valid option | -| | | | | | | | | | | -| MulDef | tag-select | | ["a"] | | | | ["a"] | true | | -| MulDefOpts | multi-select | | ["a"] | a,b | | | ["a"] | true | | -| MulDefNotOpts | multi-select | | ["a"] | b,c | | | | | valid option | -| | | | | | | | | | | -| | Input Vals | | | | | | | | | -| NumIns | number | 3 | | | | | 3 | false | | -| NumInsOptsNaN | number | 3 | 5 | a,1,2,3,4,5 | 1-3 | | | | type "number" | -| NumInsNotNum | number | a | | | | | | | type "number" | -| NumInsNotNumInv | number | a | | | 1-3 | | | | 1 < a < 3 | -| NumInsDef | number | 3 | 5 | | | | 3 | true | | -| NumIns/DefInv | number | 3 | 5 | | 1-3 | | 3 | true | | -| NumIns=DefInv | number | 5 | 5 | | 1-3 | | | | 1 < 5 < 3 | -| NumInsOpts | number | 3 | 5 | 1,2,3,4,5 | 1-3 | | 3 | true | | -| NumInsNotOptsVal | number | 3 | 5 | 1,2,4,5 | 1-3 | | | | valid option | -| NumInsNotOptsInv | number | 3 | 5 | 1,2,4,5 | 1-2 | | | true | valid option | -| NumInsNotOpts | number | 3 | 5 | 1,2,4,5 | | | | | valid option | -| NumInsNotOpts/NoDef | number | 3 | | 1,2,4,5 | | | | | valid option | -| | | | | | | | | | | -| StrIns | string | c | | | | | c | false | | -| StrInsDupeOpts | string | c | | a,b,c,c | | | | | unique | -| StrInsDef | string | c | e | | | | c | true | | -| StrIns/DefInv | string | c | e | | [a-c] | | c | true | | -| StrIns=DefInv | string | e | e | | [a-c] | | | | regex error | -| StrInsOpts | string | c | e | a,b,c,d,e | [a-c] | | c | true | | -| StrInsNotOptsVal | string | c | e | a,b,d,e | [a-c] | | | | valid option | -| StrInsNotOptsInv | string | c | e | a,b,d,e | [a-b] | | | | valid option | -| StrInsNotOpts | string | c | e | a,b,d,e | | | | | valid option | -| StrInsNotOpts/NoDef | string | c | | a,b,d,e | | | | | valid option | -| StrInsBadVal | string | c | | a,b,c,d,e | 1-10 | | | | min cannot | -| | | | | | | | | | | -| | list(string) | | | | | | | | | -| LStrIns | list(string) | ["c"] | | | | | ["c"] | false | | -| LStrInsNotList | list(string) | c | | | | | | | list of strings | -| LStrInsDef | list(string) | ["c"] | ["e"] | | | | ["c"] | true | | -| LStrIns/DefInv | list(string) | ["c"] | ["e"] | | [a-c] | | | | regex cannot | -| LStrInsOpts | list(string) | ["c"] | ["e"] | ["c"],["d"],["e"] | | | ["c"] | true | | -| LStrInsNotOpts | list(string) | ["c"] | ["e"] | ["d"],["e"] | | | | | valid option | -| LStrInsNotOpts/NoDef | list(string) | ["c"] | | ["d"],["e"] | | | | | valid option | -| | | | | | | | | | | -| MulInsOpts | multi-select | ["c"] | ["e"] | c,d,e | | | ["c"] | true | | -| MulInsNotListOpts | multi-select | c | ["e"] | c,d,e | | | | | json encoded | -| MulInsNotOpts | multi-select | ["c"] | ["e"] | d,e | | | | | valid option | -| MulInsNotOpts/NoDef | multi-select | ["c"] | | d,e | | | | | valid option | -| MulInsInvOpts | multi-select | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot | \ No newline at end of file +| Name | Type | Previous | Input | Default | Options | Validation | -> | Output | Optional | ErrorCreate | +|----------------------|---------------|----------|-----------|---------|-------------------|------------|----|--------|----------|-----------------| +| | Empty Vals | | | | | | | | | | +| Empty | string,number | | | | | | | "" | false | | +| EmptyDupeOps | string,number | | | | 1,1,1 | | | | | unique | +| EmptyList | list(string) | | | | | | | "" | false | | +| EmptyListDupeOpts | list(string) | | | | ["a"],["a"] | | | | | unique | +| EmptyMulti | tag-select | | | | | | | "" | false | | +| EmptyOpts | string,number | | | | 1,2,3 | | | "" | false | | +| EmptyRegex | string | | | | | world | | | | regex error | +| EmptyMin | number | | | | | 1-10 | | | | 1 < < 10 | +| EmptyMinOpt | number | | | | 1,2,3 | 2-5 | | | | valid option | +| EmptyRegexOpt | string | | | | "hello","goodbye" | goodbye | | | | valid option | +| EmptyRegexOk | string | | | | | .* | | "" | false | | +| | | | | | | | | | | | +| | Default Set | | No inputs | | | | | | | | +| NumDef | number | | | 5 | | | | 5 | true | | +| NumDefVal | number | | | 5 | | 3-7 | | 5 | true | | +| NumDefInv | number | | | 5 | | 10- | | | | 10 < 5 < 0 | +| NumDefOpts | number | | | 5 | 1,3,5,7 | 2-6 | | 5 | true | | +| NumDefNotOpts | number | | | 5 | 1,3,7,9 | 2-6 | | | | valid option | +| NumDefInvOpt | number | | | 5 | 1,3,5,7 | 6-10 | | | | 6 < 5 < 10 | +| NumDefNotNum | number | | | a | | | | | | type "number" | +| NumDefOptsNotNum | number | | | 1 | 1,a,2 | | | | | type "number" | +| NumDefInc | number | 4 | | 5 | | increasing | | 5 | true | | +| NumDefIncBad | number | 6 | | 5 | | increasing | | | | greater | +| NumDefDec | number | 6 | | 5 | | decreasing | | 5 | true | | +| NumDefDecBad | number | 4 | | 5 | | decreasing | | | | lower | +| NumDefDecEq | number | 5 | | 5 | | decreasing | | 5 | true | | +| NumDefIncEq | number | 5 | | 5 | | increasing | | 5 | true | | +| | | | | | | | | | | | +| StrDef | string | | | hello | | | | hello | true | | +| StrMonotonicity | string | | | hello | | increasing | | | | monotonic | +| StrDefInv | string | | | hello | | world | | | | regex error | +| StrDefOpts | string | | | a | a,b,c | | | a | true | | +| StrDefNotOpts | string | | | a | b,c,d | | | | | valid option | +| StrDefValOpts | string | | | a | a,b,c,d,e,f | [a-c] | | a | true | | +| StrDefInvOpt | string | | | d | a,b,c,d,e,f | [a-c] | | | | regex error | +| | | | | | | | | | | | +| LStrDef | list(string) | | | ["a"] | | | | ["a"] | true | | +| LStrDefOpts | list(string) | | | ["a"] | ["a"], ["b"] | | | ["a"] | true | | +| LStrDefNotOpts | list(string) | | | ["a"] | ["b"], ["c"] | | | | | valid option | +| | | | | | | | | | | | +| MulDef | tag-select | | | ["a"] | | | | ["a"] | true | | +| MulDefOpts | multi-select | | | ["a"] | a,b | | | ["a"] | true | | +| MulDefNotOpts | multi-select | | | ["a"] | b,c | | | | | valid option | +| | | | | | | | | | | | +| | Input Vals | | | | | | | | | | +| NumIns | number | | 3 | | | | | 3 | false | | +| NumInsOptsNaN | number | | 3 | 5 | a,1,2,3,4,5 | 1-3 | | | | type "number" | +| NumInsNotNum | number | | a | | | | | | | type "number" | +| NumInsNotNumInv | number | | a | | | 1-3 | | | | 1 < a < 3 | +| NumInsDef | number | | 3 | 5 | | | | 3 | true | | +| NumIns/DefInv | number | | 3 | 5 | | 1-3 | | 3 | true | | +| NumIns=DefInv | number | | 5 | 5 | | 1-3 | | | | 1 < 5 < 3 | +| NumInsOpts | number | | 3 | 5 | 1,2,3,4,5 | 1-3 | | 3 | true | | +| NumInsNotOptsVal | number | | 3 | 5 | 1,2,4,5 | 1-3 | | | | valid option | +| NumInsNotOptsInv | number | | 3 | 5 | 1,2,4,5 | 1-2 | | | true | valid option | +| NumInsNotOpts | number | | 3 | 5 | 1,2,4,5 | | | | | valid option | +| NumInsNotOpts/NoDef | number | | 3 | | 1,2,4,5 | | | | | valid option | +| NumInsInc | number | 4 | 5 | 3 | | increasing | | 5 | true | | +| NumInsIncBad | number | 6 | 5 | 7 | | increasing | | | | greater | +| NumInsDec | number | 6 | 5 | 7 | | decreasing | | 5 | true | | +| NumInsDecBad | number | 4 | 5 | 3 | | decreasing | | | | lower | +| NumInsDecEq | number | 5 | 5 | 5 | | decreasing | | 5 | true | | +| NumInsIncEq | number | 5 | 5 | 5 | | increasing | | 5 | true | | +| | | | | | | | | | | | +| StrIns | string | | c | | | | | c | false | | +| StrInsDupeOpts | string | | c | | a,b,c,c | | | | | unique | +| StrInsDef | string | | c | e | | | | c | true | | +| StrIns/DefInv | string | | c | e | | [a-c] | | c | true | | +| StrIns=DefInv | string | | e | e | | [a-c] | | | | regex error | +| StrInsOpts | string | | c | e | a,b,c,d,e | [a-c] | | c | true | | +| StrInsNotOptsVal | string | | c | e | a,b,d,e | [a-c] | | | | valid option | +| StrInsNotOptsInv | string | | c | e | a,b,d,e | [a-b] | | | | valid option | +| StrInsNotOpts | string | | c | e | a,b,d,e | | | | | valid option | +| StrInsNotOpts/NoDef | string | | c | | a,b,d,e | | | | | valid option | +| StrInsBadVal | string | | c | | a,b,c,d,e | 1-10 | | | | min cannot | +| | | | | | | | | | | | +| | list(string) | | | | | | | | | | +| LStrIns | list(string) | | ["c"] | | | | | ["c"] | false | | +| LStrInsNotList | list(string) | | c | | | | | | | list of strings | +| LStrInsDef | list(string) | | ["c"] | ["e"] | | | | ["c"] | true | | +| LStrIns/DefInv | list(string) | | ["c"] | ["e"] | | [a-c] | | | | regex cannot | +| LStrInsOpts | list(string) | | ["c"] | ["e"] | ["c"],["d"],["e"] | | | ["c"] | true | | +| LStrInsNotOpts | list(string) | | ["c"] | ["e"] | ["d"],["e"] | | | | | valid option | +| LStrInsNotOpts/NoDef | list(string) | | ["c"] | | ["d"],["e"] | | | | | valid option | +| | | | | | | | | | | | +| MulInsOpts | multi-select | | ["c"] | ["e"] | c,d,e | | | ["c"] | true | | +| MulInsNotListOpts | multi-select | | c | ["e"] | c,d,e | | | | | json encoded | +| MulInsNotOpts | multi-select | | ["c"] | ["e"] | d,e | | | | | valid option | +| MulInsNotOpts/NoDef | multi-select | | ["c"] | | d,e | | | | | valid option | +| MulInsInvOpts | multi-select | | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot | \ No newline at end of file From f2a4d107a14e954fed9086289300f6e235b96d30 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 8 May 2025 09:30:19 -0500 Subject: [PATCH 3/3] allow NaN previous values to prevent any regressions --- provider/parameter.go | 8 +++++++- provider/testdata/parameter_table.md | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/provider/parameter.go b/provider/parameter.go index 3e52c47..f0a26c9 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -653,7 +653,13 @@ func (v *Validation) Valid(typ OptionType, value string, previous *string) error if previous != nil { // Only check if previous value exists previousNum, err := strconv.Atoi(*previous) if err != nil { - return fmt.Errorf("previous value %q is not a number", *previous) + // Do not throw an error for the previous value not being a number. Throwing an + // error here would cause an unrepairable state for the user. This is + // unfortunate, but there is not much we can do at this point. + // TODO: Maybe we should enforce this, and have the calling coderd + // do something to resolve it. Such as doing this check before calling + // terraform apply. + break } if v.Monotonic == ValidationMonotonicIncreasing && !(num >= previousNum) { diff --git a/provider/testdata/parameter_table.md b/provider/testdata/parameter_table.md index e14c08b..6087460 100644 --- a/provider/testdata/parameter_table.md +++ b/provider/testdata/parameter_table.md @@ -28,6 +28,8 @@ | NumDefDecBad | number | 4 | | 5 | | decreasing | | | | lower | | NumDefDecEq | number | 5 | | 5 | | decreasing | | 5 | true | | | NumDefIncEq | number | 5 | | 5 | | increasing | | 5 | true | | +| NumDefIncNaN | number | a | | 5 | | increasing | | 5 | true | | +| NumDefDecNaN | number | b | | 5 | | decreasing | | 5 | true | | | | | | | | | | | | | | | StrDef | string | | | hello | | | | hello | true | | | StrMonotonicity | string | | | hello | | increasing | | | | monotonic |