Skip to content
2 changes: 2 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ All notable changes to this project will be documented in this file.
### Fixed

- Don't default the `termination_grace_period` of the `ProbeBuilder` to 0, as this is an invalid value ([#1090]).
- Don't allow uppercase characters in Kubernetes object names ([#1095]).

[#1085]: https://github.com/stackabletech/operator-rs/pull/1085
[#1087]: https://github.com/stackabletech/operator-rs/pull/1087
[#1090]: https://github.com/stackabletech/operator-rs/pull/1090
[#1095]: https://github.com/stackabletech/operator-rs/pull/1095

## [0.96.0] - 2025-08-25

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

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

Expand All @@ -623,7 +623,7 @@ mod tests {
assert_container_builder_err(
ContainerBuilder::new("name_name"),
&format!(
r#"(e.g. "example-label", or "1-label-1", regex used for validation is "{RFC_1123_LABEL_FMT}""#
r#"(e.g. "example-label", or "1-label-1", regex used for validation is "{LOWERCASE_RFC_1123_LABEL_FMT}""#
),
);
}
Expand Down
117 changes: 63 additions & 54 deletions crates/stackable-operator/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,27 @@ 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;
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 modified RFC 1123 format according to the Kubernetes specification, see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
pub const LOWERCASE_RFC_1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
const LOWERCASE_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";

// This is a RFC 1123 format, see https://www.rfc-editor.org/rfc/rfc1123
const RFC_1123_LABEL_FMT: &str = "[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?";

/// 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_FMT: &str = concatcp!(
LOWERCASE_RFC_1123_LABEL_FMT,
"(\\.",
LOWERCASE_RFC_1123_LABEL_FMT,
")*"
);
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";

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, "\\.?");

/// Same as [`RFC_1123_LABEL_FMT`], but allows a trailing dot
const DOMAIN_FMT: &str = concatcp!(RFC_1123_LABEL_FMT, "(\\.", RFC_1123_LABEL_FMT, ")*\\.?");
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
Expand All @@ -54,8 +64,14 @@ pub(crate) static DOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!("^{DOMAIN_FMT}$")).expect("failed to compile domain 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")
static LOWERCASE_RFC_1123_LABEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!("^{LOWERCASE_RFC_1123_LABEL_FMT}$"))
.expect("failed to compile RFC 1123 label regex")
});

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_1035_LABEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Expand Down Expand Up @@ -205,13 +221,26 @@ pub fn is_rfc_1123_label(value: &str) -> Result {
validate_str_length(value, RFC_1123_LABEL_MAX_LENGTH),
validate_str_regex(
value,
&RFC_1123_LABEL_REGEX,
RFC_1123_LABEL_ERROR_MSG,
&LOWERCASE_RFC_1123_LABEL_REGEX,
LOWERCASE_RFC_1123_LABEL_ERROR_MSG,
&["example-label", "1-label-1"],
),
])
}

/// 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 1035).
pub fn is_rfc_1035_label(value: &str) -> Result {
validate_all([
Expand Down Expand Up @@ -277,28 +306,14 @@ 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 @@ -325,6 +340,24 @@ 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("0.A")]
#[case("01.A")]
#[case("012.A")]
#[case("1A.a")]
#[case("1a.A")]
#[case("1A.1")]
#[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("a@b")]
#[case("a,b")]
#[case("a_b")]
Expand All @@ -340,67 +373,43 @@ 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 Down