use super::crypto::secret_dir;
use super::peer_db::{load_peers, save_peers};
use super::save_csk;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use ed25519_dalek::SigningKey;
use eyre::{Report, eyre};
use std::fs;
use std::path::PathBuf;
use toml;
use volli_core::{ManagerPeerEntry, profile as core_profile};
type CertData = (Vec<u8>, Vec<u8>, bool, Option<PathBuf>, Option<PathBuf>);
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
struct ManagerProfile {
#[serde(default)]
name: Option<String>,
#[serde(default)]
host: Option<String>,
#[serde(default)]
bind_host: Option<String>,
#[serde(default)]
tcp_port: Option<u16>,
#[serde(default)]
quic_port: Option<u16>,
#[serde(default)]
max_workers: Option<u32>,
#[serde(default)]
worker_whitelist: Option<Vec<String>>,
#[serde(default)]
manager_whitelist: Option<Vec<String>>,
}
fn load_profile(profile: &str) -> Result<ManagerProfile, Report> {
Ok(core_profile::load_profile("manager", profile)?.unwrap_or_default())
}
fn save_profile(profile: &str, data: &ManagerProfile) -> Result<(), Report> {
core_profile::save_profile("manager", profile, data)
}
pub fn save_profile_host(profile: &str, host: &str) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.host = Some(host.to_string());
save_profile(profile, &cfg)
}
pub fn load_profile_host(profile: &str) -> Result<Option<String>, Report> {
Ok(load_profile(profile)?.host)
}
pub fn save_bind_host(profile: &str, host: &str) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.bind_host = Some(host.to_string());
save_profile(profile, &cfg)
}
pub fn load_bind_host(profile: &str) -> Result<Option<String>, Report> {
Ok(load_profile(profile)?.bind_host)
}
pub fn save_tcp_port(profile: &str, port: u16) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.tcp_port = Some(port);
save_profile(profile, &cfg)
}
pub fn load_tcp_port(profile: &str) -> Result<Option<u16>, Report> {
Ok(load_profile(profile)?.tcp_port)
}
pub fn save_quic_port(profile: &str, port: u16) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.quic_port = Some(port);
save_profile(profile, &cfg)
}
pub fn load_quic_port(profile: &str) -> Result<Option<u16>, Report> {
Ok(load_profile(profile)?.quic_port)
}
pub fn save_worker_whitelist(profile: &str, addrs: &[String]) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.worker_whitelist = Some(addrs.to_vec());
save_profile(profile, &cfg)
}
pub fn load_worker_whitelist(profile: &str) -> Result<Option<Vec<String>>, Report> {
Ok(load_profile(profile)?.worker_whitelist)
}
pub fn save_max_workers(profile: &str, max_workers: u32) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.max_workers = Some(max_workers);
save_profile(profile, &cfg)
}
pub fn load_max_workers(profile: &str) -> Result<Option<u32>, Report> {
Ok(load_profile(profile)?.max_workers)
}
pub fn save_manager_whitelist(profile: &str, list: &[String]) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.manager_whitelist = Some(list.to_vec());
save_profile(profile, &cfg)
}
pub fn load_manager_whitelist(profile: &str) -> Result<Option<Vec<String>>, Report> {
Ok(load_profile(profile)?.manager_whitelist)
}
pub fn load_manager_name(profile: &str) -> Result<Option<String>, Report> {
let cfg = load_profile(profile)?;
Ok(cfg.name)
}
pub fn save_manager_name(profile: &str, name: &str) -> Result<(), Report> {
let mut cfg = load_profile(profile)?;
cfg.name = Some(name.to_string());
save_profile(profile, &cfg)
}
pub struct ManagerProfileBuilder {
profile_name: String,
config: ManagerProfile,
signing_key_data: Option<(SigningKey, bool, Option<PathBuf>, Option<PathBuf>)>,
cert_data: Option<CertData>,
csk_data: Option<([u8; 32], u32, bool)>,
secret_dir_override: Option<PathBuf>,
}
impl ManagerProfileBuilder {
pub fn new(profile: &str) -> Result<Self, Report> {
let config = load_profile(profile)?;
Ok(Self {
profile_name: profile.to_string(),
config,
signing_key_data: None,
cert_data: None,
csk_data: None,
secret_dir_override: None,
})
}
pub fn name(mut self, name: &str) -> Self {
self.config.name = Some(name.to_string());
self
}
pub fn host(mut self, host: &str) -> Self {
self.config.host = Some(host.to_string());
self
}
pub fn bind_host(mut self, bind_host: &str) -> Self {
self.config.bind_host = Some(bind_host.to_string());
self
}
pub fn tcp_port(mut self, port: u16) -> Self {
self.config.tcp_port = Some(port);
self
}
pub fn quic_port(mut self, port: u16) -> Self {
self.config.quic_port = Some(port);
self
}
pub fn max_workers(mut self, max: u32) -> Self {
self.config.max_workers = Some(max);
self
}
pub fn worker_whitelist(mut self, addrs: &[String]) -> Self {
self.config.worker_whitelist = Some(addrs.to_vec());
self
}
pub fn manager_whitelist(mut self, list: &[String]) -> Self {
self.config.manager_whitelist = Some(list.to_vec());
self
}
pub fn signing_key(
mut self,
signing_key: &SigningKey,
persist: bool,
sk_path: Option<PathBuf>,
pk_path: Option<PathBuf>,
) -> Self {
self.signing_key_data = Some((signing_key.clone(), persist, sk_path, pk_path));
self
}
pub fn certificate(
mut self,
cert_der: &[u8],
key_der: &[u8],
persist: bool,
cert_path: Option<PathBuf>,
key_path: Option<PathBuf>,
) -> Self {
self.cert_data = Some((
cert_der.to_vec(),
key_der.to_vec(),
persist,
cert_path,
key_path,
));
self
}
pub fn cluster_key(mut self, csk: &[u8; 32], version: u32, persist: bool) -> Self {
self.csk_data = Some((*csk, version, persist));
self
}
pub fn secret_dir(mut self, dir: PathBuf) -> Self {
self.secret_dir_override = Some(dir);
self
}
pub fn save(self) -> Result<(), Report> {
save_profile(&self.profile_name, &self.config)
}
pub fn save_all(self) -> Result<(), Report> {
tracing::debug!(
"ProfileBuilder::save_all starting for profile '{}'",
self.profile_name
);
let secret_dir = self
.secret_dir_override
.unwrap_or_else(|| secret_dir(Some(&self.profile_name)));
tracing::debug!("Using secret_dir: {}", secret_dir.display());
if let Some((csk, version, persist)) = self.csk_data
&& persist
{
tracing::debug!(
"Saving CSK for profile '{}' version {}",
self.profile_name,
version
);
save_csk(&self.profile_name, &csk, version)?;
tracing::debug!("CSK saved successfully");
}
if let Some((signing_key, persist, sk_path, pk_path)) = self.signing_key_data
&& persist
{
tracing::debug!("Saving signing key for profile '{}'", self.profile_name);
let sk_path = sk_path.unwrap_or_else(|| secret_dir.join("manager_sk"));
let pk_path = pk_path.unwrap_or_else(|| secret_dir.join("manager_pk"));
fs::create_dir_all(&secret_dir)?;
fs::write(&sk_path, hex::encode(signing_key.to_bytes()))?;
let ver_bytes = signing_key.verifying_key().to_bytes();
fs::write(&pk_path, hex::encode(ver_bytes))?;
tracing::debug!("Generated manager keypair at {}", secret_dir.display());
}
if let Some((cert_der, key_der, persist, cert_path, key_path)) = self.cert_data
&& persist
{
tracing::debug!("Saving certificate for profile '{}'", self.profile_name);
let cert_path = cert_path.unwrap_or_else(|| secret_dir.join("tls_cert.der"));
let key_path = key_path.unwrap_or_else(|| secret_dir.join("tls_key.der"));
if let Some(parent) = cert_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&cert_path, &cert_der)?;
fs::write(&key_path, &key_der)?;
tracing::debug!("Certificate saved successfully");
}
tracing::debug!("Saving profile configuration for '{}'", self.profile_name);
save_profile(&self.profile_name, &self.config)?;
tracing::debug!(
"ProfileBuilder::save_all completed successfully for profile '{}'",
self.profile_name
);
Ok(())
}
}
pub fn update_profile(profile: &str) -> Result<ManagerProfileBuilder, Report> {
ManagerProfileBuilder::new(profile)
}
pub fn list_profiles() -> Result<Vec<String>, Report> {
core_profile::list_profiles("manager")
}
pub fn delete_profile(profile: &str) -> Result<(), Report> {
core_profile::delete_profile("manager", profile)
}
pub fn profile_exists(profile: &str) -> bool {
core_profile::profile_exists("manager", profile)
}
pub fn rename_profile(old: &str, new: &str) -> Result<(), Report> {
core_profile::rename_profile("manager", old, new)
}
pub fn save_bootstrap(profile: &str) -> Result<(), Report> {
let dir = secret_dir(Some(profile));
fs::create_dir_all(&dir)?;
fs::write(dir.join("bootstrap"), b"1")?;
Ok(())
}
pub fn load_bootstrap(profile: &str) -> bool {
secret_dir(Some(profile)).join("bootstrap").exists()
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ManagerProfileExport {
pub name: String,
pub host: Option<String>,
pub bind_host: Option<String>,
pub peers: Vec<ManagerPeerEntry>,
pub worker_whitelist: Option<Vec<String>>,
pub manager_whitelist: Option<Vec<String>>,
pub max_workers: Option<u32>,
pub manager_sk: Option<String>,
pub manager_pk: Option<String>,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
pub tls_fp: Option<String>,
}
pub fn export_profile(profile: &str) -> Result<String, Report> {
let dir = secret_dir(Some(profile));
if !dir.exists() {
return Err(eyre!("profile not found"));
}
let host = load_profile_host(profile).ok().flatten();
let bind_host = load_bind_host(profile).ok().flatten();
let peers = load_peers(profile).unwrap_or_default();
let worker_whitelist = load_worker_whitelist(profile).ok().flatten();
let manager_whitelist = load_manager_whitelist(profile).ok().flatten();
let max_workers = load_max_workers(profile).ok().flatten();
let manager_sk = std::fs::read_to_string(dir.join("manager_sk")).ok();
let manager_pk = std::fs::read_to_string(dir.join("manager_pk")).ok();
let tls_cert = std::fs::read(dir.join("tls_cert.der"))
.ok()
.map(|b| STANDARD_NO_PAD.encode(b));
let tls_key = std::fs::read(dir.join("tls_key.der"))
.ok()
.map(|b| STANDARD_NO_PAD.encode(b));
let manager_name = load_manager_name(profile)
.ok()
.flatten()
.unwrap_or_else(|| profile.to_string());
let exp = ManagerProfileExport {
name: manager_name,
host,
bind_host,
peers,
worker_whitelist,
manager_whitelist,
max_workers,
manager_sk,
manager_pk,
tls_cert,
tls_key,
tls_fp: None,
};
Ok(toml::to_string(&exp)?)
}
pub fn import_profile(data: &str, name: Option<&str>, force: bool) -> Result<String, Report> {
let mut exp: ManagerProfileExport = toml::from_str(data)?;
if let Some(n) = name {
exp.name = n.to_string();
}
let dir = secret_dir(Some(&exp.name));
if dir.exists() && !force {
return Err(eyre!("profile exists"));
}
std::fs::create_dir_all(&dir)?;
let mut builder = ManagerProfileBuilder::new(&exp.name)?;
if let Some(ref v) = exp.host {
builder = builder.host(v);
}
if let Some(ref v) = exp.bind_host {
builder = builder.bind_host(v);
}
if let Some(ref v) = exp.worker_whitelist {
builder = builder.worker_whitelist(v);
}
if let Some(ref v) = exp.manager_whitelist {
builder = builder.manager_whitelist(v);
}
if let Some(v) = exp.max_workers {
builder = builder.max_workers(v);
}
builder = builder.name(&exp.name);
builder.save()?;
if !exp.peers.is_empty() {
save_peers(&exp.name, &exp.peers)?;
}
if let Some(ref v) = exp.manager_sk {
std::fs::write(dir.join("manager_sk"), v)?;
}
if let Some(ref v) = exp.manager_pk {
std::fs::write(dir.join("manager_pk"), v)?;
}
if let Some(ref v) = exp.tls_cert {
std::fs::write(
dir.join("tls_cert.der"),
STANDARD_NO_PAD.decode(v.as_bytes())?,
)?;
}
if let Some(ref v) = exp.tls_key {
std::fs::write(
dir.join("tls_key.der"),
STANDARD_NO_PAD.decode(v.as_bytes())?,
)?;
}
Ok(exp.name)
}