Make some more changes to machine setup

Work being done as an attempt to be able to
create a small monitoring server


Former-commit-id: f7d4bef46c
This commit is contained in:
Kaare Hoff Skovgaard 2025-07-09 15:12:11 +02:00
parent 84818b01be
commit 07360c41da
17 changed files with 449 additions and 289 deletions

View file

@ -0,0 +1,108 @@
use std::{collections::BTreeMap, marker::PhantomData, vec::IntoIter};
use serde::Deserialize;
pub trait EnvEntryConfig {
const SECRETS: &'static [&'static str];
const BAO_KEY: &'static str;
}
pub struct EnvEntry<T>(Vec<(&'static str, String)>, PhantomData<T>);
impl<T: EnvEntryConfig> EnvEntry<T> {
pub fn try_new_from_env() -> anyhow::Result<Self> {
let mut result = Vec::with_capacity(T::SECRETS.len());
for key in T::SECRETS {
let value = common::env::read_env(key)?;
result.push((*key, value));
}
Ok(Self(result, PhantomData))
}
fn new_from_values(values: Vec<(&'static str, String)>) -> Self {
Self(values, PhantomData)
}
pub fn read_from_bao() -> anyhow::Result<Self> {
read_bao_data()
}
}
impl<T> From<EnvEntry<T>> for Vec<(&'static str, String)> {
fn from(value: EnvEntry<T>) -> Self {
value.0
}
}
impl<T> IntoIterator for EnvEntry<T> {
type Item = (&'static str, String);
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'de, T: EnvEntryConfig> serde::Deserialize<'de> for EnvEntry<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(EnvEntryVisitor(PhantomData))
}
}
struct EnvEntryVisitor<T>(PhantomData<T>);
impl<'de, T: EnvEntryConfig> serde::de::Visitor<'de> for EnvEntryVisitor<T> {
type Value = EnvEntry<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_fmt(format_args!(
"a map with unique keys {} with string values",
T::SECRETS.join(", "),
))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut values = BTreeMap::<&'static str, String>::new();
while let Some((key, value)) = map.next_entry::<&'de str, String>()? {
let mapped_key = T::SECRETS.iter().find(|n| **n == key).copied();
let Some(key) = mapped_key else {
return Err(serde::de::Error::unknown_field(key, T::SECRETS));
};
if values.contains_key(key) {
return Err(serde::de::Error::duplicate_field(key));
}
values.insert(key, value);
}
for key in T::SECRETS {
if !values.contains_key(key) {
return Err(serde::de::Error::missing_field(key));
}
}
let values = values.into_iter().collect();
let entry = EnvEntry::<T>::new_from_values(values);
Ok(entry)
}
}
#[derive(Debug, Deserialize)]
struct OpenBaoKvEntry<T> {
data: OpenBaoKvEntryData<T>,
}
#[derive(Debug, Deserialize)]
struct OpenBaoKvEntryData<T> {
data: T,
}
fn read_bao_data<T: EnvEntryConfig>() -> anyhow::Result<EnvEntry<T>> {
let mut cmd = common::proc::Command::new("bao");
cmd.args(["kv", "get", "-format=json", "-mount=opentofu", T::BAO_KEY]);
let result: OpenBaoKvEntry<EnvEntry<T>> = cmd.try_spawn_to_json()?;
Ok(result.data.data)
}

View file

@ -2,7 +2,10 @@ use std::{collections::BTreeSet, ffi::CString};
use anyhow::Context as _;
use clap::{Parser, Subcommand};
use serde::Deserialize;
mod enventry;
use enventry::*;
fn main() {
common::entrypoint(program);
@ -45,6 +48,10 @@ pub enum Endpoint {
Hcloud,
#[value(name = "unifi")]
Unifi,
#[value(name = "vault")]
Vault,
#[value(name = "authentik")]
Authentik,
}
impl Endpoint {
@ -52,23 +59,31 @@ impl Endpoint {
match self {
Self::Openstack => {
let data = OpenstackData::read_from_bao()?;
Ok(data.into_env_data())
Ok(data.into())
}
Self::Aws => {
let data = AwsData::read_from_bao()?;
Ok(data.into_env_data())
Ok(data.into())
}
Self::Hcloud => {
let data = HcloudData::read_from_bao()?;
Ok(data.into_env_data())
Ok(data.into())
}
Self::Cloudflare => {
let data = CloudflareData::read_from_bao()?;
Ok(data.into_env_data())
Ok(data.into())
}
Self::Unifi => {
let data = UnifiData::read_from_bao()?;
Ok(data.into_env_data())
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())
}
}
}
@ -82,267 +97,88 @@ fn program() -> anyhow::Result<()> {
}
}
#[derive(Debug, Deserialize)]
struct OpenBaoKvEntry<T> {
data: OpenBaoKvEntryData<T>,
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>;
};
}
#[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("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<Self> {
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 = <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()
}
}
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::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()?;
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", openstack)?;
write_kv_data("cloudflare", cloudflare)?;
write_kv_data("aws", aws)?;
write_kv_data("hcloud", hcloud)?;
write_kv_data("unifi", unifi)?;
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(
key: &str,
data: impl IntoIterator<Item = (&'static str, String)>,
) -> anyhow::Result<()> {
fn write_kv_data<T: EnvEntryConfig>(entry: EnvEntry<T>) -> 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(T::BAO_KEY);
for (key, value) in entry {
cmd.arg(format!("{key}={value}"));
}
cmd.try_spawn_to_string()?;