diff --git a/.gitignore b/.gitignore index f5bd2abf8..975a8c365 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ report.xml *.cid *.iid *.out +*.test .vscode \ No newline at end of file diff --git a/conv/conv.go b/conv/conv.go index f1a4ae14e..9d3e9d508 100644 --- a/conv/conv.go +++ b/conv/conv.go @@ -18,6 +18,7 @@ import ( // All other values are considered false. // // See ToBool also for a more flexible version. +// // Deprecated: use ToBool instead func Bool(in string) bool { if b, err := strconv.ParseBool(in); err == nil { diff --git a/funcs/base64.go b/funcs/base64.go index 1e9f24683..03c4ef8d4 100644 --- a/funcs/base64.go +++ b/funcs/base64.go @@ -8,12 +8,14 @@ import ( ) // Base64NS - the base64 namespace +// // Deprecated: don't use func Base64NS() *Base64Funcs { return &Base64Funcs{} } // AddBase64Funcs - +// // Deprecated: use CreateBase64Funcs instead func AddBase64Funcs(f map[string]interface{}) { for k, v := range CreateBase64Funcs(context.Background()) { diff --git a/funcs/cel_exports.go b/funcs/cel_exports.go index 63a4e4006..578fe429f 100644 --- a/funcs/cel_exports.go +++ b/funcs/cel_exports.go @@ -194,4 +194,5 @@ var CelEnvOption = []cel.EnvOption{ uuidNilGen, uuidIsValidGen, uuidParseGen, + uuidHashUUIDGen, } diff --git a/funcs/coll.go b/funcs/coll.go index b7e22663d..19881f9c8 100644 --- a/funcs/coll.go +++ b/funcs/coll.go @@ -14,12 +14,14 @@ import ( ) // CollNS - +// // Deprecated: don't use func CollNS() *CollFuncs { return &CollFuncs{} } // AddCollFuncs - +// // Deprecated: use CreateCollFuncs instead func AddCollFuncs(f map[string]interface{}) { for k, v := range CreateCollFuncs(context.Background()) { @@ -78,7 +80,7 @@ func (CollFuncs) Dict(in ...interface{}) (map[string]interface{}, error) { // Keys - func (CollFuncs) Keys(in map[string]any) []string { - keys := []string{} + keys := make([]string, 0, len(in)) for k := range in { keys = append(keys, k) } @@ -87,7 +89,7 @@ func (CollFuncs) Keys(in map[string]any) []string { // Values - func (CollFuncs) Values(in map[string]any) []any { - values := []any{} + values := make([]any, 0, len(in)) for _, v := range in { values = append(values, v) } diff --git a/funcs/conv.go b/funcs/conv.go index 5a0ce725c..5af3c9ac8 100644 --- a/funcs/conv.go +++ b/funcs/conv.go @@ -10,12 +10,14 @@ import ( ) // ConvNS - +// // Deprecated: don't use func ConvNS() *ConvFuncs { return &ConvFuncs{} } // AddConvFuncs - +// // Deprecated: use CreateConvFuncs instead func AddConvFuncs(f map[string]interface{}) { for k, v := range CreateConvFuncs(context.Background()) { @@ -43,6 +45,7 @@ type ConvFuncs struct { } // Bool - +// // Deprecated: use ToBool instead func (f *ConvFuncs) Bool(s interface{}) bool { return conv.Bool(conv.ToString(s)) @@ -59,6 +62,7 @@ func (ConvFuncs) ToBools(in ...interface{}) []bool { } // Slice - +// // Deprecated: use coll.Slice instead func (f *ConvFuncs) Slice(args ...interface{}) []interface{} { return coll.Slice(args...) @@ -70,6 +74,7 @@ func (ConvFuncs) Join(in interface{}, sep string) (string, error) { } // Has - +// // Deprecated: use coll.Has instead func (f *ConvFuncs) Has(in interface{}, key string) bool { return coll.Has(in, key) @@ -149,6 +154,7 @@ func (ConvFuncs) Default(def, in interface{}) interface{} { } // Dict - +// // Deprecated: use coll.Dict instead func (f *ConvFuncs) Dict(in ...interface{}) (map[string]interface{}, error) { return coll.Dict(in...) diff --git a/funcs/crypto.go b/funcs/crypto.go index 115188a9a..88a6a6bae 100644 --- a/funcs/crypto.go +++ b/funcs/crypto.go @@ -9,12 +9,14 @@ import ( ) // CryptoNS - the crypto namespace +// // Deprecated: don't use func CryptoNS() *CryptoFuncs { return &CryptoFuncs{} } // AddCryptoFuncs - +// // Deprecated: use CreateCryptoFuncs instead func AddCryptoFuncs(f map[string]interface{}) { for k, v := range CreateCryptoFuncs(context.Background()) { diff --git a/funcs/data.go b/funcs/data.go index 2dd750de5..6731099c1 100644 --- a/funcs/data.go +++ b/funcs/data.go @@ -8,6 +8,7 @@ import ( ) // DataNS - +// // Deprecated: don't use func DataNS() *DataFuncs { return &DataFuncs{} diff --git a/funcs/filepath.go b/funcs/filepath.go index dff918b60..bef3a9825 100644 --- a/funcs/filepath.go +++ b/funcs/filepath.go @@ -8,12 +8,14 @@ import ( ) // FilePathNS - the Path namespace +// // Deprecated: don't use func FilePathNS() *FilePathFuncs { return &FilePathFuncs{} } // AddFilePathFuncs - +// // Deprecated: use CreateFilePathFuncs instead func AddFilePathFuncs(f map[string]interface{}) { for k, v := range CreateFilePathFuncs(context.Background()) { diff --git a/funcs/math.go b/funcs/math.go index c087cf4e7..8756cc1c9 100644 --- a/funcs/math.go +++ b/funcs/math.go @@ -12,12 +12,14 @@ import ( ) // MathNS - the math namespace +// // Deprecated: don't use func MathNS() *MathFuncs { return &MathFuncs{} } // AddMathFuncs - +// // Deprecated: use CreateMathFuncs instead func AddMathFuncs(f map[string]interface{}) { for k, v := range CreateMathFuncs(context.Background()) { diff --git a/funcs/path.go b/funcs/path.go index df0b46c47..8225e41de 100644 --- a/funcs/path.go +++ b/funcs/path.go @@ -8,12 +8,14 @@ import ( ) // PathNS - the Path namespace +// // Deprecated: don't use func PathNS() *PathFuncs { return &PathFuncs{} } // AddPathFuncs - +// // Deprecated: use CreatePathFuncs instead func AddPathFuncs(f map[string]interface{}) { for k, v := range CreatePathFuncs(context.Background()) { diff --git a/funcs/random.go b/funcs/random.go index cf1fe6dc8..d305dc76b 100644 --- a/funcs/random.go +++ b/funcs/random.go @@ -12,12 +12,14 @@ import ( ) // RandomNS - +// // Deprecated: don't use func RandomNS() *RandomFuncs { return &RandomFuncs{} } // AddRandomFuncs - +// // Deprecated: use CreateRandomFuncs instead func AddRandomFuncs(f map[string]interface{}) { for k, v := range CreateRandomFuncs(context.Background()) { diff --git a/funcs/regexp.go b/funcs/regexp.go index 0775e4f45..5da7cba0e 100644 --- a/funcs/regexp.go +++ b/funcs/regexp.go @@ -10,12 +10,14 @@ import ( ) // ReNS - +// // Deprecated: don't use func ReNS() *ReFuncs { return &ReFuncs{} } // AddReFuncs - +// // Deprecated: use CreateReFuncs instead func AddReFuncs(f map[string]interface{}) { for k, v := range CreateReFuncs(context.Background()) { diff --git a/funcs/strings.go b/funcs/strings.go index a0646d151..5d8cc57d4 100644 --- a/funcs/strings.go +++ b/funcs/strings.go @@ -25,12 +25,14 @@ import ( ) // StrNS - +// // Deprecated: don't use func StrNS() *StringFuncs { return &StringFuncs{} } // AddStringFuncs - +// // Deprecated: use CreateStringFuncs instead func AddStringFuncs(f map[string]interface{}) { for k, v := range CreateStringFuncs(context.Background()) { diff --git a/funcs/test.go b/funcs/test.go index 35f65f94d..c0f8742b5 100644 --- a/funcs/test.go +++ b/funcs/test.go @@ -11,12 +11,14 @@ import ( ) // TestNS - +// // Deprecated: don't use func TestNS() *TestFuncs { return &TestFuncs{} } // AddTestFuncs - +// // Deprecated: use CreateTestFuncs instead func AddTestFuncs(f map[string]interface{}) { for k, v := range CreateTestFuncs(context.Background()) { diff --git a/funcs/time.go b/funcs/time.go index 51f6e4a74..4c13e9ecd 100644 --- a/funcs/time.go +++ b/funcs/time.go @@ -17,6 +17,7 @@ import ( ) // TimeNS - +// // Deprecated: don't use func TimeNS() *TimeFuncs { return &TimeFuncs{ @@ -39,6 +40,7 @@ func TimeNS() *TimeFuncs { } // AddTimeFuncs - +// // Deprecated: use CreateTimeFuncs instead func AddTimeFuncs(f map[string]interface{}) { for k, v := range CreateTimeFuncs(context.Background()) { diff --git a/funcs/uuid.go b/funcs/uuid.go index fd04effdc..94a331da2 100644 --- a/funcs/uuid.go +++ b/funcs/uuid.go @@ -2,6 +2,7 @@ package funcs import ( "context" + "crypto/sha256" "github.com/flanksource/gomplate/v3/conv" @@ -9,12 +10,14 @@ import ( ) // UUIDNS - +// // Deprecated: don't use func UUIDNS() *UUIDFuncs { return &UUIDFuncs{} } // AddUUIDFuncs - +// // Deprecated: use CreateUUIDFuncs instead func AddUUIDFuncs(f map[string]interface{}) { for k, v := range CreateUUIDFuncs(context.Background()) { @@ -79,3 +82,20 @@ func (UUIDFuncs) Parse(in interface{}) (string, error) { } return u.String(), err } + +// HashUUID - return a deterministic UUID based on the SHA256 hash of the input arguments. +// This function always returns the same UUID for the same input, making it idempotent. +// It uses the nil UUID as the namespace and SHA256 as the hashing algorithm. +func (UUIDFuncs) HashUUID(args ...interface{}) (string, error) { + // Concatenate all arguments into a single byte slice + data := make([]byte, 0, len(args)*16) + for _, arg := range args { + data = append(data, []byte(conv.ToString(arg))...) + } + + // Use uuid.Nil as the namespace + // Generate a version 5-style UUID (SHA-based) using SHA256 + u := uuid.NewHash(sha256.New(), uuid.Nil, data, 5) + + return u.String(), nil +} diff --git a/funcs/uuid_gen.go b/funcs/uuid_gen.go index ec94e5427..cde21a54e 100644 --- a/funcs/uuid_gen.go +++ b/funcs/uuid_gen.go @@ -99,3 +99,29 @@ var uuidParseGen = cel.Function("uuid.Parse", }), ), ) + +var uuidHashUUIDGen = cel.Function("uuid.HashUUID", + cel.Overload("uuid.HashUUID_list", + + []*cel.Type{ + cel.ListType(cel.DynType), + }, + cel.StringType, + cel.FunctionBinding(func(args ...ref.Val) ref.Val { + + var x UUIDFuncs + + list, err := sliceToNative[interface{}](args...) + if err != nil { + return types.WrapErr(err) + } + + result, err := x.HashUUID(list...) + if err != nil { + return types.WrapErr(err) + } + return types.String(result) + + }), + ), +) diff --git a/funcs/uuid_test.go b/funcs/uuid_test.go index 525ea013b..ec06e9cc7 100644 --- a/funcs/uuid_test.go +++ b/funcs/uuid_test.go @@ -113,3 +113,63 @@ func TestParse(t *testing.T) { assert.Equal(t, in, uid) } } + +func TestHashUUID(t *testing.T) { + t.Parallel() + + u := UUIDNS() + + // Test that the same input produces the same UUID + uuid1, err := u.HashUUID("test") + assert.NoError(t, err) + assert.NotEmpty(t, uuid1) + + uuid2, err := u.HashUUID("test") + assert.NoError(t, err) + assert.Equal(t, uuid1, uuid2, "Same input should produce same UUID") + + // Test that different inputs produce different UUIDs + uuid3, err := u.HashUUID("different") + assert.NoError(t, err) + assert.NotEqual(t, uuid1, uuid3, "Different inputs should produce different UUIDs") + + // Test with multiple arguments + uuid4, err := u.HashUUID("arg1", "arg2", "arg3") + assert.NoError(t, err) + assert.NotEmpty(t, uuid4) + + uuid5, err := u.HashUUID("arg1", "arg2", "arg3") + assert.NoError(t, err) + assert.Equal(t, uuid4, uuid5, "Same multiple arguments should produce same UUID") + + // Test that order matters + uuid6, err := u.HashUUID("arg2", "arg1", "arg3") + assert.NoError(t, err) + assert.NotEqual(t, uuid4, uuid6, "Different order should produce different UUIDs") + + // Test with numeric arguments + uuid7, err := u.HashUUID(123, 456) + assert.NoError(t, err) + assert.NotEmpty(t, uuid7) + + uuid8, err := u.HashUUID(123, 456) + assert.NoError(t, err) + assert.Equal(t, uuid7, uuid8, "Same numeric arguments should produce same UUID") + + // Test with no arguments + uuid9, err := u.HashUUID() + assert.NoError(t, err) + assert.NotEmpty(t, uuid9) + + uuid10, err := u.HashUUID() + assert.NoError(t, err) + assert.Equal(t, uuid9, uuid10, "No arguments should produce same UUID each time") + + // Verify the UUID format is valid (version 5, SHA-based) + parsed, err := u.Parse(uuid1) + assert.NoError(t, err) + assert.Equal(t, uuid1, parsed) + + // Check that it's a valid UUID v5 pattern + assert.Regexp(t, "^[[:xdigit:]]{8}-[[:xdigit:]]{4}-5[[:xdigit:]]{3}-[89ab][[:xdigit:]]{3}-[[:xdigit:]]{12}$", uuid1) +} diff --git a/gencel/templates.go b/gencel/templates.go index 06f89adf7..2ea66a193 100644 --- a/gencel/templates.go +++ b/gencel/templates.go @@ -30,7 +30,7 @@ func getArgs(args []Ident) string { var tplFuncs = map[string]any{ "getReturnIdentifiers": func(args []Ident) string { - var output []string + output := make([]string, 0, len(args)) for i := range args { output = append(output, fmt.Sprintf("a%d", i)) } @@ -38,7 +38,7 @@ var tplFuncs = map[string]any{ return strings.Join(output, ", ") }, "fnSuffix": func(args []Ident) string { - var output []string + output := make([]string, 0, len(args)) for _, a := range args { output = append(output, a.GoType) } diff --git a/kubernetes/lists.go b/kubernetes/lists.go index 4e0487cf7..c44169496 100644 --- a/kubernetes/lists.go +++ b/kubernetes/lists.go @@ -173,7 +173,7 @@ var listsLibraryDecls = map[string][]cel.FunctionOpt{ } func (*lists) CompileOptions() []cel.EnvOption { - options := []cel.EnvOption{} + options := make([]cel.EnvOption, 0, len(listsLibraryDecls)) for name, overloads := range listsLibraryDecls { options = append(options, cel.Function(name, overloads...)) } diff --git a/kubernetes/regex.go b/kubernetes/regex.go index 87055094a..473869b25 100644 --- a/kubernetes/regex.go +++ b/kubernetes/regex.go @@ -82,7 +82,7 @@ var regexLibraryDecls = map[string][]cel.FunctionOpt{ } func (*regex) CompileOptions() []cel.EnvOption { - options := []cel.EnvOption{} + options := make([]cel.EnvOption, 0, len(regexLibraryDecls)) for name, overloads := range regexLibraryDecls { options = append(options, cel.Function(name, overloads...)) } diff --git a/kubernetes/url.go b/kubernetes/url.go index 95e4f03ba..36087d28c 100644 --- a/kubernetes/url.go +++ b/kubernetes/url.go @@ -198,7 +198,7 @@ var urlLibraryDecls = map[string][]cel.FunctionOpt{ } func (*urls) CompileOptions() []cel.EnvOption { - options := []cel.EnvOption{} + options := make([]cel.EnvOption, 0, len(urlLibraryDecls)) for name, overloads := range urlLibraryDecls { options = append(options, cel.Function(name, overloads...)) } diff --git a/tests/cel_test.go b/tests/cel_test.go index b5533d60b..8ddd86001 100644 --- a/tests/cel_test.go +++ b/tests/cel_test.go @@ -742,4 +742,40 @@ func TestCelUUID(t *testing.T) { {nil, "uuid.IsValid('2a42e576-c308-4db9-8525-0513af307586')", "true"}, {nil, "string(uuid.Parse('2a42e576-c308-4db9-8525-0513af307586'))", "2a42e576-c308-4db9-8525-0513af307586"}, }) + + // Test HashUUID for idempotency + expr1 := `uuid.HashUUID(['test'])` + out1, err := gomplate.RunTemplate(nil, gomplate.Template{ + Expression: expr1, + }) + assert.NoError(t, err) + assert.NotEmpty(t, out1) + + out2, err := gomplate.RunTemplate(nil, gomplate.Template{ + Expression: expr1, + }) + assert.NoError(t, err) + assert.Equal(t, out1, out2, "Same input should produce same UUID") + + // Test with different input + expr2 := `uuid.HashUUID(['different'])` + out3, err := gomplate.RunTemplate(nil, gomplate.Template{ + Expression: expr2, + }) + assert.NoError(t, err) + assert.NotEqual(t, out1, out3, "Different input should produce different UUID") + + // Test with multiple arguments + expr3 := `uuid.HashUUID(['arg1', 'arg2'])` + out4, err := gomplate.RunTemplate(nil, gomplate.Template{ + Expression: expr3, + }) + assert.NoError(t, err) + assert.NotEmpty(t, out4) + + out5, err := gomplate.RunTemplate(nil, gomplate.Template{ + Expression: expr3, + }) + assert.NoError(t, err) + assert.Equal(t, out4, out5, "Same multiple arguments should produce same UUID") } diff --git a/tests/gomplate_test.go b/tests/gomplate_test.go index 5e2e5fd46..8cced5360 100644 --- a/tests/gomplate_test.go +++ b/tests/gomplate_test.go @@ -119,6 +119,45 @@ func TestGomplate(t *testing.T) { } } +func TestHashUUID(t *testing.T) { + // Test that HashUUID returns the same UUID for the same input + template := `{{ uuid.HashUUID "test" }}` + + out1, err := gomplate.RunTemplate(map[string]any{}, gomplate.Template{ + Template: template, + }) + assert.NoError(t, err) + assert.NotEmpty(t, out1) + + out2, err := gomplate.RunTemplate(map[string]any{}, gomplate.Template{ + Template: template, + }) + assert.NoError(t, err) + assert.Equal(t, out1, out2, "Same input should produce same UUID") + + // Test with different input produces different UUID + template2 := `{{ uuid.HashUUID "different" }}` + out3, err := gomplate.RunTemplate(map[string]any{}, gomplate.Template{ + Template: template2, + }) + assert.NoError(t, err) + assert.NotEqual(t, out1, out3, "Different input should produce different UUID") + + // Test with multiple arguments + template3 := `{{ uuid.HashUUID "arg1" "arg2" }}` + out4, err := gomplate.RunTemplate(map[string]any{}, gomplate.Template{ + Template: template3, + }) + assert.NoError(t, err) + assert.NotEmpty(t, out4) + + out5, err := gomplate.RunTemplate(map[string]any{}, gomplate.Template{ + Template: template3, + }) + assert.NoError(t, err) + assert.Equal(t, out4, out5, "Same multiple arguments should produce same UUID") +} + func TestGomplateHeaders(t *testing.T) { tests := []struct { env map[string]any