use std::{collections::BTreeSet, ffi::CString}; use anyhow::Context as _; use clap::{Parser, Subcommand}; use serde::Deserialize; 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, } impl Endpoint { pub fn try_into_env_data(self) -> anyhow::Result> { match self { Self::Openstack => { let data = OpenstackData::read_from_bao()?; Ok(data.into_env_data()) } Self::Aws => { let data = AwsData::read_from_bao()?; Ok(data.into_env_data()) } Self::Hcloud => { let data = HcloudData::read_from_bao()?; Ok(data.into_env_data()) } Self::Cloudflare => { let data = CloudflareData::read_from_bao()?; Ok(data.into_env_data()) } Self::Unifi => { let data = UnifiData::read_from_bao()?; Ok(data.into_env_data()) } } } } fn program() -> anyhow::Result<()> { let args = Args::parse(); match args.command { Commands::Transfer => transfer(), Commands::WrapProgram(w) => wrap_program(w), } } #[derive(Debug, Deserialize)] struct OpenBaoKvEntry { data: OpenBaoKvEntryData, } #[derive(Debug, Deserialize)] struct OpenBaoKvEntryData { data: T, } #[derive(Debug, Deserialize)] struct OpenstackData { username: String, password: String, tenant_name: String, auth_url: String, endpoint_type: String, region: String, } fn read_bao_data Deserialize<'de>>(key: &str) -> anyhow::Result { let mut cmd = common::proc::Command::new("bao"); cmd.args(["kv", "get", "-format=json", "-mount=opentofu", key]); let result: OpenBaoKvEntry = cmd.try_spawn_to_json()?; Ok(result.data.data) } impl OpenstackData { pub fn read_from_env() -> anyhow::Result { let username = common::env::read_env("TF_VAR_openstack_username")?; let password = common::env::read_env("TF_VAR_openstack_password")?; let tenant_name = common::env::read_env("TF_VAR_openstack_tenant_name")?; let auth_url = common::env::read_env("TF_VAR_openstack_auth_url")?; let endpoint_type = common::env::read_env("TF_VAR_openstack_endpoint_type")?; let region = common::env::read_env("TF_VAR_openstack_region")?; Ok(Self { username, password, tenant_name, auth_url, endpoint_type, region, }) } pub fn read_from_bao() -> anyhow::Result { let data = read_bao_data("openstack")?; Ok(data) } pub fn into_env_data(self) -> Vec<(&'static str, String)> { vec![ ("TF_VAR_openstack_username", self.username), ("TF_VAR_openstack_password", self.password), ("TF_VAR_openstack_tenant_name", self.tenant_name), ("TF_VAR_openstack_auth_url", self.auth_url), ("TF_VAR_openstack_endpoint_type", self.endpoint_type), ("TF_VAR_openstack_region", self.region), ] } } impl IntoIterator for OpenstackData { type Item = (&'static str, String); type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![ ("username", self.username), ("password", self.password), ("tenant_name", self.tenant_name), ("auth_url", self.auth_url), ("endpoint_type", self.endpoint_type), ("region", self.region), ] .into_iter() } } #[derive(Debug, Deserialize)] struct CloudflareData { token: String, email: String, } impl CloudflareData { pub fn read_from_env() -> anyhow::Result { let token = common::env::read_env("TF_VAR_cloudflare_token")?; let email = common::env::read_env("TF_VAR_cloudflare_email")?; Ok(Self { token, email }) } pub fn read_from_bao() -> anyhow::Result { let data = read_bao_data("cloudflare")?; Ok(data) } pub fn into_env_data(self) -> Vec<(&'static str, String)> { vec![ ("TF_VAR_cloudflare_token", self.token), ("TF_VAR_cloudflare_email", self.email), ] } } impl IntoIterator for CloudflareData { type Item = (&'static str, String); type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![("token", self.token), ("email", self.email)].into_iter() } } #[derive(Debug, Deserialize)] struct AwsData { key_id: String, secret_access_key: String, } impl AwsData { pub fn read_from_env() -> anyhow::Result { let key_id = common::env::read_env("AWS_ACCESS_KEY_ID")?; let secret_access_key = common::env::read_env("AWS_SECRET_ACCESS_KEY")?; Ok(Self { key_id, secret_access_key, }) } pub fn read_from_bao() -> anyhow::Result { let data = read_bao_data("aws")?; Ok(data) } pub fn into_env_data(self) -> Vec<(&'static str, String)> { vec![ ("AWS_ACCESS_KEY_ID", self.key_id), ("AWS_SECRET_ACCESS_KEY", self.secret_access_key), ] } } impl IntoIterator for AwsData { type Item = (&'static str, String); type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![ ("key_id", self.key_id), ("secret_access_key", self.secret_access_key), ] .into_iter() } } #[derive(Debug, Deserialize)] struct HcloudData { api_token: String, } impl HcloudData { pub fn read_from_env() -> anyhow::Result { let api_token = common::env::read_env("TF_VAR_hcloud_api_token")?; Ok(Self { api_token }) } pub fn read_from_bao() -> anyhow::Result { let data = read_bao_data("hcloud")?; Ok(data) } pub fn into_env_data(self) -> Vec<(&'static str, String)> { vec![("TF_VAR_hcloud_api_token", self.api_token)] } } impl IntoIterator for HcloudData { type Item = (&'static str, String); type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![("api_token", self.api_token)].into_iter() } } #[derive(Debug, Deserialize)] struct UnifiData { username: String, password: String, url: String, } impl UnifiData { pub fn read_from_env() -> anyhow::Result { let username = common::env::read_env("UNIFI_USERNAME")?; let password = common::env::read_env("UNIFI_PASSWORD")?; let url = common::env::read_env("UNIFI_API")?; Ok(Self { username, password, url, }) } pub fn read_from_bao() -> anyhow::Result { let data = read_bao_data("unifi")?; Ok(data) } pub fn into_env_data(self) -> Vec<(&'static str, String)> { vec![ ("UNIFI_USERNAME", self.username), ("UNIFI_PASSWORD", self.password), ("UNIFI_API", self.url), ] } } impl IntoIterator for UnifiData { type Item = (&'static str, String); type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![ ("username", self.username), ("password", self.password), ("url", self.url), ] .into_iter() } } fn transfer() -> anyhow::Result<()> { let openstack = OpenstackData::read_from_env()?; let cloudflare = CloudflareData::read_from_env()?; let aws = AwsData::read_from_env()?; let hcloud = HcloudData::read_from_env()?; let unifi = UnifiData::read_from_env()?; write_kv_data("openstack", openstack)?; write_kv_data("cloudflare", cloudflare)?; write_kv_data("aws", aws)?; write_kv_data("hcloud", hcloud)?; write_kv_data("unifi", unifi)?; Ok(()) } fn write_kv_data( key: &str, data: impl IntoIterator, ) -> anyhow::Result<()> { let mut cmd = common::proc::Command::new("bao"); cmd.args(["kv", "put", "-mount=opentofu"]); cmd.arg(key); for (key, value) in data { 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::::new(); for (key, value) in std::env::vars() { env.push( CString::new(format!("{key}={value}")) .with_context(|| format!("Environment variable {key} contained a null byte"))?, ); } for env_set in unique { for (key, value) in env_set.try_into_env_data()? { env.push(CString::new(format!("{key}={value}")).with_context(|| { format!("Environment variable {key} contained a null byte") })?); } } 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) }; nix::unistd::execvpe(&args[0], args.as_slice(), env.as_slice())?; // This will never get executed Ok(()) }