harmont_cli/orchestrator/
cache.rs1use hm_plugin_protocol::CommandStep;
10
11use super::docker_client::DockerClient;
12
13fn sanitize_for_tag(s: &str) -> String {
14 s.chars()
15 .map(|c| {
16 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
17 c
18 } else {
19 '-'
20 }
21 })
22 .collect()
23}
24
25#[must_use]
30pub fn stable_cache_tag(step: &CommandStep) -> Option<String> {
31 let cache = step.cache.as_ref()?;
32 if cache.policy == "none" {
33 return None;
34 }
35 let key = cache.key.as_deref()?;
36 let safe = sanitize_for_tag(&step.key);
37 let short = &key[..key.len().min(16)];
38 Some(format!("harmont-cache/{safe}:{short}"))
39}
40
41pub async fn evict_stale_docker_tags(
43 docker: &DockerClient,
44 step_key: &str,
45 current_tag: Option<&str>,
46) {
47 let safe = sanitize_for_tag(step_key);
48 let reference = format!("harmont-cache/{safe}");
49 let tags = match docker.list_images_by_reference(&reference).await {
50 Ok(t) => t,
51 Err(e) => {
52 tracing::warn!(%e, "failed to list images for stale eviction");
53 return;
54 }
55 };
56 for tag in tags {
57 if Some(tag.as_str()) == current_tag {
58 continue;
59 }
60 if let Err(e) = docker.remove_image(&tag).await {
61 tracing::warn!(image = %tag, %e, "failed to remove stale cached Docker image");
62 }
63 }
64}
65
66#[cfg(test)]
67#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
68mod tests {
69 use super::*;
70 use hm_plugin_protocol::Cache;
71
72 fn step(cache: Option<Cache>) -> CommandStep {
73 CommandStep {
74 key: "build".into(),
75 label: None,
76 cmd: "true".into(),
77 image: None,
78 env: None,
79 timeout_seconds: None,
80 cache,
81 runner: None,
82 runner_args: None,
83 }
84 }
85
86 #[test]
87 fn sanitize_replaces_invalid_chars() {
88 assert_eq!(sanitize_for_tag("my/step.name:v1"), "my-step-name-v1");
89 assert_eq!(sanitize_for_tag("simple"), "simple");
90 assert_eq!(sanitize_for_tag("a_b-c"), "a_b-c");
91 }
92
93 #[test]
94 fn stable_cache_tag_for_cacheable_step() {
95 let s = step(Some(Cache {
96 policy: "ttl".into(),
97 key: Some("0123456789abcdef0000".into()),
98 }));
99 let tag = stable_cache_tag(&s);
100 assert_eq!(
101 tag,
102 Some("harmont-cache/build:0123456789abcdef".to_string())
103 );
104 }
105
106 #[test]
107 fn stable_cache_tag_none_for_uncacheable() {
108 let s = step(None);
109 assert_eq!(stable_cache_tag(&s), None);
110 }
111
112 #[test]
113 fn stable_cache_tag_none_for_policy_none() {
114 let s = step(Some(Cache {
115 policy: "none".into(),
116 key: Some("abc".into()),
117 }));
118 assert_eq!(stable_cache_tag(&s), None);
119 }
120}