#[derive(Debug, Default)]
pub struct HealthcheckAnalysis {
pub has_healthcheck: bool,
pub has_cmd_or_entrypoint: bool,
pub healthcheck_line: usize,
pub cmd_line: usize,
pub is_healthcheck_none: bool,
pub interval_seconds: Option<u32>,
pub timeout_seconds: Option<u32>,
pub retries: Option<u32>,
}
pub fn is_healthcheck_line(line: &str) -> bool {
line.trim().to_uppercase().starts_with("HEALTHCHECK ")
}
pub fn is_healthcheck_none(line: &str) -> bool {
line.trim().to_uppercase().contains("HEALTHCHECK") && line.to_uppercase().contains(" NONE")
}
pub fn is_cmd_or_entrypoint(line: &str) -> bool {
let upper = line.trim().to_uppercase();
upper.starts_with("CMD ") || upper.starts_with("ENTRYPOINT ")
}
pub fn extract_duration(line: &str, option: &str) -> Option<u32> {
line.find(option).and_then(|start| {
let value_start = start + option.len();
let rest = &line[value_start..];
let value: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
value.parse().ok()
})
}
pub fn is_interval_too_aggressive(seconds: u32) -> bool {
seconds < 5
}
pub fn is_timeout_too_short(seconds: u32) -> bool {
seconds < 1
}
pub fn is_retries_too_low(retries: u32) -> bool {
retries < 1
}
pub fn is_healthcheck_after_cmd(healthcheck_line: usize, cmd_line: usize) -> bool {
healthcheck_line > cmd_line && cmd_line > 0
}
pub fn analyze_dockerfile(source: &str) -> HealthcheckAnalysis {
let mut analysis = HealthcheckAnalysis::default();
for (line_num, line) in source.lines().enumerate() {
if is_healthcheck_line(line) {
analysis.has_healthcheck = true;
analysis.healthcheck_line = line_num + 1;
analysis.is_healthcheck_none = is_healthcheck_none(line);
analysis.interval_seconds = extract_duration(line, "--interval=");
analysis.timeout_seconds = extract_duration(line, "--timeout=");
analysis.retries = extract_duration(line, "--retries=");
}
if is_cmd_or_entrypoint(line) {
analysis.has_cmd_or_entrypoint = true;
analysis.cmd_line = line_num + 1;
}
}
analysis
}
pub fn should_suggest_healthcheck(analysis: &HealthcheckAnalysis) -> bool {
analysis.has_cmd_or_entrypoint && !analysis.has_healthcheck
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_healthcheck_line_true() {
assert!(is_healthcheck_line("HEALTHCHECK CMD curl localhost"));
assert!(is_healthcheck_line(
" HEALTHCHECK --interval=30s CMD curl localhost"
));
assert!(is_healthcheck_line("healthcheck cmd curl")); }
#[test]
fn test_is_healthcheck_line_false() {
assert!(!is_healthcheck_line("# HEALTHCHECK"));
assert!(!is_healthcheck_line("CMD echo"));
assert!(!is_healthcheck_line("RUN echo HEALTHCHECK"));
}
#[test]
fn test_is_healthcheck_none_true() {
assert!(is_healthcheck_none("HEALTHCHECK NONE"));
assert!(is_healthcheck_none(" HEALTHCHECK NONE "));
assert!(is_healthcheck_none("healthcheck none"));
}
#[test]
fn test_is_healthcheck_none_false() {
assert!(!is_healthcheck_none("HEALTHCHECK CMD curl localhost"));
assert!(!is_healthcheck_none("CMD echo none"));
}
#[test]
fn test_is_cmd_or_entrypoint_cmd() {
assert!(is_cmd_or_entrypoint("CMD echo hello"));
assert!(is_cmd_or_entrypoint(" CMD [\"python\", \"app.py\"]"));
assert!(is_cmd_or_entrypoint("cmd echo")); }
#[test]
fn test_is_cmd_or_entrypoint_entrypoint() {
assert!(is_cmd_or_entrypoint("ENTRYPOINT [\"./app\"]"));
assert!(is_cmd_or_entrypoint(" entrypoint ./run.sh"));
}
#[test]
fn test_is_cmd_or_entrypoint_false() {
assert!(!is_cmd_or_entrypoint("RUN echo"));
assert!(!is_cmd_or_entrypoint("# CMD echo"));
assert!(!is_cmd_or_entrypoint("FROM ubuntu"));
}
#[test]
fn test_extract_duration_interval() {
assert_eq!(
extract_duration("--interval=30s CMD", "--interval="),
Some(30)
);
assert_eq!(extract_duration("--interval=5s", "--interval="), Some(5));
assert_eq!(
extract_duration("--interval=120s --timeout=3s", "--interval="),
Some(120)
);
}
#[test]
fn test_extract_duration_timeout() {
assert_eq!(extract_duration("--timeout=3s CMD", "--timeout="), Some(3));
assert_eq!(
extract_duration("--interval=30s --timeout=10s", "--timeout="),
Some(10)
);
}
#[test]
fn test_extract_duration_retries() {
assert_eq!(extract_duration("--retries=3", "--retries="), Some(3));
assert_eq!(extract_duration("--retries=5 CMD", "--retries="), Some(5));
}
#[test]
fn test_extract_duration_missing() {
assert_eq!(extract_duration("CMD curl localhost", "--interval="), None);
assert_eq!(extract_duration("--timeout=3s", "--interval="), None);
}
#[test]
fn test_is_interval_too_aggressive() {
assert!(is_interval_too_aggressive(1));
assert!(is_interval_too_aggressive(4));
assert!(!is_interval_too_aggressive(5));
assert!(!is_interval_too_aggressive(30));
}
#[test]
fn test_is_timeout_too_short() {
assert!(is_timeout_too_short(0));
assert!(!is_timeout_too_short(1));
assert!(!is_timeout_too_short(3));
}
#[test]
fn test_is_retries_too_low() {
assert!(is_retries_too_low(0));
assert!(!is_retries_too_low(1));
assert!(!is_retries_too_low(3));
}
#[test]
fn test_is_healthcheck_after_cmd() {
assert!(is_healthcheck_after_cmd(10, 5)); assert!(!is_healthcheck_after_cmd(5, 10)); assert!(!is_healthcheck_after_cmd(5, 0)); }
#[test]
fn test_analyze_dockerfile_with_healthcheck() {
let dockerfile = "FROM ubuntu\nHEALTHCHECK --interval=30s CMD curl localhost\nCMD echo";
let analysis = analyze_dockerfile(dockerfile);
assert!(analysis.has_healthcheck);
assert!(analysis.has_cmd_or_entrypoint);
assert_eq!(analysis.healthcheck_line, 2);
assert_eq!(analysis.cmd_line, 3);
assert_eq!(analysis.interval_seconds, Some(30));
}
#[test]
fn test_analyze_dockerfile_without_healthcheck() {
let dockerfile = "FROM ubuntu\nCMD echo hello";
let analysis = analyze_dockerfile(dockerfile);
assert!(!analysis.has_healthcheck);
assert!(analysis.has_cmd_or_entrypoint);
}
#[test]
fn test_analyze_dockerfile_healthcheck_none() {
let dockerfile = "FROM ubuntu\nHEALTHCHECK NONE\nCMD echo";
let analysis = analyze_dockerfile(dockerfile);
assert!(analysis.has_healthcheck);
assert!(analysis.is_healthcheck_none);
}
#[test]
fn test_should_suggest_healthcheck_true() {
let analysis = HealthcheckAnalysis {
has_cmd_or_entrypoint: true,
has_healthcheck: false,
..Default::default()
};
assert!(should_suggest_healthcheck(&analysis));
}
#[test]
fn test_should_suggest_healthcheck_false_has_healthcheck() {
let analysis = HealthcheckAnalysis {
has_cmd_or_entrypoint: true,
has_healthcheck: true,
..Default::default()
};
assert!(!should_suggest_healthcheck(&analysis));
}
#[test]
fn test_should_suggest_healthcheck_false_no_cmd() {
let analysis = HealthcheckAnalysis {
has_cmd_or_entrypoint: false,
has_healthcheck: false,
..Default::default()
};
assert!(!should_suggest_healthcheck(&analysis));
}
}