Skip to content

Commit 96217cf

Browse files
nightkrTechassi
andauthored
Move validated Hostname type from secret-operator (#851)
* Refactor validation module to use typed errors * Remove separate regex_str parameter * Upstream Hostname from secret-operator * Add kerberos realm name type This used to use Hostname, but secret-op's Hostname had looser validation constraints * fmt * Changelog * Remove module name from error types * Update crates/stackable-operator/CHANGELOG.md Co-authored-by: Techassi <git@techassi.dev> * Update crates/stackable-operator/src/commons/networking.rs Co-authored-by: Techassi <git@techassi.dev> * Update crates/stackable-operator/src/commons/networking.rs Co-authored-by: Techassi <git@techassi.dev> * Update crates/stackable-operator/src/validation.rs Co-authored-by: Techassi <git@techassi.dev> * Update crates/stackable-operator/src/validation.rs Co-authored-by: Techassi <git@techassi.dev> --------- Co-authored-by: Techassi <git@techassi.dev>
1 parent 68d0cb2 commit 96217cf

File tree

5 files changed

+284
-122
lines changed

5 files changed

+284
-122
lines changed

crates/stackable-operator/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add `Hostname` and `KerberosRealmName` types extracted from secret-operator ([#851]).
10+
11+
### Changed
12+
13+
- BREAKING: `validation` module now uses typed errors ([#851]).
14+
715
### Fixed
816

917
- Fix the CRD description of `ClientAuthenticationDetails` to not contain internal Rust doc, but a public CRD description ([#846]).
1018

1119
[#846]: https://github.com/stackabletech/operator-rs/pull/846
20+
[#851]: https://github.com/stackabletech/operator-rs/pull/851
1221

1322
## [0.74.0] - 2024-08-22
1423

crates/stackable-operator/src/builder/pod/container.rs

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ use k8s_openapi::api::core::v1::{
55
LifecycleHandler, ObjectFieldSelector, Probe, ResourceRequirements, SecretKeySelector,
66
SecurityContext, VolumeMount,
77
};
8-
use snafu::Snafu;
8+
use snafu::{ResultExt as _, Snafu};
99

1010
use crate::{
11-
commons::product_image_selection::ResolvedProductImage, validation::is_rfc_1123_label,
11+
commons::product_image_selection::ResolvedProductImage,
12+
validation::{self, is_rfc_1123_label},
1213
};
1314

1415
type Result<T, E = Error> = std::result::Result<T, E>;
1516

16-
#[derive(Debug, PartialEq, Snafu)]
17+
#[derive(Debug, Snafu)]
1718
pub enum Error {
18-
#[snafu(display("container name {container_name:?} is invalid: {violation:?}"))]
19+
#[snafu(display("container name {container_name:?} is invalid"))]
1920
InvalidContainerName {
21+
source: validation::Errors,
2022
container_name: String,
21-
violation: String,
2223
},
2324
}
2425

@@ -276,16 +277,8 @@ impl ContainerBuilder {
276277

277278
/// Validates a container name is according to the [RFC 1123](https://www.ietf.org/rfc/rfc1123.txt) standard.
278279
/// Returns [Ok] if the name is according to the standard, and [Err] if not.
279-
fn validate_container_name(name: &str) -> Result<()> {
280-
let validation_result = is_rfc_1123_label(name);
281-
282-
match validation_result {
283-
Ok(_) => Ok(()),
284-
Err(err) => Err(Error::InvalidContainerName {
285-
container_name: name.to_owned(),
286-
violation: err.join(", "),
287-
}),
288-
}
280+
fn validate_container_name(container_name: &str) -> Result<()> {
281+
is_rfc_1123_label(container_name).context(InvalidContainerNameSnafu { container_name })
289282
}
290283
}
291284

@@ -497,19 +490,20 @@ mod tests {
497490
"lengthexceededlengthexceededlengthexceededlengthexceededlengthex";
498491
assert_eq!(long_container_name.len(), 64); // 63 characters is the limit for container names
499492
let result = ContainerBuilder::new(long_container_name);
500-
match result {
501-
Ok(_) => {
502-
panic!("Container name exceeding 63 characters should cause an error");
493+
match result
494+
.err()
495+
.expect("Container name exceeding 63 characters should cause an error")
496+
{
497+
Error::InvalidContainerName {
498+
container_name,
499+
source,
500+
} => {
501+
assert_eq!(container_name, long_container_name);
502+
assert_eq!(
503+
source.to_string(),
504+
"input is 64 bytes long but must be no more than 63"
505+
)
503506
}
504-
Err(error) => match error {
505-
Error::InvalidContainerName {
506-
container_name,
507-
violation,
508-
} => {
509-
assert_eq!(container_name.as_str(), long_container_name);
510-
assert_eq!(violation.as_str(), "must be no more than 63 characters")
511-
}
512-
},
513507
}
514508
// One characters shorter name is valid
515509
let max_len_container_name: String = long_container_name.chars().skip(1).collect();
@@ -527,11 +521,11 @@ mod tests {
527521
assert!(ContainerBuilder::new("name-with-hyphen").is_ok());
528522
assert_container_builder_err(
529523
ContainerBuilder::new("ends-with-hyphen-"),
530-
"regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?",
524+
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
531525
);
532526
assert_container_builder_err(
533527
ContainerBuilder::new("-starts-with-hyphen"),
534-
"regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?",
528+
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
535529
);
536530
}
537531

@@ -545,7 +539,7 @@ mod tests {
545539
assert!(ContainerBuilder::new("name_name").is_err());
546540
assert_container_builder_err(
547541
ContainerBuilder::new("name_name"),
548-
"(e.g. 'example-label', or '1-label-1', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
542+
"(e.g. \"example-label\", or \"1-label-1\", regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\")",
549543
);
550544
}
551545

@@ -573,19 +567,16 @@ mod tests {
573567
result: Result<ContainerBuilder, Error>,
574568
expected_err_contains: &str,
575569
) {
576-
match result {
577-
Ok(_) => {
578-
panic!("Container name exceeding 63 characters should cause an error");
570+
match result
571+
.err()
572+
.expect("Container name exceeding 63 characters should cause an error")
573+
{
574+
Error::InvalidContainerName {
575+
container_name: _,
576+
source,
577+
} => {
578+
assert!(dbg!(source.to_string()).contains(dbg!(expected_err_contains)));
579579
}
580-
Err(error) => match error {
581-
Error::InvalidContainerName {
582-
container_name: _,
583-
violation,
584-
} => {
585-
println!("{violation}");
586-
assert!(violation.contains(expected_err_contains));
587-
}
588-
},
589580
}
590581
}
591582
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod affinity;
44
pub mod authentication;
55
pub mod cluster_operation;
66
pub mod listener;
7+
pub mod networking;
78
pub mod opa;
89
pub mod pdb;
910
pub mod product_image_selection;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::{fmt::Display, ops::Deref};
2+
3+
use schemars::JsonSchema;
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::validation;
7+
8+
/// A validated hostname type, for use in CRDs.
9+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
10+
#[serde(try_from = "String", into = "String")]
11+
pub struct Hostname(#[validate(regex(path = "validation::RFC_1123_SUBDOMAIN_REGEX"))] String);
12+
13+
impl TryFrom<String> for Hostname {
14+
type Error = validation::Errors;
15+
16+
fn try_from(value: String) -> Result<Self, Self::Error> {
17+
validation::is_rfc_1123_subdomain(&value)?;
18+
Ok(Hostname(value))
19+
}
20+
}
21+
22+
impl From<Hostname> for String {
23+
fn from(value: Hostname) -> Self {
24+
value.0
25+
}
26+
}
27+
28+
impl Display for Hostname {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
f.write_str(&self.0)
31+
}
32+
}
33+
34+
impl Deref for Hostname {
35+
type Target = str;
36+
37+
fn deref(&self) -> &Self::Target {
38+
&self.0
39+
}
40+
}
41+
42+
/// A validated kerberos realm name type, for use in CRDs.
43+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
44+
#[serde(try_from = "String", into = "String")]
45+
pub struct KerberosRealmName(
46+
#[validate(regex(path = "validation::KERBEROS_REALM_NAME_REGEX"))] String,
47+
);
48+
49+
impl TryFrom<String> for KerberosRealmName {
50+
type Error = validation::Errors;
51+
52+
fn try_from(value: String) -> Result<Self, Self::Error> {
53+
validation::is_kerberos_realm_name(&value)?;
54+
Ok(KerberosRealmName(value))
55+
}
56+
}
57+
58+
impl From<KerberosRealmName> for String {
59+
fn from(value: KerberosRealmName) -> Self {
60+
value.0
61+
}
62+
}
63+
64+
impl Display for KerberosRealmName {
65+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66+
f.write_str(&self.0)
67+
}
68+
}
69+
70+
impl Deref for KerberosRealmName {
71+
type Target = str;
72+
73+
fn deref(&self) -> &Self::Target {
74+
&self.0
75+
}
76+
}

0 commit comments

Comments
 (0)