1use std::{
2 fs,
3 hash::{DefaultHasher, Hash, Hasher},
4 ops::Not,
5 path::PathBuf,
6};
7
8use cached::proc_macro::cached;
9use comlexr::cmd;
10use miette::{Context, IntoDiagnostic, Result, bail};
11use serde::{Deserialize, Serialize};
12use tempfile::TempDir;
13use zeroize::Zeroizing;
14
15use crate::{BUILD_ID, string};
16
17mod private {
18 pub trait Private {}
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
22#[serde(tag = "type")]
23pub enum Secret {
24 #[serde(rename = "env")]
25 Env { name: String, mount: SecretMount },
26 #[serde(rename = "file")]
27 File { source: PathBuf, mount: SecretMount },
28 #[serde(rename = "exec")]
29 Exec(SecretExec),
30 #[serde(rename = "ssh")]
31 Ssh,
32}
33
34impl Secret {
35 #[must_use]
36 pub fn get_hash(&self) -> String {
37 get_hash(self)
38 }
39
40 #[must_use]
41 pub fn mount(&self) -> String {
42 let hash = self.get_hash();
43 let prefix = format!("--mount=type=secret,id={hash}");
44 match self {
45 Self::Env {
46 name: _,
47 mount: SecretMount::Env { name: _ },
48 }
49 | Self::Exec(SecretExec {
50 command: _,
51 args: _,
52 mount: SecretMount::Env { name: _ },
53 })
54 | Self::File {
55 source: _,
56 mount: SecretMount::Env { name: _ },
57 } => format!("{prefix},dst=/tmp/secrets/{hash}"),
58
59 Self::Env {
60 name: _,
61 mount: SecretMount::File { destination },
62 }
63 | Self::File {
64 source: _,
65 mount: SecretMount::File { destination },
66 }
67 | Self::Exec(SecretExec {
68 command: _,
69 args: _,
70 mount: SecretMount::File { destination },
71 }) => format!("{prefix},dst={}", destination.display()),
72 Self::Ssh => string!("--ssh"),
73 }
74 }
75
76 #[must_use]
77 pub fn env(&self) -> Option<String> {
78 let hash = self.get_hash();
79 match self {
80 Self::Env {
81 name: _,
82 mount: SecretMount::Env { name },
83 }
84 | Self::File {
85 source: _,
86 mount: SecretMount::Env { name },
87 }
88 | Self::Exec(SecretExec {
89 command: _,
90 args: _,
91 mount: SecretMount::Env { name },
92 }) => Some(format!(r#"{name}="$(cat /tmp/secrets/{hash})""#)),
93 _ => None,
94 }
95 }
96}
97
98#[cached(key = "Secret", convert = "{secret.clone()}", sync_writes = "by_key")]
99fn get_hash(secret: &Secret) -> String {
100 let mut hasher = DefaultHasher::new();
101 secret.hash(&mut hasher);
102 BUILD_ID.hash(&mut hasher);
103 format!("{:x}", hasher.finish())
104}
105
106impl private::Private for Vec<Secret> {}
107
108pub trait SecretMounts: private::Private {
109 fn mounts(&self) -> Vec<String>;
110 fn envs(&self) -> Vec<String>;
111}
112
113impl SecretMounts for Vec<Secret> {
114 fn mounts(&self) -> Vec<String> {
115 self.iter().map(Secret::mount).collect()
116 }
117
118 fn envs(&self) -> Vec<String> {
119 self.iter().filter_map(Secret::env).collect()
120 }
121}
122
123impl private::Private for &[&Secret] {}
124
125pub trait SecretArgs: private::Private {
126 fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>>;
134
135 fn ssh(&self) -> bool;
137}
138
139impl SecretArgs for &[&Secret] {
140 fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>> {
141 Ok(self
142 .iter()
143 .map(|&secret| {
144 Ok(match secret {
145 Secret::Env { name, mount: _ } => Some(format!(
146 "--secret=id={},type=env,src={}",
147 secret.get_hash(),
148 name.trim()
149 )),
150 Secret::File { source, mount: _ } => Some(format!(
151 "--secret=id={},type=file,src={}",
152 secret.get_hash(),
153 source.display()
154 )),
155 Secret::Exec(exec) => {
156 let result = exec.exec()?;
157 let hash = secret.get_hash();
158 let secret_path = temp_dir.path().join(&hash);
159 fs::write(&secret_path, result.value())
160 .into_diagnostic()
161 .wrap_err("Failed to write secret to temp file")?;
162 Some(format!("--secret=id={hash},src={}", secret_path.display()))
163 }
164 Secret::Ssh => None,
165 })
166 })
167 .collect::<Result<Vec<_>>>()?
168 .into_iter()
169 .flatten()
170 .collect())
171 }
172
173 fn ssh(&self) -> bool {
174 self.contains(&&Secret::Ssh)
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
179pub struct SecretExec {
180 pub command: String,
181 pub args: Vec<String>,
182 pub mount: SecretMount,
183}
184
185impl SecretExec {
186 pub fn exec(&self) -> Result<SecretValue> {
191 let output = cmd!(&self.command, for &self.args)
192 .output()
193 .into_diagnostic()
194 .wrap_err_with(|| format!("Unable to execute `{}`", self.command))?;
195
196 if output.status.success().not() {
197 bail!("Failed to execute `{}` to retrieve secret", self.command);
198 }
199
200 String::from_utf8(output.stdout)
201 .map(SecretValue::from)
202 .into_diagnostic()
203 .wrap_err_with(|| "Failed to read output")
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
208#[serde(tag = "type")]
209pub enum SecretMount {
210 #[serde(rename = "env")]
211 Env { name: String },
212 #[serde(rename = "file")]
213 File { destination: PathBuf },
214}
215
216#[derive(Clone, Deserialize)]
217pub struct SecretValue(Zeroizing<String>);
218
219macro_rules! impl_secret_value {
220 ($($type:ty),*) => {
221 $(
222 impl From<$type> for SecretValue {
223 fn from(value: $type) -> Self {
224 Self(String::from(value.trim()).into())
225 }
226 }
227 )*
228 };
229}
230
231impl_secret_value!(String, &String, &str);
232
233impl SecretValue {
234 #[must_use]
236 pub fn value(&self) -> &str {
237 &self.0
238 }
239
240 #[must_use]
242 pub fn is_empty(&self) -> bool {
243 self.0.is_empty()
244 }
245}
246
247impl std::fmt::Display for SecretValue {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 write!(f, "[REDACTED]")
250 }
251}
252
253impl std::fmt::Debug for SecretValue {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(f, "[REDACTED]")
256 }
257}