From a412d4202830470c599e5883c2de6c35b2bea4fe Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sat, 18 Sep 2021 16:31:43 +0000 Subject: [PATCH] feat: fleetdata --- --- a/src/cmds/build_systems.rs +++ b/src/cmds/build_systems.rs @@ -1,20 +1,13 @@ use std::process::Command; -use crate::{ - command::CommandExt, - db::{secret::SecretDb, Db, DbData}, - host::FleetOpts, - nix::SYSTEMS_ATTRIBUTE, -}; +use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE}; use anyhow::Result; use clap::Clap; -use log::{info, warn}; +use log::info; #[derive(Clap)] #[clap(group = clap::ArgGroup::new("target"))] pub struct BuildSystems { - #[clap(flatten)] - fleet_opts: FleetOpts, /// --builders arg for nix #[clap(long)] builders: Option, @@ -53,18 +46,18 @@ } impl BuildSystems { - pub fn run(self) -> Result<()> { - let fleet = self.fleet_opts.build()?; - let db = Db::new(".fleet")?; - let hosts = fleet.list_hosts()?; - let data = SecretDb::open(&db)?.generate_nix_data()?; + pub fn run(self, config: &Config) -> Result<()> { + println!("Build"); + // let db = Db::new(".fleet")?; + let hosts = config.list_hosts()?; + dbg!(&hosts); + // let data = SecretDb::open(&db)?.generate_nix_data()?; for host in hosts.iter() { - if host.skip() { - warn!("Skipping host {}", host.hostname); + if config.should_skip(host) { continue; } - info!("Building host {}", host.hostname); + info!("Building host {}", host); let built = { let dir = tempfile::tempdir()?; dir.path().to_owned() @@ -82,9 +75,9 @@ .arg(&built) .arg(format!( "{}.{}.config.system.build.toplevel", - SYSTEMS_ATTRIBUTE, host.hostname, - )) - .env("SECRET_DATA", data.clone()); + SYSTEMS_ATTRIBUTE, host, + )); + // .env("SECRET_DATA", data.clone()); if let Some(builders) = &self.builders { println!("Using builders: {}", builders); @@ -101,11 +94,11 @@ nix_build.inherit_stdio().run()?; let built = std::fs::canonicalize(built)?; info!("Built closure: {:?}", built); - if !host.is_local() { + if !config.is_local(host) { info!("Uploading system closure"); Command::new("nix") .args(&["copy", "--to"]) - .arg(format!("ssh://root@{}", host.hostname)) + .arg(format!("ssh://root@{}", host)) .arg(&built) .inherit_stdio() .run()?; @@ -113,18 +106,20 @@ if let Some(subcommand) = &self.subcommand { if subcommand.should_switch_profile() { info!("Switching generation"); - host.command_on("nix-env", true) + dbg!(&mut config + .command_on(host, "nix-env", true) .args(&["-p", "/nix/var/nix/profiles/system", "--set"]) .arg(&built) - .inherit_stdio() - .run()?; + .inherit_stdio()) + .run()?; } info!("Executing activation script"); let mut switch_script = built.clone(); switch_script.push("bin"); switch_script.push("switch-to-configuration"); info!("{:?}", switch_script); - host.command_on(switch_script, true) + config + .command_on(host, switch_script, true) .arg(subcommand.name()) .inherit_stdio() .run()?; --- a/src/cmds/generate_secrets.rs +++ b/src/cmds/generate_secrets.rs @@ -4,13 +4,19 @@ use clap::Clap; use log::info; -use crate::db::{ - secret::{list_secrets, SecretDb}, - Db, DbData, +use crate::{ + db::{ + secret::{list_secrets, SecretDb}, + Db, DbData, + }, + host::FleetOpts, }; #[derive(Clap)] pub struct GenerateSecrets { + #[clap(flatten)] + fleet_opts: FleetOpts, + /// If set - remove orphaned secrets #[clap(long)] cleanup: bool, @@ -23,8 +29,8 @@ let defined_secrets = list_secrets()?; for (secret, data) in defined_secrets.iter() { - // let keys = KeyDb::open(&db)?; - // secrets.ensure_generated(&keys, &secret, &data)?; + //let keys = KeyDb::open(&db)?; + secrets.ensure_generated(&self.fleet_opts, secret, data)?; } let key_names = defined_secrets .keys() --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -1,3 +1,4 @@ pub mod build_systems; -pub mod fetch_keys; +// pub mod fetch_keys; pub mod generate_secrets; +pub mod secrets; --- a/src/command.rs +++ b/src/command.rs @@ -23,14 +23,14 @@ fn run(&mut self) -> Result<()> { let out = self.output()?; if !out.status.success() { - anyhow::bail!("command failed"); + anyhow::bail!("command failed with status {}", out.status); } Ok(()) } fn run_json(&mut self) -> Result { let str = self.run_string()?; - Ok(serde_json::from_str(&str).with_context(|| format!("{:?}", str))?) + serde_json::from_str(&str).with_context(|| format!("{:?}", str)) } fn run_string(&mut self) -> Result { --- /dev/null +++ b/src/fleetdata.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Default)] +pub struct HostData { + #[serde(default)] + pub encryption_key: String, + #[serde(default)] + pub encrypted_secrets: BTreeMap, +} + +#[derive(Serialize, Deserialize)] +pub struct FleetData { + #[serde(default)] + pub hosts: BTreeMap, +} --- a/src/host.rs +++ b/src/host.rs @@ -1,4 +1,5 @@ use std::{ + cell::{Ref, RefCell, RefMut}, env::current_dir, ffi::{OsStr, OsString}, ops::Deref, @@ -10,17 +11,18 @@ use anyhow::Result; use clap::Clap; -use crate::command::CommandExt; +use crate::{command::CommandExt, fleetdata::FleetData}; pub struct FleetConfigInternals { pub directory: PathBuf, pub opts: FleetOpts, + pub data: RefCell, } #[derive(Clone)] -pub struct FleetConfig(Arc); +pub struct Config(Arc); -impl Deref for FleetConfig { +impl Deref for Config { type Target = FleetConfigInternals; fn deref(&self) -> &Self::Target { @@ -28,70 +30,71 @@ } } -impl FleetConfig { - pub fn data_dir(&self) -> PathBuf { - let mut out = self.directory.clone(); - out.push(".fleet"); - out +impl Config { + pub fn should_skip(&self, host: &str) -> bool { + if !self.opts.skip.is_empty() { + self.opts.skip.iter().any(|h| h as &str == host) + } else if !self.opts.only.is_empty() { + !self.opts.only.iter().any(|h| h as &str == host) + } else { + false + } + } + pub fn is_local(&self, host: &str) -> bool { + self.opts.localhost.as_ref().map(|s| s as &str) == Some(host) + } + + pub fn command_on(&self, host: &str, program: impl AsRef, sudo: bool) -> Command { + if self.is_local(host) { + if sudo { + let mut cmd = Command::new("sudo"); + cmd.arg(program); + cmd + } else { + Command::new(program) + } + } else { + let mut cmd = Command::new("ssh"); + cmd.arg(host).arg("--"); + if sudo { + cmd.arg("sudo"); + } + cmd.arg(program); + cmd + } } pub fn full_attr_name(&self, attr_name: &str) -> OsString { let mut str = self.directory.as_os_str().to_owned(); str.push("#"); str.push(attr_name); + + println!("{:?}", str); str } - pub fn list_host_names(&self) -> Result> { - Ok(Command::new("nix") + pub fn list_hosts(&self) -> Result> { + Command::new("nix") .arg("eval") .arg(self.full_attr_name("fleetConfigurations.default.configuredHosts")) - .args(&["--apply", "builtins.attrNames", "--json"]) + .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"]) .inherit_stdio() - .run_json()?) + .run_json() } - pub fn list_hosts(&self) -> Result> { - Ok(self - .list_host_names()? - .into_iter() - .map(|hostname| Host { - fleet_config: self.clone(), - hostname, - }) - .collect()) + pub fn data(&self) -> Ref { + self.data.borrow() + } + pub fn data_mut(&self) -> RefMut { + self.data.borrow_mut() } -} -pub struct Host { - pub fleet_config: FleetConfig, - - pub hostname: String, -} - -impl Host { - pub fn skip(&self) -> bool { - self.fleet_config.0.opts.should_skip(&self.hostname) - } - pub fn is_local(&self) -> bool { - self.fleet_config.0.opts.is_local(&self.hostname) - } - pub fn command_on(&self, cmd: impl AsRef, sudo: bool) -> Command { - if !self.is_local() { - let mut out = Command::new("ssh"); - out.arg(&self.hostname).arg("--"); - if sudo { - out.arg("sudo"); - } - out.arg(cmd); - out - } else if sudo { - let mut out = Command::new("sudo"); - out.arg(cmd); - out - } else { - Command::new(cmd) - } + pub fn save(&self) -> Result<()> { + let mut fleet_data_path = self.directory.clone(); + fleet_data_path.push("fleet.nix"); + let data = nixlike::serialize(&self.data() as &FleetData)?; + std::fs::write(fleet_data_path, data)?; + Ok(()) } } @@ -112,27 +115,22 @@ } impl FleetOpts { - pub fn should_skip(&self, host: &str) -> bool { - if self.skip.len() > 0 { - self.skip.iter().find(|h| h as &str == host).is_some() - } else if self.only.len() > 0 { - self.only.iter().find(|h| h as &str == host).is_none() - } else { - false - } - } - pub fn is_local(&self, host: &str) -> bool { - self.localhost.as_ref().map(|s| &s as &str) == Some(host) - } - pub fn build(mut self) -> Result { + pub fn build(mut self) -> Result { if self.localhost.is_none() { self.localhost .replace(hostname::get().unwrap().to_str().unwrap().to_owned()); } let directory = current_dir()?; - Ok(FleetConfig(Arc::new(FleetConfigInternals { + + let mut fleet_data_path = directory.clone(); + fleet_data_path.push("fleet.nix"); + let bytes = std::fs::read_to_string(fleet_data_path)?; + let data = nixlike::parse_str(&bytes)?; + + Ok(Config(Arc::new(FleetConfigInternals { opts: self, directory, + data, }))) } } --- a/src/keys.rs +++ b/src/keys.rs @@ -1,65 +1,67 @@ -use crate::{ - command::CommandExt, - host::{FleetConfig, Host}, -}; -use anyhow::Result; +use std::str::FromStr; + +use crate::{command::CommandExt, host::Config}; +use anyhow::{anyhow, Result}; use log::warn; -use std::{ - fs::{create_dir_all, metadata, read, read_dir, write}, - path::PathBuf, -}; -impl FleetConfig { - fn host_keys_dir(&self) -> Result { - let mut out = self.data_dir().clone(); - out.push("host_keys"); - create_dir_all(&out)?; - Ok(out) +impl Config { + pub fn cached_key(&self, host: &str) -> Option { + let data = self.data(); + let key = data.hosts.get(host).map(|h| &h.encryption_key); + if let Some(key) = key { + if key.is_empty() { + return None; + } + } + key.cloned() } - - fn host_key_file(&self, host: &str) -> Result { - let mut dir = self.host_keys_dir()?; - dir.push(format!("{}.asc", host)); - Ok(dir) + pub fn update_key(&self, host: &str, key: String) { + let mut data = self.data_mut(); + let host = data.hosts.entry(host.to_string()).or_default(); + host.encryption_key = key.trim().to_string(); } - - pub fn list_orphaned_keys(&self) -> Result> { - let mut out = Vec::new(); - let host_names = self.list_host_names()?; - for file in read_dir(&self.host_keys_dir()?)? { - let file = file?; - anyhow::ensure!( - file.file_type()?.is_file(), - "host_keys dir should contain only files" - ); - let name = file.file_name(); - let name = name.to_str().unwrap(); - if let Some(hostname) = name.strip_suffix(".asc") { - if !host_names.contains(&hostname.to_owned()) { - out.push((hostname.to_owned(), file.path())) - } - } else { - out.push(("".to_owned(), file.path())) - } - } - - Ok(out) + pub fn update_secret(&self, host: &str, name: &str, value: &[u8]) { + let mut data = self.data_mut(); + let host = data.hosts.entry(host.to_string()).or_default(); + host.encrypted_secrets.insert( + name.to_string(), + format!("[ENCRYPTED:{}]", base64::encode(value)), + ); } -} -impl Host { - pub fn key(&self) -> anyhow::Result { - let key_path = self.fleet_config.host_key_file(&self.hostname)?; - if metadata(&key_path).map(|m| m.is_file()).unwrap_or(false) { - Ok(String::from_utf8(read(key_path)?)?) + pub fn key(&self, host: &str) -> anyhow::Result { + if let Some(key) = self.cached_key(host) { + Ok(key) } else { - warn!("Loading key for {}", self.hostname); + warn!("Loading key for {}", host); let key = self - .command_on("cat", false) + .command_on("host", "cat", false) .arg("/etc/ssh/ssh_host_ed25519_key.pub") .run_string()?; - write(key_path, key.clone())?; + self.update_key(host, key.clone()); Ok(key) } } + pub fn recipient(&self, host: &str) -> anyhow::Result { + let key = self.key(host)?; + age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e)) + } + + pub fn orphaned_data(&self) -> Result> { + let mut out = Vec::new(); + let host_names = self.list_hosts()?; + for hostname in self + .data() + .hosts + .iter() + .filter(|(_, host)| !host.encryption_key.is_empty()) + .map(|(n, _)| n) + { + if !host_names.contains(&hostname.to_owned()) { + out.push(hostname.to_owned()) + } + } + + Ok(out) + } } --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,14 @@ pub mod db; pub mod nix; +mod fleetdata; + use anyhow::Result; use clap::Clap; -use cmds::{build_systems::BuildSystems, fetch_keys::FetchKeys, generate_secrets::GenerateSecrets}; +use cmds::{build_systems::BuildSystems, generate_secrets::GenerateSecrets, secrets::Secrets}; +use host::{Config, FleetOpts}; + #[derive(Clap)] #[clap(version = "1.0", author = "CertainLach ")] enum Opts { @@ -23,16 +27,38 @@ Secrets(Secrets), } +#[derive(Clap)] +struct RootOpts { + #[clap(flatten)] + fleet_opts: FleetOpts, + #[clap(subcommand)] + command: Opts, +} + +fn run_command(config: &Config, command: Opts) -> Result<()> { + match command { + Opts::BuildSystems(c) => c.run(config)?, + Opts::GenerateSecrets(c) => c.run()?, + Opts::Secrets(s) => s.run(config)?, + }; + Ok(()) +} + fn main() -> Result<()> { env_logger::Builder::new() .filter_level(log::LevelFilter::Info) .init(); - let opts = Opts::parse(); + let opts = RootOpts::parse(); + let config = opts.fleet_opts.build()?; - match opts { - Opts::FetchKeys(c) => c.run()?, - Opts::BuildSystems(c) => c.run()?, - Opts::GenerateSecrets(c) => c.run()?, - }; - Ok(()) + match run_command(&config, opts.command) { + Ok(()) => { + config.save()?; + Ok(()) + } + Err(e) => { + let _ = config.save(); + Err(e) + } + } } -- gitstuff