1use std::collections::BTreeMap;
7use std::time::Duration;
8
9pub mod sherpack {
11 pub const HOOK: &str = "sherpack.io/hook";
13 pub const HOOK_WEIGHT: &str = "sherpack.io/hook-weight";
15 pub const HOOK_TIMEOUT: &str = "sherpack.io/hook-timeout";
17 pub const HOOK_DELETE_POLICY: &str = "sherpack.io/hook-delete-policy";
19 pub const HOOK_FAILURE_POLICY: &str = "sherpack.io/hook-failure-policy";
21 pub const HOOK_RETRIES: &str = "sherpack.io/hook-retries";
23 pub const SYNC_WAVE: &str = "sherpack.io/sync-wave";
25 pub const WAIT_FOR: &str = "sherpack.io/wait-for";
27 pub const HEALTH_CHECK: &str = "sherpack.io/health-check";
29 pub const SKIP_WAIT: &str = "sherpack.io/skip-wait";
31}
32
33pub mod helm {
35 pub const HOOK: &str = "helm.sh/hook";
37 pub const HOOK_WEIGHT: &str = "helm.sh/hook-weight";
39 pub const HOOK_DELETE_POLICY: &str = "helm.sh/hook-delete-policy";
41 pub const RESOURCE_POLICY: &str = "helm.sh/resource-policy";
43}
44
45pub fn get_annotation<'a>(
47 annotations: &'a BTreeMap<String, String>,
48 sherpack_key: &str,
49 helm_key: &str,
50) -> Option<&'a str> {
51 annotations
52 .get(sherpack_key)
53 .or_else(|| annotations.get(helm_key))
54 .map(|s| s.as_str())
55}
56
57pub fn get_sherpack_annotation<'a>(
59 annotations: &'a BTreeMap<String, String>,
60 key: &str,
61) -> Option<&'a str> {
62 annotations.get(key).map(|s| s.as_str())
63}
64
65pub fn parse_hook_phases(value: &str) -> Vec<String> {
67 value
68 .split(',')
69 .map(|s| s.trim().to_string())
70 .filter(|s| !s.is_empty())
71 .collect()
72}
73
74pub fn parse_hook_weight(annotations: &BTreeMap<String, String>) -> i32 {
76 get_annotation(annotations, sherpack::HOOK_WEIGHT, helm::HOOK_WEIGHT)
77 .and_then(|s| s.parse().ok())
78 .unwrap_or(0)
79}
80
81pub fn parse_sync_wave(annotations: &BTreeMap<String, String>) -> i32 {
83 get_sherpack_annotation(annotations, sherpack::SYNC_WAVE)
84 .and_then(|s| s.parse().ok())
85 .unwrap_or(0)
86}
87
88pub fn parse_wait_for(annotations: &BTreeMap<String, String>) -> Vec<ResourceRef> {
91 get_sherpack_annotation(annotations, sherpack::WAIT_FOR)
92 .map(|s| {
93 s.split(',')
94 .filter_map(|dep| {
95 let dep = dep.trim();
96 let parts: Vec<&str> = dep.split('/').collect();
97 if parts.len() == 2 {
98 Some(ResourceRef {
99 kind: parts[0].to_string(),
100 name: parts[1].to_string(),
101 })
102 } else {
103 None
104 }
105 })
106 .collect()
107 })
108 .unwrap_or_default()
109}
110
111pub fn parse_duration(value: &str) -> Option<Duration> {
113 let value = value.trim();
114 if value.is_empty() {
115 return None;
116 }
117
118 let (num_str, unit) = if let Some(stripped) = value.strip_suffix("ms") {
119 (stripped, "ms")
120 } else if let Some(stripped) = value.strip_suffix('s') {
121 (stripped, "s")
122 } else if let Some(stripped) = value.strip_suffix('m') {
123 (stripped, "m")
124 } else if let Some(stripped) = value.strip_suffix('h') {
125 (stripped, "h")
126 } else {
127 (value, "s")
129 };
130
131 let num: u64 = num_str.parse().ok()?;
132
133 Some(match unit {
134 "ms" => Duration::from_millis(num),
135 "s" => Duration::from_secs(num),
136 "m" => Duration::from_secs(num * 60),
137 "h" => Duration::from_secs(num * 3600),
138 _ => return None,
139 })
140}
141
142pub fn parse_hook_timeout(annotations: &BTreeMap<String, String>) -> Duration {
144 get_sherpack_annotation(annotations, sherpack::HOOK_TIMEOUT)
145 .and_then(parse_duration)
146 .unwrap_or(Duration::from_secs(300))
147}
148
149pub fn parse_delete_policy(annotations: &BTreeMap<String, String>) -> DeletePolicy {
151 let value = get_annotation(
152 annotations,
153 sherpack::HOOK_DELETE_POLICY,
154 helm::HOOK_DELETE_POLICY,
155 );
156
157 match value {
158 Some(s) => {
159 let policies: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
160
161 if policies.contains(&"before-hook-creation") {
162 DeletePolicy::BeforeHookCreation
163 } else if policies.contains(&"hook-succeeded") && policies.contains(&"hook-failed") {
164 DeletePolicy::Always
165 } else if policies.contains(&"hook-succeeded") {
166 DeletePolicy::OnSuccess
167 } else if policies.contains(&"hook-failed") {
168 DeletePolicy::OnFailure
169 } else {
170 DeletePolicy::BeforeHookCreation }
172 }
173 None => DeletePolicy::BeforeHookCreation,
174 }
175}
176
177pub fn parse_failure_policy(annotations: &BTreeMap<String, String>) -> FailurePolicy {
179 get_sherpack_annotation(annotations, sherpack::HOOK_FAILURE_POLICY)
180 .map(|s| match s.to_lowercase().as_str() {
181 "continue" => FailurePolicy::Continue,
182 "rollback" => FailurePolicy::Rollback,
183 "fail" | "abort" => FailurePolicy::Fail,
184 s if s.starts_with("retry") => {
185 let count = s
187 .trim_start_matches("retry")
188 .trim_start_matches('(')
189 .trim_start_matches(':')
190 .trim_end_matches(')')
191 .parse()
192 .unwrap_or(3);
193 FailurePolicy::Retry(count)
194 }
195 _ => FailurePolicy::Fail,
196 })
197 .unwrap_or(FailurePolicy::Fail)
198}
199
200pub fn should_skip_wait(annotations: &BTreeMap<String, String>) -> bool {
202 get_sherpack_annotation(annotations, sherpack::SKIP_WAIT)
203 .map(|s| s.to_lowercase() == "true" || s == "1")
204 .unwrap_or(false)
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Hash)]
209pub struct ResourceRef {
210 pub kind: String,
211 pub name: String,
212}
213
214impl ResourceRef {
215 pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
216 Self {
217 kind: kind.into(),
218 name: name.into(),
219 }
220 }
221}
222
223impl std::fmt::Display for ResourceRef {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 write!(f, "{}/{}", self.kind, self.name)
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231pub enum DeletePolicy {
232 #[default]
234 BeforeHookCreation,
235 OnSuccess,
237 OnFailure,
239 Always,
241 Never,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
247pub enum FailurePolicy {
248 #[default]
250 Fail,
251 Continue,
253 Rollback,
255 Retry(u32),
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn make_annotations(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
264 pairs
265 .iter()
266 .map(|(k, v)| (k.to_string(), v.to_string()))
267 .collect()
268 }
269
270 #[test]
271 fn test_get_annotation_prefers_sherpack() {
272 let annotations = make_annotations(&[
273 ("sherpack.io/hook", "pre-install"),
274 ("helm.sh/hook", "post-install"),
275 ]);
276
277 let result = get_annotation(&annotations, sherpack::HOOK, helm::HOOK);
278 assert_eq!(result, Some("pre-install"));
279 }
280
281 #[test]
282 fn test_get_annotation_falls_back_to_helm() {
283 let annotations = make_annotations(&[("helm.sh/hook", "post-install")]);
284
285 let result = get_annotation(&annotations, sherpack::HOOK, helm::HOOK);
286 assert_eq!(result, Some("post-install"));
287 }
288
289 #[test]
290 fn test_parse_hook_phases() {
291 assert_eq!(
292 parse_hook_phases("pre-install,post-upgrade"),
293 vec!["pre-install", "post-upgrade"]
294 );
295 assert_eq!(parse_hook_phases("pre-install"), vec!["pre-install"]);
296 assert_eq!(
297 parse_hook_phases(" pre-install , post-install "),
298 vec!["pre-install", "post-install"]
299 );
300 }
301
302 #[test]
303 fn test_parse_sync_wave() {
304 let annotations = make_annotations(&[("sherpack.io/sync-wave", "2")]);
305 assert_eq!(parse_sync_wave(&annotations), 2);
306
307 let annotations = make_annotations(&[("sherpack.io/sync-wave", "-1")]);
308 assert_eq!(parse_sync_wave(&annotations), -1);
309
310 let empty: BTreeMap<String, String> = BTreeMap::new();
311 assert_eq!(parse_sync_wave(&empty), 0);
312 }
313
314 #[test]
315 fn test_parse_wait_for() {
316 let annotations = make_annotations(&[("sherpack.io/wait-for", "Deployment/postgres")]);
317 let deps = parse_wait_for(&annotations);
318 assert_eq!(deps.len(), 1);
319 assert_eq!(deps[0].kind, "Deployment");
320 assert_eq!(deps[0].name, "postgres");
321
322 let annotations =
323 make_annotations(&[("sherpack.io/wait-for", "Deployment/db, Service/cache")]);
324 let deps = parse_wait_for(&annotations);
325 assert_eq!(deps.len(), 2);
326 }
327
328 #[test]
329 fn test_parse_duration() {
330 assert_eq!(parse_duration("5m"), Some(Duration::from_secs(300)));
331 assert_eq!(parse_duration("30s"), Some(Duration::from_secs(30)));
332 assert_eq!(parse_duration("1h"), Some(Duration::from_secs(3600)));
333 assert_eq!(parse_duration("100ms"), Some(Duration::from_millis(100)));
334 assert_eq!(parse_duration("60"), Some(Duration::from_secs(60)));
335 assert_eq!(parse_duration(""), None);
336 }
337
338 #[test]
339 fn test_parse_delete_policy() {
340 let annotations = make_annotations(&[("helm.sh/hook-delete-policy", "hook-succeeded")]);
341 assert_eq!(parse_delete_policy(&annotations), DeletePolicy::OnSuccess);
342
343 let annotations =
344 make_annotations(&[("helm.sh/hook-delete-policy", "hook-succeeded,hook-failed")]);
345 assert_eq!(parse_delete_policy(&annotations), DeletePolicy::Always);
346
347 let annotations =
348 make_annotations(&[("helm.sh/hook-delete-policy", "before-hook-creation")]);
349 assert_eq!(
350 parse_delete_policy(&annotations),
351 DeletePolicy::BeforeHookCreation
352 );
353 }
354
355 #[test]
356 fn test_parse_failure_policy() {
357 let annotations = make_annotations(&[("sherpack.io/hook-failure-policy", "continue")]);
358 assert_eq!(parse_failure_policy(&annotations), FailurePolicy::Continue);
359
360 let annotations = make_annotations(&[("sherpack.io/hook-failure-policy", "retry(5)")]);
361 assert_eq!(parse_failure_policy(&annotations), FailurePolicy::Retry(5));
362
363 let annotations = make_annotations(&[("sherpack.io/hook-failure-policy", "retry:3")]);
364 assert_eq!(parse_failure_policy(&annotations), FailurePolicy::Retry(3));
365 }
366}