Begin adding support for using opentofu through openbao secrets
This commit is contained in:
parent
8e31f30762
commit
e61b3b06f3
12 changed files with 551 additions and 39 deletions
14
rust/program/openbao-helper/Cargo.toml
Normal file
14
rust/program/openbao-helper/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "openbao-helper"
|
||||
edition = "2024"
|
||||
version = "1.0.0"
|
||||
metadata.crane.name = "openbao-helper"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
common = { path = "../../lib/common" }
|
||||
log = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
hakari = { version = "0.1", path = "../../lib/hakari" }
|
387
rust/program/openbao-helper/src/main.rs
Normal file
387
rust/program/openbao-helper/src/main.rs
Normal file
|
@ -0,0 +1,387 @@
|
|||
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<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,
|
||||
}
|
||||
|
||||
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()?;
|
||||
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<T> {
|
||||
data: OpenBaoKvEntryData<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenBaoKvEntryData<T> {
|
||||
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<T: for<'de> Deserialize<'de>>(key: &str) -> anyhow::Result<T> {
|
||||
let mut cmd = common::proc::Command::new("bao");
|
||||
cmd.args(["kv", "get", "-format=json", "-mount=opentofu", key]);
|
||||
let result: OpenBaoKvEntry<T> = cmd.try_spawn_to_json()?;
|
||||
Ok(result.data.data)
|
||||
}
|
||||
|
||||
impl OpenstackData {
|
||||
pub fn read_from_env() -> anyhow::Result<Self> {
|
||||
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<Self> {
|
||||
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 = <Vec<(&'static str, String)> 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<Self> {
|
||||
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<Self> {
|
||||
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 = <Vec<(&'static str, String)> 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<Self> {
|
||||
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<Self> {
|
||||
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 = <Vec<(&'static str, String)> 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<Self> {
|
||||
let api_token = common::env::read_env("TF_VAR_hcloud_api_token")?;
|
||||
Ok(Self { api_token })
|
||||
}
|
||||
|
||||
pub fn read_from_bao() -> anyhow::Result<Self> {
|
||||
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 = <Vec<(&'static str, String)> 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<Self> {
|
||||
let username = common::env::read_env("TF_VAR_unifi_username")?;
|
||||
let password = common::env::read_env("TF_VAR_unifi_password")?;
|
||||
let url = common::env::read_env("TF_VAR_unifi_url")?;
|
||||
Ok(Self {
|
||||
username,
|
||||
password,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_from_bao() -> anyhow::Result<Self> {
|
||||
let data = read_bao_data("unifi")?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn into_env_data(self) -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
("TF_VAR_unifi_username", self.username),
|
||||
("TF_VAR_unifi_password", self.password),
|
||||
("TF_VAR_unifi_url", self.url),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for UnifiData {
|
||||
type Item = (&'static str, String);
|
||||
|
||||
type IntoIter = <Vec<(&'static str, String)> 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<Item = (&'static str, String)>,
|
||||
) -> 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::<CString>::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(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue