Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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, features = ["derive"] }
kube-runtime = "0.49"
lazy_static = "1"
regex = "1"
serde = "1.0"
serde_json = "1.0"
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 @@ -9,6 +9,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");
}
}