feat: Add ability to mount secrets
This commit is contained in:
parent
22ef8392b7
commit
4fabd3e5db
27 changed files with 463 additions and 69 deletions
|
|
@ -20,6 +20,7 @@ lenient_semver = "0.4"
|
|||
process_control = { version = "4", features = ["crossbeam-channel"] }
|
||||
which = "8"
|
||||
|
||||
bon.workspace = true
|
||||
cached.workspace = true
|
||||
chrono.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
|
|
@ -32,7 +33,9 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
syntect.workspace = true
|
||||
bon.workspace = true
|
||||
tempfile.workspace = true
|
||||
uuid.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
syntect.workspace = true
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub const BB_PASSWORD: &str = "BB_PASSWORD";
|
|||
pub const BB_PRIVATE_KEY: &str = "BB_PRIVATE_KEY";
|
||||
pub const BB_REGISTRY: &str = "BB_REGISTRY";
|
||||
pub const BB_REGISTRY_NAMESPACE: &str = "BB_REGISTRY_NAMESPACE";
|
||||
pub const BB_SKIP_VALIDATION: &str = "BB_SKIP_VALIDATION";
|
||||
pub const BB_USERNAME: &str = "BB_USERNAME";
|
||||
pub const BB_BUILD_RECHUNK: &str = "BB_BUILD_RECHUNK";
|
||||
pub const BB_BUILD_RECHUNK_CLEAR_PLAN: &str = "BB_BUILD_RECHUNK_CLEAR_PLAN";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod command_output;
|
|||
pub mod constants;
|
||||
pub mod credentials;
|
||||
mod macros;
|
||||
pub mod secret;
|
||||
pub mod semver;
|
||||
pub mod syntax_highlighting;
|
||||
#[cfg(feature = "test")]
|
||||
|
|
@ -27,11 +28,15 @@ use comlexr::cmd;
|
|||
use format_serde_error::SerdeError;
|
||||
use log::{trace, warn};
|
||||
use miette::{Context, IntoDiagnostic, Result, miette};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::constants::CONTAINER_FILE;
|
||||
|
||||
pub use command_output::*;
|
||||
|
||||
/// UUID used to mark the current builds
|
||||
pub static BUILD_ID: std::sync::LazyLock<Uuid> = std::sync::LazyLock::new(Uuid::new_v4);
|
||||
|
||||
/// Checks for the existance of a given command.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
233
utils/src/secret.rs
Normal file
233
utils/src/secret.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
ops::Not,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use comlexr::cmd;
|
||||
use miette::{Context, IntoDiagnostic, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempfile::TempDir;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::{BUILD_ID, string};
|
||||
|
||||
mod private {
|
||||
pub trait Private {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Secret {
|
||||
#[serde(rename = "env")]
|
||||
Env { name: String },
|
||||
#[serde(rename = "file")]
|
||||
File {
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
},
|
||||
#[serde(rename = "exec")]
|
||||
Exec(SecretExec),
|
||||
#[serde(rename = "ssh")]
|
||||
Ssh,
|
||||
}
|
||||
|
||||
impl Secret {
|
||||
#[must_use]
|
||||
pub fn get_hash(&self) -> String {
|
||||
get_hash(self)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mount(&self) -> String {
|
||||
let hash = self.get_hash();
|
||||
let prefix = format!("--mount=type=secret,id={hash}");
|
||||
match self {
|
||||
Self::Env { name: _ }
|
||||
| Self::Exec(SecretExec {
|
||||
command: _,
|
||||
args: _,
|
||||
output: SecretExecOutput::Env { name: _ },
|
||||
}) => format!("{prefix},dst=/tmp/secrets/{hash}"),
|
||||
Self::File {
|
||||
source: _,
|
||||
destination,
|
||||
}
|
||||
| Self::Exec(SecretExec {
|
||||
command: _,
|
||||
args: _,
|
||||
output: SecretExecOutput::File { destination },
|
||||
}) => format!("{prefix},dst={}", destination.display()),
|
||||
Self::Ssh => string!("--ssh"),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn env(&self) -> Option<String> {
|
||||
let hash = self.get_hash();
|
||||
match self {
|
||||
Self::Env { name }
|
||||
| Self::Exec(SecretExec {
|
||||
command: _,
|
||||
args: _,
|
||||
output: SecretExecOutput::Env { name },
|
||||
}) => Some(format!(r#"{name}="$(cat /tmp/secrets/{hash})""#)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cached(key = "Secret", convert = "{secret.clone()}", sync_writes = "by_key")]
|
||||
fn get_hash(secret: &Secret) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
secret.hash(&mut hasher);
|
||||
BUILD_ID.hash(&mut hasher);
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
impl private::Private for Vec<Secret> {}
|
||||
|
||||
pub trait SecretMounts: private::Private {
|
||||
fn mounts(&self) -> Vec<String>;
|
||||
fn envs(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
impl SecretMounts for Vec<Secret> {
|
||||
fn mounts(&self) -> Vec<String> {
|
||||
self.iter().map(Secret::mount).collect()
|
||||
}
|
||||
|
||||
fn envs(&self) -> Vec<String> {
|
||||
self.iter().filter_map(Secret::env).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: std::hash::BuildHasher> private::Private for HashSet<&Secret, H> {}
|
||||
|
||||
#[allow(private_bounds)]
|
||||
pub trait SecretArgs: private::Private {
|
||||
/// Retrieves the args for the image builder.
|
||||
///
|
||||
/// If exec based secrets are included, will run the commands
|
||||
/// to put the results into files for mounting.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if an exec based secret fails to run.
|
||||
fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>>;
|
||||
}
|
||||
|
||||
impl<H: std::hash::BuildHasher> SecretArgs for HashSet<&Secret, H> {
|
||||
fn args(&self, temp_dir: &TempDir) -> Result<Vec<String>> {
|
||||
self.iter()
|
||||
.map(|secret| {
|
||||
Ok(match secret {
|
||||
Secret::Env { name } => {
|
||||
format!(
|
||||
"--secret=id={},type=env,src={}",
|
||||
secret.get_hash(),
|
||||
name.trim()
|
||||
)
|
||||
}
|
||||
Secret::File {
|
||||
source,
|
||||
destination: _,
|
||||
} => {
|
||||
format!(
|
||||
"--secret=id={},type=file,src={}",
|
||||
secret.get_hash(),
|
||||
source.display()
|
||||
)
|
||||
}
|
||||
Secret::Exec(exec) => {
|
||||
let result = exec.exec()?;
|
||||
let hash = secret.get_hash();
|
||||
let secret_path = temp_dir.path().join(&hash);
|
||||
fs::write(&secret_path, result.value())
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to write secret to temp file")?;
|
||||
format!("--secret=id={hash},src={}", secret_path.display())
|
||||
}
|
||||
Secret::Ssh => string!("--ssh"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
pub struct SecretExec {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub output: SecretExecOutput,
|
||||
}
|
||||
|
||||
impl SecretExec {
|
||||
/// Executes the command to retrieve the secret value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the command fails to execute.
|
||||
pub fn exec(&self) -> Result<SecretValue> {
|
||||
let output = cmd!(&self.command, for &self.args)
|
||||
.output()
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Unable to execute `{}`", self.command))?;
|
||||
|
||||
if output.status.success().not() {
|
||||
bail!("Failed to execute `{}` to retrieve secret", self.command);
|
||||
}
|
||||
|
||||
String::from_utf8(output.stdout)
|
||||
.map(SecretValue::from)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| "Failed to read output")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SecretExecOutput {
|
||||
#[serde(rename = "env")]
|
||||
Env { name: String },
|
||||
#[serde(rename = "file")]
|
||||
File { destination: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SecretValue(Zeroizing<String>);
|
||||
|
||||
macro_rules! impl_secret_value {
|
||||
($($type:ty),*) => {
|
||||
$(
|
||||
impl From<$type> for SecretValue {
|
||||
fn from(value: $type) -> Self {
|
||||
Self(String::from(value.trim()).into())
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_secret_value!(String, &String, &str);
|
||||
|
||||
impl SecretValue {
|
||||
/// Get the value of the secret.
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SecretValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[REDACTED]")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecretValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[REDACTED]")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue