Skip to content

Commit 8692a56

Browse files
authored
feat: enforce monotonicity in terraform provider (#392)
Previous value must come from env var.
1 parent 3c74804 commit 8692a56

File tree

3 files changed

+241
-109
lines changed

3 files changed

+241
-109
lines changed

provider/parameter.go

Lines changed: 49 additions & 6 deletions
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,34 @@ 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+
// Do not throw an error for the previous value not being a number. Throwing an
657+
// error here would cause an unrepairable state for the user. This is
658+
// unfortunate, but there is not much we can do at this point.
659+
// TODO: Maybe we should enforce this, and have the calling coderd
660+
// do something to resolve it. Such as doing this check before calling
661+
// terraform apply.
662+
break
663+
}
664+
665+
if v.Monotonic == ValidationMonotonicIncreasing && !(num >= previousNum) {
666+
return fmt.Errorf("parameter value '%d' must be equal or greater than previous value: %d", num, previousNum)
667+
}
668+
669+
if v.Monotonic == ValidationMonotonicDecreasing && !(num <= previousNum) {
670+
return fmt.Errorf("parameter value '%d' must be equal or lower than previous value: %d", num, previousNum)
671+
}
672+
}
673+
default:
674+
return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing)
675+
}
642676
case OptionTypeListString:
643677
var listOfStrings []string
644678
err := json.Unmarshal([]byte(value), &listOfStrings)
@@ -666,6 +700,15 @@ func ParameterEnvironmentVariable(name string) string {
666700
return "CODER_PARAMETER_" + hex.EncodeToString(sum[:])
667701
}
668702

