From ec5b604d1086a96b76f8e1541b9357c76aff0d85 Mon Sep 17 00:00:00 2001
From: Danny Kopping <dannykopping@gmail.com>
Date: Fri, 22 Mar 2024 15:05:27 +0200
Subject: [PATCH] feat: allow `validation` to be used with `option`s (#202)

---
 README.md                                     | 38 ++++++++++-
 .../resources/coder_parameter/resource.tf     | 30 ++++++++
 provider/parameter.go                         | 23 ++++---
 provider/parameter_test.go                    | 68 ++++++++++++++++++-
 4 files changed, 145 insertions(+), 14 deletions(-)

diff --git a/README.md b/README.md
index cbec7099..151a7299 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,39 @@
 # terraform-provider-coder
 
-See [Coder](https://github.com/coder/coder).
+Terraform provider for [Coder](https://github.com/coder/coder).
+
+### Developing
+
+Follow the instructions outlined in the [Terraform documentation](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers)
+to setup your local Terraform to use your local version rather than the registry version.
+
+1. Create a file named `.terraformrc` in your `$HOME` directory
+2. Add the following content:
+   ```hcl
+    provider_installation {
+        # Override the coder/coder provider to use your local version
+        dev_overrides {
+          "coder/coder" = "/path/to/terraform-provider-coder"
+        }
+
+        # For all other providers, install them directly from their origin provider
+        # registries as normal. If you omit this, Terraform will _only_ use
+        # the dev_overrides block, and so no other providers will be available.
+        direct {}
+    }
+   ```
+3. (optional, but recommended) Validate your configuration:
+    1. Create a new `main.tf` file and include:
+      ```hcl
+      terraform {
+          required_providers {
+              coder = {
+                  source = "coder/coder"
+              }
+          }
+      }
+      ```
+   2. Run `terraform init` and observe a warning like `Warning: Provider development overrides are in effect`
+4. Run `go build -o terraform-provider-coder` to build the provider binary, which Terraform will try locate and execute
+5. All local Terraform runs will now use your local provider!
+6. _**NOTE**: we vendor in this provider into `github.com/coder/coder`, so if you're testing with a local clone then you should also run `go mod edit -replace github.com/coder/terraform-provider-coder=/path/to/terraform-provider-coder` in your clone._
\ No newline at end of file
diff --git a/examples/resources/coder_parameter/resource.tf b/examples/resources/coder_parameter/resource.tf
index 3c674f1c..4efc3320 100644
--- a/examples/resources/coder_parameter/resource.tf
+++ b/examples/resources/coder_parameter/resource.tf
@@ -84,4 +84,34 @@ data "coder_parameter" "users" {
   display_name = "System users"
   type         = "list(string)"
   default      = jsonencode(["root", "user1", "user2"])
+}
+
+data "coder_parameter" "home_volume_size" {
+  name        = "Home Volume Size"
+  description = <<-EOF
+  How large should your home volume be?
+  EOF
+  type        = "number"
+  default     = 30
+  mutable     = true
+  order       = 3
+
+  option {
+    name  = "30GB"
+    value = 30
+  }
+
+  option {
+    name  = "60GB"
+    value = 60
+  }
+
+  option {
+    name  = "100GB"
+    value = 100
+  }
+
+  validation {
+    monotonic = "increasing"
+  }
 }
\ No newline at end of file
diff --git a/provider/parameter.go b/provider/parameter.go
index f4a31b7f..12dbc019 100644
--- a/provider/parameter.go
+++ b/provider/parameter.go
@@ -231,12 +231,11 @@ func parameterDataSource() *schema.Resource {
 				},
 			},
 			"option": {
-				Type:          schema.TypeList,
-				Description:   "Each \"option\" block defines a value for a user to select from.",
-				ForceNew:      true,
-				Optional:      true,
-				MaxItems:      64,
-				ConflictsWith: []string{"validation"},
+				Type:        schema.TypeList,
+				Description: "Each \"option\" block defines a value for a user to select from.",
+				ForceNew:    true,
+				Optional:    true,
+				MaxItems:    64,
 				Elem: &schema.Resource{
 					Schema: map[string]*schema.Schema{
 						"name": {
@@ -276,11 +275,10 @@ func parameterDataSource() *schema.Resource {
 				},
 			},
 			"validation": {
-				Type:          schema.TypeList,
-				MaxItems:      1,
-				Optional:      true,
-				Description:   "Validate the input of a parameter.",
-				ConflictsWith: []string{"option"},
+				Type:        schema.TypeList,
+				MaxItems:    1,
+				Optional:    true,
+				Description: "Validate the input of a parameter.",
 				Elem: &schema.Resource{
 					Schema: map[string]*schema.Schema{
 						"min": {
@@ -410,6 +408,9 @@ func (v *Validation) Valid(typ, value string) error {
 		if !v.MaxDisabled {
 			return fmt.Errorf("a max cannot be specified for a %s type", typ)
 		}
+		if v.Monotonic != "" {
+			return fmt.Errorf("monotonic validation can only be specified for number types, not %s types", typ)
+		}
 	}
 	if typ != "string" && v.Regex != "" {
 		return fmt.Errorf("a regex cannot be specified for a %s type", typ)
diff --git a/provider/parameter_test.go b/provider/parameter_test.go
index e60c96f7..05d3604b 100644
--- a/provider/parameter_test.go
+++ b/provider/parameter_test.go
@@ -73,7 +73,7 @@ func TestParameter(t *testing.T) {
 			}
 		},
 	}, {
-		Name: "ValidationWithOptions",
+		Name: "RegexValidationWithOptions",
 		Config: `
 			data "coder_parameter" "region" {
 				name = "Region"
@@ -88,7 +88,23 @@ func TestParameter(t *testing.T) {
 				}
 			}
 			`,
-		ExpectError: regexp.MustCompile("conflicts with option"),
+		ExpectError: regexp.MustCompile("a regex cannot be specified for a number type"),
+	}, {
+		Name: "MonotonicValidationWithNonNumberType",
+		Config: `
+			data "coder_parameter" "region" {
+				name = "Region"
+				type = "string"
+				option {
+					name = "1"
+					value = "1"
+				}
+				validation {
+					monotonic = "increasing"
+				}
+			}
+			`,
+		ExpectError: regexp.MustCompile("monotonic validation can only be specified for number types, not string types"),
 	}, {
 		Name: "ValidationRegexMissingError",
 		Config: `
@@ -424,6 +440,54 @@ data "coder_parameter" "region" {
 				require.Equal(t, expected, state.Primary.Attributes[key])
 			}
 		},
+	}, {
+		Name: "NumberValidation_MonotonicWithOptions",
+		Config: `
+			data "coder_parameter" "region" {
+			  name        = "Region"
+			  type        = "number"
+			  description = <<-EOF
+			  Always pick a larger region.
+			  EOF
+			  default     = 1
+
+			  option {
+				name  = "Small"
+				value = 1
+			  }
+
+			  option {
+				name  = "Medium"
+				value = 2
+			  }
+
+			  option {
+				name  = "Large"
+				value = 3
+			  }
+
+			  validation {
+				monotonic = "increasing"
+			  }
+			}
+			`,
+		Check: func(state *terraform.ResourceState) {
+			for key, expected := range map[string]any{
+				"name":                   "Region",
+				"type":                   "number",
+				"validation.#":           "1",
+				"option.0.name":          "Small",
+				"option.0.value":         "1",
+				"option.1.name":          "Medium",
+				"option.1.value":         "2",
+				"option.2.name":          "Large",
+				"option.2.value":         "3",
+				"default":                "1",
+				"validation.0.monotonic": "increasing",
+			} {
+				require.Equal(t, expected, state.Primary.Attributes[key])
+			}
+		},
 	}, {
 		Name: "NumberValidation_Min",
 		Config: `
