From 5e798accff1df8b4964e5e52ba668b5dc3ac0c4f Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:19:48 -0700 Subject: [PATCH] Add `region_from_zone` provider function (#10073) * add regionFromZone function logic * add region_from_zone_test.go * add region_from_zone_internal_test.go * add region_from_zone.html.markdown * get region_from_zone_test.go passing * fix region_from_zone_internal_test.go tests * shorten test config setup time * have more explicit error for invalid input * Update mmv1/third_party/terraform/functions/region_from_zone.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> * use compute_disk for better test time * Update mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> * Update mmv1/third_party/terraform/functions/region_from_zone_test.go * Add `location_from_id` provider-defined function (#10061) * Add `location_from_id` function, tests, docs * Fix whitespace in acc test HCL config * fix runtime error and update internal test output * Update mmv1/third_party/terraform/functions/region_from_zone_test.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> * update to use projectRegionRegex * Update mmv1/third_party/terraform/functions/region_from_zone.go --------- Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --- .../terraform/functions/region_from_zone.go | 64 ++++++++++++++ .../region_from_zone_internal_test.go | 87 +++++++++++++++++++ .../functions/region_from_zone_test.go | 66 ++++++++++++++ .../fwprovider/framework_provider.go.erb | 1 + .../functions/region_from_zone.html.markdown | 57 ++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 mmv1/third_party/terraform/functions/region_from_zone.go create mode 100644 mmv1/third_party/terraform/functions/region_from_zone_internal_test.go create mode 100644 mmv1/third_party/terraform/functions/region_from_zone_test.go create mode 100644 mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown diff --git a/mmv1/third_party/terraform/functions/region_from_zone.go b/mmv1/third_party/terraform/functions/region_from_zone.go new file mode 100644 index 000000000000..61a1addbdd3a --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_zone.go @@ -0,0 +1,64 @@ +package functions + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = RegionFromZoneFunction{} + +func NewRegionFromZoneFunction() function.Function { + return &RegionFromZoneFunction{} +} + +type RegionFromZoneFunction struct{} + +func (f RegionFromZoneFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "region_from_zone" +} + +func (f RegionFromZoneFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the region within a provided resource's zone", + Description: "Takes a single string argument, which should be a resource's zone.", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "zone", + Description: "A string of a resource's zone.", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f RegionFromZoneFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) + + if resp.Diagnostics.HasError() { + return + } + + if arg0 == "" { + resp.Diagnostics.AddArgumentError( + 0, + noMatchesErrorSummary, + "The input string cannot be empty.", + ) + return + } + + if arg0[len(arg0)-2] != '-' { + resp.Diagnostics.AddArgumentError( + 0, + noMatchesErrorSummary, + fmt.Sprintf("The input string \"%s\" is not a valid zone name.", arg0), + ) + return + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, arg0[:len(arg0)-2])...) +} diff --git a/mmv1/third_party/terraform/functions/region_from_zone_internal_test.go b/mmv1/third_party/terraform/functions/region_from_zone_internal_test.go new file mode 100644 index 000000000000..bd506ae679d6 --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_zone_internal_test.go @@ -0,0 +1,87 @@ +package functions + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFunctionRun_region_from_zone(t *testing.T) { + t.Parallel() + + region := "us-central1" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid zone input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("us-central1-b")}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(region)), + }, + }, + "it returns an error when given input is empty": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("")}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Diagnostics: diag.Diagnostics{ + diag.NewArgumentErrorDiagnostic( + 0, + noMatchesErrorSummary, + "The input string cannot be empty.", + ), + }, + }, + }, + "it returns an error when given input is not a zone": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("foobar")}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Diagnostics: diag.Diagnostics{ + diag.NewArgumentErrorDiagnostic( + 0, + noMatchesErrorSummary, + "The input string \"foobar\" is not a valid zone name.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + tn, tc := name, testCase + + t.Run(tn, func(t *testing.T) { + t.Parallel() + + // Arrange + got := function.RunResponse{ + Result: function.NewResultData(basetypes.StringValue{}), + } + + // Act + NewRegionFromZoneFunction().Run(context.Background(), tc.request, &got) + + // Assert + if diff := cmp.Diff(got.Result, tc.expected.Result); diff != "" { + t.Errorf("unexpected diff between expected and received result: %s", diff) + } + if diff := cmp.Diff(got.Diagnostics, tc.expected.Diagnostics); diff != "" { + t.Errorf("unexpected diff between expected and received diagnostics: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/region_from_zone_test.go b/mmv1/third_party/terraform/functions/region_from_zone_test.go new file mode 100644 index 000000000000..68c001ada13a --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_zone_test.go @@ -0,0 +1,66 @@ +package functions_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccProviderFunction_region_from_zone(t *testing.T) { + t.Parallel() + // Skipping due to requiring TF 1.8.0 in VCR systems : https://github.com/hashicorp/terraform-provider-google/issues/17451 + acctest.SkipIfVcr(t) + projectZone := "us-central1-a" + projectRegion := "us-central1" + projectRegionRegex := regexp.MustCompile(fmt.Sprintf("^%s$", projectRegion)) + + context := map[string]interface{}{ + "function_name": "region_from_zone", + "output_name": "zone", + "resource_name": fmt.Sprintf("tf-test-region-from-zone-func-%s", acctest.RandString(t, 10)), + "resource_location": projectZone, + } + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testProviderFunction_get_region_from_zone(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), projectRegionRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_region_from_zone(context map[string]interface{}) string { + return acctest.Nprintf(` +# terraform block required for provider function to be found +terraform { + required_providers { + google = { + source = "hashicorp/google" + } + } +} + +resource "google_compute_disk" "default" { + name = "%{resource_name}" + type = "pd-ssd" + zone = "%{resource_location}" + image = "debian-11-bullseye-v20220719" + labels = { + environment = "dev" + } + physical_block_size_bytes = 4096 + } + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_disk.default.zone) +} +`, context) +} diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb b/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb index fdb355b48886..88c54dbc29e4 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb @@ -304,6 +304,7 @@ func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resou func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Function { return []func() function.Function{ functions.NewProjectFromIdFunction, + functions.NewRegionFromZoneFunction, functions.NewLocationFromIdFunction, } } \ No newline at end of file diff --git a/mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown b/mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown new file mode 100644 index 000000000000..2fc328f7580c --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown @@ -0,0 +1,57 @@ +--- +page_title: region_from_zone Function - terraform-provider-google +description: |- + Returns the region within a provided zone. +--- + +# Function: region_from_zone + +Returns a region name derived from a provided zone. + +For more information about using provider-defined functions with Terraform [see the official documentation](https://developer.hashicorp.com/terraform/plugin/framework/functions/concepts). + +## Example Usage + +### Use with the `google` provider + +```terraform +terraform { + required_providers { + google = { + source = "hashicorp/google" + } + } +} + +# Value is "us-central1" +output "function_output" { + value = provider::google::region_from_zone("us-central1-b") +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +# Value is "us-central1" +output "function_output" { + value = provider::google-beta::region_from_zone("us-central1-b") +} +``` + +## Signature + +```text +region_from_zone(zone string) string +``` + +## Arguments + +1. `zone` (String) A string of a resource's zone