This is an automated email from the ASF dual-hosted git repository.
fanningpj pushed a commit to branch 1.3.x
in repository https://gitbox.apache.org/repos/asf/pekko-management.git
The following commit(s) were added to refs/heads/1.3.x by this push:
new 44051b9d Add hash suffix on truncation for Kubernetes lease names
(#708) (#723)
44051b9d is described below
commit 44051b9dc27082b17a77d92d2dbab9923606d74f
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Mar 28 11:01:21 2026 +0100
Add hash suffix on truncation for Kubernetes lease names (#708) (#723)
* Initial plan
* Add on-truncate-add-hash-length config with SHA-256/base32 hash suffix on
truncation
* Add Base32EncodeSpec with edge-case tests; widen base32Encode to
private[kubernetes]
* Switch hash suffix to take first N chars; add tests for names differing
by last 1-2 chars
* Handle hashLength >= maxLength gracefully; fix test bugs and add
edge-case tests
* scalafmt
* Refactor string concatenation to use string interpolation
* Update Base32EncodeSpec.scala
* Update Base32EncodeSpec.scala
* update long hash len logic
* Update MakeDNS1039CompatibleSpec.scala
---------
Co-authored-by: copilot-swe-agent[bot]
<[email protected]>
Co-authored-by: pjfanning <[email protected]>
---
lease-kubernetes/src/main/resources/reference.conf | 19 ++
.../lease/kubernetes/AbstractKubernetesLease.scala | 74 +++++++-
.../lease/kubernetes/KubernetesSettings.scala | 8 +-
.../lease/kubernetes/Base32EncodeSpec.scala | 144 +++++++++++++++
.../lease/kubernetes/KubernetesSettingsSpec.scala | 12 ++
.../kubernetes/MakeDNS1039CompatibleSpec.scala | 205 +++++++++++++++++++++
6 files changed, 453 insertions(+), 9 deletions(-)
diff --git a/lease-kubernetes/src/main/resources/reference.conf
b/lease-kubernetes/src/main/resources/reference.conf
index c1ee5f7a..dc88efbf 100644
--- a/lease-kubernetes/src/main/resources/reference.conf
+++ b/lease-kubernetes/src/main/resources/reference.conf
@@ -53,6 +53,25 @@ pekko.coordination.lease.kubernetes {
# on the way back from the API server but will be reported as not taken
and can be safely retried.
lease-operation-timeout = 5s
+ # The maximum length of the lease name after sanitization for DNS 1039
compatibility.
+ # DNS 1039 labels must be 63 characters or less. Some subsystems of
Kubernetes cannot
+ # manage longer names, so the default is 63.
+ # You may be able to set this to 253 if you are sure that your Kubernetes
cluster can handle it, see
+ #
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
+ lease-name-max-length = 63
+
+ # When the lease name needs to be truncated to fit within
lease-name-max-length, a hash suffix
+ # is appended to help distinguish names that would otherwise truncate to
the same value.
+ # The suffix is derived from a SHA-256 digest of the original lease name,
base32-encoded
+ # (lowercase), with the last N characters taken where N is this setting.
+ # Set to 0 to disable hash suffixing on truncation.
+ # We base32 encode the SHA-256 digest to ensure that the suffix is DNS
1039 compatible, and we
+ # take characters from the start of the hash. This encoded value about 52
characters long.
+ # If you choose a value greater than about 52 then the full digest will be
used as the suffix.
+ # If you choose a value greater than the lease-name-max-length then the
suffix will be used on its
+ # own with no prefix but will still be truncated to fit within
lease-name-max-length.
+ on-truncate-add-hash-length = 8
+
# Settings that are specific to retrying requests with 401 responses due
to possible token rotation
token-rotation-retry {
# Number of total attempts to make
diff --git
a/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/AbstractKubernetesLease.scala
b/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/AbstractKubernetesLease.scala
index d8a6a20d..5c6dcd88 100644
---
a/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/AbstractKubernetesLease.scala
+++
b/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/AbstractKubernetesLease.scala
@@ -13,6 +13,8 @@
package org.apache.pekko.coordination.lease.kubernetes
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
import java.text.Normalizer
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -20,7 +22,6 @@ import scala.concurrent.Future
import scala.util.{ Failure, Success }
import scala.annotation.nowarn
import org.apache.pekko
-import
pekko.coordination.lease.kubernetes.AbstractKubernetesLease.makeDNS1039Compatible
import pekko.actor.ExtendedActorSystem
import pekko.coordination.lease.kubernetes.LeaseActor._
import pekko.coordination.lease.scaladsl.Lease
@@ -37,11 +38,47 @@ object AbstractKubernetesLease {
val configPath = "pekko.coordination.lease.kubernetes"
private val leaseCounter = new AtomicInteger(1)
+ // Base32 alphabet (RFC 4648 §6), **lowercased** so every character is a
valid DNS 1039 label
+ // character. '=' padding is intentionally omitted: we stream full 5-bit
groups and emit one
+ // final partial group when bits remain, so the output contains only
[a-z2-7] characters.
+ private val base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"
+
+ /**
+ * Base32-encode a byte array using a lowercase alphabet and no '=' padding.
+ * Every output character is in [a-z2-7], making the result safe for use in
DNS 1039 labels.
+ */
+ private[kubernetes] def base32Encode(bytes: Array[Byte]): String = {
+ val sb = new StringBuilder
+ var buffer = 0
+ var bitsLeft = 0
+ for (b <- bytes) {
+ buffer = (buffer << 8) | (b & 0xFF)
+ bitsLeft += 8
+ while (bitsLeft >= 5) {
+ bitsLeft -= 5
+ sb.append(base32Alphabet((buffer >> bitsLeft) & 0x1F))
+ }
+ }
+ if (bitsLeft > 0) {
+ sb.append(base32Alphabet((buffer << (5 - bitsLeft)) & 0x1F))
+ }
+ sb.toString()
+ }
+
+ /**
+ * Compute a short hash suffix for the given name: SHA-256 → base32 → first
`length` chars.
+ */
+ private def computeHashSuffix(name: String, length: Int): String = {
+ val digest = MessageDigest.getInstance("SHA-256")
+ val hashBytes = digest.digest(name.getBytes(StandardCharsets.UTF_8))
+ base32Encode(hashBytes).take(length)
+ }
+
/**
- * Limit the length of a name to 63 characters.
+ * Limit the length of a name to the given number of characters.
* Some subsystem of Kubernetes cannot manage longer names.
*/
- private def truncateTo63Characters(name: String): String = name.take(63)
+ private def truncateToLength(name: String, maxLength: Int): String =
name.take(maxLength)
/**
* Removes from the leading and trailing positions the specified characters.
@@ -52,12 +89,31 @@ object AbstractKubernetesLease {
/**
* Make a name compatible with DNS 1039 standard: like a single domain name
segment.
* Regex to follow: [a-z]([-a-z0-9]*[a-z0-9])
- * Limit the resulting name to 63 characters
+ * Limit the resulting name to maxLength characters (default 63).
+ * When truncation is necessary and hashLength > 0, the last (hashLength +
1) characters of the
+ * truncated name are replaced by a hyphen followed by a
hashLength-character hash suffix derived
+ * from a SHA-256 digest of the original name (base32-encoded, first
hashLength chars taken).
+ * If hashLength >= maxLength the result consists entirely of the first
maxLength hash characters.
*/
- private def makeDNS1039Compatible(name: String): String = {
+ private[kubernetes] def makeDNS1039Compatible(name: String, maxLength: Int =
63, hashLength: Int = 0): String = {
val normalized =
Normalizer.normalize(name,
Normalizer.Form.NFKD).toLowerCase.replaceAll("[_.]",
"-").replaceAll("[^-a-z0-9]", "")
- trim(truncateTo63Characters(normalized), List('-'))
+ if (normalized.length <= maxLength || hashLength <= 0) {
+ trim(truncateToLength(normalized, maxLength), List('-'))
+ } else {
+ val maxSuffixLength = math.min(hashLength, maxLength)
+ val hashSuffix = computeHashSuffix(name, maxSuffixLength)
+ if (hashSuffix.length >= maxLength - 1) {
+ // Hash suffix alone fills or exceeds the max length, so return only
hash chars (capped at maxLength)
+ // also account for the '-' that would be added if we had room for a
prefix
+ hashSuffix.take(maxLength)
+ } else {
+ // Truncate prefix to fit the hash suffix and hyphen within maxLength
+ val prefixLength = maxLength - hashSuffix.length - 1
+ val prefix = trim(truncateToLength(normalized, prefixLength),
List('-'))
+ s"$prefix-$hashSuffix"
+ }
+ }
}
}
@@ -74,7 +130,11 @@ abstract class AbstractKubernetesLease(system:
ExtendedActorSystem, leaseTaken:
private implicit val timeout: Timeout =
Timeout(settings.timeoutSettings.operationTimeout)
- private val leaseName = makeDNS1039Compatible(settings.leaseName)
+ private val leaseName =
+ AbstractKubernetesLease.makeDNS1039Compatible(
+ settings.leaseName,
+ k8sSettings.leaseLabelMaxLength,
+ k8sSettings.onTruncateAddHashLength)
private val leaseActor = system.systemActorOf(
LeaseActor.props(k8sApi, settings, leaseName, leaseTaken),
s"kubernetesLease${AbstractKubernetesLease.leaseCounter.incrementAndGet}")
diff --git
a/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettings.scala
b/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettings.scala
index d8ba328e..589bdae8 100644
---
a/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettings.scala
+++
b/lease-kubernetes/src/main/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettings.scala
@@ -73,7 +73,9 @@ private[pekko] object KubernetesSettings {
secure = config.getBoolean("secure-api-server"),
tlsVersion = config.getString("tls-version"),
bodyReadTimeout = apiServerRequestTimeout / 2,
- tokenRetrySettings = tokenRetrySettings)
+ tokenRetrySettings = tokenRetrySettings,
+ leaseLabelMaxLength = config.getInt("lease-name-max-length"),
+ onTruncateAddHashLength = config.getInt("on-truncate-add-hash-length"))
}
}
@@ -107,4 +109,6 @@ private[pekko] class KubernetesSettings(
10.millis,
1.minute,
0.3
- ))
+ ),
+ val leaseLabelMaxLength: Int = 63,
+ val onTruncateAddHashLength: Int = 8)
diff --git
a/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/Base32EncodeSpec.scala
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/Base32EncodeSpec.scala
new file mode 100644
index 00000000..a4cdd83d
--- /dev/null
+++
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/Base32EncodeSpec.scala
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.coordination.lease.kubernetes
+
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+/**
+ * Tests for [[AbstractKubernetesLease.base32Encode]].
+ *
+ * Expected values are derived from RFC 4648 §10 test vectors, with the
standard uppercase
+ * alphabet replaced by the lowercase alphabet used here ([a-z2-7] instead of
[A-Z2-7]) and
+ * without the '=' padding characters.
+ */
+class Base32EncodeSpec extends AnyWordSpec with Matchers {
+
+ private def encode(bytes: Array[Byte]): String =
+ AbstractKubernetesLease.base32Encode(bytes)
+
+ private def encode(bytes: Int*): String =
+ encode(bytes.map(_.toByte).toArray)
+
+ "base32Encode" should {
+
+ // ---- edge cases -------------------------------------------------------
+
+ "return empty string for an empty array" in {
+ encode(Array.empty[Byte]) shouldEqual ""
+ }
+
+ "encode a single zero byte" in {
+ // 0x00 = 00000000 → 5 bits: 00000='a', remaining 3 bits:
000<<2=00000='a'
+ encode(0x00) shouldEqual "aa"
+ }
+
+ "encode a single all-ones byte" in {
+ // 0xFF = 11111111 → 5 bits: 11111='7', remaining 3 bits:
111<<2=11100=28='4'
+ encode(0xFF) shouldEqual "74"
+ }
+
+ "encode two zero bytes" in {
+ // 16 bits of zeros: 3 full groups of 5 + 1 remaining bit
+ // 00000 00000 00000 0[0000] → 'a','a','a','a'
+ encode(0x00, 0x00) shouldEqual "aaaa"
+ }
+
+ "encode two all-ones bytes" in {
+ // 0xFF 0xFF = 11111111 11111111 → 3×5 full + 1 remaining
+ // 11111=31='7', 11111=31='7', 11111=31='7', 1[0000]=16='q'
+ encode(0xFF, 0xFF) shouldEqual "777q"
+ }
+
+ // ---- RFC 4648 §10 test vectors (lowercase, no padding) ----------------
+
+ "encode 'f' (single byte 0x66) matching RFC 4648 vector" in {
+ // 0x66 = 01100110 → 01100=12='m', 110<<2=11000=24='y'
+ encode("f".getBytes(UTF_8)) shouldEqual "my"
+ }
+
+ "encode 'fo' (two bytes) matching RFC 4648 vector" in {
+ encode("fo".getBytes(UTF_8)) shouldEqual "mzxq"
+ }
+
+ "encode 'foo' (three bytes) matching RFC 4648 vector" in {
+ encode("foo".getBytes(UTF_8)) shouldEqual "mzxw6"
+ }
+
+ "encode 'foob' (four bytes) matching RFC 4648 vector" in {
+ encode("foob".getBytes(UTF_8)) shouldEqual "mzxw6yq"
+ }
+
+ "encode 'fooba' (five bytes — full 8-char group) matching RFC 4648 vector"
in {
+ encode("fooba".getBytes(UTF_8)) shouldEqual "mzxw6ytb"
+ }
+
+ "encode 'foobar' (six bytes) matching RFC 4648 vector" in {
+ encode("foobar".getBytes(UTF_8)) shouldEqual "mzxw6ytboi"
+ }
+
+ // ---- output properties ------------------------------------------------
+
+ "produce output containing only [a-z2-7] characters for arbitrary inputs"
in {
+ val inputs: Seq[Array[Byte]] = Seq(
+ Array.empty,
+ Array(0x00.toByte),
+ Array(0xFF.toByte),
+ Array.fill(1)(0xAB.toByte),
+ Array.fill(3)(0xCD.toByte),
+ (0 to 255).map(_.toByte).toArray)
+ for (input <- inputs) {
+ withClue(s"input length ${input.length}: ") {
+ (encode(input) should fullyMatch).regex("[a-z2-7]*")
+ }
+ }
+ }
+
+ "never emit '=' padding characters" in {
+ // Lengths 0–6 bytes cover all residue classes mod 5
+ for (len <- 0 to 6) {
+ val input = Array.fill(len)(0x42.toByte)
+ withClue(s"length $len: ") {
+ (encode(input) should not).include("=")
+ }
+ }
+ }
+
+ "produce correct output length (ceiling of 8n/5 characters for n input
bytes)" in {
+ // RFC 4648 unpadded length = ceil(n * 8 / 5)
+ for (n <- 0 to 10) {
+ val expectedLen = if (n == 0) 0 else math.ceil(n * 8.0 / 5).toInt
+ val input = Array.fill(n)(0x00.toByte)
+ withClue(s"n=$n: ") {
+ encode(input).length shouldEqual expectedLen
+ }
+ }
+ }
+
+ "produce the same output for the same input (determinism)" in {
+ val input = "hello world".getBytes(UTF_8)
+ encode(input) shouldEqual encode(input)
+ }
+
+ "produce different output for different inputs" in {
+ (encode("abc".getBytes(UTF_8)) should
not).equal(encode("abd".getBytes(UTF_8)))
+ }
+ }
+}
diff --git
a/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettingsSpec.scala
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettingsSpec.scala
index 9a8d853c..8de798a9 100644
---
a/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettingsSpec.scala
+++
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/KubernetesSettingsSpec.scala
@@ -47,6 +47,18 @@ class KubernetesSettingsSpec extends AnyWordSpec with
Matchers {
"support tls-version override" in {
conf("tls-version=TLSv1.3").tlsVersion shouldEqual "TLSv1.3"
}
+ "default lease-name-max-length to 63" in {
+ conf("").leaseLabelMaxLength shouldEqual 63
+ }
+ "support lease-name-max-length override" in {
+ conf("lease-name-max-length=40").leaseLabelMaxLength shouldEqual 40
+ }
+ "default on-truncate-add-hash-length to 8" in {
+ conf("").onTruncateAddHashLength shouldEqual 8
+ }
+ "support on-truncate-add-hash-length override" in {
+ conf("on-truncate-add-hash-length=12").onTruncateAddHashLength
shouldEqual 12
+ }
"not allow server request timeout greater than operation timeout" in {
intercept[IllegalArgumentException] {
conf("""
diff --git
a/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/MakeDNS1039CompatibleSpec.scala
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/MakeDNS1039CompatibleSpec.scala
new file mode 100644
index 00000000..d650b5d4
--- /dev/null
+++
b/lease-kubernetes/src/test/scala/org/apache/pekko/coordination/lease/kubernetes/MakeDNS1039CompatibleSpec.scala
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.coordination.lease.kubernetes
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class MakeDNS1039CompatibleSpec extends AnyWordSpec with Matchers {
+
+ "makeDNS1039Compatible" should {
+
+ "leave a simple lowercase name unchanged" in {
+ AbstractKubernetesLease.makeDNS1039Compatible("my-lease") shouldEqual
"my-lease"
+ }
+
+ "convert underscores and dots to hyphens" in {
+ AbstractKubernetesLease.makeDNS1039Compatible("my.lease_name")
shouldEqual "my-lease-name"
+ }
+
+ "strip leading and trailing hyphens after normalization" in {
+ AbstractKubernetesLease.makeDNS1039Compatible("-my-lease-") shouldEqual
"my-lease"
+ }
+
+ "remove characters that are not allowed in DNS 1039 labels" in {
+ AbstractKubernetesLease.makeDNS1039Compatible("my@lease!name")
shouldEqual "myleasename"
+ }
+
+ "convert uppercase to lowercase" in {
+ AbstractKubernetesLease.makeDNS1039Compatible("MyLease") shouldEqual
"mylease"
+ }
+
+ "truncate to 63 characters by default (no hash when hashLength is 0)" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
0)
+ (result should have).length(63)
+ result shouldEqual "a" * 63
+ }
+
+ "truncate to a custom maxLength (no hash when hashLength is 0)" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 40,
0)
+ (result should have).length(40)
+ result shouldEqual "a" * 40
+ }
+
+ "trim trailing hyphens after truncation (no hash)" in {
+ val name = "a" * 30 + "-" + "b" * 30
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(name, 31, 0)
+ (result should not).endWith("-")
+ }
+
+ "not truncate when name fits within maxLength" in {
+ val name63 = "a" * 63
+ AbstractKubernetesLease.makeDNS1039Compatible(name63, 63, 8) shouldEqual
name63
+ (AbstractKubernetesLease.makeDNS1039Compatible(name63 + "extra", 63, 8)
should not).equal(name63 + "extra")
+ }
+
+ "append hash suffix when truncation is needed and hashLength > 0" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
8)
+ (result should have).length(63)
+ (result.takeRight(8) should fullyMatch).regex("[a-z2-7]{8}")
+ result should include("-")
+ }
+
+ "produce a deterministic hash suffix for the same input" in {
+ val name =
"my-very-long-lease-name-that-exceeds-the-maximum-allowed-kubernetes-length"
+ val r1 = AbstractKubernetesLease.makeDNS1039Compatible(name, 63, 8)
+ val r2 = AbstractKubernetesLease.makeDNS1039Compatible(name, 63, 8)
+ r1 shouldEqual r2
+ }
+
+ "produce different hash suffixes for different original names that
truncate to the same prefix" in {
+ // Both names normalize to 'a' * N, but originate from different strings
+ val name1 = "a" * 100
+ val name2 = "A" * 100 // normalizes to same 'a'*100 but is a different
original
+ val r1 = AbstractKubernetesLease.makeDNS1039Compatible(name1, 63, 8)
+ val r2 = AbstractKubernetesLease.makeDNS1039Compatible(name2, 63, 8)
+ // The prefix is identical but hash suffixes differ because originals
differ
+ (r1 should not).equal(r2)
+ }
+
+ "produce different results for long names that differ only in the last
character" in {
+ // Both names are 70 chars and share the first 69 chars; they truncate
to the same prefix
+ // without a hash but must produce different DNS1039 results with one
+ val base = "a" * 69
+ val name1 = base + "b"
+ val name2 = base + "c"
+ val r1 = AbstractKubernetesLease.makeDNS1039Compatible(name1, 63, 8)
+ val r2 = AbstractKubernetesLease.makeDNS1039Compatible(name2, 63, 8)
+ (r1 should not).equal(r2)
+ // Both must be valid length
+ (r1 should have).length(63)
+ (r2 should have).length(63)
+ }
+
+ "produce different results for long names that differ only in the last two
characters" in {
+ val base = "a" * 68
+ val name1 = base + "bc"
+ val name2 = base + "de"
+ val r1 = AbstractKubernetesLease.makeDNS1039Compatible(name1, 63, 8)
+ val r2 = AbstractKubernetesLease.makeDNS1039Compatible(name2, 63, 8)
+ (r1 should not).equal(r2)
+ (r1 should have).length(63)
+ (r2 should have).length(63)
+ }
+
+ "produce only valid DNS 1039 characters when hash suffix is added" in {
+ val longName =
"My-Very-Long-Lease.Name_With_Special-Characters-That-Exceeds-63-Chars-Limit"
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
8)
+ (result should fullyMatch).regex("[a-z][a-z0-9-]*[a-z0-9]")
+ (result should have).length(63)
+ }
+
+ "respect a custom hashLength" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
12)
+ (result should have).length(63)
+ // last 12 chars are the hash suffix
+ (result.takeRight(12) should fullyMatch).regex("[a-z2-7]{12}")
+ }
+
+ "not add hash when hashLength is 0 even if truncation occurs" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
0)
+ result shouldEqual "a" * 63
+ }
+
+ "return full hash when hashLength equals maxLength" in {
+ val longName = "a" * 100
+ // SHA-256 → 32 bytes → 52 base32 chars; take(maxLength=63) returns the
full 52-char hash
+ // but that leaves room for a short prefix and hyphen
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
63)
+ (result should have).length(63)
+ assert(result.startsWith("aaaaaaaaaa-"))
+ }
+
+ "return full (capped at maxLength) when hashLength exceeds maxLength" in {
+ val longName = "a" * 100
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 63,
100)
+ (result should have).length(63)
+ assert(result.startsWith("aaaaaaaaaa-"))
+ }
+
+ "return different names when hashLength >= maxLength and original names
differ" in {
+ val name1 = "a" * 100
+ val name2 = ("a" * 99) + "b" // normalizes to same 'a'*100 but is a
different original
+ val r1 = AbstractKubernetesLease.makeDNS1039Compatible(name1, 63, 100)
+ val r2 = AbstractKubernetesLease.makeDNS1039Compatible(name2, 63, 100)
+ (r1 should not).equal(r2)
+ (r1 should have).length(63)
+ (r2 should have).length(63)
+ }
+
+ "return a valid DNS 1039 name when hashLength equals maxLength for a small
maxLength" in {
+ // maxLength=10, hashLength=10: take(10) from 52 base32 chars → exactly
10 chars
+ val longName = "My-Very-Long-Lease.Name_With_Special-Characters"
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 10,
10)
+ (result should have).length(10)
+ (result should fullyMatch).regex("[a-z2-7]{10}")
+ }
+
+ "return a valid DNS 1039 name when hashLength 1 less than maxLength for a
small maxLength" in {
+ // maxLength=10, hashLength=9: take(9) from 52 base32 chars → exactly 9
chars
+ val longName = "My-Very-Long-Lease.Name_With_Special-Characters"
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(longName, 10,
9)
+ (result should have).length(9)
+ (result should fullyMatch).regex("[a-z2-7]{9}")
+ }
+
+ "hash suffix contains only lowercase letters and digits (no uppercase, no
'=' padding)" in {
+ // Use many different inputs to exercise the full base32 output,
including partial-group chars
+ val inputs = Seq(
+ "a" * 100,
+ "my-very-long-lease-name-that-needs-to-be-truncated-for-kubernetes",
+
"UPPER_CASE.DOTTED_NAME-that-is-indeed-too-long-for-kubernetes-label-limit",
+ "x" * 70)
+ for (name <- inputs) {
+ val result = AbstractKubernetesLease.makeDNS1039Compatible(name, 63, 8)
+ val suffix = result.takeRight(8)
+ withClue(s"suffix '$suffix' of '$result' (from '$name') must match
[a-z2-7]{8}: ") {
+ (suffix should fullyMatch).regex("[a-z2-7]{8}")
+ }
+ withClue(s"result '$result' must not contain '=': ") {
+ (result should not).include("=")
+ }
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]