Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9274e81

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 0fd96ee commit 9274e81

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
}
@@ -393,7 +399,7 @@ func valueIsType(typ OptionType, value string) error {
393399
return nil
394400
}
395401

396-
func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) {
402+
func (v *Parameter) ValidateInput(input *string, previous *string) (string, diag.Diagnostics) {
397403
var err error
398404
var optionType OptionType
399405

@@ -436,7 +442,7 @@ func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) {
436442
forcedValue = *value
437443
}
438444

439-
d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
445+
d := v.validValue(forcedValue, previous, optionType, optionValues, cty.Path{})
440446
if d.HasError() {
441447
return "", d
442448
}
@@ -500,7 +506,7 @@ func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, di
500506
return optionValues, nil
501507
}
502508

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

568574
if len(v.Validation) == 1 {
569575
validCheck := &v.Validation[0]
570-
err := validCheck.Valid(v.Type, value)
576+
err := validCheck.Valid(v.Type, value, previous)
571577
if err != nil {
572578
return diag.Diagnostics{
573579
{
@@ -583,7 +589,7 @@ func (v *Parameter) validValue(value string, optionType OptionType, optionValues
583589
return nil
584590
}
585591

586-
func (v *Validation) Valid(typ OptionType, value string) error {
592+
func (v *Validation) Valid(typ OptionType, value string, previous *string) error {
587593
if typ != OptionTypeNumber {
588594
if !v.MinDisabled {
589595
return fmt.Errorf("a min cannot be specified for a %s type", typ)
@@ -633,6 +639,28 @@ func (v *Validation) Valid(typ OptionType, value string) error {
633639
if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing {
634640
return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing)
635641
}
642+
643+
switch v.Monotonic {
644+
case "":
645+
// No monotonicity check
646+
case ValidationMonotonicIncreasing, ValidationMonotonicDecreasing:
647+
if previous != nil { // Only check if previous value exists
648+
previousNum, err := strconv.Atoi(*previous)
649+
if err != nil {
650+
return fmt.Errorf("previous value %q is not a number", *previous)
651+
}
652+
653+
if v.Monotonic == ValidationMonotonicIncreasing && !(num >= previousNum) {
654+
return fmt.Errorf("parameter value '%d' must be equal or greater than previous value: %d", num, previousNum)
655+
}
656+
657+
if v.Monotonic == ValidationMonotonicDecreasing && !(num <= previousNum) {
658+
return fmt.Errorf("parameter value '%d' must be equal or lower than previous value: %d", num, previousNum)
659+
}
660+
}
661+
default:
662+
return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing)
663+
}
636664
case OptionTypeListString:
637665
var listOfStrings []string
638666
err := json.Unmarshal([]byte(value), &listOfStrings)
@@ -660,6 +688,15 @@ func ParameterEnvironmentVariable(name string) string {
660688
return "CODER_PARAMETER_" + hex.EncodeToString(sum[:])
661689
}
662690

691+
// ParameterEnvironmentVariablePrevious returns the environment variable to
692+
// specify for a parameter's previous value. This is used for workspace
693+
// subsequent builds after the first. Primarily to validate monotonicity in the
694+
// `validation` block.
695+
func ParameterEnvironmentVariablePrevious(name string) string {
696+
sum := sha256.Sum256([]byte(name))
697+
return "CODER_PARAMETER_PREVIOUS_" + hex.EncodeToString(sum[:])
698+
}
699+
663700
func takeFirstError(errs ...error) error {
664701
for _, err := range errs {
665702
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)