From b8425ed5e7406255736a91f69080a928de429a34 Mon Sep 17 00:00:00 2001
From: Wenying Dong <wenyingd@vmware.com>
Date: Wed, 8 Jan 2025 15:31:32 +0800
Subject: [PATCH] New string generator for the hash string in NSX resources

1. Use chars 0-9,a-z,A-Z to generate the hash string for ID/DisplayName in NSX
resources with VPC scenario. It also works on the Security Policy related
resources' display name in T1.
2. The length is 6 chars when using the new hash string function.
---
 pkg/nsx/services/common/types.go              |  1 +
 .../services/securitypolicy/builder_test.go   |  4 +-
 pkg/nsx/services/vpc/builder_test.go          |  2 +-
 pkg/util/utils.go                             | 69 ++++++++++++++-----
 pkg/util/utils_test.go                        | 45 ++++++++----
 5 files changed, 88 insertions(+), 33 deletions(-)

diff --git a/pkg/nsx/services/common/types.go b/pkg/nsx/services/common/types.go
index f4fc46914..6f09432ac 100644
--- a/pkg/nsx/services/common/types.go
+++ b/pkg/nsx/services/common/types.go
@@ -16,6 +16,7 @@ import (
 
 const (
 	HashLength                         int    = 8
+	Base62HashLength                   int    = 6
 	MaxTagsCount                       int    = 26
 	MaxTagScopeLength                  int    = 128
 	MaxTagValueLength                  int    = 256
diff --git a/pkg/nsx/services/securitypolicy/builder_test.go b/pkg/nsx/services/securitypolicy/builder_test.go
index 80a18a280..8eacd9d5b 100644
--- a/pkg/nsx/services/securitypolicy/builder_test.go
+++ b/pkg/nsx/services/securitypolicy/builder_test.go
@@ -1274,8 +1274,8 @@ func Test_BuildSecurityPolicyName(t *testing.T) {
 				},
 			},
 			createdFor: common.ResourceTypeNetworkPolicy,
-			expName:    fmt.Sprintf("%s_c64163f0", strings.Repeat("a", 246)),
-			expId:      fmt.Sprintf("%s_fb85d834", strings.Repeat("a", 246)),
+			expName:    fmt.Sprintf("%s_shQDwf", strings.Repeat("a", 248)),
+			expId:      fmt.Sprintf("%s_zT4Byn", strings.Repeat("a", 248)),
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/pkg/nsx/services/vpc/builder_test.go b/pkg/nsx/services/vpc/builder_test.go
index e6683715b..f52dc4d45 100644
--- a/pkg/nsx/services/vpc/builder_test.go
+++ b/pkg/nsx/services/vpc/builder_test.go
@@ -171,7 +171,7 @@ func TestBuildNSXVPC(t *testing.T) {
 			},
 			expVPC: &model.Vpc{
 				Id:            common.String("test-ns-03a2def3-0087-4077-904e-23e4dd788fb7_ecc6eb9f-92b5-4893-b809-e3ebc1fcf59e"),
-				DisplayName:   common.String("test-ns-03a2def3-0087-4077-904e-23e4dd788fb7_f4f0080e"),
+				DisplayName:   common.String("test-ns-03a2def3-0087-4077-904e-23e4dd788fb7_yWOLBB"),
 				PrivateIps:    []string{"192.168.3.0/24"},
 				IpAddressType: common.String("IPV4"),
 				Tags: []model.Tag{
diff --git a/pkg/util/utils.go b/pkg/util/utils.go
index fe0169afd..9576bf756 100644
--- a/pkg/util/utils.go
+++ b/pkg/util/utils.go
@@ -9,6 +9,7 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
+	"math/big"
 	"net"
 	"strconv"
 	"strings"
@@ -31,6 +32,7 @@ import (
 
 const (
 	wcpSystemResource = "vmware-system-shared-t1"
+	base62Chars       = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 )
 
 var (
@@ -39,44 +41,47 @@ var (
 
 var log = &logger.Log
 
+func truncateLabelHash(data string) string {
+	return Sha1(data)[:common.HashLength]
+}
 func NormalizeLabels(matchLabels *map[string]string) *map[string]string {
 	newLabels := make(map[string]string)
 	for k, v := range *matchLabels {
-		newLabels[NormalizeLabelKey(k)] = NormalizeName(v)
+		newLabels[NormalizeLabelKey(k, truncateLabelHash)] = NormalizeName(v, truncateLabelHash)
 	}
 	return &newLabels
 }
 
-func NormalizeLabelKey(key string) string {
+func NormalizeLabelKey(key string, shaFn func(data string) string) string {
 	if len(key) <= common.MaxTagScopeLength {
 		return key
 	}
 	splitted := strings.Split(key, "/")
 	key = splitted[len(splitted)-1]
-	return normalizeNameByLimit(key, "", common.MaxTagScopeLength)
+	return normalizeNameByLimit(key, "", common.MaxTagScopeLength, shaFn)
 }
 
-func NormalizeName(name string) string {
-	return normalizeNameByLimit(name, "", common.MaxTagValueLength)
+func NormalizeName(name string, shaFn func(data string) string) string {
+	return normalizeNameByLimit(name, "", common.MaxTagValueLength, shaFn)
 }
 
-func normalizeNameByLimit(name string, suffix string, limit int) string {
+func normalizeNameByLimit(name string, suffix string, limit int, hashFn func(data string) string) string {
 	newName := connectStrings(common.ConnectorUnderline, name, suffix)
 	if len(newName) <= limit {
 		return newName
 	}
 
-	var hashString string
+	hashedTarget := name
 	if len(suffix) > 0 {
-		hashString = Sha1(suffix)
-	} else {
-		hashString = Sha1(name)
+		hashedTarget = suffix
 	}
-	nameLength := limit - common.HashLength - 1
+
+	hashString := hashFn(hashedTarget)
+	nameLength := limit - len(hashString) - 1
 	if len(name) < nameLength {
 		nameLength = len(name)
 	}
-	return strings.Join([]string{name[:nameLength], hashString[:common.HashLength]}, common.ConnectorUnderline)
+	return strings.Join([]string{name[:nameLength], hashString}, common.ConnectorUnderline)
 }
 
 func NormalizeId(name string) string {
@@ -94,10 +99,36 @@ func NormalizeId(name string) string {
 }
 
 func Sha1(data string) string {
+	sum := getSha1Bytes(data)
+	return fmt.Sprintf("%x", sum)
+}
+
+func getSha1Bytes(data string) []byte {
 	h := sha1.New() // #nosec G401: not used for security purposes
 	h.Write([]byte(data))
 	sum := h.Sum(nil)
-	return fmt.Sprintf("%x", sum)
+	return sum
+}
+
+// Sha1WithBase62 uses the chars in `base62Chars` to present the hash result on the input data. We now use Sha1 as
+// the hash algorithm.
+func Sha1WithBase62(data string) string {
+	sum := getSha1Bytes(data)
+	value := new(big.Int).SetBytes(sum[:])
+	base := big.NewInt(int64(len(base62Chars)))
+	var result []byte
+	for value.Cmp(big.NewInt(0)) > 0 {
+		mod := new(big.Int).Mod(value, base)
+		result = append(result, base62Chars[mod.Int64()])
+		value.Div(value, base)
+	}
+
+	// Reverse the result because the encoding process generates characters in reverse order
+	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+		result[i], result[j] = result[j], result[i]
+	}
+
+	return string(result)
 }
 
 func RemoveDuplicateStr(strSlice []string) []string {
@@ -339,11 +370,15 @@ func UpdateK8sResourceAnnotation(client client.Client, ctx context.Context, k8sO
 	return nil
 }
 
+func truncateNameOrIDHash(data string) string {
+	return Sha1WithBase62(data)[:common.Base62HashLength]
+}
+
 // GenerateIDByObject generate string id for NSX resource using the provided Object's name and uid. Note,
 // this function is used on the resources with VPC scenario, and the provided obj is the K8s CR which is
 // used to generate the NSX resource.
 func GenerateIDByObject(obj metav1.Object) string {
-	return normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), common.MaxIdLength)
+	return normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), common.MaxIdLength, truncateNameOrIDHash)
 }
 
 // GenerateIDByObjectByLimit generate string id for NSX resource using the provided Object's name and uid,
@@ -352,13 +387,13 @@ func GenerateIDByObjectByLimit(obj metav1.Object, limit int) string {
 	if limit == 0 {
 		limit = common.MaxIdLength
 	}
-	return normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), limit)
+	return normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), limit, truncateNameOrIDHash)
 }
 
 func GenerateIDByObjectWithSuffix(obj metav1.Object, suffix string) string {
 	limit := common.MaxIdLength
 	limit -= len(suffix) + 1
-	return connectStrings(common.ConnectorUnderline, normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), limit), suffix)
+	return connectStrings(common.ConnectorUnderline, normalizeNameByLimit(obj.GetName(), string(obj.GetUID()), limit, truncateNameOrIDHash), suffix)
 }
 
 // GenerateID generate id for NSX resource, some resources has complex index, so set it type to string
