use anyhow::Result;
use hm_plugin_protocol::{CacheDecision, CommandStep, SnapshotRef};
use crate::orchestrator::docker_client::DockerClient;
fn cache_image_tag(step: &CommandStep) -> Option<String> {
let cache = step.cache.as_ref()?;
if cache.policy == "none" {
return None;
}
let key = cache.key.as_deref()?;
let safe = sanitize_for_tag(&step.key);
let short = &key[..key.len().min(16)];
Some(format!("harmont-local/{safe}:{short}"))
}
fn sanitize_for_tag(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'-'
}
})
.collect()
}
#[derive(Debug)]
pub struct CacheOutcome {
pub decision: CacheDecision,
pub stale_tags: Vec<String>,
}
pub async fn decide(docker: &DockerClient, step: &CommandStep) -> Result<CacheOutcome> {
let Some(tag) = cache_image_tag(step) else {
return Ok(CacheOutcome {
decision: CacheDecision::MissNoCommit,
stale_tags: vec![],
});
};
if docker.image_exists(&tag).await? {
Ok(CacheOutcome {
decision: CacheDecision::Hit {
tag: SnapshotRef::from(tag),
},
stale_tags: vec![],
})
} else {
let safe = sanitize_for_tag(&step.key);
let prefix = format!("harmont-local/{safe}");
let stale = docker
.list_images_by_reference(&prefix)
.await
.unwrap_or_default()
.into_iter()
.filter(|t| *t != tag)
.collect();
Ok(CacheOutcome {
decision: CacheDecision::MissBuildAs {
tag: SnapshotRef::from(tag),
},
stale_tags: stale,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use hm_plugin_protocol::Cache;
fn step(cache: Option<Cache>) -> CommandStep {
CommandStep {
key: "build".into(),
label: None,
cmd: "true".into(),
image: None,
env: None,
timeout_seconds: None,
cache,
runner: None,
runner_args: None,
}
}
#[test]
fn no_cache_yields_none() {
assert!(cache_image_tag(&step(None)).is_none());
}
#[test]
fn policy_none_yields_none() {
let s = step(Some(Cache {
policy: "none".into(),
key: Some("abcdef".into()),
}));
assert!(cache_image_tag(&s).is_none());
}
#[test]
fn ttl_with_key_yields_tag() {
let s = step(Some(Cache {
policy: "ttl".into(),
key: Some("0123456789abcdefffff".into()),
}));
let tag = cache_image_tag(&s).unwrap();
assert!(tag.starts_with("harmont-local/build:"));
}
#[test]
fn sanitize_replaces_invalid_chars() {
assert_eq!(sanitize_for_tag("my/step.name:v1"), "my-step-name-v1");
assert_eq!(sanitize_for_tag("simple"), "simple");
assert_eq!(sanitize_for_tag("a_b-c"), "a_b-c");
}
}