Skip to content

fix: allow uppercase characters in domain names #1064

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
- Update `kube` to `1.1.0` ([#1049]).
- BREAKING: Return type for `ListenerOperatorVolumeSourceBuilder::new()` is no onger a `Result` ([#1058]).

### Fixed

- Allow uppercase characters in domain names ([#1064]).

### Removed

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

## [0.93.2] - 2025-05-26

Expand Down
4 changes: 2 additions & 2 deletions crates/stackable-operator/crds/DummyCluster.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions crates/stackable-operator/src/builder/pod/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ mod tests {
resources::ResourceRequirementsBuilder,
},
commons::resources::ResourceRequirementsType,
validation::RFC_1123_LABEL_FMT,
};

#[test]
Expand Down Expand Up @@ -603,11 +604,11 @@ mod tests {
assert!(ContainerBuilder::new("name-with-hyphen").is_ok());
assert_container_builder_err(
ContainerBuilder::new("ends-with-hyphen-"),
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
&format!(r#"regex used for validation is "{RFC_1123_LABEL_FMT}""#),
);
assert_container_builder_err(
ContainerBuilder::new("-starts-with-hyphen"),
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
&format!(r#"regex used for validation is "{RFC_1123_LABEL_FMT}""#),
);
}

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

Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/commons/networking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use snafu::Snafu;

use crate::validation;

/// A validated domain name type conforming to RFC 1123, so e.g. not an IP addresses
/// A validated domain name type conforming to RFC 1123, so e.g. not an IP address
#[derive(
Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, JsonSchema,
)]
Expand Down
115 changes: 51 additions & 64 deletions crates/stackable-operator/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,21 @@ use snafu::Snafu;

/// Minimal length required by RFC 1123 is 63. Up to 255 allowed, unsupported by k8s.
const RFC_1123_LABEL_MAX_LENGTH: usize = 63;
// FIXME: According to https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1 domain names must start with a letter
// (and not a number).
const RFC_1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
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";
pub const RFC_1123_LABEL_FMT: &str = "[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?";
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";

/// This is a subdomain's max length in DNS (RFC 1123)
const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
const RFC_1123_SUBDOMAIN_FMT: &str =
concatcp!(RFC_1123_LABEL_FMT, "(\\.", RFC_1123_LABEL_FMT, ")*");
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";

const DOMAIN_MAX_LENGTH: usize = RFC_1123_SUBDOMAIN_MAX_LENGTH;
/// Same as [`RFC_1123_SUBDOMAIN_FMT`], but allows a trailing dot
const DOMAIN_FMT: &str = concatcp!(RFC_1123_SUBDOMAIN_FMT, "\\.?");
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 '.'";
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 '.'";

// FIXME: According to https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1 domain names must start with a letter
// (and not a number).
const RFC_1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
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";

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

pub(crate) static RFC_1123_SUBDOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$"))
.expect("failed to compile RFC 1123 subdomain regex")
});

static RFC_1123_LABEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!("^{RFC_1123_LABEL_FMT}$")).expect("failed to compile RFC 1123 label regex")
});
Expand Down Expand Up @@ -204,19 +198,6 @@ pub fn is_domain(value: &str) -> Result {
])
}

/// Tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123).
pub fn is_rfc_1123_subdomain(value: &str) -> Result {
validate_all([
validate_str_length(value, RFC_1123_SUBDOMAIN_MAX_LENGTH),
validate_str_regex(
value,
&RFC_1123_SUBDOMAIN_REGEX,
RFC_1123_SUBDOMAIN_ERROR_MSG,
&["example.com"],
),
])
}

/// Tests for a string that conforms to the definition of a label in DNS (RFC 1123).
/// Maximum label length supported by k8s is 63 characters (minimum required).
pub fn is_rfc_1123_label(value: &str) -> Result {
Expand Down Expand Up @@ -267,22 +248,6 @@ fn mask_trailing_dash(mut name: String) -> String {
name
}

/// name_is_dns_subdomain checks whether the passed in name is a valid
/// DNS subdomain name
///
/// # Arguments
///
/// * `name` - is the name to check for validity
/// * `prefix` - indicates whether `name` is just a prefix (ending in a dash, which would otherwise not be legal at the end)
pub fn name_is_dns_subdomain(name: &str, prefix: bool) -> Result {
let mut name = name.to_string();
if prefix {
name = mask_trailing_dash(name);
}

is_rfc_1123_subdomain(&name)
}

/// name_is_dns_label checks whether the passed in name is a valid DNS label
/// according to RFC 1035.
///
Expand Down Expand Up @@ -312,14 +277,28 @@ mod tests {

use super::*;

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";

static RFC_1123_SUBDOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$"))
.expect("failed to compile RFC 1123 subdomain regex")
});

/// Tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123).
fn is_rfc_1123_subdomain(value: &str) -> Result {
validate_all([
validate_str_length(value, RFC_1123_SUBDOMAIN_MAX_LENGTH),
validate_str_regex(
value,
&RFC_1123_SUBDOMAIN_REGEX,
RFC_1123_SUBDOMAIN_ERROR_MSG,
&["example.com"],
),
])
}

#[rstest]
#[case("")]
#[case("A")]
#[case("ABC")]
#[case("aBc")]
#[case("A1")]
#[case("A-1")]
#[case("1-A")]
#[case("-")]
#[case("a-")]
#[case("-a")]
Expand All @@ -346,24 +325,6 @@ mod tests {
#[case("1 ")]
#[case(" 1")]
#[case("1 2")]
#[case("A.a")]
#[case("aB.a")]
#[case("ab.A")]
#[case("A1.a")]
#[case("a1.A")]
#[case("A.1")]
#[case("aB.1")]
#[case("A1.1")]
#[case("1A.1")]
#[case("0.A")]
#[case("01.A")]
#[case("012.A")]
#[case("1A.a")]
#[case("1a.A")]
#[case("A.B.C.D.E")]
#[case("AA.BB.CC.DD.EE")]
#[case("a.B.c.d.e")]
#[case("aa.bB.cc.dd.ee")]
#[case("a@b")]
#[case("a,b")]
#[case("a_b")]
Expand All @@ -379,43 +340,67 @@ mod tests {

#[rstest]
#[case("a")]
#[case("A")]
#[case("ab")]
#[case("abc")]
#[case("aBc")]
#[case("ABC")]
#[case("a1")]
#[case("A1")]
#[case("a-1")]
#[case("A-1")]
#[case("a--1--2--b")]
#[case("0")]
#[case("01")]
#[case("012")]
#[case("1a")]
#[case("1-a")]
#[case("1-A")]
#[case("1--a--b--2")]
#[case("a.a")]
#[case("A.a")]
#[case("ab.a")]
#[case("aB.a")]
#[case("ab.A")]
#[case("abc.a")]
#[case("a1.a")]
#[case("A1.a")]
#[case("a1.A")]
#[case("a-1.a")]
#[case("a--1--2--b.a")]
#[case("a.1")]
#[case("A.1")]
#[case("ab.1")]
#[case("aB.1")]
#[case("abc.1")]
#[case("a1.1")]
#[case("A1.1")]
#[case("a-1.1")]
#[case("a--1--2--b.1")]
#[case("0.a")]
#[case("0.A")]
#[case("01.a")]
#[case("01.A")]
#[case("012.a")]
#[case("012.A")]
#[case("1a.a")]
#[case("1A.a")]
#[case("1a.A")]
#[case("1-a.a")]
#[case("1--a--b--2")]
#[case("0.1")]
#[case("01.1")]
#[case("012.1")]
#[case("1a.1")]
#[case("1A.1")]
#[case("1-a.1")]
#[case("1--a--b--2.1")]
#[case("a.b.c.d.e")]
#[case("a.B.c.d.e")]
#[case("A.B.C.D.E")]
#[case("aa.bb.cc.dd.ee")]
#[case("aa.bB.cc.dd.ee")]
#[case("AA.BB.CC.DD.EE")]
#[case("1.2.3.4.5")]
#[case("11.22.33.44.55")]
#[case(&"a".repeat(253))]
Expand All @@ -427,7 +412,9 @@ mod tests {

#[rstest]
#[case("cluster.local")]
#[case("CLUSTER.LOCAL")]
#[case("cluster.local.")]
#[case("CLUSTER.LOCAL.")]
fn is_domain_pass(#[case] value: &str) {
assert!(is_domain(value).is_ok());
}
Expand Down