Skip to content

Commit 946db5a

Browse files
authored
fix: coder_parameter: validation min and max are truly optional (#121)
1 parent dbc530b commit 946db5a

File tree

3 files changed

+119
-32
lines changed

3 files changed

+119
-32
lines changed

Diff for: provider/decode_test.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/coder/terraform-provider-coder/provider"
77
"github.com/mitchellh/mapstructure"
8+
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910
)
1011

@@ -18,15 +19,25 @@ func TestDecode(t *testing.T) {
1819

1920
aMap := map[string]interface{}{
2021
"name": "Parameter Name",
22+
"type": "number",
2123
"display_name": displayName,
2224
"legacy_variable": legacyVariable,
2325
"legacy_variable_name": legacyVariableName,
26+
"min": nil,
27+
"validation": []map[string]interface{}{
28+
{
29+
"min": nil,
30+
"max": 5,
31+
},
32+
},
2433
}
2534

2635
var param provider.Parameter
2736
err := mapstructure.Decode(aMap, &param)
2837
require.NoError(t, err)
29-
require.Equal(t, displayName, param.DisplayName)
30-
require.Equal(t, legacyVariable, param.LegacyVariable)
31-
require.Equal(t, legacyVariableName, param.LegacyVariableName)
38+
assert.Equal(t, displayName, param.DisplayName)
39+
assert.Equal(t, legacyVariable, param.LegacyVariable)
40+
assert.Equal(t, legacyVariableName, param.LegacyVariableName)
41+
assert.Equal(t, (*int)(nil), param.Validation[0].Min)
42+
assert.Equal(t, 5, *param.Validation[0].Max)
3243
}

Diff for: provider/parameter.go

+65-17
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"strconv"
1313

1414
"github.com/google/uuid"
15+
"github.com/hashicorp/go-cty/cty"
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1617
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1718
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1819
"github.com/mitchellh/mapstructure"
20+
"golang.org/x/xerrors"
1921
)
2022