703+
// ParameterEnvironmentVariablePrevious returns the environment variable to
704+
// specify for a parameter's previous value. This is used for workspace
705+
// subsequent builds after the first. Primarily to validate monotonicity in the
706+
// `validation` block.
707+
func ParameterEnvironmentVariablePrevious(name string) string {
708+
sum := sha256.Sum256([]byte(name))
709+
return "CODER_PARAMETER_PREVIOUS_" + hex.EncodeToString(sum[:])
710+
}
711+
669712
func takeFirstError(errs ...error) error {
670713
for _, err := range errs {
671714
if err != nil {

provider/parameter_test.go

Lines changed: 97 additions & 23 deletions
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
@@ -881,6 +881,7 @@ func TestParameterValidationEnforcement(t *testing.T) {
881881
OutputValue string
882882
Optional bool
883883
CreateError *regexp.Regexp
884+
Previous *string
884885
}
885886

886887
rows := make([]row, 0)
@@ -898,33 +899,44 @@ func TestParameterValidationEnforcement(t *testing.T) {
898899
continue // Skip rows with empty names
899900
}
900901

901-
optional, err := strconv.ParseBool(columns[8])
902-
if columns[8] != "" {
902+
cname, ctype, cprev, cinput, cdefault, coptions, cvalidation, _, coutput, coptional, cerr :=
903+
columns[0], columns[1], columns[2], columns[3], columns[4], columns[5], columns[6], columns[7], columns[8], columns[9], columns[10]
904+
905+
optional, err := strconv.ParseBool(coptional)
906+
if coptional != "" {
903907
// Value does not matter if not specified
904908
require.NoError(t, err)
905909
}
906910

907911
var rerr *regexp.Regexp
908-
if columns[9] != "" {
909-
rerr, err = regexp.Compile(columns[9])
912+
if cerr != "" {
913+
rerr, err = regexp.Compile(cerr)
910914
if err != nil {
911-
t.Fatalf("failed to parse error column %q: %v", columns[9], err)
915+
t.Fatalf("failed to parse error column %q: %v", cerr, err)
912916
}
913917
}
914918

915919
var options []string
916-
if columns[4] != "" {
917-
options = strings.Split(columns[4], ",")
920+
if coptions != "" {
921+
options = strings.Split(coptions, ",")
918922
}
919923

920924
var validation *provider.Validation
921-
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]) {
927-
parts := strings.Split(columns[5], "-")
925+
if cvalidation != "" {
926+
switch {
927+
case cvalidation == provider.ValidationMonotonicIncreasing || cvalidation == provider.ValidationMonotonicDecreasing:
928+
validation = &provider.Validation{
929+
MinDisabled: true,
930+
MaxDisabled: true,
931+
Monotonic: cvalidation,
932+
Error: "monotonicity",
933+
}
934+
case validMinMax.MatchString(cvalidation):
935+
// Min-Max validation should look like:
936+
// 1-10 :: min=1, max=10
937+
// -10 :: max=10
938+
// 1- :: min=1
939+
parts := strings.Split(cvalidation, "-")
928940
min, _ := strconv.ParseInt(parts[0], 10, 64)
929941
max, _ := strconv.ParseInt(parts[1], 10, 64)
930942
validation = &provider.Validation{
@@ -936,29 +948,37 @@ func TestParameterValidationEnforcement(t *testing.T) {
936948
Regex: "",
937949
Error: "{min} < {value} < {max}",
938950
}
939-
} else {
951+
default:
940952
validation = &provider.Validation{
941953
Min: 0,
942954
MinDisabled: true,
943955
Max: 0,
944956
MaxDisabled: true,
945957
Monotonic: "",
946-
Regex: columns[5],
958+
Regex: cvalidation,
947959
Error: "regex error",
948960
}
949961
}
950962
}
951963

964+
var prev *string
965+
if cprev != "" {
966+
prev = ptr(cprev)
967+
if cprev == `""` {
968+
prev = ptr("")
969+
}
970+
}
952971
rows = append(rows, row{
953-
Name: columns[0],
954-
Types: strings.Split(columns[1], ","),
955-
InputValue: columns[2],
956-
Default: columns[3],
972+
Name: cname,
973+
Types: strings.Split(ctype, ","),
974+
InputValue: cinput,
975+
Default: cdefault,
957976
Options: options,
958977
Validation: validation,
959-
OutputValue: columns[7],
978+
OutputValue: coutput,
960979
Optional: optional,
961980
CreateError: rerr,
981+
Previous: prev,
962982
})
963983
}
964984

@@ -976,6 +996,9 @@ func TestParameterValidationEnforcement(t *testing.T) {
976996
if row.InputValue != "" {
977997
t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue)
978998
}
999+
if row.Previous != nil {
1000+
t.Setenv(provider.ParameterEnvironmentVariablePrevious("parameter"), *row.Previous)
1001+
}
9791002

9801003
if row.CreateError != nil && row.OutputValue != "" {
9811004
t.Errorf("output value %q should not be set if both errors are set", row.OutputValue)
@@ -1067,6 +1090,7 @@ func TestValueValidatesType(t *testing.T) {
10671090
Name string
10681091
Type provider.OptionType
10691092
Value string
1093+
Previous *string
10701094
Regex string
10711095
RegexError string
10721096
Min int
@@ -1154,6 +1178,56 @@ func TestValueValidatesType(t *testing.T) {
11541178
Min: 0,
11551179
Max: 2,
11561180
Monotonic: "decreasing",
1181+
}, {
1182+
Name: "IncreasingMonotonicityEqual",
1183+
Type: "number",
1184+
Previous: ptr("1"),
1185+
Value: "1",
1186+
Monotonic: "increasing",
1187+
MinDisabled: true,
1188+
MaxDisabled: true,
1189+
}, {
1190+
Name: "DecreasingMonotonicityEqual",
1191+
Type: "number",
1192+
Value: "1",
1193+
Previous: ptr("1"),
1194+
Monotonic: "decreasing",
1195+
MinDisabled: true,
1196+
MaxDisabled: true,
1197+
}, {
1198+
Name: "IncreasingMonotonicityGreater",
1199+
Type: "number",
1200+
Previous: ptr("0"),
1201+
Value: "1",
1202+
Monotonic: "increasing",
1203+
MinDisabled: true,
1204+
MaxDisabled: true,
1205+
}, {
1206+
Name: "DecreasingMonotonicityGreater",
1207+
Type: "number",
1208+
Value: "1",
1209+
Previous: ptr("0"),
1210+
Monotonic: "decreasing",
1211+
MinDisabled: true,
1212+
MaxDisabled: true,
1213+
Error: regexp.MustCompile("must be equal or"),
1214+
}, {
1215+
Name: "IncreasingMonotonicityLesser",
1216+
Type: "number",
1217+
Previous: ptr("2"),
1218+
Value: "1",
1219+
Monotonic: "increasing",
1220+
MinDisabled: true,
1221+
MaxDisabled: true,
1222+
Error: regexp.MustCompile("must be equal or"),
1223+
}, {
1224+
Name: "DecreasingMonotonicityLesser",
1225+
Type: "number",
1226+
Value: "1",
1227+
Previous: ptr("2"),
1228+
Monotonic: "decreasing",
1229+
MinDisabled: true,
1230+
MaxDisabled: true,
11571231
}, {
11581232
Name: "ValidListOfStrings",
11591233
Type: "list(string)",
@@ -1205,7 +1279,7 @@ func TestValueValidatesType(t *testing.T) {
12051279
Regex: tc.Regex,
12061280
Error: tc.RegexError,
12071281
}
1208-
err := v.Valid(tc.Type, tc.Value)
1282+
err := v.Valid(tc.Type, tc.Value, tc.Previous)
12091283
if tc.Error != nil {
12101284
require.Error(t, err)
12111285
require.True(t, tc.Error.MatchString(err.Error()), "got: %s", err.Error())

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy