diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 1332c9c3..f523428c 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -71,26 +71,7 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Type: types.StringType, Description: "The region on Vercel's network to which your Serverless Functions are deployed. It should be close to any data source your Serverless Function might depend on. A new Deployment is required for your changes to take effect. Please see [Vercel's documentation](https://vercel.com/docs/concepts/edge-network/regions) for a full list of regions.", Validators: []tfsdk.AttributeValidator{ - stringOneOf( - "arn1", - "bom1", - "cdg1", - "cle1", - "cpt1", - "dub1", - "fra1", - "gru1", - "hkg1", - "hnd1", - "iad1", - "icn1", - "kix1", - "lhr1", - "pdx1", - "sfo1", - "sin1", - "syd1", - ), + validateServerlessFunctionRegion(), }, }, "environment": { diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index 063231c7..a376119e 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -3,6 +3,7 @@ package vercel_test import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -23,6 +24,25 @@ func TestAcc_Project(t *testing.T) { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, CheckDestroy: testAccProjectDestroy("vercel_project.test", testTeam()), Steps: []resource.TestStep{ + // Ensure we get nice framework / serverless_function_region errors + { + Config: ` + resource "vercel_project" "test" { + name = "foo" + serverless_function_region = "notexist" + } + `, + ExpectError: regexp.MustCompile("Invalid Serverless Function Region"), + }, + { + Config: ` + resource "vercel_project" "test" { + name = "foo" + framework = "notexist" + } + `, + ExpectError: regexp.MustCompile("Invalid Framework"), + }, // Create and Read testing { Config: testAccProjectConfig(projectSuffix, teamIDConfig()), diff --git a/vercel/validator_framework.go b/vercel/validator_framework.go index c68e1c97..7588d6f1 100644 --- a/vercel/validator_framework.go +++ b/vercel/validator_framework.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) func validateFramework() validatorFramework { @@ -15,21 +17,21 @@ func validateFramework() validatorFramework { } type validatorFramework struct { - frameworks []string + frameworks map[string]struct{} } func (v validatorFramework) Description(ctx context.Context) string { if v.frameworks == nil { return "The framework provided is not supported on Vercel" } - return stringOneOf(v.frameworks...).Description(ctx) + return fmt.Sprintf("The framework provided is not supported on Vercel. Must be one of %s.", strings.Join(keys(v.frameworks), ", ")) } func (v validatorFramework) MarkdownDescription(ctx context.Context) string { if v.frameworks == nil { return "The framework provided is not supported on Vercel" } - return stringOneOf(v.frameworks...).MarkdownDescription(ctx) + return fmt.Sprintf("The framework provided is not supported on Vercel. Must be one of `%s`.", strings.Join(keys(v.frameworks), "`, `")) } func (v validatorFramework) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -74,8 +76,28 @@ func (v validatorFramework) Validate(ctx context.Context, req tfsdk.ValidateAttr return } for _, fw := range fwList { - v.frameworks = append(v.frameworks, fw.Slug) + if v.frameworks == nil { + v.frameworks = map[string]struct{}{} + } + v.frameworks[fw.Slug] = struct{}{} } - stringOneOf(v.frameworks...).Validate(ctx, req, resp) + var item types.String + diags := tfsdk.ValueAs(ctx, req.AttributeConfig, &item) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + if item.Unknown || item.Null { + return + } + + if _, ok := v.frameworks[item.Value]; !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Framework", + fmt.Sprintf("The framework %s is not supported on Vercel. Must be one of %s.", item.Value, strings.Join(keys(v.frameworks), ", ")), + ) + return + } } diff --git a/vercel/validator_serverless_function_region.go b/vercel/validator_serverless_function_region.go new file mode 100644 index 00000000..8c89d3ee --- /dev/null +++ b/vercel/validator_serverless_function_region.go @@ -0,0 +1,123 @@ +package vercel + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func validateServerlessFunctionRegion() validatorServerlessFunctionRegion { + return validatorServerlessFunctionRegion{} +} + +type validatorServerlessFunctionRegion struct { + regions map[string]struct{} +} + +func (v validatorServerlessFunctionRegion) Description(ctx context.Context) string { + if v.regions == nil { + return "The serverless function region provided is not supported on Vercel" + } + return fmt.Sprintf("The serverless function region provided is not supported on Vercel. Must be one of %s.", strings.Join(keys(v.regions), ", ")) +} + +func (v validatorServerlessFunctionRegion) MarkdownDescription(ctx context.Context) string { + if v.regions == nil { + return "The serverless function region provided is not supported on Vercel" + } + return fmt.Sprintf("The serverless function region provided is not supported on Vercel. Must be one of `%s`.", strings.Join(keys(v.regions), "`, `")) +} + +func contains(items []string, i string) bool { + for _, j := range items { + if j == i { + return true + } + } + return false +} + +func keys(v map[string]struct{}) (out []string) { + for k := range v { + out = append(out, k) + } + return +} + +func (v validatorServerlessFunctionRegion) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + apires, err := http.Get("https://dcs.vercel-infra.com") + if err != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel serverless function regions: unexpected error: %s", err), + ) + return + } + if apires.StatusCode != 200 { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel serverless function regions: unexpected status code: %d", apires.StatusCode), + ) + return + } + + defer apires.Body.Close() + responseBody, err := io.ReadAll(apires.Body) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel serverless function regions: error reading response body: %s", err), + ) + return + } + + var regions map[string]struct { + Caps []string `json:"caps"` + } + err = json.Unmarshal(responseBody, ®ions) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel serverless function regions: error parsing serverless function regions response: %s", err), + ) + return + } + + for region, regionInfo := range regions { + if contains(regionInfo.Caps, "V2_DEPLOYMENT_CREATE") { + if v.regions == nil { + v.regions = map[string]struct{}{} + } + v.regions[region] = struct{}{} + } + } + + var item types.String + diags := tfsdk.ValueAs(ctx, req.AttributeConfig, &item) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + if item.Unknown || item.Null { + return + } + + if _, ok := v.regions[item.Value]; !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Serverless Function Region", + fmt.Sprintf("The serverless function region %s is not supported on Vercel. Must be one of %s.", item.Value, strings.Join(keys(v.regions), ", ")), + ) + return + } +} diff --git a/vercel/validator_string_one_of.go b/vercel/validator_string_one_of.go index 0c18385f..a7e723df 100644 --- a/vercel/validator_string_one_of.go +++ b/vercel/validator_string_one_of.go @@ -52,7 +52,7 @@ func (v validatorStringOneOf) Validate(ctx context.Context, req tfsdk.ValidateAt resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid value provided", - fmt.Sprintf("Item must be one of %s, got: %s.", strings.Join(v.keys(), " "), item.Value), + fmt.Sprintf("Item must be one of %s, got: %s.", strings.Join(v.keys(), ", "), item.Value), ) return } diff --git a/vercel/validator_string_set_items_in.go b/vercel/validator_string_set_items_in.go index f0eb1186..3e482384 100644 --- a/vercel/validator_string_set_items_in.go +++ b/vercel/validator_string_set_items_in.go @@ -31,10 +31,10 @@ func (v validatorStringSetItemsIn) keys() (out []string) { } func (v validatorStringSetItemsIn) Description(ctx context.Context) string { - return fmt.Sprintf("set item must be one of %s", strings.Join(v.keys(), " ")) + return fmt.Sprintf("Set item must be one of %s", strings.Join(v.keys(), ", ")) } func (v validatorStringSetItemsIn) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("set item must be one of `%s`", strings.Join(v.keys(), "` `")) + return fmt.Sprintf("Set item must be one of `%s`", strings.Join(v.keys(), ",` `")) } func (v validatorStringSetItemsIn) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -62,7 +62,7 @@ func (v validatorStringSetItemsIn) Validate(ctx context.Context, req tfsdk.Valid resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid value provided", - fmt.Sprintf("Set item must be one of %s, got: %s.", strings.Join(v.keys(), " "), item.Value), + fmt.Sprintf("%s, got %s", v.Description(ctx), item.Value), ) return }