2025-07-10 20:57:01 +02:00
|
|
|
use std::{
|
|
|
|
collections::BTreeSet,
|
|
|
|
convert::Infallible,
|
|
|
|
ffi::{CStr, CString, OsStr, OsString},
|
|
|
|
};
|
2025-07-07 23:10:53 +02:00
|
|
|
|
|
|
|
use anyhow::Context as _;
|
|
|
|
use clap::{Parser, Subcommand};
|
2025-07-09 15:12:11 +02:00
|
|
|
|
|
|
|
mod enventry;
|
|
|
|
|
|
|
|
use enventry::*;
|
2025-07-07 23:10:53 +02:00
|
|
|
|
|
|
|
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<Endpoint>,
|
|
|
|
/// Command to wrap
|
|
|
|
#[arg(allow_hyphen_values = true, last = true)]
|
|
|
|
pub cmd: Vec<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
2025-07-09 15:12:11 +02:00
|
|
|
#[value(name = "vault")]
|
|
|
|
Vault,
|
|
|
|
#[value(name = "authentik")]
|
|
|
|
Authentik,
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Endpoint {
|
|
|
|
pub fn try_into_env_data(self) -> anyhow::Result<Vec<(&'static str, String)>> {
|
|
|
|
match self {
|
|
|
|
Self::Openstack => {
|
|
|
|
let data = OpenstackData::read_from_bao()?;
|
2025-07-09 15:12:11 +02:00
|
|
|
Ok(data.into())
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
Self::Aws => {
|
|
|
|
let data = AwsData::read_from_bao()?;
|
2025-07-09 15:12:11 +02:00
|
|
|
Ok(data.into())
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
Self::Hcloud => {
|
|
|
|
let data = HcloudData::read_from_bao()?;
|
2025-07-09 15:12:11 +02:00
|
|
|
Ok(data.into())
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
Self::Cloudflare => {
|
|
|
|
let data = CloudflareData::read_from_bao()?;
|
2025-07-09 15:12:11 +02:00
|
|
|
Ok(data.into())
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
Self::Unifi => {
|
|
|
|
let data = UnifiData::read_from_bao()?;
|
2025-07-09 15:12:11 +02:00
|
|
|
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())
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn program() -> anyhow::Result<()> {
|
|
|
|
let args = Args::parse();
|
|
|
|
match args.command {
|
|
|
|
Commands::Transfer => transfer(),
|
|
|
|
Commands::WrapProgram(w) => wrap_program(w),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-09 15:12:11 +02:00
|
|
|
macro_rules! entry_definition {
|
|
|
|
($config_id:ident, $id: ident, $bao_key: expr, $secrets: expr) => {
|
|
|
|
struct $config_id;
|
2025-07-07 23:10:53 +02:00
|
|
|
|
2025-07-09 15:12:11 +02:00
|
|
|
impl EnvEntryConfig for $config_id {
|
|
|
|
const SECRETS: &'static [&'static str] = $secrets;
|
|
|
|
const BAO_KEY: &'static str = $bao_key;
|
|
|
|
}
|
2025-07-07 23:10:53 +02:00
|
|
|
|
2025-07-09 15:12:11 +02:00
|
|
|
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"]
|
|
|
|
);
|
2025-07-07 23:10:53 +02:00
|
|
|
|
|
|
|
fn transfer() -> anyhow::Result<()> {
|
2025-07-09 15:12:11 +02:00
|
|
|
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)?;
|
2025-07-07 23:10:53 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2025-07-09 15:12:11 +02:00
|
|
|
fn write_kv_data<T: EnvEntryConfig>(entry: EnvEntry<T>) -> anyhow::Result<()> {
|
2025-07-07 23:10:53 +02:00
|
|
|
let mut cmd = common::proc::Command::new("bao");
|
|
|
|
cmd.args(["kv", "put", "-mount=opentofu"]);
|
2025-07-09 15:12:11 +02:00
|
|
|
cmd.arg(T::BAO_KEY);
|
|
|
|
for (key, value) in entry {
|
2025-07-07 23:10:53 +02:00
|
|
|
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);
|
2025-07-10 20:57:01 +02:00
|
|
|
let mut env = Vec::<(OsString, OsString)>::new();
|
2025-07-07 23:10:53 +02:00
|
|
|
for (key, value) in std::env::vars() {
|
2025-07-10 20:57:01 +02:00
|
|
|
env.push((OsString::from(key), OsString::from(value)));
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
for env_set in unique {
|
|
|
|
for (key, value) in env_set.try_into_env_data()? {
|
2025-07-10 20:57:01 +02:00
|
|
|
env.push((OsString::from(key), OsString::from(value)));
|
2025-07-07 23:10:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
};
|
2025-07-10 20:57:01 +02:00
|
|
|
unsafe {
|
|
|
|
execvpe(&args[0], args.as_slice(), env.as_slice())?;
|
|
|
|
}
|
2025-07-07 23:10:53 +02:00
|
|
|
// This will never get executed
|
|
|
|
Ok(())
|
|
|
|
}
|
2025-07-10 20:57:01 +02:00
|
|
|
|
|
|
|
#[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<SA: AsRef<CStr>, SEK: AsRef<OsStr>, SEV: AsRef<OsStr>>(
|
|
|
|
filename: &CStr,
|
|
|
|
args: &[SA],
|
|
|
|
environ: &[(SEK, SEV)],
|
|
|
|
) -> anyhow::Result<Infallible> {
|
|
|
|
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<SA: AsRef<CStr>, SEK: AsRef<OsStr>, SEV: AsRef<OsStr>>(
|
|
|
|
filename: &CStr,
|
|
|
|
args: &[SA],
|
|
|
|
environ: &[(SEK, SEV)],
|
|
|
|
) -> anyhow::Result<Infallible> {
|
|
|
|
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"),
|
|
|
|
}
|
|
|
|
}
|