diff --git a/internal/common/constant/constant.go b/internal/common/constant/constant.go index c7a41a8..72dc8e9 100644 --- a/internal/common/constant/constant.go +++ b/internal/common/constant/constant.go @@ -39,6 +39,9 @@ const ( // EmptyString defines a constant for the empty string const EmptyString = "" +// NameDelimiter defines character that is used to separate name parts +const NameDelimiter = "-" + // TrueString defines a constant for the True string const TrueString = "True" diff --git a/internal/common/function/function.go b/internal/common/function/function.go index 583210b..eab35ca 100644 --- a/internal/common/function/function.go +++ b/internal/common/function/function.go @@ -24,6 +24,7 @@ import ( "fmt" "github.com/go-logr/logr" + "github.com/google/uuid" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" @@ -139,6 +140,51 @@ func GenerateVeleroBackupName(namespace, nabName string) string { return veleroBackupName } +// GenerateVeleroBackupNameWithUUID generates a Velero backup name based on the provided namespace and NonAdminBackup name. +// It includes a UUID suffix. If the name exceeds the maximum length, it truncates nabName first, then namespace. +func GenerateVeleroBackupNameWithUUID(namespace, nabName string) string { + // Generate UUID suffix + uuidSuffix := uuid.New().String() + + // Build the initial backup name based on the presence of namespace and nabName + veleroBackupName := uuidSuffix + if len(nabName) > 0 { + veleroBackupName = nabName + constant.NameDelimiter + veleroBackupName + } + if len(namespace) > 0 { + veleroBackupName = namespace + constant.NameDelimiter + veleroBackupName + } + + // Ensure the name is within the character limit + maxLength := validation.DNS1123SubdomainMaxLength + + if len(veleroBackupName) > maxLength { + // Calculate remaining length after UUID + remainingLength := maxLength - len(uuidSuffix) + + delimeterLength := len(constant.NameDelimiter) + + // Subtract two delimiter lengths to avoid a corner case where the namespace + // and delimiters leave no space for any part of nabName + if len(namespace) > remainingLength-delimeterLength-delimeterLength { + namespace = namespace[:remainingLength-delimeterLength-delimeterLength] + veleroBackupName = namespace + constant.NameDelimiter + uuidSuffix + } else { + remainingLength = remainingLength - len(namespace) - delimeterLength - delimeterLength + nabName = nabName[:remainingLength] + veleroBackupName = uuidSuffix + if len(nabName) > 0 { + veleroBackupName = nabName + constant.NameDelimiter + veleroBackupName + } + if len(namespace) > 0 { + veleroBackupName = namespace + constant.NameDelimiter + veleroBackupName + } + } + } + + return veleroBackupName +} + // CheckVeleroBackupMetadata return true if Velero Backup object has required Non Admin labels and annotations, false otherwise func CheckVeleroBackupMetadata(obj client.Object) bool { labels := obj.GetLabels() diff --git a/internal/common/function/function_test.go b/internal/common/function/function_test.go index ce02091..d1451b9 100644 --- a/internal/common/function/function_test.go +++ b/internal/common/function/function_test.go @@ -23,6 +23,7 @@ import ( "strings" "testing" + "github.com/google/uuid" "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/assert" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -287,6 +288,82 @@ func TestGenerateVeleroBackupName(t *testing.T) { } } +func TestGenerateVeleroBackupNameWithUUID(t *testing.T) { + tests := []struct { + name string + namespace string + nabName string + }{ + { + name: "Valid names without truncation", + namespace: "default", + nabName: "my-backup", + }, + { + name: "Truncate nabName due to length", + namespace: "some", + nabName: strings.Repeat("q", validation.DNS1123SubdomainMaxLength+10), // too long for DNS limit + }, + { + name: "Truncate very long namespace and very long name", + namespace: strings.Repeat("w", validation.DNS1123SubdomainMaxLength+10), + nabName: strings.Repeat("e", validation.DNS1123SubdomainMaxLength+10), + }, + { + name: "nabName empty", + namespace: "example", + nabName: constant.EmptyString, + }, + { + name: "namespace empty", + namespace: constant.EmptyString, + nabName: "my-backup", + }, + { + name: "very long name and namespace empty", + namespace: constant.EmptyString, + nabName: strings.Repeat("r", validation.DNS1123SubdomainMaxLength+10), + }, + { + name: "very long namespace and name empty", + namespace: strings.Repeat("t", validation.DNS1123SubdomainMaxLength+10), + nabName: constant.EmptyString, + }, + { + name: "empty namespace and empty name", + namespace: constant.EmptyString, + nabName: constant.EmptyString, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GenerateVeleroBackupNameWithUUID(tt.namespace, tt.nabName) + + // Check length + if len(result) > validation.DNS1123SubdomainMaxLength { + t.Errorf("Generated name is too long: %s", result) + } + + // Extract the last 36 characters, which should be the UUID + if len(result) < 36 { + t.Errorf("Generated name is too short to contain a valid UUID: %s", result) + } + uuidPart := result[len(result)-36:] // The UUID is always the last 36 characters + + // Attempt to parse the UUID part + if _, err := uuid.Parse(uuidPart); err != nil { + t.Errorf("Last part is not a valid UUID: %s", uuidPart) + } + + // Check if no double hyphens are present + if strings.Contains(result, "--") { + t.Errorf("Generated name contains double hyphens: %s", result) + } + }) + } +} + func TestGetNonAdminBackupFromVeleroBackup(t *testing.T) { log := zap.New(zap.UseDevMode(true)) ctx := context.Background()