diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index 6b4b7c09..c54e54ed 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -63,4 +63,9 @@ Optional: - `monotonic` (String) Number monotonicity, either increasing or decreasing. - `regex` (String) A regex for the input parameter to match against. +Read-Only: + +- `max_disabled` (Boolean) Helper field to check if max is present +- `min_disabled` (Boolean) Helper field to check if min is present + diff --git a/provider/decode_test.go b/provider/decode_test.go index ca3feccd..de56922c 100644 --- a/provider/decode_test.go +++ b/provider/decode_test.go @@ -3,10 +3,11 @@ package provider_test import ( "testing" - "github.com/coder/terraform-provider-coder/provider" "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/coder/terraform-provider-coder/provider" ) func TestDecode(t *testing.T) { @@ -23,11 +24,12 @@ func TestDecode(t *testing.T) { "display_name": displayName, "legacy_variable": legacyVariable, "legacy_variable_name": legacyVariableName, - "min": nil, "validation": []map[string]interface{}{ { - "min": nil, - "max": 5, + "min": nil, + "min_disabled": false, + "max": 5, + "max_disabled": true, }, }, } @@ -38,6 +40,8 @@ func TestDecode(t *testing.T) { assert.Equal(t, displayName, param.DisplayName) assert.Equal(t, legacyVariable, param.LegacyVariable) assert.Equal(t, legacyVariableName, param.LegacyVariableName) - assert.Equal(t, (*int)(nil), param.Validation[0].Min) - assert.Equal(t, 5, *param.Validation[0].Max) + assert.Equal(t, 5, param.Validation[0].Max) + assert.True(t, param.Validation[0].MaxDisabled) + assert.Equal(t, 0, param.Validation[0].Min) + assert.False(t, param.Validation[0].MinDisabled) } diff --git a/provider/parameter.go b/provider/parameter.go index 79ea2480..f736579b 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -28,8 +28,11 @@ type Option struct { } type Validation struct { - Min *int - Max *int + Min int + MinDisabled bool `mapstructure:"min_disabled"` + Max int + MaxDisabled bool `mapstructure:"max_disabled"` + Monotonic string Regex string @@ -288,11 +291,21 @@ func parameterDataSource() *schema.Resource { Optional: true, Description: "The minimum of a number parameter.", }, + "min_disabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Helper field to check if min is present", + }, "max": { Type: schema.TypeInt, Optional: true, Description: "The maximum of a number parameter.", }, + "max_disabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Helper field to check if max is present", + }, "monotonic": { Type: schema.TypeString, Optional: true, @@ -363,13 +376,8 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int return nil, xerrors.New("validation rule should be a map") } - // Fix the resource data - if rawValidationRule["min"].IsNull() { - validationRule["min"] = nil - } - if rawValidationRule["max"].IsNull() { - validationRule["max"] = nil - } + validationRule["min_disabled"] = rawValidationRule["min"].IsNull() + validationRule["max_disabled"] = rawValidationRule["max"].IsNull() return vArr, nil } @@ -401,10 +409,10 @@ func valueIsType(typ, value string) diag.Diagnostics { func (v *Validation) Valid(typ, value string) error { if typ != "number" { - if v.Min != nil { + if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) } - if v.Max != nil { + if !v.MaxDisabled { return fmt.Errorf("a max cannot be specified for a %s type", typ) } } @@ -437,11 +445,11 @@ func (v *Validation) Valid(typ, value string) error { if err != nil { return fmt.Errorf("value %q is not a number", value) } - if v.Min != nil && num < *v.Min { - return fmt.Errorf("value %d is less than the minimum %d", num, *v.Min) + if !v.MinDisabled && num < v.Min { + return fmt.Errorf("value %d is less than the minimum %d", num, v.Min) } - if v.Max != nil && num > *v.Max { - return fmt.Errorf("value %d is more than the maximum %d", num, *v.Max) + if !v.MaxDisabled && num > v.Max { + return fmt.Errorf("value %d is more than the maximum %d", num, v.Max) } if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing { return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 945820a1..d749592b 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -109,30 +109,20 @@ func TestParameter(t *testing.T) { } } `, - }, { - Name: "NumberValidation_Min", - Config: ` - data "coder_parameter" "region" { - name = "Region" - type = "number" - default = 2 - validation { - min = 1 - } - } - `, - }, { - Name: "NumberValidation_Max", - Config: ` - data "coder_parameter" "region" { - name = "Region" - type = "number" - default = 2 - validation { - max = 9 - } + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "2", + "validation.0.min": "1", + "validation.0.max": "5", + "validation.0.min_disabled": "false", + "validation.0.max_disabled": "false", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) } - `, + }, }, { Name: "DefaultNotNumber", Config: ` @@ -430,6 +420,186 @@ data "coder_parameter" "region" { require.Equal(t, expected, attributeValue) } }, + }, { + Name: "NumberValidation_Max", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 2 + validation { + max = 9 + } + } + `, + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "2", + "validation.0.max": "9", + "validation.0.min_disabled": "true", + "validation.0.max_disabled": "false", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) + } + }, + }, { + Name: "NumberValidation_MaxZero", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = -1 + validation { + max = 0 + } + } + `, + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "-1", + "validation.0.max": "0", + "validation.0.min_disabled": "true", + "validation.0.max_disabled": "false", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) + } + }, + }, { + Name: "NumberValidation_Min", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 2 + validation { + min = 1 + } + } + `, + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "2", + "validation.0.min": "1", + "validation.0.min_disabled": "false", + "validation.0.max_disabled": "true", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) + } + }, + }, { + Name: "NumberValidation_MinZero", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 2 + validation { + min = 0 + } + } + `, + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "2", + "validation.0.min": "0", + "validation.0.min_disabled": "false", + "validation.0.max_disabled": "true", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) + } + }, + }, { + Name: "NumberValidation_MinMaxZero", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 0 + validation { + max = 0 + min = 0 + } + } + `, + Check: func(state *terraform.ResourceState) { + for key, expected := range map[string]string{ + "name": "Region", + "type": "number", + "validation.#": "1", + "default": "0", + "validation.0.min": "0", + "validation.0.max": "0", + "validation.0.min_disabled": "false", + "validation.0.max_disabled": "false", + } { + require.Equal(t, expected, state.Primary.Attributes[key]) + } + }, + }, { + Name: "NumberValidation_LesserThanMin", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 5 + validation { + min = 7 + } + } + `, + ExpectError: regexp.MustCompile("is less than the minimum"), + }, { + Name: "NumberValidation_GreaterThanMin", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 5 + validation { + max = 3 + } + } + `, + ExpectError: regexp.MustCompile("is more than the maximum"), + }, { + Name: "NumberValidation_NotInRange", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 8 + validation { + min = 3 + max = 5 + } + } + `, + ExpectError: regexp.MustCompile("is more than the maximum"), + }, { + Name: "NumberValidation_BoolWithMin", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "bool" + default = true + validation { + min = 7 + } + } + `, + ExpectError: regexp.MustCompile("a min cannot be specified for a bool type"), }} { tc := tc t.Run(tc.Name, func(t *testing.T) { @@ -467,102 +637,121 @@ func TestValueValidatesType(t *testing.T) { Regex, RegexError string Min, - Max *int - Monotonic string - Error *regexp.Regexp + Max int + MinDisabled, MaxDisabled bool + Monotonic string + Error *regexp.Regexp }{{ - Name: "StringWithMin", - Type: "string", - Min: ptrNumber(1), - Error: regexp.MustCompile("cannot be specified"), - }, { - Name: "StringWithMax", - Type: "string", - Max: ptrNumber(1), - Error: regexp.MustCompile("cannot be specified"), + Name: "StringWithMin", + Type: "string", + Min: 1, + MaxDisabled: true, + Error: regexp.MustCompile("cannot be specified"), + }, { + Name: "StringWithMax", + Type: "string", + Max: 1, + MinDisabled: true, + Error: regexp.MustCompile("cannot be specified"), }, { Name: "NonStringWithRegex", Type: "number", Regex: "banana", Error: regexp.MustCompile("a regex cannot be specified"), }, { - Name: "Bool", - Type: "bool", - Value: "true", + Name: "Bool", + Type: "bool", + Value: "true", + MinDisabled: true, + MaxDisabled: true, }, { Name: "InvalidNumber", Type: "number", Value: "hi", Error: regexp.MustCompile("is not a number"), }, { - Name: "NumberBelowMin", - Type: "number", - Value: "0", - Min: ptrNumber(1), - Error: regexp.MustCompile("is less than the minimum 1"), - }, { - Name: "NumberAboveMax", - Type: "number", - Value: "2", - Max: ptrNumber(1), - Error: regexp.MustCompile("is more than the maximum 1"), - }, { - Name: "InvalidBool", - Type: "bool", - Value: "cat", - Error: regexp.MustCompile("boolean value can be either"), - }, { - Name: "BadStringWithRegex", - Type: "string", - Regex: "banana", - RegexError: "bad fruit", - Value: "apple", - Error: regexp.MustCompile(`bad fruit`), + Name: "NumberBelowMin", + Type: "number", + Value: "0", + Min: 1, + MaxDisabled: true, + Error: regexp.MustCompile("is less than the minimum 1"), + }, { + Name: "NumberAboveMax", + Type: "number", + Value: "2", + Max: 1, + MinDisabled: true, + Error: regexp.MustCompile("is more than the maximum 1"), + }, { + Name: "InvalidBool", + Type: "bool", + Value: "cat", + MinDisabled: true, + MaxDisabled: true, + Error: regexp.MustCompile("boolean value can be either"), + }, { + Name: "BadStringWithRegex", + Type: "string", + Regex: "banana", + RegexError: "bad fruit", + Value: "apple", + MinDisabled: true, + MaxDisabled: true, + Error: regexp.MustCompile(`bad fruit`), }, { Name: "InvalidMonotonicity", Type: "number", Value: "1", - Min: ptrNumber(0), - Max: ptrNumber(2), + Min: 0, + Max: 2, Monotonic: "foobar", Error: regexp.MustCompile(`number monotonicity can be either "increasing" or "decreasing"`), }, { Name: "IncreasingMonotonicity", Type: "number", Value: "1", - Min: ptrNumber(0), - Max: ptrNumber(2), + Min: 0, + Max: 2, Monotonic: "increasing", }, { Name: "DecreasingMonotonicity", Type: "number", Value: "1", - Min: ptrNumber(0), - Max: ptrNumber(2), + Min: 0, + Max: 2, Monotonic: "decreasing", }, { - Name: "ValidListOfStrings", - Type: "list(string)", - Value: `["first","second","third"]`, - }, { - Name: "InvalidListOfStrings", - Type: "list(string)", - Value: `["first","second","third"`, - Error: regexp.MustCompile("is not valid list of strings"), - }, { - Name: "EmptyListOfStrings", - Type: "list(string)", - Value: `[]`, + Name: "ValidListOfStrings", + Type: "list(string)", + Value: `["first","second","third"]`, + MinDisabled: true, + MaxDisabled: true, + }, { + Name: "InvalidListOfStrings", + Type: "list(string)", + Value: `["first","second","third"`, + MinDisabled: true, + MaxDisabled: true, + Error: regexp.MustCompile("is not valid list of strings"), + }, { + Name: "EmptyListOfStrings", + Type: "list(string)", + Value: `[]`, + MinDisabled: true, + MaxDisabled: true, }} { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() v := &provider.Validation{ - Min: tc.Min, - Max: tc.Max, - Monotonic: tc.Monotonic, - Regex: tc.Regex, - Error: tc.RegexError, + Min: tc.Min, + MinDisabled: tc.MinDisabled, + Max: tc.Max, + MaxDisabled: tc.MaxDisabled, + Monotonic: tc.Monotonic, + Regex: tc.Regex, + Error: tc.RegexError, } err := v.Valid(tc.Type, tc.Value) if tc.Error != nil { @@ -574,7 +763,3 @@ func TestValueValidatesType(t *testing.T) { }) } } - -func ptrNumber(i int) *int { - return &i -}