Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

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: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ edition = "2018"

[dependencies]
async-trait = "0.1"
either = "1.6"
const_format = "0.2"
either = "1"
futures = "0.3"
k8s-openapi = { version = "0.11", default-features = false }
kube = { version = "0.49", default-features = false }
kube-runtime = "0.49"
lazy_static = "1"
regex = "1"
serde = "1"
serde_json = "1"
serde_yaml = "0.8"
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod k8s_errors;
pub mod metadata;
pub mod podutils;
pub mod reconcile;
pub mod validation;

use crate::error::OperatorResult;

Expand Down
143 changes: 143 additions & 0 deletions src/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// See apimachinery/pkg/util/validation/validation.go
// See also TODO apimachinery/pkg/api/validation/generic.go and pkg/apis/core/validation/validation.go

use const_format::concatcp;
use lazy_static::lazy_static;
use regex::Regex;

const RFC_1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
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";

// This is a subdomain's max length in DNS (RFC 1123)
const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;

lazy_static! {
static ref RFC_1123_SUBDOMAIN_REGEX: Regex =
Regex::new(&format!("^{}$", RFC_1123_SUBDOMAIN_FMT)).unwrap();
}

/// Returns a formatted error message for maximum length violations.
fn max_len_error(length: usize) -> String {
format!("must be no more than {} characters", length)
}

/// Returns a formatted error message for regex violations.
///
/// # Arguments
///
/// * `msg` - this is the main error message to return
/// * `fmt` - this is the regular expression that did not match the input
/// * `examples` - are optional well, formed examples that would match the regex
fn regex_error(msg: &str, fmt: &str, examples: &[&str]) -> String {
if examples.is_empty() {
return format!("{} (regex used for validation is '{}')", msg, fmt);
}

let mut msg = msg.to_string();
msg.push_str(" (e.g. ");
for (i, example) in examples.iter().enumerate() {
if i > 0 {
msg.push_str(" or ");
}
msg.push('\'');
msg.push_str(example);
msg.push_str("', ");
}

msg.push_str("regex used for validation is '");
msg.push_str(&fmt);
msg.push_str("')");
msg
}

pub fn is_rfc_1123_subdomain(value: &str) -> Vec<String> {
let mut errors = vec![];
if value.len() > RFC_1123_SUBDOMAIN_MAX_LENGTH {
errors.push(max_len_error(RFC_1123_SUBDOMAIN_MAX_LENGTH))
}

if !RFC_1123_SUBDOMAIN_REGEX.is_match(value) {
errors.push(regex_error(
RFC_1123_SUBDOMAIN_ERROR_MSG,
RFC_1123_SUBDOMAIN_FMT,
&["example.com"],
))
}

errors
}

// mask_trailing_dash replaces the final character of a string with a subdomain safe
// value if is a dash.
fn mask_trailing_dash(mut name: String) -> String {
if name.ends_with('-') {
name.pop();
name.push('a');
}

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) -> Vec<String> {
let mut name = name.to_string();
if prefix {
name = mask_trailing_dash(name);
}

is_rfc_1123_subdomain(&name)
}

#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;

#[rstest(
value,
case(""), case("A"), case("ABC"), case("aBc"), case("A1"), case("A-1"), case("1-A"),
case("-"), case("a-"), case("-a"), case("1-"), case("-1"), case("_"), case("a_"),
case("_a"), case("a_b"), case("1_"), case("_1"), case("1_2"), case("."), case("a."),
case(".a"), case("a..b"), case("1."), case(".1"), case("1..2"), case(" "), case("a "),
case(" a"), case("a b"), 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"), case("a;b"), case("a:b"), case("a%b"), case("a?b"),
case("a$b"), case(&"a".repeat(254))
)]
fn test_bad_values_is_rfc_1123_subdomain(value: &str) {
assert!(!is_rfc_1123_subdomain(value).is_empty());
}

#[rstest(
value,
case("a"), case("ab"), case("abc"), case("a1"), case("a-1"), case("a--1--2--b"), case("0"),
case("01"), case("012"), case("1a"), case("1-a"), case("1--a--b--2"), case("a.a"),
case("ab.a"), case("abc.a"), case("a1.a"), case("a-1.a"), case("a--1--2--b.a"), case("a.1"),
case("ab.1"), case("abc.1"), case("a1.1"), case("a-1.1"), case("a--1--2--b.1"), case("0.a"),
case("01.a"), case("012.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("1-a.1"), case("1--a--b--2.1"),
case("a.b.c.d.e"), case("aa.bb.cc.dd.ee"), case("1.2.3.4.5"), case("11.22.33.44.55"),
case(&"a".repeat(253))
)]
fn test_good_values_is_rfc_1123_subdomain(value: &str) {
assert!(is_rfc_1123_subdomain(value).is_empty());
}

#[test]
fn test_mask_trailing_dash() {
assert_eq!(mask_trailing_dash("abc-".to_string()), "abca");
assert_eq!(mask_trailing_dash("abc".to_string()), "abc");
assert_eq!(mask_trailing_dash(String::new()), String::new());
assert_eq!(mask_trailing_dash("-".to_string()), "a");
}
}