Skip to content

Commit b2bf0e8

Browse files
authored
fix: allow uppercase characters in domain names (#1064)
* fix: allow uppercase characters in domain names * add changelog entry * raw string format
1 parent 7d5a43e commit b2bf0e8

File tree

5 files changed

+65
-70
lines changed

5 files changed

+65
-70
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
99
- Update `kube` to `1.1.0` ([#1049]).
1010
- BREAKING: Return type for `ListenerOperatorVolumeSourceBuilder::new()` is no onger a `Result` ([#1058]).
1111

12+
### Fixed
13+
14+
- Allow uppercase characters in domain names ([#1064]).
15+
1216
### Removed
1317

1418
- BREAKING: Removed `last_update_time` from CRD ClusterCondition status ([#1054]).
@@ -18,6 +22,7 @@ All notable changes to this project will be documented in this file.
1822
[#1054]: https://github.com/stackabletech/operator-rs/pull/1054
1923
[#1058]: https://github.com/stackabletech/operator-rs/pull/1058
2024
[#1060]: https://github.com/stackabletech/operator-rs/pull/1060
25+
[#1064]: https://github.com/stackabletech/operator-rs/pull/1064
2126

2227
## [0.93.2] - 2025-05-26
2328

crates/stackable-operator/crds/DummyCluster.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/stackable-operator/src/builder/pod/container.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ mod tests {
441441
resources::ResourceRequirementsBuilder,
442442
},
443443
commons::resources::ResourceRequirementsType,
444+
validation::RFC_1123_LABEL_FMT,
444445
};
445446

446447
#[test]
@@ -603,11 +604,11 @@ mod tests {
603604
assert!(ContainerBuilder::new("name-with-hyphen").is_ok());
604605
assert_container_builder_err(
605606
ContainerBuilder::new("ends-with-hyphen-"),
606-
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
607+
&format!(r#"regex used for validation is "{RFC_1123_LABEL_FMT}""#),
607608
);
608609
assert_container_builder_err(
609610
ContainerBuilder::new("-starts-with-hyphen"),
610-
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
611+
&format!(r#"regex used for validation is "{RFC_1123_LABEL_FMT}""#),
611612
);
612613
}
613614

@@ -621,7 +622,9 @@ mod tests {
621622
assert!(ContainerBuilder::new("name_name").is_err());
622623
assert_container_builder_err(
623624
ContainerBuilder::new("name_name"),
624-
"(e.g. \"example-label\", or \"1-label-1\", regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\")",
625+
&format!(
626+
r#"(e.g. "example-label", or "1-label-1", regex used for validation is "{RFC_1123_LABEL_FMT}""#
627+
),
625628
);
626629
}
627630

crates/stackable-operator/src/commons/networking.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use snafu::Snafu;
66

77
use crate::validation;
88

9-
/// A validated domain name type conforming to RFC 1123, so e.g. not an IP addresses
9+
/// A validated domain name type conforming to RFC 1123, so e.g. not an IP address
1010
#[derive(
1111
Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, JsonSchema,
1212
)]

crates/stackable-operator/src/validation.rs

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,21 @@ use snafu::Snafu;
1717

1818
/// Minimal length required by RFC 1123 is 63. Up to 255 allowed, unsupported by k8s.
1919
const RFC_1123_LABEL_MAX_LENGTH: usize = 63;
20-
// FIXME: According to https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1 domain names must start with a letter
21-
// (and not a number).
22-
const RFC_1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
23-
const RFC_1123_LABEL_ERROR_MSG: &str = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
20+
pub const RFC_1123_LABEL_FMT: &str = "[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?";
21+
const RFC_1123_LABEL_ERROR_MSG: &str = "a RFC 1123 label must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
2422

2523
/// This is a subdomain's max length in DNS (RFC 1123)
2624
const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
2725
const RFC_1123_SUBDOMAIN_FMT: &str =
2826
concatcp!(RFC_1123_LABEL_FMT, "(\\.", RFC_1123_LABEL_FMT, ")*");
29-
const RFC_1123_SUBDOMAIN_ERROR_MSG: &str = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
3027

3128
const DOMAIN_MAX_LENGTH: usize = RFC_1123_SUBDOMAIN_MAX_LENGTH;
3229
/// Same as [`RFC_1123_SUBDOMAIN_FMT`], but allows a trailing dot
3330
const DOMAIN_FMT: &str = concatcp!(RFC_1123_SUBDOMAIN_FMT, "\\.?");
34-
const DOMAIN_ERROR_MSG: &str = "a domain must consist of lower case alphanumeric characters, '-' or '.', and must start with an alphanumeric character and end with an alphanumeric character or '.'";
31+
const DOMAIN_ERROR_MSG: &str = "a domain must consist of alphanumeric characters, '-' or '.', and must start with an alphanumeric character and end with an alphanumeric character or '.'";
3532

33+
// FIXME: According to https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1 domain names must start with a letter
34+
// (and not a number).
3635
const RFC_1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
3736
const RFC_1035_LABEL_ERROR_MSG: &str = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character";
3837

@@ -55,11 +54,6 @@ pub(crate) static DOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
5554
Regex::new(&format!("^{DOMAIN_FMT}$")).expect("failed to compile domain regex")
5655
});
5756

58-
pub(crate) static RFC_1123_SUBDOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
59-
Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$"))
60-
.expect("failed to compile RFC 1123 subdomain regex")
61-
});
62-
6357
static RFC_1123_LABEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
6458
Regex::new(&format!("^{RFC_1123_LABEL_FMT}$")).expect("failed to compile RFC 1123 label regex")
6559
});
@@ -204,19 +198,6 @@ pub fn is_domain(value: &str) -> Result {
204198
])
205199
}
206200

207-
/// Tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123).
208-
pub fn is_rfc_1123_subdomain(value: &str) -> Result {
209-
validate_all([
210-
validate_str_length(value, RFC_1123_SUBDOMAIN_MAX_LENGTH),
211-
validate_str_regex(
212-
value,
213-
&RFC_1123_SUBDOMAIN_REGEX,
214-
RFC_1123_SUBDOMAIN_ERROR_MSG,
215-
&["example.com"],
216-
),
217-
])
218-
}
219-
220201
/// Tests for a string that conforms to the definition of a label in DNS (RFC 1123).
221202
/// Maximum label length supported by k8s is 63 characters (minimum required).
222203
pub fn is_rfc_1123_label(value: &str) -> Result {
@@ -267,22 +248,6 @@ fn mask_trailing_dash(mut name: String) -> String {
267248
name
268249
}
269250

270-
/// name_is_dns_subdomain checks whether the passed in name is a valid
271-
/// DNS subdomain name
272-
///
273-
/// # Arguments
274-
///
275-
/// * `name` - is the name to check for validity
276-
/// * `prefix` - indicates whether `name` is just a prefix (ending in a dash, which would otherwise not be legal at the end)
277-
pub fn name_is_dns_subdomain(name: &str, prefix: bool) -> Result {
278-
let mut name = name.to_string();
279-
if prefix {
280-
name = mask_trailing_dash(name);
281-
}
282-
283-
is_rfc_1123_subdomain(&name)
284-
}
285-
286251
/// name_is_dns_label checks whether the passed in name is a valid DNS label
287252
/// according to RFC 1035.
288253
///
@@ -312,14 +277,28 @@ mod tests {
312277

313278
use super::*;
314279

280+
const RFC_1123_SUBDOMAIN_ERROR_MSG: &str = "a RFC 1123 subdomain must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
281+
282+
static RFC_1123_SUBDOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
283+
Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$"))
284+
.expect("failed to compile RFC 1123 subdomain regex")
285+
});
286+
287+
/// Tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123).
288+
fn is_rfc_1123_subdomain(value: &str) -> Result {
289+
validate_all([
290+
validate_str_length(value, RFC_1123_SUBDOMAIN_MAX_LENGTH),
291+
validate_str_regex(
292+
value,
293+
&RFC_1123_SUBDOMAIN_REGEX,
294+
RFC_1123_SUBDOMAIN_ERROR_MSG,
295+
&["example.com"],
296+
),
297+
])
298+
}
299+
315300
#[rstest]
316301
#[case("")]
317-
#[case("A")]
318-
#[case("ABC")]
319-
#[case("aBc")]
320-
#[case("A1")]
321-
#[case("A-1")]
322-
#[case("1-A")]
323302
#[case("-")]
324303
#[case("a-")]
325304
#[case("-a")]
@@ -346,24 +325,6 @@ mod tests {
346325
#[case("1 ")]
347326
#[case(" 1")]
348327
#[case("1 2")]
349-
#[case("A.a")]
350-
#[case("aB.a")]
351-
#[case("ab.A")]
352-
#[case("A1.a")]
353-
#[case("a1.A")]
354-
#[case("A.1")]
355-
#[case("aB.1")]
356-
#[case("A1.1")]
357-
#[case("1A.1")]
358-
#[case("0.A")]
359-
#[case("01.A")]
360-
#[case("012.A")]
361-
#[case("1A.a")]
362-
#[case("1a.A")]
363-
#[case("A.B.C.D.E")]
364-
#[case("AA.BB.CC.DD.EE")]
365-
#[case("a.B.c.d.e")]
366-
#[case("aa.bB.cc.dd.ee")]
367328
#[case("a@b")]
368329
#[case("a,b")]
369330
#[case("a_b")]
@@ -379,43 +340,67 @@ mod tests {
379340

380341
#[rstest]
381342
#[case("a")]
343+
#[case("A")]
382344
#[case("ab")]
383345
#[case("abc")]
346+
#[case("aBc")]
347+
#[case("ABC")]
384348
#[case("a1")]
349+
#[case("A1")]
385350
#[case("a-1")]
351+
#[case("A-1")]
386352
#[case("a--1--2--b")]
387353
#[case("0")]
388354
#[case("01")]
389355
#[case("012")]
390356
#[case("1a")]
391357
#[case("1-a")]
358+
#[case("1-A")]
392359
#[case("1--a--b--2")]
393360
#[case("a.a")]
361+
#[case("A.a")]
394362
#[case("ab.a")]
363+
#[case("aB.a")]
364+
#[case("ab.A")]
395365
#[case("abc.a")]
396366
#[case("a1.a")]
367+
#[case("A1.a")]
368+
#[case("a1.A")]
397369
#[case("a-1.a")]
398370
#[case("a--1--2--b.a")]
399371
#[case("a.1")]
372+
#[case("A.1")]
400373
#[case("ab.1")]
374+
#[case("aB.1")]
401375
#[case("abc.1")]
402376
#[case("a1.1")]
377+
#[case("A1.1")]
403378
#[case("a-1.1")]
404379
#[case("a--1--2--b.1")]
405380
#[case("0.a")]
381+
#[case("0.A")]
406382
#[case("01.a")]
383+
#[case("01.A")]
407384
#[case("012.a")]
385+
#[case("012.A")]
408386
#[case("1a.a")]
387+
#[case("1A.a")]
388+
#[case("1a.A")]
409389
#[case("1-a.a")]
410390
#[case("1--a--b--2")]
411391
#[case("0.1")]
412392
#[case("01.1")]
413393
#[case("012.1")]
414394
#[case("1a.1")]
395+
#[case("1A.1")]
415396
#[case("1-a.1")]
416397
#[case("1--a--b--2.1")]
417398
#[case("a.b.c.d.e")]
399+
#[case("a.B.c.d.e")]
400+
#[case("A.B.C.D.E")]
418401
#[case("aa.bb.cc.dd.ee")]
402+
#[case("aa.bB.cc.dd.ee")]
403+
#[case("AA.BB.CC.DD.EE")]
419404
#[case("1.2.3.4.5")]
420405
#[case("11.22.33.44.55")]
421406
#[case(&"a".repeat(253))]
@@ -427,7 +412,9 @@ mod tests {
427412

428413
#[rstest]
429414
#[case("cluster.local")]
415+
#[case("CLUSTER.LOCAL")]
430416
#[case("cluster.local.")]
417+
#[case("CLUSTER.LOCAL.")]
431418
fn is_domain_pass(#[case] value: &str) {
432419
assert!(is_domain(value).is_ok());
433420
}

0 commit comments

Comments
 (0)