Skip to content

Commit 6f1ef43

Browse files
dervoetisbernauermaltesandernightkr
authored
feat: support marking cluster domains as FQDNs, and change the default to FQDN (#939)
* fix: Avllow trailing dots in DomainName * Add trailing dot to KubernetesClusterInfo::cluster_domain * Update crates/stackable-operator/src/validation.rs * Update crates/stackable-operator/src/utils/cluster_info.rs Co-authored-by: Malte Sander <malte.sander.it@gmail.com> * Update crates/stackable-operator/src/utils/cluster_info.rs Co-authored-by: Malte Sander <malte.sander.it@gmail.com> * chore: update CHANGELOG to reflect breaking changes in cluster domain handling * test: add additional test cases for FQDN validation * chore: fixed changelog entry * Update crates/stackable-operator/src/utils/cluster_info.rs Co-authored-by: Natalie Klestrup Röijezon <nat.roijezon@stackable.tech> * docs: be more explicit about the change in the CHANGELOG * fix: renamed some variables and functions, since we validate domains, not just FQDNs --------- Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.tech> Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de> Co-authored-by: Malte Sander <malte.sander.it@gmail.com> Co-authored-by: Natalie Klestrup Röijezon <nat.roijezon@stackable.tech>
1 parent 953567d commit 6f1ef43

File tree

4 files changed

+56
-11
lines changed

4 files changed

+56
-11
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ All notable changes to this project will be documented in this file.
2020

2121
- BREAKING: Bump Rust dependencies to enable Kubernetes 1.32 (via `kube` 0.98.0 and `k8s-openapi`
2222
0.23.0) ([#867]).
23+
- BREAKING: Append a dot to the default cluster domain to make it a FQDN and allow FQDNs when validating a `DomainName` ([#939]).
24+
25+
[#939]: https://github.com/stackabletech/operator-rs/pull/939
2326

2427
## [0.83.0] - 2024-12-03
2528

@@ -316,7 +319,7 @@ All notable changes to this project will be documented in this file.
316319

317320
[#808]: https://github.com/stackabletech/operator-rs/pull/808
318321

319-
## [0.69.1] 2024-06-10
322+
## [0.69.1] - 2024-06-10
320323

321324
### Added
322325

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ use crate::validation;
1111
Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, JsonSchema,
1212
)]
1313
#[serde(try_from = "String", into = "String")]
14-
pub struct DomainName(#[validate(regex(path = "validation::RFC_1123_SUBDOMAIN_REGEX"))] String);
14+
pub struct DomainName(#[validate(regex(path = "validation::DOMAIN_REGEX"))] String);
1515

1616
impl FromStr for DomainName {
1717
type Err = validation::Errors;
1818

1919
fn from_str(value: &str) -> Result<Self, Self::Err> {
20-
validation::is_rfc_1123_subdomain(value)?;
20+
validation::is_domain(value)?;
2121
Ok(DomainName(value.to_owned()))
2222
}
2323
}

crates/stackable-operator/src/utils/cluster_info.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ use std::str::FromStr;
22

33
use crate::commons::networking::DomainName;
44

5-
const KUBERNETES_CLUSTER_DOMAIN_DEFAULT: &str = "cluster.local";
5+
const KUBERNETES_CLUSTER_DOMAIN_DEFAULT: &str = "cluster.local.";
66

77
/// Some information that we know about the Kubernetes cluster.
88
#[derive(Debug, Clone)]
99
pub struct KubernetesClusterInfo {
10-
/// The Kubernetes cluster domain, typically `cluster.local`.
10+
/// The Kubernetes cluster domain, typically `cluster.local.`.
1111
pub cluster_domain: DomainName,
1212
}
1313

1414
#[derive(clap::Parser, Debug, Default, PartialEq, Eq)]
1515
pub struct KubernetesClusterInfoOpts {
16-
/// Kubernetes cluster domain, usually this is `cluster.local`.
16+
/// Kubernetes cluster domain, usually this is `cluster.local.`.
17+
///
18+
/// Please note that we recommend adding a trailing dot (".") to reduce DNS requests, see
19+
/// <https://github.com/stackabletech/issues/issues/656> for details.
20+
//
1721
// We are not using a default value here, as operators will probably do an more advanced
1822
// auto-detection of the cluster domain in case it is not specified in the future.
1923
#[arg(long, env)]
@@ -25,6 +29,9 @@ impl KubernetesClusterInfo {
2529
let cluster_domain = match &cluster_info_opts.kubernetes_cluster_domain {
2630
Some(cluster_domain) => {
2731
tracing::info!(%cluster_domain, "Using configured Kubernetes cluster domain");
32+
if !cluster_domain.ends_with('.') {
33+
tracing::warn!(%cluster_domain, "Your configured Kubernetes cluster domain is not fully qualified (it does not end with a dot (\".\")). We recommend adding a trailing dot to reduce DNS requests, see https://github.com/stackabletech/issues/issues/656 for details");
34+
}
2835

2936
cluster_domain.clone()
3037
}

crates/stackable-operator/src/validation.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,23 @@ use const_format::concatcp;
1515
use regex::Regex;
1616
use snafu::Snafu;
1717

18+
/// Minimal length required by RFC 1123 is 63. Up to 255 allowed, unsupported by k8s.
19+
const RFC_1123_LABEL_MAX_LENGTH: usize = 63;
1820
// FIXME: According to https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1 domain names must start with a letter
1921
// (and not a number).
2022
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";
24+
25+
/// This is a subdomain's max length in DNS (RFC 1123)
26+
const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
2127
const RFC_1123_SUBDOMAIN_FMT: &str =
2228
concatcp!(RFC_1123_LABEL_FMT, "(\\.", RFC_1123_LABEL_FMT, ")*");
2329
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";
24-
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";
2530

26-
// This is a subdomain's max length in DNS (RFC 1123)
27-
const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
28-
// Minimal length required by RFC 1123 is 63. Up to 255 allowed, unsupported by k8s.
29-
const RFC_1123_LABEL_MAX_LENGTH: usize = 63;
31+
const DOMAIN_MAX_LENGTH: usize = RFC_1123_SUBDOMAIN_MAX_LENGTH;
32+
/// Same as [`RFC_1123_SUBDOMAIN_FMT`], but allows a trailing dot
33+
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 '.'";
3035

3136
const RFC_1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
3237
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";
@@ -46,6 +51,10 @@ const KERBEROS_REALM_NAME_ERROR_MSG: &str =
4651
"Kerberos realm name must only contain alphanumeric characters, '-', and '.'";
4752

4853
// Lazily initialized regular expressions
54+
pub(crate) static DOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
55+
Regex::new(&format!("^{DOMAIN_FMT}$")).expect("failed to compile domain regex")
56+
});
57+
4958
pub(crate) static RFC_1123_SUBDOMAIN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
5059
Regex::new(&format!("^{RFC_1123_SUBDOMAIN_FMT}$"))
5160
.expect("failed to compile RFC 1123 subdomain regex")
@@ -178,6 +187,23 @@ fn validate_all(validations: impl IntoIterator<Item = Result<(), Error>>) -> Res
178187
}
179188
}
180189

190+
pub fn is_domain(value: &str) -> Result {
191+
validate_all([
192+
validate_str_length(value, DOMAIN_MAX_LENGTH),
193+
validate_str_regex(
194+
value,
195+
&DOMAIN_REGEX,
196+
DOMAIN_ERROR_MSG,
197+
&[
198+
"example.com",
199+
"example.com.",
200+
"cluster.local",
201+
"cluster.local.",
202+
],
203+
),
204+
])
205+
}
206+
181207
/// Tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123).
182208
pub fn is_rfc_1123_subdomain(value: &str) -> Result {
183209
validate_all([
@@ -394,6 +420,15 @@ mod tests {
394420
#[case(&"a".repeat(253))]
395421
fn is_rfc_1123_subdomain_pass(#[case] value: &str) {
396422
assert!(is_rfc_1123_subdomain(value).is_ok());
423+
// Every valid RFC1123 is also a valid domain
424+
assert!(is_domain(value).is_ok());
425+
}
426+
427+
#[rstest]
428+
#[case("cluster.local")]
429+
#[case("cluster.local.")]
430+
fn is_domain_pass(#[case] value: &str) {
431+
assert!(is_domain(value).is_ok());
397432
}
398433

399434
#[test]

0 commit comments

Comments
 (0)