Skip to content

Commit

Permalink
Add resource name_from_id provider-defined function (#10106)
Browse files Browse the repository at this point in the history
* Add `name_from_id` function, tests, docs

* Fix whitespace in acc tests' HCL config

* Skip provider-defined acceptance test in VCR, as VCR system doesn't use required Terraform version yet

* Update documentation examples to meet feedback given in #10060

* Update `name_from_id` acc tests to not use networking-related resources

* Update mmv1/third_party/terraform/functions/name_from_id_test.go
  • Loading branch information
SarahFrench authored Mar 15, 2024
1 parent 533afb6 commit f6c50c4
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 2 deletions.
59 changes: 59 additions & 0 deletions mmv1/third_party/terraform/functions/name_from_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package functions

import (
"context"
"regexp"

"github.com/hashicorp/terraform-plugin-framework/function"
)

var _ function.Function = NameFromIdFunction{}

func NewNameFromIdFunction() function.Function {
return &NameFromIdFunction{}
}

type NameFromIdFunction struct{}

func (f NameFromIdFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "name_from_id"
}

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.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...)

if resp.Diagnostics.HasError() {
return
}

// Prepare how we'll identify resource name from input string
regex := regexp.MustCompile("/(?P<ResourceName>[^/]+)$") // 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
ValidateElementFromIdArguments(arg0, regex, pattern, resp)
if resp.Diagnostics.HasError() {
return
}

// Get and return element from input string
name := GetElementFromId(arg0, regex, template)
resp.Diagnostics.Append(resp.Result.Set(ctx, name)...)
}
97 changes: 97 additions & 0 deletions mmv1/third_party/terraform/functions/name_from_id_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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/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_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()),
Diagnostics: diag.Diagnostics{
diag.NewArgumentErrorDiagnostic(
0,
noMatchesErrorSummary,
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.Diagnostics, tc.expected.Diagnostics); diff != "" {
t.Errorf("unexpected diff between expected and received diagnostics: %s", diff)
}
})
}
}
88 changes: 88 additions & 0 deletions mmv1/third_party/terraform/functions/name_from_id_test.go
Original file line number Diff line number Diff line change
@@ -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 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), nameRegex),
),
},
{
// Can get the project from a resource's self_link in one step
// Uses google_compute_disk resource's self_link attribute
Config: testProviderFunction_get_project_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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,11 @@ func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resou
// Functions defines the provider functions implemented in the provider.
func (p *FrameworkProvider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
functions.NewProjectFromIdFunction,
functions.NewRegionFromZoneFunction,
functions.NewLocationFromIdFunction,
functions.NewNameFromIdFunction,
functions.NewProjectFromIdFunction,
functions.NewRegionFromIdFunction,
functions.NewRegionFromZoneFunction,
functions.NewZoneFromIdFunction,
}
}
Original file line number Diff line number Diff line change
@@ -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"`

0 comments on commit f6c50c4

Please sign in to comment.