Skip to content

Commit e8d5253

Browse files
Add location_from_id provider-defined function (#10061) (#17462)
* Add `location_from_id` function, tests, docs * Fix whitespace in acc test HCL config [upstream:880aad8f718fff47db62a04c3d39b570069f9bfe] Signed-off-by: Modular Magician <[email protected]>
1 parent 8b5dc7a commit e8d5253

File tree

6 files changed

+317
-0
lines changed

6 files changed

+317
-0
lines changed

.changelog/10061.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
provider: added provider-defined function `location_from_id` for retrieving the location from a resource's self link or id
3+
```

google/functions/location_from_id.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
package functions
4+
5+
import (
6+
"context"
7+
"regexp"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/function"
10+
)
11+
12+
var _ function.Function = LocationFromIdFunction{}
13+
14+
func NewLocationFromIdFunction() function.Function {
15+
return &LocationFromIdFunction{}
16+
}
17+
18+
type LocationFromIdFunction struct{}
19+
20+
func (f LocationFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
21+
resp.Name = "location_from_id"
22+
}
23+
24+
func (f LocationFromIdFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
25+
resp.Definition = function.Definition{
26+
Summary: "Returns the location name within a provided resource id, self link, or OP style resource name.",
27+
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\".",
28+
Parameters: []function.Parameter{
29+
function.StringParameter{
30+
Name: "id",
31+
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",
32+
},
33+
},
34+
Return: function.StringReturn{},
35+
}
36+
}
37+
38+
func (f LocationFromIdFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
39+
// Load arguments from function call
40+
var arg0 string
41+
resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...)
42+
43+
if resp.Diagnostics.HasError() {
44+
return
45+
}
46+
47+
// Prepare how we'll identify location name from input string
48+
regex := regexp.MustCompile("locations/(?P<LocationName>[^/]+)/") // Should match the pattern below
49+
template := "$LocationName" // Should match the submatch identifier in the regex
50+
pattern := "locations/{location}/" // Human-readable pseudo-regex pattern used in errors and warnings
51+
52+
// Validate input
53+
ValidateElementFromIdArguments(arg0, regex, pattern, resp)
54+
if resp.Diagnostics.HasError() {
55+
return
56+
}
57+
58+
// Get and return element from input string
59+
location := GetElementFromId(arg0, regex, template)
60+
resp.Diagnostics.Append(resp.Result.Set(ctx, location)...)
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
package functions
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/attr"
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/function"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
16+
)
17+
18+
func TestFunctionRun_location_from_id(t *testing.T) {
19+
t.Parallel()
20+
21+
location := "us-central1"
22+
23+
// Happy path inputs
24+
validId := fmt.Sprintf("projects/my-project/locations/%s/services/my-service", location)
25+
validSelfLink := fmt.Sprintf("https://run.googleapis.com/v2/%s", validId)
26+
validOpStyleResourceName := fmt.Sprintf("//run.googleapis.com/v2/%s", validId)
27+
28+
// Unhappy path inputs
29+
repetitiveInput := fmt.Sprintf("https://run.googleapis.com/v2/projects/my-project/locations/%s/locations/not-this-one/services/my-service", location) // Multiple /locations/{{location}}/
30+
invalidInput := "zones/us-central1-c/instances/my-instance"
31+
32+
testCases := map[string]struct {
33+
request function.RunRequest
34+
expected function.RunResponse
35+
}{
36+
"it returns the expected output value when given a valid resource id input": {
37+
request: function.RunRequest{
38+
Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validId)}),
39+
},
40+
expected: function.RunResponse{
41+
Result: function.NewResultData(types.StringValue(location)),
42+
},
43+
},
44+
"it returns the expected output value when given a valid resource self_link input": {
45+
request: function.RunRequest{
46+
Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validSelfLink)}),
47+
},
48+
expected: function.RunResponse{
49+
Result: function.NewResultData(types.StringValue(location)),
50+
},
51+
},
52+
"it returns the expected output value when given a valid OP style resource name input": {
53+
request: function.RunRequest{
54+
Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(validOpStyleResourceName)}),
55+
},
56+
expected: function.RunResponse{
57+
Result: function.NewResultData(types.StringValue(location)),
58+
},
59+
},
60+
"it returns a warning and the first submatch when given repetitive input": {
61+
request: function.RunRequest{
62+
Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(repetitiveInput)}),
63+
},
64+
expected: function.RunResponse{
65+
Result: function.NewResultData(types.StringValue(location)),
66+
Diagnostics: diag.Diagnostics{
67+
diag.NewArgumentWarningDiagnostic(
68+
0,
69+
ambiguousMatchesWarningSummary,
70+
fmt.Sprintf("The input string \"%s\" contains more than one match for the pattern \"locations/{location}/\". Terraform will use the first found match.", repetitiveInput),
71+
),
72+
},
73+
},
74+
},
75+
"it returns an error when given input with no submatches": {
76+
request: function.RunRequest{
77+
Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(invalidInput)}),
78+
},
79+
expected: function.RunResponse{
80+
Result: function.NewResultData(types.StringNull()),
81+
Diagnostics: diag.Diagnostics{
82+
diag.NewArgumentErrorDiagnostic(
83+
0,
84+
noMatchesErrorSummary,
85+
fmt.Sprintf("The input string \"%s\" doesn't contain the expected pattern \"locations/{location}/\".", invalidInput),
86+
),
87+
},
88+
},
89+
},
90+
}
91+
92+
for name, testCase := range testCases {
93+
tn, tc := name, testCase
94+
95+
t.Run(tn, func(t *testing.T) {
96+
t.Parallel()
97+
98+
// Arrange
99+
got := function.RunResponse{
100+
Result: function.NewResultData(basetypes.StringValue{}),
101+
}
102+
103+
// Act
104+
NewLocationFromIdFunction().Run(context.Background(), tc.request, &got)
105+
106+
// Assert
107+
if diff := cmp.Diff(got.Result, tc.expected.Result); diff != "" {
108+
t.Errorf("unexpected diff between expected and received result: %s", diff)
109+
}
110+
if diff := cmp.Diff(got.Diagnostics, tc.expected.Diagnostics); diff != "" {
111+
t.Errorf("unexpected diff between expected and received diagnostics: %s", diff)
112+
}
113+
})
114+
}
115+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
package functions_test
4+
5+
import (
6+
"fmt"
7+
"regexp"
8+
"testing"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11+
"github.com/hashicorp/terraform-provider-google/google/acctest"
12+
"github.com/hashicorp/terraform-provider-google/google/envvar"
13+
)
14+
15+
func TestAccProviderFunction_location_from_id(t *testing.T) {
16+
t.Parallel()
17+
18+
location := envvar.GetTestRegionFromEnv()
19+
locationRegex := regexp.MustCompile(fmt.Sprintf("^%s$", location))
20+
21+
context := map[string]interface{}{
22+
"function_name": "location_from_id",
23+
"output_name": "location",
24+
"resource_name": fmt.Sprintf("tf-test-location-id-func-%s", acctest.RandString(t, 10)),
25+
"resource_location": location,
26+
}
27+
28+
acctest.VcrTest(t, resource.TestCase{
29+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
30+
Steps: []resource.TestStep{
31+
{
32+
// Can get the location from a resource's id in one step
33+
// Uses google_cloud_run_service resource's id attribute with format projects/{project}/locations/{location}/services/{service}.
34+
Config: testProviderFunction_get_location_from_resource_id(context),
35+
Check: resource.ComposeTestCheckFunc(
36+
resource.TestMatchOutput(context["output_name"].(string), locationRegex),
37+
),
38+
},
39+
},
40+
})
41+
}
42+
43+
func testProviderFunction_get_location_from_resource_id(context map[string]interface{}) string {
44+
return acctest.Nprintf(`
45+
# terraform block required for provider function to be found
46+
terraform {
47+
required_providers {
48+
google = {
49+
source = "hashicorp/google"
50+
}
51+
}
52+
}
53+
54+
resource "google_cloud_run_service" "default" {
55+
name = "%{resource_name}"
56+
location = "%{resource_location}"
57+
58+
template {
59+
spec {
60+
containers {
61+
image = "us-docker.pkg.dev/cloudrun/container/hello"
62+
}
63+
}
64+
}
65+
66+
traffic {
67+
percent = 100
68+
latest_revision = true
69+
}
70+
}
71+
72+
output "%{output_name}" {
73+
value = provider::google::%{function_name}(google_cloud_run_service.default.id)
74+
}
75+
`, context)
76+
}

google/fwprovider/framework_provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -952,5 +952,6 @@ func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resou
952952
func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Function {
953953
return []func() function.Function{
954954
functions.NewProjectFromIdFunction,
955+
functions.NewLocationFromIdFunction,
955956
}
956957
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
page_title: location_from_id Function - terraform-provider-google
3+
description: |-
4+
Returns the location within a provided resource id, self link, or OP style resource name.
5+
---
6+
7+
# Function: location_from_id
8+
9+
Returns the location within a provided resource's id, resource URI, self link, or full resource name.
10+
11+
For more information about using provider-defined functions with Terraform [see the official documentation](https://developer.hashicorp.com/terraform/plugin/framework/functions/concepts).
12+
13+
## Example Usage
14+
15+
### Use with the `google` provider
16+
17+
```terraform
18+
terraform {
19+
required_providers {
20+
google = {
21+
source = "hashicorp/google"
22+
}
23+
}
24+
}
25+
26+
# Value is "us-central1"
27+
output "function_output" {
28+
value = provider::google::location_from_id("https://run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service")
29+
}
30+
```
31+
32+
### Use with the `google-beta` provider
33+
34+
```terraform
35+
terraform {
36+
required_providers {
37+
google-beta = {
38+
source = "hashicorp/google-beta"
39+
}
40+
}
41+
}
42+
43+
# Value is "us-central1"
44+
output "function_output" {
45+
value = provider::google-beta::location_from_id("https://run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service")
46+
}
47+
```
48+
49+
## Signature
50+
51+
```text
52+
location_from_id(id string) string
53+
```
54+
55+
## Arguments
56+
57+
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:
58+
59+
* `"projects/my-project/locations/us-central1/services/my-service"`
60+
* `"https://run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service"`
61+
* `"//run.googleapis.com/v2/projects/my-project/locations/us-central1/services/my-service"`

0 commit comments

Comments
 (0)