use std::path::Path;
use globset::Glob;
use crate::config::{ProbeConfig, ProbeTarget};
use crate::values::UpDuration;
use super::NormalizeError;
pub(super) fn validate_template(
name: &str,
field: &str,
value: &str,
shep_home: Option<&Path>,
) -> Result<(), NormalizeError> {
crate::config::template::validate(value).map_err(|reason| NormalizeError::BadTemplate {
name: name.to_string(),
field: field.to_string(),
reason: reason.to_string(),
})?;
if shep_home.is_none() && crate::config::template::holds_shep_home(value) {
return Err(NormalizeError::NoShepHome {
name: name.to_string(),
field: field.to_string(),
});
}
Ok(())
}
pub(super) fn validate_watch_globs(
name: &str,
field: &'static str,
patterns: &[String],
) -> Result<(), NormalizeError> {
for pattern in patterns {
Glob::new(pattern).map_err(|err| NormalizeError::InvalidWatchGlob {
name: name.to_string(),
field,
pattern: pattern.clone(),
reason: err.to_string(),
})?;
}
Ok(())
}
pub(super) fn validate_probe(
probe: Option<&ProbeConfig>,
name: &'static str,
min_interval: UpDuration,
) -> Result<(), NormalizeError> {
let Some(probe) = probe else {
return Ok(());
};
ProbeTarget::parse(probe).map_err(|reason| NormalizeError::InvalidProbe {
probe: name,
reason: reason.to_string(),
})?;
if probe.failure_threshold == 0 {
return Err(NormalizeError::ZeroFailureThreshold { probe: name });
}
if probe.interval < min_interval {
return Err(NormalizeError::IntervalBelowMinimum {
probe: name,
value: probe.interval,
min: min_interval,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AppConfig;
use crate::config::normalize::normalize;
use crate::config::normalize::resolved_app::{MIN_LIVENESS_INTERVAL, MIN_READINESS_INTERVAL};
fn probe_config(target: &str) -> crate::config::ProbeConfig {
crate::config::ProbeConfig {
kind: crate::config::ProbeKind::Http,
target: target.to_string(),
interval: crate::values::UpDuration::from_millis(10_000),
timeout: crate::values::UpDuration::from_millis(5_000),
failure_threshold: 3,
}
}
#[test]
fn malformed_readiness_probe_target_rejected_naming_the_field() {
let mut app = AppConfig::minimal("web", "./srv");
app.readiness_probe = Some(probe_config("not-a-url"));
match normalize(app).unwrap_err() {
NormalizeError::InvalidProbe { probe, reason } => {
assert_eq!(probe, "readiness_probe");
assert!(!reason.is_empty());
}
other => panic!("expected InvalidProbe, got {other:?}"),
}
}
#[test]
fn malformed_liveness_probe_target_rejected_naming_the_field() {
let mut app = AppConfig::minimal("web", "./srv");
app.liveness_probe = Some(probe_config("not-a-url"));
match normalize(app).unwrap_err() {
NormalizeError::InvalidProbe { probe, .. } => assert_eq!(probe, "liveness_probe"),
other => panic!("expected InvalidProbe, got {other:?}"),
}
}
#[test]
fn valid_probe_targets_accepted() {
let mut app = AppConfig::minimal("web", "./srv");
app.readiness_probe = Some(probe_config("http://127.0.0.1:8080/healthz"));
assert!(normalize(app).is_ok());
}
#[test]
fn zero_failure_threshold_rejected() {
let mut app = AppConfig::minimal("web", "./srv");
let mut probe = probe_config("http://127.0.0.1:8080/healthz");
probe.failure_threshold = 0;
app.readiness_probe = Some(probe);
let err = normalize(app).unwrap_err();
assert_eq!(
err,
NormalizeError::ZeroFailureThreshold {
probe: "readiness_probe"
}
);
assert!(err.to_string().contains("at least 1"), "{err}");
}
#[test]
fn zero_interval_rejected() {
let mut app = AppConfig::minimal("web", "./srv");
let mut probe = probe_config("http://127.0.0.1:8080/healthz");
probe.interval = UpDuration::from_millis(0);
app.readiness_probe = Some(probe);
let err = normalize(app).unwrap_err();
assert_eq!(
err,
NormalizeError::IntervalBelowMinimum {
probe: "readiness_probe",
value: UpDuration::from_millis(0),
min: MIN_READINESS_INTERVAL,
}
);
assert!(err.to_string().contains("must be at least"), "{err}");
}
#[test]
fn default_failure_threshold_from_toml_accepted() {
let src = r#"
name = "web"
script = "./srv"
[readiness_probe]
kind = "http"
target = "http://127.0.0.1:8080/healthz"
"#;
let app: AppConfig = toml::from_str(src).unwrap();
assert!(normalize(app).is_ok());
}
#[test]
fn a_liveness_interval_under_the_floor_is_rejected_rather_than_clamped() {
let mut app = AppConfig::minimal("web", "./srv");
let mut probe = probe_config("http://127.0.0.1:8080/healthz");
probe.interval = UpDuration::from_millis(500);
app.liveness_probe = Some(probe);
let err = normalize(app).unwrap_err();
assert_eq!(
err,
NormalizeError::IntervalBelowMinimum {
probe: "liveness_probe",
value: UpDuration::from_millis(500),
min: MIN_LIVENESS_INTERVAL,
}
);
assert!(err.to_string().contains("500"), "{err}");
}
#[test]
fn a_liveness_interval_exactly_at_the_floor_is_accepted() {
let mut app = AppConfig::minimal("web", "./srv");
let mut probe = probe_config("http://127.0.0.1:8080/healthz");
probe.interval = MIN_LIVENESS_INTERVAL;
app.liveness_probe = Some(probe);
assert!(normalize(app).is_ok());
}
#[test]
fn a_sub_second_readiness_interval_is_accepted() {
let mut app = AppConfig::minimal("web", "./srv");
let mut probe = probe_config("http://127.0.0.1:8080/healthz");
probe.interval = UpDuration::from_millis(50);
app.readiness_probe = Some(probe);
assert!(normalize(app).is_ok());
}
#[test]
fn a_watch_options_glob_that_will_not_compile_is_rejected() {
let mut app = AppConfig::minimal("web", "./srv");
app.watch = true;
app.cwd = Some("/srv/web".to_string());
app.watch_options = vec!["src/**".to_string(), "[".to_string()];
let err = normalize(app).unwrap_err();
assert_eq!(
err,
NormalizeError::InvalidWatchGlob {
name: "web".to_string(),
field: "watch_options",
pattern: "[".to_string(),
reason: Glob::new("[").unwrap_err().to_string(),
}
);
let rendered = err.to_string();
for expected in ["web", "watch_options", "`[`"] {
assert!(
rendered.contains(expected),
"{expected} missing: {rendered}"
);
}
}
#[test]
fn an_ignore_watch_glob_that_will_not_compile_is_rejected() {
let mut app = AppConfig::minimal("web", "./srv");
app.watch = true;
app.cwd = Some("/srv/web".to_string());
app.ignore_watch = vec!["[".to_string()];
match normalize(app).unwrap_err() {
NormalizeError::InvalidWatchGlob { field, pattern, .. } => {
assert_eq!(field, "ignore_watch");
assert_eq!(pattern, "[");
}
other => panic!("expected InvalidWatchGlob, got {other:?}"),
}
}
#[test]
fn a_glob_that_will_not_compile_is_rejected_with_watch_off() {
let mut app = AppConfig::minimal("web", "./srv");
app.watch_options = vec!["[".to_string()];
assert!(matches!(
normalize(app).unwrap_err(),
NormalizeError::InvalidWatchGlob { .. }
));
}
#[test]
fn well_formed_watch_globs_are_accepted() {
let mut app = AppConfig::minimal("web", "./srv");
app.watch = true;
app.cwd = Some("/srv/web".to_string());
app.watch_options = vec!["src/**/*.rs".to_string(), "*.[ch]".to_string()];
app.ignore_watch = vec!["target/**".to_string(), "**/[!.]*.{tmp,swp}".to_string()];
assert!(normalize(app).is_ok());
}
}