diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 28887ac64..4a740e29a 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -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]). @@ -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 diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index ac49653cc..2bf17f3f0 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -58,8 +58,8 @@ spec: type: boolean type: object domainName: - description: A validated domain name type conforming to RFC 1123, so e.g. not an IP addresses - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\.?$ + description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*\.?$ type: string hostName: type: string diff --git a/crates/stackable-operator/src/builder/pod/container.rs b/crates/stackable-operator/src/builder/pod/container.rs index 9c954d020..3f1620a2f 100644 --- a/crates/stackable-operator/src/builder/pod/container.rs +++ b/crates/stackable-operator/src/builder/pod/container.rs @@ -441,6 +441,7 @@ mod tests { resources::ResourceRequirementsBuilder, }, commons::resources::ResourceRequirementsType, + validation::RFC_1123_LABEL_FMT, }; #[test] @@ -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}""#), ); } @@ -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}""# + ), ); } diff --git a/crates/stackable-operator/src/commons/networking.rs b/crates/stackable-operator/src/commons/networking.rs index 742065764..39b6738ca 100644 --- a/crates/stackable-operator/src/commons/networking.rs +++ b/crates/stackable-operator/src/commons/networking.rs @@ -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, )] diff --git a/crates/stackable-operator/src/validation.rs b/crates/stackable-operator/src/validation.rs index 524d4f7d9..d9b895715 100644 --- a/crates/stackable-operator/src/validation.rs +++ b/crates/stackable-operator/src/validation.rs @@ -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"; @@ -55,11 +54,6 @@ pub(crate) static DOMAIN_REGEX: LazyLock = LazyLock::new(|| { Regex::new(&format!("^{DOMAIN_FMT}$")).expect("failed to compile domain regex") }); -pub(crate) static RFC_1123_SUBDOMAIN_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$")) - .expect("failed to compile RFC 1123 subdomain regex") -}); - static RFC_1123_LABEL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(&format!("^{RFC_1123_LABEL_FMT}$")).expect("failed to compile RFC 1123 label regex") }); @@ -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 { @@ -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. /// @@ -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 = 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")] @@ -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")] @@ -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))] @@ -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()); }