diff --git a/pkg/controller/hash.go b/pkg/controller/hash.go index 6c98643..f66790e 100644 --- a/pkg/controller/hash.go +++ b/pkg/controller/hash.go @@ -2,6 +2,7 @@ package controller import ( "crypto/sha3" + "encoding/base32" "errors" "fmt" "strings" @@ -90,3 +91,25 @@ func K8sObjectUUIDUnsafe(obj client.Object) string { } return uuid } + +// NameHashSHAKE128Base32 takes any number of string arguments and computes a hash out of it. The output string will be 8 characters long. +// The arguments are joined with '/' before being hashed. +func NameHashSHAKE128Base32(names ...string) string { + name := strings.Join(names, "/") + + // Desired output length = 8 chars + // 8 chars * 5 bits (base32) / 8 bits per byte = 5 bytes + hash := sha3.SumSHAKE128([]byte(name), 5) + + return base32.NewEncoding(Base32EncodeStdLowerCase).WithPadding(base32.NoPadding).EncodeToString(hash) +} + +// ObjectHashSHAKE128Base32 takes a client object and computes a hash out of the namespace and name. The output string will be 8 characters long. +// An empty namespace will be replaced by "default". +func ObjectHashSHAKE128Base32(obj client.Object) string { + name, namespace := obj.GetName(), obj.GetNamespace() + if namespace == "" { + namespace = corev1.NamespaceDefault + } + return NameHashSHAKE128Base32(namespace, name) +} diff --git a/pkg/controller/hash_test.go b/pkg/controller/hash_test.go index a183388..34f13f7 100644 --- a/pkg/controller/hash_test.go +++ b/pkg/controller/hash_test.go @@ -2,6 +2,7 @@ package controller import ( "crypto/sha3" + "strings" "testing" "github.com/google/uuid" @@ -132,3 +133,59 @@ func Test_K8sObjectUUID(t *testing.T) { }) } } + +func Test_NameHashSHAKE128Base32(t *testing.T) { + testCases := []struct { + input []string + expected string + }{ + { + input: []string{"example"}, + expected: "epgccknc", + }, + { + input: []string{corev1.NamespaceDefault, "example"}, + expected: "fphxsdub", + }, + } + for _, tC := range testCases { + t.Run(strings.Join(tC.input, " "), func(t *testing.T) { + actual := NameHashSHAKE128Base32(tC.input...) + assert.Equal(t, tC.expected, actual) + }) + } +} + +func Test_ObjectHashSHAKE128Base32(t *testing.T) { + testCases := []struct { + desc string + obj client.Object + expected string + }{ + { + desc: "should work with config map", + obj: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + }, + }, + expected: "fphxsdub", // same as in Test_K8sNameUUID + }, + { + desc: "should work with config map and empty namespace", + obj: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + }, + expected: "fphxsdub", // same as above + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + actual := ObjectHashSHAKE128Base32(tC.obj) + assert.Equal(t, tC.expected, actual) + }) + } +}