@@ -391,7 +426,7 @@ func GenerateTruncName(limit int, resName string, prefix, suffix, project, clust
 	}
 	oldName := generateDisplayName(common.ConnectorUnderline, resName, "", "", project, cluster)
 	if len(oldName) > adjustedLimit {
-		newName := normalizeNameByLimit(oldName, "", adjustedLimit)
+		newName := normalizeNameByLimit(oldName, "", adjustedLimit, truncateNameOrIDHash)
 		return generateDisplayName(common.ConnectorUnderline, newName, prefix, suffix, "", "")
 	}
 	return generateDisplayName(common.ConnectorUnderline, resName, prefix, suffix, project, cluster)
diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go
index c39c19460..cf51ec688 100644
--- a/pkg/util/utils_test.go
+++ b/pkg/util/utils_test.go
@@ -12,14 +12,16 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
-
-	"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
-
+	"github.com/stretchr/testify/require"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common"
 )
 
 func TestSha1(t *testing.T) {
@@ -28,16 +30,16 @@ func TestSha1(t *testing.T) {
 
 func TestNormalizeName(t *testing.T) {
 	shortName := strings.Repeat("a", 256)
-	assert.Equal(t, NormalizeName(shortName), shortName)
+	assert.Equal(t, NormalizeName(shortName, truncateLabelHash), shortName)
 	longName := strings.Repeat("a", 257)
-	assert.Equal(t, NormalizeName(longName), fmt.Sprintf("%s_%s", strings.Repeat("a", 256-common.HashLength-1), "0c103888"))
+	assert.Equal(t, NormalizeName(longName, truncateLabelHash), fmt.Sprintf("%s_%s", strings.Repeat("a", 256-common.HashLength-1), "0c103888"))
 }
 
 func TestNormalizeLabelKey(t *testing.T) {
 	shortKey := strings.Repeat("a", 128)
-	assert.Equal(t, NormalizeLabelKey(shortKey), shortKey)
+	assert.Equal(t, NormalizeLabelKey(shortKey, truncateLabelHash), shortKey)
 	longKey := strings.Repeat("a", 129) + "/def"
-	assert.Equal(t, NormalizeLabelKey(longKey), "def")
+	assert.Equal(t, NormalizeLabelKey(longKey, truncateLabelHash), "def")
 }
 
 func TestNormalizeLabels(t *testing.T) {
@@ -55,7 +57,7 @@ func TestNormalizeLabels(t *testing.T) {
 				longKey: longValue,
 			},
 			expectedLabels: &map[string]string{
-				"def": NormalizeName(longValue),
+				"def": NormalizeName(longValue, truncateLabelHash),
 			},
 		},
 		{
@@ -64,7 +66,7 @@ func TestNormalizeLabels(t *testing.T) {
 				shortKey: longValue,
 			},
 			expectedLabels: &map[string]string{
-				shortKey: NormalizeName(longValue),
+				shortKey: NormalizeName(longValue, truncateLabelHash),
 			},
 		},
 	}
@@ -500,7 +502,7 @@ func TestGenerateTruncName(t *testing.T) {
 				project:  strings.Repeat("s", 300),
 				cluster:  "k8scl-one",
 			},
-			want: "sr_k8scl-one_1234-456_ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_e89b45cc_scope",
+			want: "sr_k8scl-one_1234-456_ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss_xbJrtX_scope",
 		},
 	}
 	for _, tt := range tests {
@@ -660,13 +662,13 @@ func TestGenerateIDByObject(t *testing.T) {
 			name:  "truncate with hash on uid",
 			obj:   &metav1.ObjectMeta{Name: "abcdefg", UID: "b720ee2c-5788-4680-9796-0f93db33d8a9"},
 			limit: 20,
-			expID: "abcdefg_df78acb2",
+			expID: "abcdefg_vSV1eZ",
 		},
 		{
 			name:  "longer name with truncate",
 			obj:   &metav1.ObjectMeta{Name: strings.Repeat("a", 256), UID: "b720ee2c-5788-4680-9796-0f93db33d8a9"},
 			limit: 0,
-			expID: fmt.Sprintf("%s_df78acb2", strings.Repeat("a", 246)),
+			expID: fmt.Sprintf("%s_vSV1eZ", strings.Repeat("a", 248)),
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
@@ -701,7 +703,7 @@ func TestGenerateIDByObjectWithSuffix(t *testing.T) {
 			obj:    &metav1.ObjectMeta{Name: strings.Repeat("a", 256), UID: "b720ee2c-5788-4680-9796-0f93db33d8a9"},
 			limit:  0,
 			suffix: "28e85c0b-21e4-4cab-b1c3-597639dfe752",
-			expID:  fmt.Sprintf("%s_df78acb2_28e85c0b-21e4-4cab-b1c3-597639dfe752", strings.Repeat("a", 209)),
+			expID:  fmt.Sprintf("%s_vSV1eZ_28e85c0b-21e4-4cab-b1c3-597639dfe752", strings.Repeat("a", 211)),
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
@@ -734,3 +736,20 @@ func TestConnectStrings(t *testing.T) {
 	expString = fmt.Sprintf("%s%s%d", string1, common.ConnectorUnderline, int2)
 	assert.Equal(t, connectString, expString)
 }
+
+func TestNewSha1(t *testing.T) {
+	assert.Equal(t, "ffN5UpVkkQYbocYDKFXOAMN4AsA", Sha1WithBase62("name"))
+	assert.Equal(t, "hZBZpydbX1XIFhgs9m6Lt2for9m", Sha1WithBase62("namee"))
+
+	allowedChars := sets.New[rune]()
+	for _, c := range base62Chars {
+		allowedChars.Insert(c)
+	}
+	randUID, err := uuid.NewRandom()
+	require.NoError(t, err)
+	hashString := Sha1WithBase62(randUID.String())
+	// Verify all chars in the hash string are contained in base62Chars.
+	for _, c := range hashString {
+		assert.True(t, allowedChars.Has(c))
+	}
+}