From 033285795b4e08d96a1fe5ce460507cfa8bba3d0 Mon Sep 17 00:00:00 2001 From: Douglas Parsons Date: Thu, 1 Sep 2022 10:58:16 +0100 Subject: [PATCH] Automatically gather serverless_function_regions Change the validation on the serverless_function_region field of a project to automatically read them, rather than using a hard-coded list. This will ease the maintenance of the provider as new regions will automatically become available. I've also taken some time to improve the error messages returned when an invalid serverless_function_region or framework is specified on a project. I've added tests for these too. --- vercel/resource_project.go | 21 +-- vercel/resource_project_test.go | 20 +++ vercel/validator_framework.go | 32 ++++- .../validator_serverless_function_region.go | 123 ++++++++++++++++++ vercel/validator_string_one_of.go | 2 +- vercel/validator_string_set_items_in.go | 6 +- 6 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 vercel/validator_serverless_function_region.go 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 }