Make some more changes to machine setup
Some checks failed
/ rust-packages (push) Successful in 1m22s
/ terraform-providers (push) Successful in 3m22s
/ check (push) Failing after 39s
/ dev-shell (push) Successful in 1m10s

Work being done as an attempt to be able to
create a small monitoring server
This commit is contained in:
Kaare Hoff Skovgaard 2025-07-09 15:12:11 +02:00
parent 89d410cb6c
commit f7d4bef46c
Signed by: khs
GPG key ID: C7D890804F01E9F0
17 changed files with 449 additions and 289 deletions

View file

@ -25,4 +25,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: | - run: |
nix build --no-link '.#packages.x86_64-linux.terraform-provider-cloudflare'
nix build --no-link '.#packages.x86_64-linux.terraform-provider-hcloud'
nix build --no-link '.#packages.x86_64-linux.terraform-provider-openstack'
nix build --no-link '.#packages.x86_64-linux.terraform-provider-unifi' nix build --no-link '.#packages.x86_64-linux.terraform-provider-unifi'

View file

@ -58,13 +58,6 @@
outputs = outputs =
inputs@{ self, ... }: inputs@{ self, ... }:
let let
dirsInPath =
path:
let
files = builtins.readDir path;
dirs = inputs.nixpkgs.lib.filterAttrs (name: kind: kind == "directory") files;
in
builtins.attrNames dirs;
inputNixosModules = [ inputNixosModules = [
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.stylix.nixosModules.stylix inputs.stylix.nixosModules.stylix

View file

@ -1,7 +1,6 @@
{ {
lib, lib,
config, config,
pkgs,
... ...
}: }:
let let

View file

@ -1 +0,0 @@
{ ... }: { }

View file

@ -1 +0,0 @@
{ ... }: { }

View file

@ -21,6 +21,8 @@ in
{ {
networking.hostName = lib.mkForce hostname; networking.hostName = lib.mkForce hostname;
networking.domain = lib.mkForce domain; networking.domain = lib.mkForce domain;
# Add the name of the server to the ssh host certificate domains, but let other configs enable getting the host certificates.
khscodes.services.openssh.hostCertificate.hostNames = [ cfg ];
boot.kernel.sysctl = { boot.kernel.sysctl = {
"kernel.hostname" = cfg; "kernel.hostname" = cfg;
}; };

View file

@ -1 +0,0 @@
{ pkgs, ... }: { }

View file

@ -1 +0,0 @@
{ ... }: { }

View file

@ -1,8 +0,0 @@
{ ... }:
{ }
# let
# modules = lib.khscodes.dirsInPath ./.;
# in
# {
# imports = lib.lists.map (d: import d args) modules;
# }

View file

@ -5,9 +5,27 @@ in
{ {
options.khscodes.services.openssh = { options.khscodes.services.openssh = {
enable = lib.mkEnableOption "Enables openssh service for the instance"; enable = lib.mkEnableOption "Enables openssh service for the instance";
hostCertificate = {
enable = lib.mkEnableOption "Enables getting host certificates from OpenBAO";
secretName = lib.mkOption {
type = lib.types.str;
description = "Secret where the certificate is stored";
example = "ssh-host/sign/ca-kaareskovgaard.net";
};
hostNames = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "The list of host names to get certificates for";
default = [ ];
};
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable (
let
certificateNames = lib.lists.unique cfg.hostCertificate.hostNames;
hostCertificatEnable = cfg.hostCertificate.enable && cfg.hostCertificate.hostNames != [ ];
in
{
services.openssh = { services.openssh = {
enable = true; enable = true;
settings = { settings = {
@ -15,6 +33,27 @@ in
PermitRootLogin = "no"; PermitRootLogin = "no";
KbdInteractiveAuthentication = false; KbdInteractiveAuthentication = false;
}; };
extraConfig = lib.mkIf hostCertificatEnable ''
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
'';
}; };
khscodes.services.vault-agent = lib.mkIf hostCertificatEnable {
enable = true;
templates = [
{
contents = ''
{{- $public_key := file "/etc/ssh/ssh_host_ed25519_key.pub" -}}
{{- $public_key = printf "public_key=%s" $public_key -}}
{{- with secret "${cfg.hostCertificate.secretName}" "cert_type=host" $public_key "valid_principals=${lib.strings.concatStringsSep "," certificateNames}" -}}
{{ .Data.signed_key }}
{{- end -}}
'';
destination = "/etc/ssh/ssh_host_ed25519_key-cert.pub";
perms = "0644";
restartUnits = [ "sshd.service" ];
}
];
}; };
}
);
} }

View file

@ -0,0 +1,187 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.input-output.openbao.agent;
secretIdFilePath = "/var/lib/vault-agent/secret-id";
roleIdFilePath = "/var/lib/vault-agent/role-id";
mkSubmodule =
{
options,
description,
}:
lib.types.submoduleWith {
description = description;
shorthandOnlyDefinesConfig = true;
modules = lib.toList { inherit options; };
};
restartUnits =
svcs:
lib.strings.concatStringsSep "\n" (
lib.lists.map (svc: "systemctl restart ${lib.escapeShellArg svc}") svcs
);
reloadOrRestartUnits =
svcs:
lib.strings.concatStringsSep "\n" (
lib.lists.map (svc: "systemctl reload-or-restart ${lib.escapeShellArg svc}") svcs
);
mapTemplate =
template:
let
command = lib.getExe (
pkgs.writeShellApplication {
name = "restart-command";
runtimeInputs = [ pkgs.systemd ];
text = ''
${restartUnits template.restartUnits}
${reloadOrRestartUnits template.reloadOrRestartUnits}
${template.exec}
'';
meta = {
mainProgram = "restart-command";
};
}
);
in
{
inherit (template) destination perms contents;
exec = {
command = command;
};
};
settings = {
vault = {
address = cfg.vault.address;
};
auto_auth = {
method = [
{
type = "approle";
config = {
mount_path = "auth/approle";
role_id_file_path = roleIdFilePath;
secret_id_file_path = secretIdFilePath;
remove_secret_id_file_after_reading = false;
};
}
];
};
template_config = {
exit_on_retry_failure = true;
static_secret_render_interval = "60m";
max_connections_per_host = 10;
leases_renewal_threshold = 0.5;
};
template = lib.mkIf (cfg.templates != [ ]) (lib.lists.map mapTemplate cfg.templates);
};
unitsDependsOnAgent = lib.lists.unique (
lib.lists.flatten (lib.lists.map (t: t.restartUnits ++ t.reloadOrRestartUnits) cfg.templates)
);
in
{
options.khscodes.services.vault-agent = {
enable = lib.mkEnableOption "Enables the OpenBAO agent";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.openbao;
defaultText = "pkgs.openbao";
};
vault.address = lib.mkOption {
type = lib.types.str;
description = "Address of the Vault/OpenBAO service";
default = "https://vault.kaareskovgaard.net";
};
templates = lib.mkOption {
default = [ ];
type = lib.types.listOf (mkSubmodule {
description = "List of templates to render";
options = {
contents = lib.mkOption {
type = lib.types.str;
description = "Contents of the template (.ctmpl)";
};
destination = lib.mkOption {
type = lib.types.str;
description = "Destination file for the template";
};
restartUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of systemd units to restart when template changes";
default = [ ];
};
reloadOrRestartUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of systemd units to reload-or-restart when template changes";
default = [ ];
};
perms = lib.mkOption {
type = lib.types.str;
description = "Permissions of the generated file, by default will only be readable by root";
default = "0600";
};
exec = lib.mkOption {
type = lib.types.lines;
default = '''';
description = "Command to execute when template renders new data";
};
};
});
};
};
config = lib.mkIf cfg.enable {
services.vault-agent.instances.openbao = {
inherit settings;
enable = true;
package = cfg.package;
user = "root";
group = "root";
};
systemd.services."vault-agent-openbao" = {
before = unitsDependsOnAgent;
wantedBy = unitsDependsOnAgent;
unitConfig = {
ConditionPathExists = [
secretIdFilePath
roleIdFilePath
];
};
};
environment.systemPackages = [
(pkgs.writeShellApplication {
name = "vault-agent-load-credentials";
meta = {
mainProgram = "vault-agent-load-credentials";
};
runtimeInputs = [
pkgs.systemd
pkgs.openbao
];
text = ''
if [[ -z "''${1:-}" || -z "''${2:-}" ]]; then
>&2 echo "Usage: vault-agent-load-credentials <role-id> <secret-id-wrapped>"
exit 1
fi
role_id="$1"
secret_id_wrapped="$2"
secret_id="$(BAO_ADDR=${lib.escapeShellArg cfg.vault.address} bao unwrap -field=secret_id "$secret_id_wrapped")"
mkdir -p "$(dirname ${lib.escapeShellArg secretIdFilePath})"
mkdir -p "$(dirname ${lib.escapeShellArg roleIdFilePath})"
echo -n "$role_id" > ${lib.escapeShellArg roleIdFilePath}
echo -n "$secret_id" > ${lib.escapeShellArg secretIdFilePath}
chown root:root "$${lib.escapeShellArg secretIdFilePath}"
chmod 0600 "$${lib.escapeShellArg secretIdFilePath}"
chown root:root "$${lib.escapeShellArg roleIdFilePath}"
chmod 0600 "$${lib.escapeShellArg roleIdFilePath}"
systemctl restart vault-agent-openbao.service
${restartUnits unitsDependsOnAgent}
${reloadOrRestartUnits unitsDependsOnAgent}
'';
})
];
};
}

View file

@ -1 +0,0 @@
{ ... }: { }

View file

@ -1 +0,0 @@
{ ... }: { }

View file

@ -21,4 +21,11 @@
UNIFI_PASSWORD = "Terraform password"; UNIFI_PASSWORD = "Terraform password";
UNIFI_API = "Terraform URL"; UNIFI_API = "Terraform URL";
}; };
"auth.kaareskovgaard.net" = {
"AUTHENTIK_TOKEN" = "Admin API Token";
"TF_VAR_authentik_username" = "login.username";
};
"vault.kaareskovgaard.net" = {
"VAULT_TOKEN" = "Initial root token";
};
} }

View file

@ -15,7 +15,7 @@ pkgs.writeShellApplication {
text = '' text = ''
hostname="$1" hostname="$1"
cmd="''${2:-apply}" cmd="''${2:-apply}"
baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.infrastructue.provisioning' baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.infrastructure.provisioning'
config="$(nix build --no-link --print-out-paths "''${baseAttr}.preConfig")" config="$(nix build --no-link --print-out-paths "''${baseAttr}.preConfig")"
secretsSource="$(nix eval --raw "''${baseAttr}.pre.secretsSource")" secretsSource="$(nix eval --raw "''${baseAttr}.pre.secretsSource")"
endpoints="$(nix eval --json "''${baseAttr}.pre.endpoints")" endpoints="$(nix eval --json "''${baseAttr}.pre.endpoints")"

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 anyhow::Context as _;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use serde::Deserialize;
mod enventry;
use enventry::*;
fn main() { fn main() {
common::entrypoint(program); common::entrypoint(program);
@ -45,6 +48,10 @@ pub enum Endpoint {
Hcloud, Hcloud,
#[value(name = "unifi")] #[value(name = "unifi")]
Unifi, Unifi,
#[value(name = "vault")]
Vault,
#[value(name = "authentik")]
Authentik,
} }
impl Endpoint { impl Endpoint {
@ -52,23 +59,31 @@ impl Endpoint {
match self { match self {
Self::Openstack => { Self::Openstack => {
let data = OpenstackData::read_from_bao()?; let data = OpenstackData::read_from_bao()?;
Ok(data.into_env_data()) Ok(data.into())
} }
Self::Aws => { Self::Aws => {
let data = AwsData::read_from_bao()?; let data = AwsData::read_from_bao()?;
Ok(data.into_env_data()) Ok(data.into())
} }
Self::Hcloud => { Self::Hcloud => {
let data = HcloudData::read_from_bao()?; let data = HcloudData::read_from_bao()?;
Ok(data.into_env_data()) Ok(data.into())
} }
Self::Cloudflare => { Self::Cloudflare => {
let data = CloudflareData::read_from_bao()?; let data = CloudflareData::read_from_bao()?;
Ok(data.into_env_data()) Ok(data.into())
} }
Self::Unifi => { Self::Unifi => {
let data = UnifiData::read_from_bao()?; 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)] macro_rules! entry_definition {
struct OpenBaoKvEntry<T> { ($config_id:ident, $id: ident, $bao_key: expr, $secrets: expr) => {
data: OpenBaoKvEntryData<T>, struct $config_id;
}
#[derive(Debug, Deserialize)] impl EnvEntryConfig for $config_id {
struct OpenBaoKvEntryData<T> { const SECRETS: &'static [&'static str] = $secrets;
data: T, const BAO_KEY: &'static str = $bao_key;
}
#[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> { type $id = EnvEntry<$config_id>;
let data = read_bao_data("openstack")?; };
Ok(data) }
}
pub fn into_env_data(self) -> Vec<(&'static str, String)> { entry_definition!(
vec![ OpenstackDataConfig,
("TF_VAR_openstack_username", self.username), OpenstackData,
("TF_VAR_openstack_password", self.password), "openstack",
("TF_VAR_openstack_tenant_name", self.tenant_name), &[
("TF_VAR_openstack_auth_url", self.auth_url), "TF_VAR_openstack_username",
("TF_VAR_openstack_endpoint_type", self.endpoint_type), "TF_VAR_openstack_password",
("TF_VAR_openstack_region", self.region), "TF_VAR_openstack_tenant_name",
"TF_VAR_openstack_auth_url",
"TF_VAR_openstack_endpoint_type",
"TF_VAR_openstack_region"
] ]
} );
} entry_definition!(
CloudflareDataConfig,
impl IntoIterator for OpenstackData { CloudflareData,
type Item = (&'static str, String); "cloudflare",
&["TF_VAR_cloudflare_token", "TF_VAR_cloudflare_email"]
type IntoIter = <Vec<(&'static str, String)> as IntoIterator>::IntoIter; );
entry_definition!(
fn into_iter(self) -> Self::IntoIter { AwsDataConfig,
vec![ AwsData,
("username", self.username), "aws",
("password", self.password), &["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
("tenant_name", self.tenant_name), );
("auth_url", self.auth_url), entry_definition!(
("endpoint_type", self.endpoint_type), HcloudDataConfig,
("region", self.region), HcloudData,
] "hcloud",
.into_iter() &["TF_VAR_hcloud_api_token"]
} );
} entry_definition!(
UnifiDataConfig,
#[derive(Debug, Deserialize)] UnifiData,
struct CloudflareData { "unifi",
token: String, &["UNIFI_USERNAME", "UNIFI_PASSWORD", "UNIFI_API"]
email: String, );
} entry_definition!(VaultDataConfig, VaultData, "vault", &["VAULT_TOKEN"]);
entry_definition!(
impl CloudflareData { AuthentikDataConfig,
pub fn read_from_env() -> anyhow::Result<Self> { AuthentikData,
let token = common::env::read_env("TF_VAR_cloudflare_token")?; "authentik",
let email = common::env::read_env("TF_VAR_cloudflare_email")?; &["AUTHENTIK_TOKEN", "TF_VAR_authentik_username"]
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()
}
}
fn transfer() -> anyhow::Result<()> { fn transfer() -> anyhow::Result<()> {
let openstack = OpenstackData::read_from_env()?; let openstack = OpenstackData::try_new_from_env()?;
let cloudflare = CloudflareData::read_from_env()?; let cloudflare = CloudflareData::try_new_from_env()?;
let aws = AwsData::read_from_env()?; let aws = AwsData::try_new_from_env()?;
let hcloud = HcloudData::read_from_env()?; let hcloud = HcloudData::try_new_from_env()?;
let unifi = UnifiData::read_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(openstack)?;
write_kv_data("cloudflare", cloudflare)?; write_kv_data(cloudflare)?;
write_kv_data("aws", aws)?; write_kv_data(aws)?;
write_kv_data("hcloud", hcloud)?; write_kv_data(hcloud)?;
write_kv_data("unifi", unifi)?; write_kv_data(unifi)?;
write_kv_data(authentik)?;
write_kv_data(vault)?;
Ok(()) Ok(())
} }
fn write_kv_data( fn write_kv_data<T: EnvEntryConfig>(entry: EnvEntry<T>) -> anyhow::Result<()> {
key: &str,
data: impl IntoIterator<Item = (&'static str, String)>,
) -> anyhow::Result<()> {
let mut cmd = common::proc::Command::new("bao"); let mut cmd = common::proc::Command::new("bao");
cmd.args(["kv", "put", "-mount=opentofu"]); cmd.args(["kv", "put", "-mount=opentofu"]);
cmd.arg(key); cmd.arg(T::BAO_KEY);
for (key, value) in data { for (key, value) in entry {
cmd.arg(format!("{key}={value}")); cmd.arg(format!("{key}={value}"));
} }
cmd.try_spawn_to_string()?; cmd.try_spawn_to_string()?;