Skip to content

Commit f9d70ae

Browse files
committed
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
1 parent 3c74804 commit f9d70ae

File tree

2 files changed

+110
-14
lines changed

2 files changed

+110
-14
lines changed

‎provider/parameter.go

+43-6
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,13 @@ func parameterDataSource() *schema.Resource {
144144
input = &envValue
145145
}
146146

147-
value, diags := parameter.ValidateInput(input)
147+
var previous *string
148+
envPreviousValue, ok := os.LookupEnv(ParameterEnvironmentVariablePrevious(parameter.Name))
149+
if ok {
150+
previous = &envPreviousValue
151+
}
152+
153+
value, diags := parameter.ValidateInput(input, previous)
148154
if diags.HasError() {
149155
return diags
150156
}
@@ -395,7 +401,7 @@ func valueIsType(typ OptionType, value string) error {
395401
return nil
396402
}
397403

398-
func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) {
404+
func (v *Parameter) ValidateInput(input *string, previous *string) (string, diag.Diagnostics) {
399405
var err error
400406
var optionType OptionType
401407

@@ -442,7 +448,7 @@ func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) {
442448
forcedValue = *value
443449
}
444450

445-
d := v.validValue(forcedValue, optionType, optionValues, valuePath)
451+
d := v.validValue(forcedValue, previous, optionType, optionValues, valuePath)
446452
if d.HasError() {
447453
return "", d
448454
}
@@ -506,7 +512,7 @@ func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, di
506512
return optionValues, nil
507513
}
508514

