From 36857a131a1b8d71b3a2418ef7fb28805acf2bc4 Mon Sep 17 00:00:00 2001 From: Douglas Parsons Date: Fri, 11 Feb 2022 17:43:08 +0000 Subject: [PATCH] Add project name validation - Refactor framework validation to improve error cases --- vercel/data_source_project.go | 5 ++ vercel/resource_project.go | 5 ++ vercel/validator_framework.go | 70 ++++++++++++++++++----- vercel/validator_int64_items_in.go | 8 +-- vercel/validator_map_items_min_count.go | 6 +- vercel/validator_string_length_between.go | 10 +++- vercel/validator_string_one_of.go | 6 +- vercel/validator_string_regex.go | 49 ++++++++++++++++ vercel/validator_string_set_items_in.go | 2 +- 9 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 vercel/validator_string_regex.go diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index e13d23ce..d28f68f9 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -3,6 +3,7 @@ package vercel import ( "context" "fmt" + "regexp" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -33,6 +34,10 @@ For more detailed information, please see the [Vercel documentation](https://ver Type: types.StringType, Validators: []tfsdk.AttributeValidator{ stringLengthBetween(1, 52), + stringRegex( + regexp.MustCompile(`^[a-z0-9\-]{0,100}$`), + "The name of a Project can only contain up to 100 alphanumeric lowercase characters and hyphens", + ), }, Description: "The name of the project.", }, diff --git a/vercel/resource_project.go b/vercel/resource_project.go index da43b0c0..73e3848d 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -43,6 +44,10 @@ deployments, you may not want to create a Project within the same terraform work Type: types.StringType, Validators: []tfsdk.AttributeValidator{ stringLengthBetween(1, 52), + stringRegex( + regexp.MustCompile(`^[a-z0-9\-]{0,100}$`), + "The name of a Project can only contain up to 100 alphanumeric lowercase characters and hyphens", + ), }, Description: "The desired name for the project.", }, diff --git a/vercel/validator_framework.go b/vercel/validator_framework.go index f838222d..6f646e6e 100644 --- a/vercel/validator_framework.go +++ b/vercel/validator_framework.go @@ -1,37 +1,81 @@ package vercel import ( + "context" "encoding/json" "fmt" "io/ioutil" "net/http" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -func validateFramework() validatorStringOneOf { - resp, err := http.Get("https://api-frameworks.zeit.sh/") +func validateFramework() validatorFramework { + return validatorFramework{} +} + +type validatorFramework struct { + frameworks []string +} + +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) +} + +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) +} + +func (v validatorFramework) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + apires, err := http.Get("https://api-frameworks.zeit.sh/") if err != nil { - panic(fmt.Errorf("unable to retrieve Vercel frameworks: unexpected error: %w", err)) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel frameworks: unexpected error: %s", err), + ) + return } - if resp.StatusCode != 200 { - panic(fmt.Errorf("unable to retrieve Vercel frameworks: unexpected status code %d", resp.StatusCode)) + if apires.StatusCode != 200 { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel frameworks: unexpected status code: %d", apires.StatusCode), + ) + return } - defer resp.Body.Close() - responseBody, err := ioutil.ReadAll(resp.Body) + defer apires.Body.Close() + responseBody, err := ioutil.ReadAll(apires.Body) if err != nil { - panic(fmt.Errorf("error reading api-frameworks.zeit.sh response body: %w", err)) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel frameworks: error reading response body: %s", err), + ) + return } var fwList []struct { Slug string `json:"slug"` } err = json.Unmarshal(responseBody, &fwList) - if resp.StatusCode != 200 { - panic(fmt.Errorf("unable to parse Vercel frameworks response: %w", err)) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Unable to validate attribute", + fmt.Sprintf("Unable to retrieve Vercel frameworks: error parsing frameworks response: %s", err), + ) + return } - var frameworks []string for _, fw := range fwList { - frameworks = append(frameworks, fw.Slug) + v.frameworks = append(v.frameworks, fw.Slug) } - return stringOneOf(frameworks...) + stringOneOf(v.frameworks...).Validate(ctx, req, resp) } diff --git a/vercel/validator_int64_items_in.go b/vercel/validator_int64_items_in.go index 0f16e3d0..d42814dd 100644 --- a/vercel/validator_int64_items_in.go +++ b/vercel/validator_int64_items_in.go @@ -32,10 +32,10 @@ func (v validatorInt64ItemsIn) keys() (out []string) { } func (v validatorInt64ItemsIn) Description(ctx context.Context) string { - return fmt.Sprintf("set item must be one of %s", strings.Join(v.keys(), " ")) + return fmt.Sprintf("Item must be one of %s", strings.Join(v.keys(), " ")) } func (v validatorInt64ItemsIn) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("set item must be one of `%s`", strings.Join(v.keys(), "` `")) + return fmt.Sprintf("Item must be one of `%s`", strings.Join(v.keys(), "` `")) } func (v validatorInt64ItemsIn) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -52,8 +52,8 @@ func (v validatorInt64ItemsIn) Validate(ctx context.Context, req tfsdk.ValidateA if _, ok := v.Items[item.Value]; !ok { resp.Diagnostics.AddAttributeError( req.AttributePath, - "Invalid Item", - fmt.Sprintf(" item must be one of %s, got: %d.", strings.Join(v.keys(), " "), item.Value), + "Invalid value provided", + fmt.Sprintf("Item must be one of %s, got: %d.", strings.Join(v.keys(), " "), item.Value), ) return } diff --git a/vercel/validator_map_items_min_count.go b/vercel/validator_map_items_min_count.go index 437485cc..d62361ca 100644 --- a/vercel/validator_map_items_min_count.go +++ b/vercel/validator_map_items_min_count.go @@ -20,10 +20,10 @@ type validatorMapItemsMinCount struct { } func (v validatorMapItemsMinCount) Description(ctx context.Context) string { - return fmt.Sprintf("map must contain at least %d item(s)", v.Min) + return fmt.Sprintf("Map must contain at least %d item(s)", v.Min) } func (v validatorMapItemsMinCount) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("map must contain at least `%d` item(s)", v.Min) + return fmt.Sprintf("Map must contain at least `%d` item(s)", v.Min) } func (v validatorMapItemsMinCount) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -40,7 +40,7 @@ func (v validatorMapItemsMinCount) Validate(ctx context.Context, req tfsdk.Valid if count < v.Min { resp.Diagnostics.AddAttributeError( req.AttributePath, - "Invalid Map Count", + "Invalid value provided", fmt.Sprintf( "Map must contain at least %d items, got: %d.", v.Min, diff --git a/vercel/validator_string_length_between.go b/vercel/validator_string_length_between.go index 12a8a706..743a2d14 100644 --- a/vercel/validator_string_length_between.go +++ b/vercel/validator_string_length_between.go @@ -21,10 +21,10 @@ type validatorStringLengthBetween struct { } func (v validatorStringLengthBetween) Description(ctx context.Context) string { - return fmt.Sprintf("string length must be between %d and %d", v.Min, v.Max) + return fmt.Sprintf("String length must be between %d and %d", v.Min, v.Max) } func (v validatorStringLengthBetween) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("string length must be between `%d` and `%d`", v.Min, v.Max) + return fmt.Sprintf("String length must be between `%d` and `%d`", v.Min, v.Max) } func (v validatorStringLengthBetween) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -39,7 +39,11 @@ func (v validatorStringLengthBetween) Validate(ctx context.Context, req tfsdk.Va } strLen := len(str.Value) if strLen < v.Min || strLen > v.Max { - resp.Diagnostics.AddAttributeError(req.AttributePath, "Invalid String Length", fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen)) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid value provided", + fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen), + ) return } } diff --git a/vercel/validator_string_one_of.go b/vercel/validator_string_one_of.go index 168b32e6..0c18385f 100644 --- a/vercel/validator_string_one_of.go +++ b/vercel/validator_string_one_of.go @@ -31,10 +31,10 @@ func (v validatorStringOneOf) keys() (out []string) { } func (v validatorStringOneOf) Description(ctx context.Context) string { - return fmt.Sprintf("item must be one of %s", strings.Join(v.keys(), " ")) + return fmt.Sprintf("Item must be one of %s", strings.Join(v.keys(), " ")) } func (v validatorStringOneOf) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("item must be one of `%s`", strings.Join(v.keys(), "` `")) + return fmt.Sprintf("Item must be one of `%s`", strings.Join(v.keys(), "` `")) } func (v validatorStringOneOf) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { @@ -51,7 +51,7 @@ func (v validatorStringOneOf) Validate(ctx context.Context, req tfsdk.ValidateAt if _, ok := v.Items[item.Value]; !ok { resp.Diagnostics.AddAttributeError( req.AttributePath, - "Invalid Item", + "Invalid value provided", fmt.Sprintf("Item must be one of %s, got: %s.", strings.Join(v.keys(), " "), item.Value), ) return diff --git a/vercel/validator_string_regex.go b/vercel/validator_string_regex.go new file mode 100644 index 00000000..7338f8f1 --- /dev/null +++ b/vercel/validator_string_regex.go @@ -0,0 +1,49 @@ +package vercel + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func stringRegex(re *regexp.Regexp, errorMessage string) validatorStringRegex { + return validatorStringRegex{ + Re: re, + ErrorMessage: errorMessage, + } +} + +type validatorStringRegex struct { + Re *regexp.Regexp + ErrorMessage string +} + +func (v validatorStringRegex) Description(ctx context.Context) string { + return v.ErrorMessage +} +func (v validatorStringRegex) MarkdownDescription(ctx context.Context) string { + return v.ErrorMessage +} + +func (v validatorStringRegex) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + var str types.String + diags := tfsdk.ValueAs(ctx, req.AttributeConfig, &str) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + if str.Unknown || str.Null { + return + } + ok := v.Re.MatchString(str.Value) + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid value provided", + v.ErrorMessage, + ) + return + } +} diff --git a/vercel/validator_string_set_items_in.go b/vercel/validator_string_set_items_in.go index cd6120cc..f0eb1186 100644 --- a/vercel/validator_string_set_items_in.go +++ b/vercel/validator_string_set_items_in.go @@ -61,7 +61,7 @@ func (v validatorStringSetItemsIn) Validate(ctx context.Context, req tfsdk.Valid if _, ok := v.Items[item.Value]; !ok { resp.Diagnostics.AddAttributeError( req.AttributePath, - "Invalid Set Item", + "Invalid value provided", fmt.Sprintf("Set item must be one of %s, got: %s.", strings.Join(v.keys(), " "), item.Value), ) return