Skip to content

Commit

Permalink
Add unique_string function
Browse files Browse the repository at this point in the history
  • Loading branch information
smokedlinq committed Oct 24, 2024
1 parent 573b662 commit 6f51d59
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
38 changes: 38 additions & 0 deletions docs/functions/unique_string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "unique_string function - terraform-provider-azapi"
subcategory: ""
description: |-
Creates a deterministic hash string based on the values provided as parameters.
---

# function: unique_string

This function constructs an Azure equivalent `uniqueString` value. It is useful for migrating existing resources based on th ARM `uniqueString` function.

## Example Usage

```terraform
locals {
resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Network/virtualNetworks/myVNet"
}
// it will output below value
# "bkysb75tbw4ig"
output "unique_string" {
value = provider::azapi::unique_string([local.resource_id])
}
```

## Signature

<!-- signature generated by tfplugindocs -->
```text
unique_string(base_string list of string) string
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `base_string` (List of String) The values used in the hash function to create a unique string.

1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ func (p Provider) Functions(ctx context.Context) []func() function.Function {
func() function.Function { return &functions.ResourceGroupResourceIdFunction{} },
func() function.Function { return &functions.ManagementGroupResourceIdFunction{} },
func() function.Function { return &functions.ExtensionResourceIdFunction{} },
func() function.Function { return &functions.UniqueStringFunction{} },
}
}

Expand Down
160 changes: 160 additions & 0 deletions internal/services/functions/unique_string_function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package functions

import (
"context"
"math/bits"
"strings"

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

type UniqueStringFunction struct{}

func (b *UniqueStringFunction) Metadata(ctx context.Context, request function.MetadataRequest, response *function.MetadataResponse) {
response.Name = "unique_string"
}

func (b *UniqueStringFunction) Definition(ctx context.Context, request function.DefinitionRequest, response *function.DefinitionResponse) {
response.Definition = function.Definition{
Parameters: []function.Parameter{
function.ListParameter{
ElementType: types.StringType,
AllowNullValue: false,
AllowUnknownValues: false,
Name: "base_string",
Description: "The values used in the hash function to create a unique string.",
MarkdownDescription: "The values used in the hash function to create a unique string.",
},
},
Return: function.StringReturn{},
Summary: "Creates a deterministic hash string based on the values provided as parameters.",
Description: "This function constructs an Azure equivalent uniqueString value. It is useful for migrating existing resources based on th ARM uniqueString function.",
MarkdownDescription: "This function constructs an Azure equivalent `uniqueString` value. It is useful for migrating existing resources based on th ARM `uniqueString` function.",
}
}

func (b *UniqueStringFunction) Run(ctx context.Context, request function.RunRequest, response *function.RunResponse) {
var baseString types.List

if response.Error = request.Arguments.Get(ctx, &baseString); response.Error != nil {
return
}

var slice []string
if diagnostics := baseString.ElementsAs(ctx, &slice, false); diagnostics.HasError() {
response.Error = function.FuncErrorFromDiags(ctx, diagnostics)
return
}

uniqueString := uniqueString(slice...)

response.Error = response.Result.Set(ctx, types.StringValue(uniqueString))
}

var _ function.Function = &UniqueStringFunction{}

func uniqueString(values ...string) string {
value := strings.Join(values, "-")
hash := murmurHash64(value)
return base32Encode(hash)
}

func base32Encode(value uint64) string {
const text = "abcdefghijklmnopqrstuvwxyz234567"
var builder strings.Builder
for i := 0; i < 13; i++ {
builder.WriteByte(text[int32(value>>59)])
value <<= 5
}
return builder.String()
}

func murmurHash64(value string) uint64 {
bytes := []byte(value)
return murmurHash64A(bytes, 0)
}

func murmurHash64A(data []byte, seed uint32) uint64 {
length := len(data)
h1 := seed
h2 := seed

var index int
for index = 0; index+7 < length; index += 8 {
k1 := uint32(data[index]) | uint32(data[index+1])<<8 | uint32(data[index+2])<<16 | uint32(data[index+3])<<24
k3 := uint32(data[index+4]) | uint32(data[index+5])<<8 | uint32(data[index+6])<<16 | uint32(data[index+7])<<24
k1 *= 597399067
k1 = bits.RotateLeft32(k1, 15)
k1 *= 2869860233
h1 ^= k1
h1 = bits.RotateLeft32(h1, 19)
h1 += h2
h1 = h1*5 + 1444728091
k3 *= 2869860233
k3 = bits.RotateLeft32(k3, 17)
k3 *= 597399067
h2 ^= k3
h2 = bits.RotateLeft32(h2, 13)
h2 += h1
h2 = h2*5 + 197830471
}

if tail := length - index; tail > 0 {
var k2 uint32

if tail >= 4 {
k2 = uint32(data[index]) | (uint32(data[index+1]) << 8) | (uint32(data[index+2]) << 16) | (uint32(data[index+3]) << 24)
} else {
switch tail {
case 2:
k2 = uint32(data[index]) | (uint32(data[index+1]) << 8)
case 3:
k2 = uint32(data[index]) | (uint32(data[index+1]) << 8) | (uint32(data[index+2]) << 16)
default:
k2 = uint32(data[index])
}
}

k2 *= 597399067
k2 = bits.RotateLeft32(k2, 15)
k2 *= 2869860233
h1 ^= k2

if tail > 4 {
var k4 int32
switch tail {
case 6:
k4 = int32(data[index+4]) | (int32(data[index+5]) << 8)
case 7:
k4 = int32(data[index+4]) | (int32(data[index+5]) << 8) | (int32(data[index+6]) << 16)
default:
k4 = int32(data[index+4])
}
k4 *= -1425107063
i4 := uint32(k4)
i4 = bits.RotateLeft32(i4, 17)
i4 *= 597399067
h2 ^= i4
}
}

h1 ^= uint32(length)
h2 ^= uint32(length)
h1 += h2
h2 += h1
h1 ^= h1 >> 16
h1 *= 2246822507
h1 ^= h1 >> 13
h1 *= 3266489909
h1 ^= h1 >> 16
h2 ^= h2 >> 16
h2 *= 2246822507
h2 ^= h2 >> 13
h2 *= 3266489909
h2 ^= h2 >> 16
h1 += h2
h2 += h1

return (uint64(h2) << 32) | uint64(h1)
}
48 changes: 48 additions & 0 deletions internal/services/functions/unique_string_function_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package functions_test

import (
"context"
"testing"

"github.com/Azure/terraform-provider-azapi/internal/services/functions"
"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"
)

func Test_UniqueStringFunction(t *testing.T) {
testCases := map[string]struct {
request function.RunRequest
expected function.RunResponse
}{
"unique-string-valid": {
request: function.RunRequest{
Arguments: function.NewArgumentsData([]attr.Value{
types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("/subscriptions/00000000-0000-0000-0000-000000000000"),
types.StringValue("resource-id"),
}),
}),
},
expected: function.RunResponse{
Result: function.NewResultData(types.StringValue("cwvxuqg24sifi")),
},
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
got := function.RunResponse{
Result: function.NewResultData(types.StringUnknown()),
}

uniqueStringFunction := functions.UniqueStringFunction{}
uniqueStringFunction.Run(context.Background(), testCase.request, &got)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

0 comments on commit 6f51d59

Please sign in to comment.