1use jiff::Timestamp;
11use miette::Result;
12use reqwest::StatusCode;
13use serde::Serialize;
14
15use crate::{
16 Redacted,
17 client::CanopyHttpError,
18 schema::{BackupTarget, CredentialProcessOutput},
19};
20
21#[derive(Debug, Clone, Serialize)]
24#[serde(rename_all = "PascalCase")]
25pub struct ContainerCreds {
26 pub access_key_id: String,
27 pub secret_access_key: Redacted<String>,
28 pub token: Redacted<String>,
29 pub expiration: Timestamp,
30}
31
32impl From<&CredentialProcessOutput> for ContainerCreds {
33 fn from(c: &CredentialProcessOutput) -> Self {
34 Self {
35 access_key_id: c.access_key_id.clone(),
36 secret_access_key: c.secret_access_key.clone(),
37 token: c.session_token.clone(),
38 expiration: c.expiration,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
47pub enum TargetOutcome {
48 Ready(BackupTarget),
49 Dormant,
50}
51
52impl TargetOutcome {
53 pub fn from_result(result: Result<BackupTarget>) -> Result<Self> {
58 match result {
59 Ok(target) => Ok(Self::Ready(target)),
60 Err(report) => match report.downcast_ref::<CanopyHttpError>() {
61 Some(err)
62 if err.status == StatusCode::PRECONDITION_FAILED
63 || err.status == StatusCode::CONFLICT =>
64 {
65 Ok(Self::Dormant)
66 }
67 _ => Err(report),
68 },
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use serde_json::json;
76
77 use super::*;
78
79 #[test]
80 fn container_creds_translate_session_token_to_token() {
81 let creds: CredentialProcessOutput = serde_json::from_value(json!({
82 "Version": 1,
83 "AccessKeyId": "AKIA",
84 "SecretAccessKey": "secret",
85 "SessionToken": "session-token",
86 "Expiration": "2026-05-21T13:00:00Z",
87 }))
88 .unwrap();
89 let container = ContainerCreds::from(&creds);
90 let out = serde_json::to_value(&container).unwrap();
91 assert_eq!(
92 out,
93 json!({
94 "AccessKeyId": "AKIA",
95 "SecretAccessKey": "secret",
96 "Token": "session-token",
97 "Expiration": "2026-05-21T13:00:00Z",
98 })
99 );
100 assert!(out.get("SessionToken").is_none());
102 }
103
104 #[test]
105 fn redacted_debug_does_not_leak() {
106 let creds = ContainerCreds {
107 access_key_id: "AKIA".to_owned(),
108 secret_access_key: Redacted("aws-sk-value-123".to_owned()),
109 token: Redacted("aws-token-value-456".to_owned()),
110 expiration: "2026-05-21T13:00:00Z".parse().unwrap(),
111 };
112 let debug = format!("{creds:?}");
113 assert!(!debug.contains("aws-sk-value-123"));
114 assert!(!debug.contains("aws-token-value-456"));
115 assert!(debug.contains("<redacted>"));
116 }
117}