Skip to main content

blue_build_utils/
secret.rs

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    /// Retrieves the args for the image builder.
127    ///
128    /// If exec based secrets are included, will run the commands
129    /// to put the results into files for mounting.
130    ///
131    /// # Errors
132    /// Will error if an exec based secret fails to run.
133    fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>>;
134
135    /// Checks to see if ssh is a required secret.
136    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    /// Executes the command to retrieve the secret value.
187    ///
188    /// # Errors
189    /// Will error if the command fails to execute.
190    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    /// Get the value of the secret.
235    #[must_use]
236    pub fn value(&self) -> &str {
237        &self.0
238    }
239
240    /// Checks if the value is empty
241    #[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}