Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move validated Hostname type from secret-operator #851

Merged
merged 12 commits into from
Sep 3, 2024
9 changes: 9 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Add `Hostname` and `KerberosRealmName` types extracted from secret-operator ([#851]).

### Changed

- BREAKING: `validation` module now uses typed errors ([#851]).

### Fixed

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

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

## [0.74.0] - 2024-08-22

Expand Down
75 changes: 33 additions & 42 deletions crates/stackable-operator/src/builder/pod/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ use k8s_openapi::api::core::v1::{
LifecycleHandler, ObjectFieldSelector, Probe, ResourceRequirements, SecretKeySelector,
SecurityContext, VolumeMount,
};
use snafu::Snafu;
use snafu::{ResultExt as _, Snafu};

use crate::{
commons::product_image_selection::ResolvedProductImage, validation::is_rfc_1123_label,
commons::product_image_selection::ResolvedProductImage,
validation::{self, is_rfc_1123_label},
};

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

#[derive(Debug, PartialEq, Snafu)]
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("container name {container_name:?} is invalid: {violation:?}"))]
#[snafu(display("container name {container_name:?} is invalid"))]
InvalidContainerName {
source: validation::Errors,
container_name: String,
violation: String,
},
}

Expand Down Expand Up @@ -276,16 +277,8 @@ impl ContainerBuilder {

/// Validates a container name is according to the [RFC 1123](https://www.ietf.org/rfc/rfc1123.txt) standard.
/// Returns [Ok] if the name is according to the standard, and [Err] if not.
fn validate_container_name(name: &str) -> Result<()> {
let validation_result = is_rfc_1123_label(name);

match validation_result {
Ok(_) => Ok(()),
Err(err) => Err(Error::InvalidContainerName {
container_name: name.to_owned(),
violation: err.join(", "),
}),
}
fn validate_container_name(container_name: &str) -> Result<()> {
is_rfc_1123_label(container_name).context(InvalidContainerNameSnafu { container_name })
}
}

Expand Down Expand Up @@ -497,19 +490,20 @@ mod tests {
"lengthexceededlengthexceededlengthexceededlengthexceededlengthex";
assert_eq!(long_container_name.len(), 64); // 63 characters is the limit for container names
let result = ContainerBuilder::new(long_container_name);
match result {
Ok(_) => {
panic!("Container name exceeding 63 characters should cause an error");
match result
.err()
.expect("Container name exceeding 63 characters should cause an error")
{
Error::InvalidContainerName {
container_name,
source,
} => {
assert_eq!(container_name, long_container_name);
assert_eq!(
source.to_string(),
"input is 64 bytes long but must be no more than 63"
)
}
Err(error) => match error {
Error::InvalidContainerName {
container_name,
violation,
} => {
assert_eq!(container_name.as_str(), long_container_name);
assert_eq!(violation.as_str(), "must be no more than 63 characters")
}
},
}
// One characters shorter name is valid
let max_len_container_name: String = long_container_name.chars().skip(1).collect();
Expand All @@ -527,11 +521,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])?",
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
);
assert_container_builder_err(
ContainerBuilder::new("-starts-with-hyphen"),
"regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?",
"regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\"",
);
}

Expand All @@ -545,7 +539,7 @@ 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])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
"(e.g. \"example-label\", or \"1-label-1\", regex used for validation is \"[a-z0-9]([-a-z0-9]*[a-z0-9])?\")",
);
}

Expand Down Expand Up @@ -573,19 +567,16 @@ mod tests {
result: Result<ContainerBuilder, Error>,
expected_err_contains: &str,
) {
match result {
Ok(_) => {
panic!("Container name exceeding 63 characters should cause an error");
match result
.err()
.expect("Container name exceeding 63 characters should cause an error")
{
Error::InvalidContainerName {
container_name: _,
source,
} => {
assert!(dbg!(source.to_string()).contains(dbg!(expected_err_contains)));
}
Err(error) => match error {
Error::InvalidContainerName {
container_name: _,
violation,
} => {
println!("{violation}");
assert!(violation.contains(expected_err_contains));
}
},
}
}
}
1 change: 1 addition & 0 deletions crates/stackable-operator/src/commons/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod affinity;
pub mod authentication;
pub mod cluster_operation;
pub mod listener;
pub mod networking;
pub mod opa;
pub mod pdb;
pub mod product_image_selection;
Expand Down
76 changes: 76 additions & 0 deletions crates/stackable-operator/src/commons/networking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::{fmt::Display, ops::Deref};

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::validation;

/// A validated hostname type, for use in CRDs.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(try_from = "String", into = "String")]
pub struct Hostname(#[validate(regex(path = "validation::RFC_1123_SUBDOMAIN_REGEX"))] String);

impl TryFrom<String> for Hostname {
type Error = validation::Errors;

fn try_from(value: String) -> Result<Self, Self::Error> {
validation::is_rfc_1123_subdomain(&value)?;
Ok(Hostname(value))
}
}

impl From<Hostname> for String {
fn from(value: Hostname) -> Self {
value.0
}
}

impl Display for Hostname {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

impl Deref for Hostname {
type Target = str;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// A validated kerberos realm name type, for use in CRDs.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(try_from = "String", into = "String")]
pub struct KerberosRealmName(
#[validate(regex(path = "validation::KERBEROS_REALM_NAME_REGEX"))] String,
);

impl TryFrom<String> for KerberosRealmName {
type Error = validation::Errors;

fn try_from(value: String) -> Result<Self, Self::Error> {
validation::is_kerberos_realm_name(&value)?;
Ok(KerberosRealmName(value))
}
}

impl From<KerberosRealmName> for String {
fn from(value: KerberosRealmName) -> Self {
value.0
}
}

impl Display for KerberosRealmName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

impl Deref for KerberosRealmName {
type Target = str;

fn deref(&self) -> &Self::Target {
&self.0
}
}
Loading
Loading