From aa07806bfc88506b13e0152f975d814175bfe16f Mon Sep 17 00:00:00 2001 From: Lars Eric Scheidler Date: Wed, 8 May 2019 11:13:30 +0200 Subject: [PATCH] lang/funcs: New "uuidv5" function This generates name-based uuids, rather than pseudorandom uuids as with the "uuid" function. --- go.mod | 1 + lang/funcs/crypto.go | 40 +++++++++ lang/funcs/crypto_test.go | 65 ++++++++++++++ lang/functions.go | 1 + lang/functions_test.go | 23 +++++ .../docs/configuration/functions/uuid.html.md | 4 + .../configuration/functions/uuidv5.html.md | 85 +++++++++++++++++++ website/layouts/functions.erb | 4 + 8 files changed, 223 insertions(+) create mode 100644 website/docs/configuration/functions/uuidv5.html.md diff --git a/go.mod b/go.mod index 239227f397..b726bff34d 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,7 @@ require ( github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c // indirect github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 // indirect github.com/posener/complete v1.2.1 + github.com/satori/go.uuid v1.2.0 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sirupsen/logrus v1.1.1 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect diff --git a/lang/funcs/crypto.go b/lang/funcs/crypto.go index 5cb4bc5c14..be006f821c 100644 --- a/lang/funcs/crypto.go +++ b/lang/funcs/crypto.go @@ -14,6 +14,7 @@ import ( "hash" uuid "github.com/hashicorp/go-uuid" + uuidv5 "github.com/satori/go.uuid" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/gocty" @@ -32,6 +33,39 @@ var UUIDFunc = function.New(&function.Spec{ }, }) +var UUIDV5Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "namespace", + Type: cty.String, + }, + { + Name: "name", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var namespace uuidv5.UUID + switch { + case args[0].AsString() == "dns": + namespace = uuidv5.NamespaceDNS + case args[0].AsString() == "url": + namespace = uuidv5.NamespaceURL + case args[0].AsString() == "oid": + namespace = uuidv5.NamespaceOID + case args[0].AsString() == "x500": + namespace = uuidv5.NamespaceX500 + default: + if namespace, err = uuidv5.FromString(args[0].AsString()); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("uuidv5() doesn't support namespace %s (%v)", args[0].AsString(), err) + } + } + val := args[1].AsString() + return cty.StringVal(uuidv5.NewV5(namespace, val).String()), nil + }, +}) + // Base64Sha256Func constructs a function that computes the SHA256 hash of a given string // and encodes it with Base64. var Base64Sha256Func = makeStringHashFunction(sha256.New, base64.StdEncoding.EncodeToString) @@ -228,6 +262,12 @@ func UUID() (cty.Value, error) { return UUIDFunc.Call(nil) } +// UUIDV5 generates and returns a Type-5 UUID in the standard hexadecimal string +// format. +func UUIDV5(namespace cty.Value, name cty.Value) (cty.Value, error) { + return UUIDV5Func.Call([]cty.Value{namespace, name}) +} + // Base64Sha256 computes the SHA256 hash of a given string and encodes it with // Base64. // diff --git a/lang/funcs/crypto_test.go b/lang/funcs/crypto_test.go index 0976517e3d..a79b67f170 100644 --- a/lang/funcs/crypto_test.go +++ b/lang/funcs/crypto_test.go @@ -20,6 +20,71 @@ func TestUUID(t *testing.T) { } } +func TestUUIDV5(t *testing.T) { + tests := []struct { + Namespace cty.Value + Name cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("dns"), + cty.StringVal("tada"), + cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"), + false, + }, + { + cty.StringVal("url"), + cty.StringVal("tada"), + cty.StringVal("2c1ff6b4-211f-577e-94de-d978b0caa16e"), + false, + }, + { + cty.StringVal("oid"), + cty.StringVal("tada"), + cty.StringVal("61eeea26-5176-5288-87fc-232d6ed30d2f"), + false, + }, + { + cty.StringVal("x500"), + cty.StringVal("tada"), + cty.StringVal("7e12415e-f7c9-57c3-9e43-52dc9950d264"), + false, + }, + { + cty.StringVal("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), + cty.StringVal("tada"), + cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"), + false, + }, + { + cty.StringVal("tada"), + cty.StringVal("tada"), + cty.UnknownVal(cty.String), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("uuidv5(%#v, %#v)", test.Namespace, test.Name), func(t *testing.T) { + got, err := UUIDV5(test.Namespace, test.Name) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + func TestBase64Sha256(t *testing.T) { tests := []struct { String cty.Value diff --git a/lang/functions.go b/lang/functions.go index ba75bcf779..5cc26d49b5 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -116,6 +116,7 @@ func (s *Scope) Functions() map[string]function.Function { "upper": stdlib.UpperFunc, "urlencode": funcs.URLEncodeFunc, "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, "values": funcs.ValuesFunc, "yamldecode": ctyyaml.YAMLDecodeFunc, "yamlencode": ctyyaml.YAMLEncodeFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index 780a0496c5..82a74ddaa7 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -777,6 +777,29 @@ func TestFunctions(t *testing.T) { }, }, + "uuidv5": { + { + `uuidv5("dns", "tada")`, + cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"), + }, + { + `uuidv5("url", "tada")`, + cty.StringVal("2c1ff6b4-211f-577e-94de-d978b0caa16e"), + }, + { + `uuidv5("oid", "tada")`, + cty.StringVal("61eeea26-5176-5288-87fc-232d6ed30d2f"), + }, + { + `uuidv5("x500", "tada")`, + cty.StringVal("7e12415e-f7c9-57c3-9e43-52dc9950d264"), + }, + { + `uuidv5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "tada")`, + cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"), + }, + }, + "values": { { `values({"hello"="world", "what's"="up"})`, diff --git a/website/docs/configuration/functions/uuid.html.md b/website/docs/configuration/functions/uuid.html.md index ccdc983242..439ec8232a 100644 --- a/website/docs/configuration/functions/uuid.html.md +++ b/website/docs/configuration/functions/uuid.html.md @@ -38,3 +38,7 @@ equivalent randomness to the `uuid` function. > uuid() b5ee72a3-54dd-c4b8-551c-4bdc0204cedb ``` + +## Related Functions + +* [`uuidv5`](./uuidv5.html), which generates name-based UUIDs. diff --git a/website/docs/configuration/functions/uuidv5.html.md b/website/docs/configuration/functions/uuidv5.html.md new file mode 100644 index 0000000000..4c10287456 --- /dev/null +++ b/website/docs/configuration/functions/uuidv5.html.md @@ -0,0 +1,85 @@ +--- +layout: "functions" +page_title: "uuidv5 - Functions - Configuration Language" +sidebar_current: "docs-funcs-crypto-uuidv5" +description: |- + The uuidv5 function generates a uuid v5 string representation of the value in the specified namespace. +--- + +# `uuidv5` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`uuidv5` generates a _name-based_ UUID, as described in +[RFC 4122 section 4.3](https://tools.ietf.org/html/rfc4122#section-4.3), +also known as a "version 5" UUID. + +``` +uuidv5(namespace, name) +``` + +Unlike the pseudo-random UUIDs generated by +[`uuid`](./uuid.html), name-based UUIDs derive from namespace and an name, +producing the same UUID value every time if the namespace and name are +unchanged. + +Name-based UUID namespaces are themselves UUIDs, but for readability this +function accepts some keywords as aliases for the namespaces that were +assigned by RFC 4122: + +| Keyword | Namespace ID | Name format | +| ------- | ------------ | ----------- | +| `"dns"` | `6ba7b810-9dad-11d1-80b4-00c04fd430c8` | A fully-qualified DNS domain name. | +| `"url"` | `6ba7b811-9dad-11d1-80b4-00c04fd430c8` | Any valid URL as defined in [RFC 3986](https://tools.ietf.org/html/rfc3986). | +| `"oid"` | `6ba7b812-9dad-11d1-80b4-00c04fd430c8` | An [ISO/IEC object identifier](https://oidref.com/) | +| `"x500"` | `6ba7b814-9dad-11d1-80b4-00c04fd430c8` | [X.500 Distinguished Name](https://tools.ietf.org/html/rfc1779) | + +To use any other namespace not included in the above table, pass its assigned +namespace ID directly in the first argument in the usual UUID string format. + +## Examples + +Use the namespace keywords where possible, to make the intent more obvious to +a future reader: + +``` +> uuidv5("dns", "www.terraform.io") +a5008fae-b28c-5ba5-96cd-82b4c53552d6 + +> uuidv5("url", "https://www.terraform.io/") +9db6f67c-dd95-5ea0-aa5b-e70e5c5f7cf5 + +> uuidv5("oid", "1.3.6.1.4") +af9d40a5-7a36-5c07-b23a-851cd99fbfa5 + +> uuidv5("x500", "CN=Example,C=GB") +84e09961-4aa4-57f8-95b7-03edb1073253 +``` + +The namespace keywords treated as equivalent to their corresponding namespace +UUIDs, and in some special cases it may be more appropriate to use the +UUID form: + +``` +> uuidv5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "www.terraform.io") +a5008fae-b28c-5ba5-96cd-82b4c53552d6 +``` + +If you wish to use a namespace defined outside of RFC 4122, using the namespace +UUID is required because no corresponding keyword is available: + +``` +> uuidv5("743ac3c0-3bf7-4a5b-9e6c-59360447c757", "LIBS:diskfont.library") +ede1a974-df7e-5f17-84b9-76208818b2c8 +``` + +When using raw UUID namespaces, consider including a comment alongside the +expression that indicates which namespace this repressents in a +human-significant manner, such as by reference to the standard that +defined it. + +## Related Functions + +* [`uuid`](./uuid.html), which generates pseudorandom UUIDs. diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index 6a8e3f438b..ecde4226f7 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -386,6 +386,10 @@ uuid +
  • + uuidv5 +
  • +