2123
type Option struct {
@@ -26,8 +28,8 @@ type Option struct {
2628
}
2729

2830
type Validation struct {
29-
Min int
30-
Max int
31+
Min *int
32+
Max *int
3133
Monotonic string
3234

3335
Regex string
@@ -62,8 +64,18 @@ func parameterDataSource() *schema.Resource {
6264
ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
6365
rd.SetId(uuid.NewString())
6466

67+
fixedValidation, err := fixValidationResourceData(rd.GetRawConfig(), rd.Get("validation"))
68+
if err != nil {
69+
return diag.FromErr(err)
70+
}
71+
72+
err = rd.Set("validation", fixedValidation)
73+
if err != nil {
74+
return diag.FromErr(err)
75+
}
76+
6577
var parameter Parameter
66-
err := mapstructure.Decode(struct {
78+
err = mapstructure.Decode(struct {
6779
Value interface{}
6880
Name interface{}
6981
DisplayName interface{}
@@ -98,7 +110,7 @@ func parameterDataSource() *schema.Resource {
98110
}(),
99111
Icon: rd.Get("icon"),
100112
Option: rd.Get("option"),
101-
Validation: rd.Get("validation"),
113+
Validation: fixedValidation,
102114
Optional: func() bool {
103115
// This hack allows for checking if the "default" field is present in the .tf file.
104116
// If "default" is missing or is "null", then it means that this field is required,
@@ -272,17 +284,14 @@ func parameterDataSource() *schema.Resource {
272284
Elem: &schema.Resource{
273285
Schema: map[string]*schema.Schema{
274286
"min": {
275-
Type: schema.TypeInt,
276-
Optional: true,
277-
Default: 0,
278-
Description: "The minimum of a number parameter.",
279-
RequiredWith: []string{"validation.0.max"},
287+
Type: schema.TypeInt,
288+
Optional: true,
289+
Description: "The minimum of a number parameter.",
280290
},
281291
"max": {
282-
Type: schema.TypeInt,
283-
Optional: true,
284-
Description: "The maximum of a number parameter.",
285-
RequiredWith: []string{"validation.0.min"},
292+
Type: schema.TypeInt,
293+
Optional: true,
294+
Description: "The maximum of a number parameter.",
286295
},
287296
"monotonic": {
288297
Type: schema.TypeString,
@@ -325,6 +334,45 @@ func parameterDataSource() *schema.Resource {
325334
}
326335
}
327336

337+
func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (interface{}, error) {
338+
// Read validation from raw config
339+
rawValidation, ok := rawConfig.AsValueMap()["validation"]
340+
if !ok {
341+
return validation, nil // no validation rules, nothing to fix
342+
}
343+
344+
rawValidationArr := rawValidation.AsValueSlice()
345+
if len(rawValidationArr) == 0 {
346+
return validation, nil // no validation rules, nothing to fix
347+
}
348+
349+
rawValidationRule := rawValidationArr[0].AsValueMap()
350+
351+
// Load validation from resource data
352+
vArr, ok := validation.([]interface{})
353+
if !ok {
354+
return nil, xerrors.New("validation should be an array")
355+
}
356+
357+
if len(vArr) == 0 {
358+
return validation, nil // no validation rules, nothing to fix
359+
}
360+
361+
validationRule, ok := vArr[0].(map[string]interface{})
362+
if !ok {
363+
return nil, xerrors.New("validation rule should be a map")
364+
}
365+
366+
// Fix the resource data
367+
if rawValidationRule["min"].IsNull() {
368+
validationRule["min"] = nil
369+
}
370+
if rawValidationRule["max"].IsNull() {
371+
validationRule["max"] = nil
372+
}
373+
return vArr, nil
374+
}
375+
328376
func valueIsType(typ, value string) diag.Diagnostics {
329377
switch typ {
330378
case "number":
@@ -353,10 +401,10 @@ func valueIsType(typ, value string) diag.Diagnostics {
353401

354402
func (v *Validation) Valid(typ, value string) error {
355403
if typ != "number" {
356-
if v.Min != 0 {
404+
if v.Min != nil {
357405
return fmt.Errorf("a min cannot be specified for a %s type", typ)
358406
}
359-
if v.Max != 0 {
407+
if v.Max != nil {
360408
return fmt.Errorf("a max cannot be specified for a %s type", typ)
361409
}
362410
}
@@ -389,10 +437,10 @@ func (v *Validation) Valid(typ, value string) error {
389437
if err != nil {
390438
return fmt.Errorf("value %q is not a number", value)
391439
}
392-
if num < v.Min {
440+
if v.Min != nil && num < *v.Min {
393441
return fmt.Errorf("value %d is less than the minimum %d", num, v.Min)
394442
}
395-
if num > v.Max {
443+
if v.Max != nil && num > *v.Max {
396444
return fmt.Errorf("value %d is more than the maximum %d", num, v.Max)
397445
}
398446
if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing {

Diff for: provider/parameter_test.go

+40-12
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@ func TestParameter(t *testing.T) {
109109
}
110110
}
111111
`,
112+
}, {
113+
Name: "NumberValidation_Min",
114+
Config: `
115+
data "coder_parameter" "region" {
116+
name = "Region"
117+
type = "number"
118+
default = 2
119+
validation {
120+
min = 1
121+
}
122+
}
123+
`,
124+
}, {
125+
Name: "NumberValidation_Max",
126+
Config: `
127+
data "coder_parameter" "region" {
128+
name = "Region"
129+
type = "number"
130+
default = 2
131+
validation {
132+
max = 9
133+
}
134+
}
135+
`,
112136
}, {
113137
Name: "DefaultNotNumber",
114138
Config: `
@@ -443,18 +467,18 @@ func TestValueValidatesType(t *testing.T) {
443467
Regex,
444468
RegexError string
445469
Min,
446-
Max int
470+
Max *int
447471
Monotonic string
448472
Error *regexp.Regexp
449473
}{{
450474
Name: "StringWithMin",
451475
Type: "string",
452-
Min: 1,
476+
Min: ptrNumber(1),
453477
Error: regexp.MustCompile("cannot be specified"),
454478
}, {
455479
Name: "StringWithMax",
456480
Type: "string",
457-
Max: 1,
481+
Max: ptrNumber(1),
458482
Error: regexp.MustCompile("cannot be specified"),
459483
}, {
460484
Name: "NonStringWithRegex",
@@ -474,13 +498,13 @@ func TestValueValidatesType(t *testing.T) {
474498
Name: "NumberBelowMin",
475499
Type: "number",
476500
Value: "0",
477-
Min: 1,
501+
Min: ptrNumber(1),
478502
Error: regexp.MustCompile("is less than the minimum"),
479503
}, {
480504
Name: "NumberAboveMax",
481505
Type: "number",
482-
Value: "1",
483-
Max: 0,
506+
Value: "2",
507+
Max: ptrNumber(1),
484508
Error: regexp.MustCompile("is more than the maximum"),
485509
}, {
486510
Name: "InvalidBool",
@@ -498,23 +522,23 @@ func TestValueValidatesType(t *testing.T) {
498522
Name: "InvalidMonotonicity",
499523
Type: "number",
500524
Value: "1",
501-
Min: 0,
502-
Max: 2,
525+
Min: ptrNumber(0),
526+
Max: ptrNumber(2),
503527
Monotonic: "foobar",
504528
Error: regexp.MustCompile(`number monotonicity can be either "increasing" or "decreasing"`),
505529
}, {
506530
Name: "IncreasingMonotonicity",
507531
Type: "number",
508532
Value: "1",
509-
Min: 0,
510-
Max: 2,
533+
Min: ptrNumber(0),
534+
Max: ptrNumber(2),
511535
Monotonic: "increasing",
512536
}, {
513537
Name: "DecreasingMonotonicity",
514538
Type: "number",
515539
Value: "1",
516-
Min: 0,
517-
Max: 2,
540+
Min: ptrNumber(0),
541+
Max: ptrNumber(2),
518542
Monotonic: "decreasing",
519543
}, {
520544
Name: "ValidListOfStrings",
@@ -550,3 +574,7 @@ func TestValueValidatesType(t *testing.T) {
550574
})
551575
}
552576
}
577+
578+
func ptrNumber(i int) *int {
579+
return &i
580+
}

0 commit comments

Comments
 (0)