Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource name_from_id provider-defined function #10106

Merged
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{}
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved

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": {
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
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)
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
}
`, 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"`