diff --git a/docs/functions/unique_string.md b/docs/functions/unique_string.md new file mode 100644 index 000000000..eba739057 --- /dev/null +++ b/docs/functions/unique_string.md @@ -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 + + +```text +unique_string(base_string list of string) string +``` + +## Arguments + + +1. `base_string` (List of String) The values used in the hash function to create a unique string. + diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9f0cec1ef..713a6597c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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{} }, } } diff --git a/internal/services/functions/unique_string_function.go b/internal/services/functions/unique_string_function.go new file mode 100644 index 000000000..daf5dd550 --- /dev/null +++ b/internal/services/functions/unique_string_function.go @@ -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) +} diff --git a/internal/services/functions/unique_string_function_test.go b/internal/services/functions/unique_string_function_test.go new file mode 100644 index 000000000..7158aabfe --- /dev/null +++ b/internal/services/functions/unique_string_function_test.go @@ -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) + } + }) + } +}