509-
func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
515+
func (v *Parameter) validValue(value string, previous *string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
510516
// name is used for constructing more precise error messages.
511517
name := "Value"
512518
if path.Equals(defaultValuePath) {
@@ -573,7 +579,7 @@ func (v *Parameter) validValue(value string, optionType OptionType, optionValues
573579

574580
if len(v.Validation) == 1 {
575581
validCheck := &v.Validation[0]
576-
err := validCheck.Valid(v.Type, value)
582+
err := validCheck.Valid(v.Type, value, previous)
577583
if err != nil {
578584
return diag.Diagnostics{
579585
{
@@ -589,7 +595,7 @@ func (v *Parameter) validValue(value string, optionType OptionType, optionValues
589595
return nil
590596
}
591597

592-
func (v *Validation) Valid(typ OptionType, value string) error {
598+
func (v *Validation) Valid(typ OptionType, value string, previous *string) error {
593599
if typ != OptionTypeNumber {
594600
if !v.MinDisabled {
595601
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 {
639645
if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing {
640646
return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing)
641647
}
648+
649+
switch v.Monotonic {
650+
case "":
651+
// No monotonicity check
652+
case ValidationMonotonicIncreasing, ValidationMonotonicDecreasing:
653+
if previous != nil { // Only check if previous value exists
654+
previousNum, err := strconv.Atoi(*previous)
655+
if err != nil {
656+
return fmt.Errorf("previous value %q is not a number", *previous)
657+
}
658+
659+
if v.Monotonic == ValidationMonotonicIncreasing && !(num >= previousNum) {
660+
return fmt.Errorf("parameter value '%d' must be equal or greater than previous value: %d", num, previousNum)
661+
}
662+
663+
if v.Monotonic == ValidationMonotonicDecreasing && !(num <= previousNum) {
664+
return fmt.Errorf("parameter value '%d' must be equal or lower than previous value: %d", num, previousNum)
665+
}
666+
}
667+
default:
668+
return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing)
669+
}
642670
case OptionTypeListString:
643671
var listOfStrings []string
644672
err := json.Unmarshal([]byte(value), &listOfStrings)
@@ -666,6 +694,15 @@ func ParameterEnvironmentVariable(name string) string {
666694
return "CODER_PARAMETER_" + hex.EncodeToString(sum[:])
667695
}
668696

697+
// ParameterEnvironmentVariablePrevious returns the environment variable to
698+
// specify for a parameter's previous value. This is used for workspace
699+
// subsequent builds after the first. Primarily to validate monotonicity in the
700+
// `validation` block.
701+
func ParameterEnvironmentVariablePrevious(name string) string {
702+
sum := sha256.Sum256([]byte(name))
703+
return "CODER_PARAMETER_PREVIOUS_" + hex.EncodeToString(sum[:])
704+
}
705+
669706
func takeFirstError(errs ...error) error {
670707
for _, err := range errs {
671708
if err != nil {

‎provider/parameter_test.go

+67-8
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,7 @@ func TestParameterValidation(t *testing.T) {
839839
t.Run(tc.Name, func(t *testing.T) {
840840
t.Parallel()
841841
value := &tc.Value
842-
_, diags := tc.Parameter.ValidateInput(value)
842+
_, diags := tc.Parameter.ValidateInput(value, nil)
843843
if tc.ExpectError != nil {
844844
require.True(t, diags.HasError())
845845
errMsg := fmt.Sprintf("%+v", diags[0]) // close enough
@@ -919,11 +919,19 @@ func TestParameterValidationEnforcement(t *testing.T) {
919919

920920
var validation *provider.Validation
921921
if columns[5] != "" {
922-
// Min-Max validation should look like:
923-
// 1-10 :: min=1, max=10
924-
// -10 :: max=10
925-
// 1- :: min=1
926-
if validMinMax.MatchString(columns[5]) {
922+
switch {
923+
case columns[5] == provider.ValidationMonotonicIncreasing || columns[5] == provider.ValidationMonotonicDecreasing:
924+
validation = &provider.Validation{
925+
MinDisabled: true,
926+
MaxDisabled: true,
927+
Monotonic: columns[5],
928+
Error: "monotonicity",
929+
}
930+
case validMinMax.MatchString(columns[5]):
931+
// Min-Max validation should look like:
932+
// 1-10 :: min=1, max=10
933+
// -10 :: max=10
934+
// 1- :: min=1
927935
parts := strings.Split(columns[5], "-")
928936
min, _ := strconv.ParseInt(parts[0], 10, 64)
929937
max, _ := strconv.ParseInt(parts[1], 10, 64)
@@ -936,7 +944,7 @@ func TestParameterValidationEnforcement(t *testing.T) {
936944
Regex: "",
937945
Error: "{min} < {value} < {max}",
938946
}
939-
} else {
947+
default:
940948
validation = &provider.Validation{
941949
Min: 0,
942950
MinDisabled: true,
@@ -1067,6 +1075,7 @@ func TestValueValidatesType(t *testing.T) {
10671075
Name string
10681076
Type provider.OptionType
10691077
Value string
1078+
Previous *string
10701079
Regex string
10711080
RegexError string
10721081
Min int
@@ -1154,6 +1163,56 @@ func TestValueValidatesType(t *testing.T) {
11541163
Min: 0,
11551164
Max: 2,
11561165
Monotonic: "decreasing",
1166+
}, {
1167+
Name: "IncreasingMonotonicityEqual",
1168+
Type: "number",
1169+
Previous: ptr("1"),
1170+
Value: "1",
1171+
Monotonic: "increasing",
1172+
MinDisabled: true,
1173+
MaxDisabled: true,
1174+
}, {
1175+
Name: "DecreasingMonotonicityEqual",
1176+
Type: "number",
1177+
Value: "1",
1178+
Previous: ptr("1"),
1179+
Monotonic: "decreasing",
1180+
MinDisabled: true,
1181+
MaxDisabled: true,
1182+
}, {
1183+
Name: "IncreasingMonotonicityGreater",
1184+
Type: "number",
1185+
Previous: ptr("0"),
1186+
Value: "1",
1187+
Monotonic: "increasing",
1188+
MinDisabled: true,
1189+
MaxDisabled: true,
1190+
}, {
1191+
Name: "DecreasingMonotonicityGreater",
1192+
Type: "number",
1193+
Value: "1",
1194+
Previous: ptr("0"),
1195+
Monotonic: "decreasing",
1196+
MinDisabled: true,
1197+
MaxDisabled: true,
1198+
Error: regexp.MustCompile("must be equal or"),
1199+
}, {
1200+
Name: "IncreasingMonotonicityLesser",
1201+
Type: "number",
1202+
Previous: ptr("2"),
1203+
Value: "1",
1204+
Monotonic: "increasing",
1205+
MinDisabled: true,
1206+
MaxDisabled: true,
1207+
Error: regexp.MustCompile("must be equal or"),
1208+
}, {
1209+
Name: "DecreasingMonotonicityLesser",
1210+
Type: "number",
1211+
Value: "1",
1212+
Previous: ptr("2"),
1213+
Monotonic: "decreasing",
1214+
MinDisabled: true,
1215+
MaxDisabled: true,
11571216
}, {
11581217
Name: "ValidListOfStrings",
11591218
Type: "list(string)",
@@ -1205,7 +1264,7 @@ func TestValueValidatesType(t *testing.T) {
12051264
Regex: tc.Regex,
12061265
Error: tc.RegexError,
12071266
}
1208-
err := v.Valid(tc.Type, tc.Value)
1267+
err := v.Valid(tc.Type, tc.Value, tc.Previous)
12091268
if tc.Error != nil {
12101269
require.Error(t, err)
12111270
require.True(t, tc.Error.MatchString(err.Error()), "got: %s", err.Error())

0 commit comments

Comments
 (0)