pkg/common/uniquename/uniquename.go (135 lines of code) (raw):

/* Copyright (c) Microsoft Corporation. Licensed under the MIT license. */ // Package uniquename features utility functions that help format unique names for exporting and importing // cluster-scoped and fleet-scoped resources. package uniquename import ( "fmt" "strings" "unicode" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/validation" ) type Format int const ( // DNS1123Subdomain dictates that the unique name should be a valid RFC 1123 DNS subdomain. DNS1123Subdomain Format = 1 // DNS1123Label dictates that the unique name should be a valid RFC 1123 DNS label. DNS1123Label Format = 2 // DNS1035Label dictates that the unique name should be a valid RFC 1035 DNS label. DNS1035Label Format = 3 uuidLength = 5 ) // minInt returns the smaller one of two integers. func minInt(a, b int) int { if a < b { return a } return b } // startsWithNumericCharacter returns if a string starts with a numeric character. func startsWithNumericCharacter(s string) bool { return unicode.IsDigit(rune(s[0])) } // removeDots removes all dot (".") occurrences in a string. func removeDots(s string) string { return strings.ReplaceAll(s, ".", "") } // ClusterScopedUniqueName returns a name that is guaranteed to be unique within a cluster. // The name is formatted using an object's namespace, its name, and a 5 character long UUID suffix; the format // is [NAMESPACE]-[NAME]-[SUFFIX], e.g. an object `app` from the namespace `work` will be assigned a unique // name like `work-app-1x2yz`. The function may truncate name components as it sees fit. // Note: this function assumes that // - the input object namespace is a valid RFC 1123 DNS label; and // - the input object name follows one of the three formats used in Kubernetes (RFC 1123 DNS subdomain, // RFC 1123 DNS label, RFC 1035 DNS label). func ClusterScopedUniqueName(format Format, namespace, name string) (string, error) { reservedSlots := 2 + uuidLength // 2 dashes + 5 character UUID string switch format { case DNS1123Subdomain: availableSlots := validation.DNS1123SubdomainMaxLength // 253 characters slotsPerSeg := (availableSlots - reservedSlots) / 2 uniqueName := fmt.Sprintf("%s-%s-%s", namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1123Subdomain(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1123 DNS subdomain name with namespace %s, name %s: %v", namespace, name, errs) } return uniqueName, nil case DNS1123Label: availableSlots := validation.DNS1123LabelMaxLength // 63 characters slotsPerSeg := (availableSlots - reservedSlots) / 2 // If the object name is a RFC 1123 DNS subdomain, it may include dot characters, which is not allowed in // RFC 1123 DNS labels. name = removeDots(name) uniqueName := fmt.Sprintf("%s-%s-%s", namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1123Label(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1123 DNS label name with namespace %s, name %s: %v", namespace, name, errs) } return uniqueName, nil case DNS1035Label: availableSlots := validation.DNS1035LabelMaxLength // 63 characters slotsPerSeg := (availableSlots - reservedSlots) / 2 // Namespace names are RFC 1123 DNS labels, which may start with an alphanumeric character; RFC 1035 DNS // labels, on the other hand, does not allow numeric characters at the beginning of the string. if startsWithNumericCharacter(namespace) { namespace = "ns" + namespace } // If the object name is a RFC 1123 DNS subdomain, it may include dot characters, which is not allowed in // RFC 1035 DNS labels. name = removeDots(name) uniqueName := fmt.Sprintf("%s-%s-%s", namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1035Label(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1035 DNS label name with namespace %s, name %s: %v", namespace, name, errs) } return uniqueName, nil } return "", fmt.Errorf("not a valid name format: %d", format) } // FleetScopedUniqueName returns a name that is guaranteed to be unique within a cluster. // The name is formatted using an object's origin cluster, an object's namespace, its name, and a 5 character // long UUID suffix; the format is [CLUSTER ID]-[NAMESPACE]-[NAME]-[SUFFIX], e.g. an object `app` from the namespace // `work` in cluster `bravelion` will be assigned a unique name like `bravelion-work-app-1x2yz`. The function may // truncate name components as it sees fit. // Note: this function assumes that // - the input cluster ID is a valid RFC 1123 DNS subdomain; and // - the input object namespace is a valid RFC 1123 DNS label; and // - the input object name follows one of the three formats used in Kubernetes (RFC 1123 DNS subdomain, // RFC 1123 DNS label, RFC 1035 DNS label). func FleetScopedUniqueName(format Format, clusterID, namespace, name string) (string, error) { reservedSlots := 3 + uuidLength // 3 dashes + 5 character UUID string switch format { case DNS1123Subdomain: availableSlots := validation.DNS1123SubdomainMaxLength // 253 characters slotsPerSeg := (availableSlots - reservedSlots) / 3 uniqueName := fmt.Sprintf("%s-%s-%s-%s", clusterID[:minInt(slotsPerSeg, len(clusterID))], namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1123Subdomain(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1123 DNS subdomain name with cluster ID %s, namespace %s, name %s: %v", clusterID, namespace, name, errs) } return uniqueName, nil case DNS1123Label: availableSlots := validation.DNS1123LabelMaxLength // 63 characters slotsPerSeg := (availableSlots - reservedSlots) / 3 // If the cluster ID and object name are valid RFC 1123 DNS subdomains, they may include dot characters, // which is not allowed in RFC 1123 DNS labels. clusterID = removeDots(clusterID) name = removeDots(name) uniqueName := fmt.Sprintf("%s-%s-%s-%s", clusterID[:minInt(slotsPerSeg, len(clusterID))], namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1123Label(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1123 DNS label name with cluster ID %s, namespace %s, name %s: %v", clusterID, namespace, name, errs) } return uniqueName, nil case DNS1035Label: availableSlots := validation.DNS1035LabelMaxLength // 63 characters slotsPerSeg := (availableSlots - reservedSlots) / 3 // If the cluster ID and object name are valid RFC 1123 DNS subdomains, they may include dot characters, // which is not allowed in RFC 1123 DNS labels. clusterID = removeDots(clusterID) name = removeDots(name) uniqueName := fmt.Sprintf("%s-%s-%s-%s", clusterID[:minInt(slotsPerSeg, len(clusterID))], namespace[:minInt(slotsPerSeg, len(namespace))], name[:minInt(slotsPerSeg, len(name))], uuid.NewUUID()[:uuidLength], ) if errs := validation.IsDNS1035Label(uniqueName); len(errs) != 0 { return "", fmt.Errorf("failed to format a unique RFC 1035 DNS label name with cluster ID %s, namespace %s, name %s: %v", clusterID, namespace, name, errs) } return uniqueName, nil } return "", fmt.Errorf("not a valid name format: %d", format) } // RandomLowerCaseAlphabeticString returns a string of lower case alphabetic characters only. This function // is best used for fallback cases where one cannot format a unique name as expected, as a lower case // alphabetic string of proper length is always a valid Kubernetes object name, regardless of the required name // format for the object (RFC 1123 DNS subdomain, RFC 1123 DNS label, or RFC 1035 DNS label). func RandomLowerCaseAlphabeticString(n int) string { alphabet := []rune("abcdefghijklmnopqrstuvwxyz") b := make([]rune, n) for i := range b { b[i] = alphabet[rand.Intn(len(alphabet))] //nolint:gosec } return string(b) }