diff --git a/mmv1/provider/terraform/common~compile.yaml b/mmv1/provider/terraform/common~compile.yaml index 2ca0e491bd18..266818c89957 100644 --- a/mmv1/provider/terraform/common~compile.yaml +++ b/mmv1/provider/terraform/common~compile.yaml @@ -102,6 +102,12 @@ -%> '<%= dir -%>/<%= fname.delete_suffix(".erb") -%>': 'third_party/terraform/framework_models/<%= fname -%>' <% end -%> +<% + Dir["third_party/terraform/functions/*.go.erb"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/functions/<%= fname.delete_suffix(".erb") -%>': 'third_party/terraform/functions/<%= fname -%>' +<% end -%> <% Dir["third_party/terraform/scripts/**/*.erb"].each do |file_path| fname = file_path.delete_prefix("third_party/terraform/") diff --git a/mmv1/provider/terraform/common~copy.yaml b/mmv1/provider/terraform/common~copy.yaml index 1409c612a3f6..1565f5b67fea 100644 --- a/mmv1/provider/terraform/common~copy.yaml +++ b/mmv1/provider/terraform/common~copy.yaml @@ -122,6 +122,13 @@ '<%= dir -%>/envvar/<%= fname -%>': 'third_party/terraform/envvar/<%= fname -%>' <% end -%> +<% + Dir["third_party/terraform/functions/*.go"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/functions/<%= fname -%>': 'third_party/terraform/functions/<%= fname -%>' +<% end -%> + <% Dir["third_party/terraform/scripts/**/*.*"].each do |file_path| next if file_path.end_with?('.erb') diff --git a/mmv1/third_party/terraform/functions/element_from_id.go b/mmv1/third_party/terraform/functions/element_from_id.go new file mode 100644 index 000000000000..3cd73983c92e --- /dev/null +++ b/mmv1/third_party/terraform/functions/element_from_id.go @@ -0,0 +1,35 @@ +package functions + +import ( + "context" + "fmt" + "log" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +// ValidateElementFromIdArguments is reusable validation logic used in provider-defined functions that use the GetElementFromId function +func ValidateElementFromIdArguments(ctx context.Context, input string, regex *regexp.Regexp, pattern string, functionName string) *function.FuncError { + submatches := regex.FindAllStringSubmatchIndex(input, -1) + + // Zero matches means unusable input; error returned + if len(submatches) == 0 { + return function.NewArgumentFuncError(0, fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"%s\".", input, pattern)) + } + + // >1 matches means input usable but not ideal; debug log + if len(submatches) > 1 { + log.Printf("[DEBUG] Provider-defined function %s was called with input string: %s. This contains more than one match for the pattern %s. Terraform will use the first found match.", functionName, input, pattern) + } + + return nil +} + +// GetElementFromId is reusable logic that is used in multiple provider-defined functions for pulling elements out of self links and ids of resources and data sources +func GetElementFromId(input string, regex *regexp.Regexp, template string) string { + submatches := regex.FindAllStringSubmatchIndex(input, -1) + submatch := submatches[0] // Take the only / left-most submatch + dst := []byte{} + return string(regex.ExpandString(dst, template, input, submatch)) +} diff --git a/mmv1/third_party/terraform/functions/element_from_id_internal_test.go b/mmv1/third_party/terraform/functions/element_from_id_internal_test.go new file mode 100644 index 000000000000..4921bc7c8f4e --- /dev/null +++ b/mmv1/third_party/terraform/functions/element_from_id_internal_test.go @@ -0,0 +1,84 @@ +package functions_test + +import ( + "context" + "regexp" + "testing" + + tpg_functions "github.com/hashicorp/terraform-provider-google/google/functions" +) + +func TestFunctionInternals_ValidateElementFromIdArguments(t *testing.T) { + + // Values here are matched to test case values below + regex := regexp.MustCompile("two/(?P[^/]+)/") + pattern := "two/{two}/" + + cases := map[string]struct { + Input string + ExpectedElement string + ExpectError bool + }{ + "it sets an error if no match is found": { + Input: "one/element-1/three/element-3", + ExpectError: true, + }, + "it doesn't set an error if more than one match is found": { + Input: "two/element-2/two/element-2/two/element-2", + ExpectedElement: "element-2", + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + + // Arrange + ctx := context.Background() + + // Act + err := tpg_functions.ValidateElementFromIdArguments(ctx, tc.Input, regex, pattern, "function-name-here") // last arg value is inconsequential for this test + + // Assert + if err != nil && !tc.ExpectError { + t.Fatalf("Unexpected error(s) were set in response diags: %s", err.Text) + } + if err == nil && tc.ExpectError { + t.Fatal("Expected error(s) to be set in response diags, but there were none.") + } + }) + } +} + +func TestFunctionInternals_GetElementFromId(t *testing.T) { + + // Values here are matched to test case values below + regex := regexp.MustCompile("two/(?P[^/]+)/") + template := "$Element" + + cases := map[string]struct { + Input string + ExpectedElement string + }{ + "it can pull out a value from a string using a regex with a submatch": { + Input: "one/element-1/two/element-2/three/element-3", + ExpectedElement: "element-2", + }, + "it will pull out the first value from a string with more than one submatch": { + Input: "one/element-1/two/element-2/two/not-this-one/three/element-3", + ExpectedElement: "element-2", + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + + // Act + result := tpg_functions.GetElementFromId(tc.Input, regex, template) + + // Assert + if result != tc.ExpectedElement { + t.Fatalf("Expected function logic to retrieve %s from input %s, got %s", tc.ExpectedElement, tc.Input, result) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/location_from_id.go b/mmv1/third_party/terraform/functions/location_from_id.go new file mode 100644 index 000000000000..3aaf4ce554b8 --- /dev/null +++ b/mmv1/third_party/terraform/functions/location_from_id.go @@ -0,0 +1,62 @@ +package functions + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = LocationFromIdFunction{} + +func NewLocationFromIdFunction() function.Function { + return &LocationFromIdFunction{ + name: "location_from_id", + } +} + +type LocationFromIdFunction struct { + name string // Makes function name available in Run logic for logging purposes +} + +func (f LocationFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = f.name +} + +func (f LocationFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the location name within a provided resource id, self link, or OP style resource name.", + Description: "Takes a single string argument, which should be a resource id, self link, or OP style resource name. This function will either return the location name from the input string or raise an error due to no location being present in the string. The function uses the presence of \"locations/{{location}}/\" in the input string to identify the location name, e.g. when the function is passed the id \"projects/my-project/locations/us-central1/services/my-service\" as an argument it will return \"us-central1\".", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "A string of a resource's id, a resource's self link, or an OP style resource name. For example, \"projects/my-project/locations/us-central1/services/my-service\" and \"https://run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service\" are valid values containing locations", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f LocationFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + // Prepare how we'll identify location name from input string + regex := regexp.MustCompile("locations/(?P[^/]+)/") // Should match the pattern below + template := "$LocationName" // Should match the submatch identifier in the regex + pattern := "locations/{location}/" // Human-readable pseudo-regex pattern used in errors and warnings + + // Validate input + resp.Error = function.ConcatFuncErrors(ValidateElementFromIdArguments(ctx, arg0, regex, pattern, f.name)) + if resp.Error != nil { + return + } + + // Get and return element from input string + location := GetElementFromId(arg0, regex, template) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, location)) +} diff --git a/mmv1/third_party/terraform/functions/location_from_id_internal_test.go b/mmv1/third_party/terraform/functions/location_from_id_internal_test.go new file mode 100644 index 000000000000..2ee280a7668e --- /dev/null +++ b/mmv1/third_party/terraform/functions/location_from_id_internal_test.go @@ -0,0 +1,99 @@ +package functions + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFunctionRun_location_from_id(t *testing.T) { + t.Parallel() + + location := "us-central1" + + // Happy path inputs + validId := fmt.Sprintf("projects/my-project/locations/%s/services/my-service", location) + validSelfLink := fmt.Sprintf("https://run.googleapis.com/v2/%s", validId) + validOpStyleResourceName := fmt.Sprintf("//run.googleapis.com/v2/%s", validId) + + // Unhappy path inputs + repetitiveInput := fmt.Sprintf("https://run.googleapis.com/v2/projects/my-project/locations/%s/locations/not-this-one/services/my-service", location) // Multiple /locations/{{location}}/ + invalidInput := "zones/us-central1-c/instances/my-instance" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid resource id input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(location)), + }, + }, + "it returns the expected output value when given a valid resource self_link input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(location)), + }, + }, + "it returns the expected output value when given a valid OP style resource name input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(location)), + }, + }, + "it returns the first submatch (with no error) when given repetitive input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(repetitiveInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(location)), + }, + }, + "it returns an error when given input with no submatches": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Error: function.NewArgumentFuncError(0, fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"locations/{location}/\".", invalidInput)), + }, + }, + } + + 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 + NewLocationFromIdFunction().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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/location_from_id_test.go b/mmv1/third_party/terraform/functions/location_from_id_test.go new file mode 100644 index 000000000000..26b73d2bc26b --- /dev/null +++ b/mmv1/third_party/terraform/functions/location_from_id_test.go @@ -0,0 +1,75 @@ +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_location_from_id(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) + + location := "us-central1" + locationRegex := regexp.MustCompile(fmt.Sprintf("^%s$", location)) + + context := map[string]interface{}{ + "function_name": "location_from_id", + "output_name": "location", + "resource_name": fmt.Sprintf("tf-test-location-id-func-%s", acctest.RandString(t, 10)), + "resource_location": location, + } + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Can get the location from a resource's id in one step + // Uses google_cloud_run_service resource's id attribute with format projects/{project}/locations/{location}/services/{service}. + Config: testProviderFunction_get_location_from_resource_id(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), locationRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_location_from_resource_id(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_cloud_run_service" "default" { + name = "%{resource_name}" + location = "%{resource_location}" + + template { + spec { + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello" + } + } + } + + traffic { + percent = 100 + latest_revision = true + } +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_cloud_run_service.default.id) +} +`, context) +} diff --git a/mmv1/third_party/terraform/functions/name_from_id.go b/mmv1/third_party/terraform/functions/name_from_id.go new file mode 100644 index 000000000000..860342517afc --- /dev/null +++ b/mmv1/third_party/terraform/functions/name_from_id.go @@ -0,0 +1,62 @@ +package functions + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = NameFromIdFunction{} + +func NewNameFromIdFunction() function.Function { + return &NameFromIdFunction{ + name: "name_from_id", + } +} + +type NameFromIdFunction struct { + name string +} + +func (f NameFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = f.name +} + +func (f NameFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the short-form name of a resource within a provided resource's id, resource URI, self link, or full resource name.", + Description: "Takes a single string argument, which should be a resource's id, resource URI, self link, or full resource name. This function will return the short-form name of a resource from the input string, or raise an error due to a problem with the input string. The function returns the final element in the input string as the resource's name, e.g. when the function is passed the id \"projects/my-project/zones/us-central1-c/instances/my-instance\" as an argument it will return \"my-instance\".", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "A string of a resource's id, resource URI, self link, or full resource name. For example, \"projects/my-project/zones/us-central1-c/instances/my-instance\", \"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance\" and \"//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/my-membership\" are valid values", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f NameFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + // Prepare how we'll identify resource name from input string + regex := regexp.MustCompile("/(?P[^/]+)$") // Should match the pattern below + template := "$ResourceName" // Should match the submatch identifier in the regex + pattern := "resourceType/{name}$" // Human-readable pseudo-regex pattern used in errors and warnings + + // Validate input + resp.Error = function.ConcatFuncErrors(ValidateElementFromIdArguments(ctx, arg0, regex, pattern, f.name)) + if resp.Error != nil { + return + } + + // Get and return element from input string + name := GetElementFromId(arg0, regex, template) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, name)) +} diff --git a/mmv1/third_party/terraform/functions/name_from_id_internal_test.go b/mmv1/third_party/terraform/functions/name_from_id_internal_test.go new file mode 100644 index 000000000000..b51849728f86 --- /dev/null +++ b/mmv1/third_party/terraform/functions/name_from_id_internal_test.go @@ -0,0 +1,90 @@ +package functions + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFunctionRun_name_from_id(t *testing.T) { + t.Parallel() + + name := "foobar" + + // Happy path inputs + validId := fmt.Sprintf("projects/my-project/zones/us-central1-c/instances/%s", name) + validSelfLink := fmt.Sprintf("https://www.googleapis.com/compute/v1/%s", validId) + validOpStyleResourceName := fmt.Sprintf("//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/%s", name) + + // Unhappy path inputs + invalidInput := "this isn't a URI or id" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid resource id input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(name)), + }, + }, + "it returns the expected output value when given a valid resource self_link input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(name)), + }, + }, + "it returns the expected output value when given a valid OP style resource name input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(name)), + }, + }, + "it returns an error when given input with no submatches": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Error: function.NewArgumentFuncError(0, fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"resourceType/{name}$\".", invalidInput)), + }, + }, + } + + 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 + NewNameFromIdFunction().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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/name_from_id_test.go b/mmv1/third_party/terraform/functions/name_from_id_test.go new file mode 100644 index 000000000000..8eaf139918c3 --- /dev/null +++ b/mmv1/third_party/terraform/functions/name_from_id_test.go @@ -0,0 +1,88 @@ +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_name_from_id(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) + + context := map[string]interface{}{ + "function_name": "name_from_id", + "output_name": "name", + "resource_name": fmt.Sprintf("tf-test-name-id-func-%s", acctest.RandString(t, 10)), + } + + nameRegex := regexp.MustCompile(fmt.Sprintf("^%s$", context["resource_name"])) + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Can get the name from a resource's id in one step + // Uses google_pubsub_topic resource's id attribute with format projects/{{project}}/topics/{{name}} + Config: testProviderFunction_get_name_from_resource_id(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), nameRegex), + ), + }, + { + // Can get the name from a resource's self_link in one step + // Uses google_compute_disk resource's self_link attribute + Config: testProviderFunction_get_name_from_resource_self_link(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), nameRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_name_from_resource_id(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_pubsub_topic" "default" { + name = "%{resource_name}" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_pubsub_topic.default.id) +} +`, context) +} + +func testProviderFunction_get_name_from_resource_self_link(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}" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_disk.default.self_link) +} +`, context) +} diff --git a/mmv1/third_party/terraform/functions/project_from_id.go b/mmv1/third_party/terraform/functions/project_from_id.go new file mode 100644 index 000000000000..70b27a74123f --- /dev/null +++ b/mmv1/third_party/terraform/functions/project_from_id.go @@ -0,0 +1,62 @@ +package functions + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = ProjectFromIdFunction{} + +func NewProjectFromIdFunction() function.Function { + return &ProjectFromIdFunction{ + name: "project_from_id", + } +} + +type ProjectFromIdFunction struct { + name string // Makes function name available in Run logic for logging purposes +} + +func (f ProjectFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = f.name +} + +func (f ProjectFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the project within a provided resource's id, resource URI, self link, or full resource name.", + Description: "Takes a single string argument, which should be a resource's id, resource URI, self link, or full resource name. This function will either return the project name from the input string or raise an error due to no project being present in the string. The function uses the presence of \"projects/{{project}}/\" in the input string to identify the project name, e.g. when the function is passed the id \"projects/my-project/zones/us-central1-c/instances/my-instance\" as an argument it will return \"my-project\".", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "A string of a resource's id, resource URI, self link, or full resource name. For example, \"projects/my-project/zones/us-central1-c/instances/my-instance\", \"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance\" and \"//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/my-membership\" are valid values", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f ProjectFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + // Prepare how we'll identify project id from input string + regex := regexp.MustCompile("projects/(?P[^/]+)/") // Should match the pattern below + template := "$ProjectId" // Should match the submatch identifier in the regex + pattern := "projects/{project}/" // Human-readable pseudo-regex pattern used in errors and warnings + + // Validate input + resp.Error = function.ConcatFuncErrors(ValidateElementFromIdArguments(ctx, arg0, regex, pattern, f.name)) + if resp.Error != nil { + return + } + + // Get and return element from input string + projectId := GetElementFromId(arg0, regex, template) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, projectId)) +} diff --git a/mmv1/third_party/terraform/functions/project_from_id_internal_test.go b/mmv1/third_party/terraform/functions/project_from_id_internal_test.go new file mode 100644 index 000000000000..dd6155e4d0c6 --- /dev/null +++ b/mmv1/third_party/terraform/functions/project_from_id_internal_test.go @@ -0,0 +1,101 @@ +package functions + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFunctionRun_project_from_id(t *testing.T) { + t.Parallel() + + projectId := "my-project" + + // Happy path inputs + validId := fmt.Sprintf("projects/%s/zones/us-central1-c/instances/my-instance", projectId) + validSelfLink := fmt.Sprintf("https://www.googleapis.com/compute/v1/%s", validId) + validOpStyleResourceName := fmt.Sprintf("//gkehub.googleapis.com/projects/%s/locations/us-central1/memberships/my-membership", projectId) + + // Unhappy path inputs + repetitiveInput := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/projects/not-this-1/projects/not-this-2/instances/my-instance", projectId) // Multiple /projects/{{project}}/ + invalidInput := "zones/us-central1-c/instances/my-instance" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid resource id input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(projectId)), + }, + }, + "it returns the expected output value when given a valid resource self_link input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(projectId)), + }, + }, + "it returns the expected output value when given a valid OP style resource name input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(projectId)), + }, + }, + "it returns the first submatch (with no error) when given repetitive input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(repetitiveInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(projectId)), + }, + }, + "it returns an error when given input with no submatches": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Error: function.NewArgumentFuncError( + 0, + fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"projects/{project}/\".", invalidInput)), + }, + }, + } + + 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 + NewProjectFromIdFunction().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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/project_from_id_test.go b/mmv1/third_party/terraform/functions/project_from_id_test.go new file mode 100644 index 000000000000..34f0f624af59 --- /dev/null +++ b/mmv1/third_party/terraform/functions/project_from_id_test.go @@ -0,0 +1,91 @@ +package functions_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccProviderFunction_project_from_id(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) + + projectId := envvar.GetTestProjectFromEnv() + projectIdRegex := regexp.MustCompile(fmt.Sprintf("^%s$", projectId)) + + context := map[string]interface{}{ + "function_name": "project_from_id", + "output_name": "project_id", + "resource_name": fmt.Sprintf("tf_test_project_id_func_%s", acctest.RandString(t, 10)), + } + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Can get the project from a resource's id in one step + // Uses google_pubsub_topic resource's id attribute with format projects/{{project}}/topics/{{name}} + Config: testProviderFunction_get_project_from_resource_id(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), projectIdRegex), + ), + }, + { + // Can get the project from a resource's self_link in one step + // Uses google_bigquery_dataset resource's self_link attribute + Config: testProviderFunction_get_project_from_resource_self_link(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), projectIdRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_project_from_resource_id(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_pubsub_topic" "default" { + name = "%{resource_name}" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_pubsub_topic.default.id) +} +`, context) +} + +func testProviderFunction_get_project_from_resource_self_link(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_bigquery_dataset" "default" { + dataset_id = "%{resource_name}" + description = "This dataset is made in an acceptance test" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_bigquery_dataset.default.self_link) +} +`, context) +} diff --git a/mmv1/third_party/terraform/functions/region_from_id.go b/mmv1/third_party/terraform/functions/region_from_id.go new file mode 100644 index 000000000000..11235c3592ea --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_id.go @@ -0,0 +1,62 @@ +package functions + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = RegionFromIdFunction{} + +func NewRegionFromIdFunction() function.Function { + return &RegionFromIdFunction{ + name: "region_from_id", + } +} + +type RegionFromIdFunction struct { + name string // Makes function name available in Run logic for logging purposes +} + +func (f RegionFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = f.name +} + +func (f RegionFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the region name within a provided resource id, self link, or OP style resource name.", + Description: "Takes a single string argument, which should be a resource id, self link, or OP style resource name. This function will either return the region name from the input string or raise an error due to no region being present in the string. The function uses the presence of \"regions/{{region}}/\" in the input string to identify the region name, e.g. when the function is passed the id \"projects/my-project/regions/us-central1/subnetworks/my-subnetwork\" as an argument it will return \"us-central1\".", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "A string of a resource's id, a resource's self link, or an OP style resource name. For example, \"projects/my-project/regions/us-central1/subnetworks/my-subnetwork\" and \"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/my-subnetwork\" are valid values containing regions", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f RegionFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + // Prepare how we'll identify region name from input string + regex := regexp.MustCompile("regions/(?P[^/]+)/") // Should match the pattern below + template := "$RegionName" // Should match the submatch identifier in the regex + pattern := "regions/{region}/" // Human-readable pseudo-regex pattern used in errors and warnings + + // Validate input + resp.Error = function.ConcatFuncErrors(ValidateElementFromIdArguments(ctx, arg0, regex, pattern, f.name)) + if resp.Error != nil { + return + } + + // Get and return element from input string + region := GetElementFromId(arg0, regex, template) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, region)) +} diff --git a/mmv1/third_party/terraform/functions/region_from_id_internal_test.go b/mmv1/third_party/terraform/functions/region_from_id_internal_test.go new file mode 100644 index 000000000000..655740b7c05a --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_id_internal_test.go @@ -0,0 +1,102 @@ +package functions + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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_id(t *testing.T) { + t.Parallel() + + regionName := "us-central1" + + // Happy path inputs + validId := fmt.Sprintf("projects/my-project/regions/%s/subnetworks/my-subnetwork", regionName) + validSelfLink := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/my-project/regions/%s/subnetworks/my-subnetwork", regionName) + validOpStyleResourceName := fmt.Sprintf("//compute.googleapis.com/projects/my-project/regions/%s/addresses/my-address", regionName) + + // Unhappy path inputs + repetitiveInput := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/my-project/regions/%s/regions/not-this-one/subnetworks/my-subnetwork", regionName) + invalidInput := "projects/my-project/zones/us-central1-c/instances/my-instance" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid resource id input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(regionName)), + }, + }, + "it returns the expected output value when given a valid resource self_link input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(regionName)), + }, + }, + "it returns the expected output value when given a valid OP style resource name input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(regionName)), + }, + }, + "it returns the first submatch (with no error) when given repetitive input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(repetitiveInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(regionName)), + }, + }, + "it returns an error when given input with no submatches": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Error: function.NewArgumentFuncError( + 0, + fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"regions/{region}/\".", invalidInput), + ), + }, + }, + } + + 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 + NewRegionFromIdFunction().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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/region_from_id_test.go b/mmv1/third_party/terraform/functions/region_from_id_test.go new file mode 100644 index 000000000000..79ef135d9a27 --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_id_test.go @@ -0,0 +1,92 @@ +package functions_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccProviderFunction_region_from_id(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) + + region := envvar.GetTestRegionFromEnv() + regionRegex := regexp.MustCompile(fmt.Sprintf("^%s$", region)) + + context := map[string]interface{}{ + "function_name": "region_from_id", + "output_name": "region", + "resource_name": fmt.Sprintf("tf-test-region-id-func-%s", acctest.RandString(t, 10)), + } + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Can get the region from a resource's id in one step + // Uses google_compute_node_template resource's id attribute with format projects/{{project}}/regions/{{region}}/nodeTemplates/{{name}} + Config: testProviderFunction_get_region_from_resource_id(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), regionRegex), + ), + }, + { + // Can get the region from a resource's self_link in one step + // Uses google_compute_node_template resource's self_link attribute + Config: testProviderFunction_get_region_from_resource_self_link(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), regionRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_region_from_resource_id(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_node_template" "default" { + name = "%{resource_name}" + node_type = "n1-node-96-624" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_node_template.default.id) +} +`, context) +} + +func testProviderFunction_get_region_from_resource_self_link(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_node_template" "default" { + name = "%{resource_name}" + node_type = "n1-node-96-624" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_node_template.default.self_link) +} +`, context) +} 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..d1ba3104e88c --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_zone.go @@ -0,0 +1,57 @@ +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.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + if arg0 == "" { + err := function.NewArgumentFuncError(0, "The input string cannot be empty.") + resp.Error = function.ConcatFuncErrors(err) + return + } + + if arg0[len(arg0)-2] != '-' { + err := function.NewArgumentFuncError(0, fmt.Sprintf("The input string \"%s\" is not a valid zone name.", arg0)) + resp.Error = function.ConcatFuncErrors(err) + return + } + + resp.Error = function.ConcatFuncErrors(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..ceeac2b1c9a3 --- /dev/null +++ b/mmv1/third_party/terraform/functions/region_from_zone_internal_test.go @@ -0,0 +1,74 @@ +package functions + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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()), + Error: function.NewArgumentFuncError(0, "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()), + Error: function.NewArgumentFuncError(0, "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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %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/functions/zone_from_id.go b/mmv1/third_party/terraform/functions/zone_from_id.go new file mode 100644 index 000000000000..c1f3ae95eeaa --- /dev/null +++ b/mmv1/third_party/terraform/functions/zone_from_id.go @@ -0,0 +1,62 @@ +package functions + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = ZoneFromIdFunction{} + +func NewZoneFromIdFunction() function.Function { + return &ZoneFromIdFunction{ + name: "zone_from_id", + } +} + +type ZoneFromIdFunction struct { + name string // Makes function name available in Run logic for logging purposes +} + +func (f ZoneFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = f.name +} + +func (f ZoneFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Returns the zone name within the resource id or self link provided as an argument.", + Description: "Takes a single string argument, which should be an id or self link of a resource. This function will either return the zone name from the input string or raise an error due to no zone being present in the string. The function uses the presence of \"zones/{{zone}}/\" in the input string to identify the zone name, e.g. when the function is passed the id \"projects/my-project/zones/us-central1-c/instances/my-instance\" as an argument it will return \"us-central1-c\".", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "An id of a resouce, or a self link. For example, both \"projects/my-project/zones/us-central1-c/instances/my-instance\" and \"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance\" are valid inputs", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f ZoneFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // Load arguments from function call + var arg0 string + resp.Error = function.ConcatFuncErrors(req.Arguments.GetArgument(ctx, 0, &arg0)) + if resp.Error != nil { + return + } + + // Prepare how we'll identify zone name from input string + regex := regexp.MustCompile("zones/(?P[^/]+)/") // Should match the pattern below + template := "$ZoneName" // Should match the submatch identifier in the regex + pattern := "zones/{zone}/" // Human-readable pseudo-regex pattern used in errors and warnings + + // Validate input + resp.Error = function.ConcatFuncErrors(ValidateElementFromIdArguments(ctx, arg0, regex, pattern, f.name)) + if resp.Error != nil { + return + } + + // Get and return element from input string + zone := GetElementFromId(arg0, regex, template) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, zone)) +} diff --git a/mmv1/third_party/terraform/functions/zone_from_id_internal_test.go b/mmv1/third_party/terraform/functions/zone_from_id_internal_test.go new file mode 100644 index 000000000000..f473f43c24f0 --- /dev/null +++ b/mmv1/third_party/terraform/functions/zone_from_id_internal_test.go @@ -0,0 +1,102 @@ +package functions + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFunctionRun_zone_from_id(t *testing.T) { + t.Parallel() + + zone := "us-central1-a" + + // Happy path inputs + validId := fmt.Sprintf("projects/my-project/zones/%s/networkEndpointGroups/my-neg", zone) + validSelfLink := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/my-project/zones/%s/networkEndpointGroups/my-neg", zone) + validOpStyleResourceName := fmt.Sprintf("//compute.googleapis.com/projects/my-project/zones/%s/instances/my-instance", zone) + + // Unhappy path inputs + repetitiveInput := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/my-project/zones/%s/zones/not-this-one/networkEndpointGroups/my-neg", zone) + invalidInput := "projects/my-project/regions/us-central1/subnetworks/my-subnetwork" + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + "it returns the expected output value when given a valid resource id input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(zone)), + }, + }, + "it returns the expected output value when given a valid resource self_link input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(zone)), + }, + }, + "it returns the expected output value when given a valid OP style resource name input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(zone)), + }, + }, + "it returns the first submatch (with no error) when given repetitive input": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(repetitiveInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue(zone)), + }, + }, + "it returns an error when given input with no submatches": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + Error: function.NewArgumentFuncError( + 0, + fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"zones/{zone}/\".", invalidInput), + ), + }, + }, + } + + 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 + NewZoneFromIdFunction().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.Error, tc.expected.Error); diff != "" { + t.Errorf("unexpected diff between expected and received errors: %s", diff) + } + }) + } +} diff --git a/mmv1/third_party/terraform/functions/zone_from_id_test.go b/mmv1/third_party/terraform/functions/zone_from_id_test.go new file mode 100644 index 000000000000..7058825a05bc --- /dev/null +++ b/mmv1/third_party/terraform/functions/zone_from_id_test.go @@ -0,0 +1,90 @@ +package functions_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccProviderFunction_zone_from_id(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) + + zone := envvar.GetTestZoneFromEnv() + zoneRegex := regexp.MustCompile(fmt.Sprintf("^%s$", zone)) + + context := map[string]interface{}{ + "function_name": "zone_from_id", + "output_name": "zone", + "resource_name": fmt.Sprintf("tf-test-zone-id-func-%s", acctest.RandString(t, 10)), + } + + acctest.VcrTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Can get the zone from a resource's id in one step + // Uses google_compute_disk resource's id attribute with format projects/{{project}}/zones/{{zone}}/disks/{{name}} + Config: testProviderFunction_get_zone_from_resource_id(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), zoneRegex), + ), + }, + { + // Can get the zone from a resource's self_link in one step + // Uses google_compute_disk resource's self_link attribute + Config: testProviderFunction_get_zone_from_resource_self_link(context), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchOutput(context["output_name"].(string), zoneRegex), + ), + }, + }, + }) +} + +func testProviderFunction_get_zone_from_resource_id(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}" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_disk.default.id) +} +`, context) +} + +func testProviderFunction_get_zone_from_resource_self_link(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}" +} + +output "%{output_name}" { + value = provider::google::%{function_name}(google_compute_disk.default.self_link) +} +`, context) +} diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb b/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb index 0d1aa04c979e..0aec3eaaf331 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.erb @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" @@ -14,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-google/google/functions" "github.com/hashicorp/terraform-provider-google/google/fwmodels" "github.com/hashicorp/terraform-provider-google/google/fwtransport" "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" @@ -28,6 +30,7 @@ import ( // Ensure the implementation satisfies the expected interfaces var ( _ provider.ProviderWithMetaSchema = &FrameworkProvider{} + _ provider.ProviderWithFunctions = &FrameworkProvider{} ) // New is a helper function to simplify provider server and testing implementation. @@ -294,5 +297,17 @@ func (p *FrameworkProvider) DataSources(_ context.Context) []func() datasource.D // Resources defines the resources implemented in the provider. func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resource { - return nil + return nil } + +// Functions defines the provider functions implemented in the provider. +func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Function { + return []func() function.Function{ + functions.NewLocationFromIdFunction, + functions.NewNameFromIdFunction, + functions.NewProjectFromIdFunction, + functions.NewRegionFromIdFunction, + functions.NewRegionFromZoneFunction, + functions.NewZoneFromIdFunction, + } +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/website/docs/functions/location_from_id.html.markdown b/mmv1/third_party/terraform/website/docs/functions/location_from_id.html.markdown new file mode 100644 index 000000000000..40eeffa32df4 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/location_from_id.html.markdown @@ -0,0 +1,99 @@ +--- +page_title: location_from_id Function - terraform-provider-google +description: |- + Returns the location within a provided resource id, self link, or OP style resource name. +--- + +# Function: location_from_id + +Returns the location within a provided resource's id, resource URI, self link, or full resource name. + +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" + } + } +} + +resource "google_cloud_run_service" "default" { + name = "my-service" + location = "us-central1" + + template { + spec { + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello" + } + } + } + + traffic { + percent = 100 + latest_revision = true + } +} + +# Value is "us-central1" +output "location_from_id" { + value = provider::google::location_from_id(google_cloud_run_service.default.id) +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_cloud_run_service" "default" { + # provider argument omitted - provisioning by google or google-beta doesn't impact this example + name = "my-service" + location = "us-central1" + + template { + spec { + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello" + } + } + } + + traffic { + percent = 100 + latest_revision = true + } +} + + +# Value is "us-central1" +output "location_from_id" { + value = provider::google-beta::location_from_id(google_cloud_run_service.default.id) +} +``` + +## Signature + +```text +location_from_id(id string) string +``` + +## Arguments + +1. `id` (String) A string of a resource's id, resource URI, self link, or full resource name. For example, these are all valid values: + +* `"projects/my-project/locations/us-central1/services/my-service"` +* `"https://run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service"` +* `"//run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service"` diff --git a/mmv1/third_party/terraform/website/docs/functions/name_from_id.html.markdown b/mmv1/third_party/terraform/website/docs/functions/name_from_id.html.markdown new file mode 100644 index 000000000000..d45b589ed8fa --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/name_from_id.html.markdown @@ -0,0 +1,70 @@ +--- +page_title: name_from_id Function - terraform-provider-google +description: |- + Returns the project within a provided resource id, self link, or OP style resource name. +--- + +# Function: name_from_id + +Returns the short-form name within a provided resource's id, resource URI, self link, or full resource name. + +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" + } + } +} + +resource "google_pubsub_topic" "default" { + name = "my-topic" +} + +# Value is "my-topic" +output "function_output" { + value = provider::google::name_from_id(google_pubsub_topic.default.id) +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_pubsub_topic" "default" { + # provider argument omitted - provisioning by google or google-beta doesn't impact this example + name = "my-topic" +} + +# Value is "my-topic" +output "function_output" { + value = provider::google-beta::name_from_id(google_pubsub_topic.default.id) +} +``` + +## Signature + +```text +name_from_id(id string) string +``` + +## Arguments + +1. `id` (String) A string of a resource's id, resource URI, self link, or full resource name. For example, these are all valid values: + +* `"projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/my-membership"` diff --git a/mmv1/third_party/terraform/website/docs/functions/project_from_id.html.markdown b/mmv1/third_party/terraform/website/docs/functions/project_from_id.html.markdown new file mode 100644 index 000000000000..33a8b05faacd --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/project_from_id.html.markdown @@ -0,0 +1,72 @@ +--- +page_title: project_from_id Function - terraform-provider-google +description: |- + Returns the project within a provided resource id, self link, or OP style resource name. +--- + +# Function: project_from_id + +Returns the project within a provided resource's id, resource URI, self link, or full resource name. + +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" + } + } +} + +resource "google_pubsub_topic" "default" { + project = "my-project" + name = "my-topic" +} + +# Value is "my-project" +output "project_from_id" { + value = provider::google::project_from_id(google_pubsub_topic.default.id) +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_pubsub_topic" "default" { + # provider argument omitted - provisioning by google or google-beta doesn't impact this example + project = "my-project" + name = "my-topic" +} + +# Value is "my-project" +output "project_from_id" { + value = provider::google-beta::project_from_id(google_pubsub_topic.default.id) +} +``` + +## Signature + +```text +project_from_id(id string) string +``` + +## Arguments + +1. `id` (String) A string of a resource's id, resource URI, self link, or full resource name. For example, these are all valid values: + +* `"projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/my-membership"` diff --git a/mmv1/third_party/terraform/website/docs/functions/region_from_id.html.markdown b/mmv1/third_party/terraform/website/docs/functions/region_from_id.html.markdown new file mode 100644 index 000000000000..18e31ff60fc7 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/region_from_id.html.markdown @@ -0,0 +1,82 @@ +--- +page_title: region_from_id Function - terraform-provider-google +description: |- + Returns the region within a provided resource id, self link, or OP style resource name. +--- + +# Function: region_from_id + +Returns the region within a provided resource's id, resource URI, self link, or full resource name. + +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" + } + } +} + +resource "google_compute_node_template" "default" { + name = "my-node-template" + region = "us-central1" +} + +# Value is "us-central1" +output "region_from_id" { + value = provider::google::region_from_id(google_compute_node_template.default.id) +} + +# Value is "us-central1" +output "region_from_self_link" { + value = provider::google::region_from_id(google_compute_node_template.default.self_link) +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_compute_node_template" "default" { + # provider argument omitted - provisioning by google or google-beta doesn't impact this example + name = "my-node-template" + region = "us-central1" +} + +# Value is "us-central1" +output "region_from_id" { + value = provider::google-beta::region_from_id(google_compute_node_template.default.id) +} + +# Value is "us-central1" +output "region_from_self_link" { + value = provider::google-beta::region_from_id(google_compute_node_template.default.self_link) +} +``` + +## Signature + +```text +region_from_id(id string) string +``` + +## Arguments + +1. `id` (String) A string of a resource's id, resource URI, self link, or full resource name. For example, these are all valid values: + +* `"projects/my-project/regions/us-central1/subnetworks/my-subnetwork"` +* `"https://www.googleapis.com/compute/v1/projects/my-project/regions/us-central1/subnetworks/my-subnetwork"` +* `"//compute.googleapis.com/projects/my-project/regions/us-central1/subnetworks/my-subnetwork"` \ 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..9fd4ab6fa422 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/region_from_zone.html.markdown @@ -0,0 +1,138 @@ +--- +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" + } + } +} + +resource "google_compute_instance" "default" { + name = "my-instance" + machine_type = "n2-standard-2" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = "debian-cloud/debian-11" + labels = { + my_label = "value" + } + } + } + + network_interface { + network = "default" + subnetwork = google_compute_subnetwork.default.id + access_config { + // Ephemeral public IP + } + } + + metadata_startup_script = "echo hi > /test.txt" +} + +data "google_compute_network" "default" { + name = "default" +} + +resource "google_compute_subnetwork" "default" { + name = "my-subnet" + region = "us-central1" + network = data.google_compute_network.default.id + ip_cidr_range = "192.168.10.0/24" +} + +// The region_from_zone function is used to assert that the VM and subnet are in the same region +check "vm_subnet_compatibility_check" { + assert { + condition = google_compute_subnetwork.default.region == provider::google::region_from_zone(google_compute_instance.default.zone) + error_message = "Subnet ${google_compute_subnetwork.default.id} and VM ${google_compute_instance.default.id} are not in the same region" + } +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_compute_instance" "default" { + provider = google-beta + name = "my-instance" + machine_type = "n2-standard-2" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = "debian-cloud/debian-11" + labels = { + my_label = "value" + } + } + } + + network_interface { + network = "default" + subnetwork = google_compute_subnetwork.default.id + access_config { + // Ephemeral public IP + } + } + + metadata_startup_script = "echo hi > /test.txt" +} + +data "google_compute_network" "default" { + provider = google-beta + name = "default" +} + +resource "google_compute_subnetwork" "default" { + provider = google-beta + name = "my-subnet" + region = "us-central1" + network = data.google_compute_network.default.id + ip_cidr_range = "192.168.10.0/24" +} + +// The region_from_zone function is used to assert that the VM and subnet are in the same region +check "vm_subnet_compatibility_check" { + assert { + condition = google_compute_subnetwork.default.region == provider::google-beta::region_from_zone(google_compute_instance.default.zone) + error_message = "Subnet ${google_compute_subnetwork.default.id} and VM ${google_compute_instance.default.id} are not in the same region" + } +} +``` + +## Signature + +```text +region_from_zone(zone string) string +``` + +## Arguments + +1. `zone` (String) A string of a resource's zone diff --git a/mmv1/third_party/terraform/website/docs/functions/zone_from_id.html.markdown b/mmv1/third_party/terraform/website/docs/functions/zone_from_id.html.markdown new file mode 100644 index 000000000000..6d2d6bfb273f --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/functions/zone_from_id.html.markdown @@ -0,0 +1,82 @@ +--- +page_title: zone_from_id Function - terraform-provider-google +description: |- + Returns the project within a provided resource id, self link, or OP style resource name. +--- + +# Function: zone_from_id + +Returns the zone within a provided resource's id, resource URI, self link, or full resource name. + +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" + } + } +} + +resource "google_compute_disk" "default" { + name = "my-disk" + zone = "us-central1-c" +} + +# Value is "us-central1-c" +output "zone_from_id" { + value = provider::google::zone_from_id(google_compute_disk.default.id) +} + +# Value is "us-central1-c" +output "zone_from_self_link" { + value = provider::google::zone_from_id(google_compute_disk.default.self_link) +} +``` + +### Use with the `google-beta` provider + +```terraform +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + } + } +} + +resource "google_compute_disk" "default" { + # provider argument omitted - provisioning by google or google-beta doesn't impact this example + name = "my-disk" + zone = "us-central1-c" +} + +# Value is "us-central1-c" +output "zone_from_id" { + value = provider::google-beta::zone_from_id(google_compute_disk.default.id) +} + +# Value is "us-central1-c" +output "zone_from_self_link" { + value = provider::google-beta::zone_from_id(google_compute_disk.default.self_link) +} +``` + +## Signature + +```text +zone_from_id(id string) string +``` + +## Arguments + +1. `id` (String) A string of a resource's id, resource URI, self link, or full resource name. For example, these are all valid values: + +* `"projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-c/instances/my-instance"` +* `"//gkehub.googleapis.com/projects/my-project/locations/us-central1/memberships/my-membership"` \ No newline at end of file