Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,13 +9,18 @@ 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]).

[#1049]: https://github.com/stackabletech/operator-rs/pull/1049
[#1054]: https://github.com/stackabletech/operator-rs/pull/1054
[#1058]: https://github.com/stackabletech/operator-rs/pull/1058
[#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!("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!("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!(
"(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