use std::{ collections::BTreeSet, convert::Infallible, ffi::{CStr, CString, OsStr, OsString}, }; use anyhow::Context as _; use clap::{Parser, Subcommand}; mod enventry; use enventry::*; fn main() { common::entrypoint(program); } #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Args { #[command(subcommand)] pub command: Commands, } #[derive(Debug, Subcommand)] pub enum Commands { /// Transfers secrets from the current environment into their respective secrets in openbao Transfer, /// Wraps the program by reading the specified data from bao and setting respective environment variables WrapProgram(WrapProgram), } #[derive(Debug, Clone, clap::Args)] pub struct WrapProgram { /// The endpoints to fetch, #[arg(short = 'e', long = "endpoint", number_of_values = 1)] pub endpoint: Vec, /// Command to wrap #[arg(allow_hyphen_values = true, last = true)] pub cmd: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] pub enum Endpoint { #[value(name = "openstack")] Openstack, #[value(name = "cloudflare")] Cloudflare, #[value(name = "aws")] Aws, #[value(name = "hcloud")] Hcloud, #[value(name = "unifi")] Unifi, #[value(name = "vault")] Vault, #[value(name = "authentik")] Authentik, } impl Endpoint { pub fn try_into_env_data(self) -> anyhow::Result> { match self { Self::Openstack => { let data = OpenstackData::read_from_bao()?; Ok(data.into()) } Self::Aws => { let data = AwsData::read_from_bao()?; Ok(data.into()) } Self::Hcloud => { let data = HcloudData::read_from_bao()?; Ok(data.into()) } Self::Cloudflare => { let data = CloudflareData::read_from_bao()?; Ok(data.into()) } Self::Unifi => { let data = UnifiData::read_from_bao()?; Ok(data.into()) } Self::Authentik => { let data = AuthentikData::read_from_bao()?; Ok(data.into()) } Self::Vault => { let data = VaultData::read_from_bao()?; Ok(data.into()) } } } } fn program() -> anyhow::Result<()> { let args = Args::parse(); match args.command { Commands::Transfer => transfer(), Commands::WrapProgram(w) => wrap_program(w), } } macro_rules! entry_definition { ($config_id:ident, $id: ident, $bao_key: expr, $secrets: expr) => { struct $config_id; impl EnvEntryConfig for $config_id { const SECRETS: &'static [&'static str] = $secrets; const BAO_KEY: &'static str = $bao_key; } type $id = EnvEntry<$config_id>; }; } entry_definition!( OpenstackDataConfig, OpenstackData, "openstack", &[ "TF_VAR_openstack_username", "TF_VAR_openstack_password", "TF_VAR_openstack_tenant_name", "TF_VAR_openstack_auth_url", "TF_VAR_openstack_endpoint_type", "TF_VAR_openstack_region" ] ); entry_definition!( CloudflareDataConfig, CloudflareData, "cloudflare", &["TF_VAR_cloudflare_token", "TF_VAR_cloudflare_email"] ); entry_definition!( AwsDataConfig, AwsData, "aws", &["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] ); entry_definition!( HcloudDataConfig, HcloudData, "hcloud", &["TF_VAR_hcloud_api_token"] ); entry_definition!( UnifiDataConfig, UnifiData, "unifi", &["UNIFI_USERNAME", "UNIFI_PASSWORD", "UNIFI_API"] ); entry_definition!(VaultDataConfig, VaultData, "vault", &["VAULT_TOKEN"]); entry_definition!( AuthentikDataConfig, AuthentikData, "authentik", &["AUTHENTIK_TOKEN", "TF_VAR_authentik_username"] ); fn transfer() -> anyhow::Result<()> { let openstack = OpenstackData::try_new_from_env()?; let cloudflare = CloudflareData::try_new_from_env()?; let aws = AwsData::try_new_from_env()?; let hcloud = HcloudData::try_new_from_env()?; let unifi = UnifiData::try_new_from_env()?; let authentik = AuthentikData::try_new_from_env()?; let vault = VaultData::try_new_from_env()?; write_kv_data(openstack)?; write_kv_data(cloudflare)?; write_kv_data(aws)?; write_kv_data(hcloud)?; write_kv_data(unifi)?; write_kv_data(authentik)?; write_kv_data(vault)?; Ok(()) } fn write_kv_data(entry: EnvEntry) -> anyhow::Result<()> { let mut cmd = common::proc::Command::new("bao"); cmd.args(["kv", "put", "-mount=opentofu"]); cmd.arg(T::BAO_KEY); for (key, value) in entry { cmd.arg(format!("{key}={value}")); } cmd.try_spawn_to_string()?; Ok(()) } fn wrap_program(wrap_program: WrapProgram) -> anyhow::Result<()> { let (args, env) = { let WrapProgram { cmd, endpoint } = wrap_program; if endpoint.is_empty() { return Err(anyhow::format_err!("Must specify at least one endpoint")); } if cmd.is_empty() { return Err(anyhow::format_err!("No command to execute was specified")); } let unique: BTreeSet<_> = BTreeSet::from_iter(endpoint); let mut env = Vec::<(OsString, OsString)>::new(); for (key, value) in std::env::vars() { env.push((OsString::from(key), OsString::from(value))); } for env_set in unique { for (key, value) in env_set.try_into_env_data()? { env.push((OsString::from(key), OsString::from(value))); } } let mut args = Vec::new(); for arg in cmd { let arg = CString::new(arg) .context("Argument to program to wrap cannot contain null bytes")?; args.push(arg); } (args, env) }; unsafe { execvpe(&args[0], args.as_slice(), env.as_slice())?; } // This will never get executed Ok(()) } #[cfg(not(target_os = "macos"))] /// Safety: No other threads may read or write environment variables when this function is called. /// The easiest way to ensure this is using a single threaded program. /// Note: On Linux specifically this safety requirement is not needed unsafe fn execvpe, SEK: AsRef, SEV: AsRef>( filename: &CStr, args: &[SA], environ: &[(SEK, SEV)], ) -> anyhow::Result { let environ: Vec<_> = environ .iter() .map(|(k, v)| { CString::new(Format!("{k}={v}")) .with_context(|| format!("Environment variable {k} contains null bytes"))? }) .collect(); Ok(nix::unistd::execvpe(filename, args, &environ)?) } #[cfg(target_os = "macos")] /// Safety: No other threads may read or write environment variables when this function is called. /// The easiest way to ensure this is using a single threaded program. // Simple "bad" version of execvpe that also works on OSX unsafe fn execvpe, SEK: AsRef, SEV: AsRef>( filename: &CStr, args: &[SA], environ: &[(SEK, SEV)], ) -> anyhow::Result { let current_env = std::env::vars_os(); // Safety: Same as this function unsafe { nix::env::clearenv()? }; for (key, val) in environ { // Safety: Same as this function unsafe { std::env::set_var(key.as_ref(), val.as_ref()) }; } match nix::unistd::execvp(filename, args) { Err(err) => { unsafe { nix::env::clearenv()? }; for (key, val) in current_env { unsafe { std::env::set_var(key.as_os_str(), val.as_os_str()) }; } Err(err.into()) } _ => unreachable!("execvp doesn't return on success"), } }