git.delta.rocks / jrsonnet / refs/commits / a412d4202830

difftreelog

feat fleetdata

Yaroslav Bolyukin2021-09-18parent: #cd4874a.patch.diff
in: trunk

9 files changed

modifiedsrc/cmds/build_systems.rsdiffbeforeafterboth
--- 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<String>,
@@ -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()?;
modifiedsrc/cmds/generate_secrets.rsdiffbeforeafterboth
--- 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()
modifiedsrc/cmds/mod.rsdiffbeforeafterboth
--- 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;
modifiedsrc/command.rsdiffbeforeafterboth
--- 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<T: DeserializeOwned>(&mut self) -> Result<T> {
 		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<String> {
addedsrc/fleetdata.rsdiffbeforeafterboth
--- /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<String, String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FleetData {
+	#[serde(default)]
+	pub hosts: BTreeMap<String, HostData>,
+}
modifiedsrc/host.rsdiffbeforeafterboth
before · src/host.rs
1use std::{2	env::current_dir,3	ffi::{OsStr, OsString},4	ops::Deref,5	path::PathBuf,6	process::Command,7	sync::Arc,8};910use anyhow::Result;11use clap::Clap;1213use crate::command::CommandExt;1415pub struct FleetConfigInternals {16	pub directory: PathBuf,17	pub opts: FleetOpts,18}1920#[derive(Clone)]21pub struct FleetConfig(Arc<FleetConfigInternals>);2223impl Deref for FleetConfig {24	type Target = FleetConfigInternals;2526	fn deref(&self) -> &Self::Target {27		&self.028	}29}3031impl FleetConfig {32	pub fn data_dir(&self) -> PathBuf {33		let mut out = self.directory.clone();34		out.push(".fleet");35		out36	}3738	pub fn full_attr_name(&self, attr_name: &str) -> OsString {39		let mut str = self.directory.as_os_str().to_owned();40		str.push("#");41		str.push(attr_name);42		str43	}4445	pub fn list_host_names(&self) -> Result<Vec<String>> {46		Ok(Command::new("nix")47			.arg("eval")48			.arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))49			.args(&["--apply", "builtins.attrNames", "--json"])50			.inherit_stdio()51			.run_json()?)52	}5354	pub fn list_hosts(&self) -> Result<Vec<Host>> {55		Ok(self56			.list_host_names()?57			.into_iter()58			.map(|hostname| Host {59				fleet_config: self.clone(),60				hostname,61			})62			.collect())63	}64}6566pub struct Host {67	pub fleet_config: FleetConfig,6869	pub hostname: String,70}7172impl Host {73	pub fn skip(&self) -> bool {74		self.fleet_config.0.opts.should_skip(&self.hostname)75	}76	pub fn is_local(&self) -> bool {77		self.fleet_config.0.opts.is_local(&self.hostname)78	}79	pub fn command_on(&self, cmd: impl AsRef<OsStr>, sudo: bool) -> Command {80		if !self.is_local() {81			let mut out = Command::new("ssh");82			out.arg(&self.hostname).arg("--");83			if sudo {84				out.arg("sudo");85			}86			out.arg(cmd);87			out88		} else if sudo {89			let mut out = Command::new("sudo");90			out.arg(cmd);91			out92		} else {93			Command::new(cmd)94		}95	}96}9798#[derive(Clap, Clone)]99#[clap(group = clap::ArgGroup::new("target_hosts"))]100pub struct FleetOpts {101	/// All hosts except those would be skipped102	#[clap(long, number_of_values = 1, group = "target_hosts")]103	only: Vec<String>,104105	/// Hosts to skip106	#[clap(long, number_of_values = 1, group = "target_hosts")]107	skip: Vec<String>,108109	/// Host, which should be threaten as current machine110	#[clap(long)]111	pub localhost: Option<String>,112}113114impl FleetOpts {115	pub fn should_skip(&self, host: &str) -> bool {116		if self.skip.len() > 0 {117			self.skip.iter().find(|h| h as &str == host).is_some()118		} else if self.only.len() > 0 {119			self.only.iter().find(|h| h as &str == host).is_none()120		} else {121			false122		}123	}124	pub fn is_local(&self, host: &str) -> bool {125		self.localhost.as_ref().map(|s| &s as &str) == Some(host)126	}127	pub fn build(mut self) -> Result<FleetConfig> {128		if self.localhost.is_none() {129			self.localhost130				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());131		}132		let directory = current_dir()?;133		Ok(FleetConfig(Arc::new(FleetConfigInternals {134			opts: self,135			directory,136		})))137	}138}
after · src/host.rs
1use std::{2	cell::{Ref, RefCell, RefMut},3	env::current_dir,4	ffi::{OsStr, OsString},5	ops::Deref,6	path::PathBuf,7	process::Command,8	sync::Arc,9};1011use anyhow::Result;12use clap::Clap;1314use crate::{command::CommandExt, fleetdata::FleetData};1516pub struct FleetConfigInternals {17	pub directory: PathBuf,18	pub opts: FleetOpts,19	pub data: RefCell<FleetData>,20}2122#[derive(Clone)]23pub struct Config(Arc<FleetConfigInternals>);2425impl Deref for Config {26	type Target = FleetConfigInternals;2728	fn deref(&self) -> &Self::Target {29		&self.030	}31}3233impl Config {34	pub fn should_skip(&self, host: &str) -> bool {35		if !self.opts.skip.is_empty() {36			self.opts.skip.iter().any(|h| h as &str == host)37		} else if !self.opts.only.is_empty() {38			!self.opts.only.iter().any(|h| h as &str == host)39		} else {40			false41		}42	}43	pub fn is_local(&self, host: &str) -> bool {44		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)45	}4647	pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {48		if self.is_local(host) {49			if sudo {50				let mut cmd = Command::new("sudo");51				cmd.arg(program);52				cmd53			} else {54				Command::new(program)55			}56		} else {57			let mut cmd = Command::new("ssh");58			cmd.arg(host).arg("--");59			if sudo {60				cmd.arg("sudo");61			}62			cmd.arg(program);63			cmd64		}65	}6667	pub fn full_attr_name(&self, attr_name: &str) -> OsString {68		let mut str = self.directory.as_os_str().to_owned();69		str.push("#");70		str.push(attr_name);7172		println!("{:?}", str);73		str74	}7576	pub fn list_hosts(&self) -> Result<Vec<String>> {77		Command::new("nix")78			.arg("eval")79			.arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))80			.args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])81			.inherit_stdio()82			.run_json()83	}8485	pub fn data(&self) -> Ref<FleetData> {86		self.data.borrow()87	}88	pub fn data_mut(&self) -> RefMut<FleetData> {89		self.data.borrow_mut()90	}9192	pub fn save(&self) -> Result<()> {93		let mut fleet_data_path = self.directory.clone();94		fleet_data_path.push("fleet.nix");95		let data = nixlike::serialize(&self.data() as &FleetData)?;96		std::fs::write(fleet_data_path, data)?;97		Ok(())98	}99}100101#[derive(Clap, Clone)]102#[clap(group = clap::ArgGroup::new("target_hosts"))]103pub struct FleetOpts {104	/// All hosts except those would be skipped105	#[clap(long, number_of_values = 1, group = "target_hosts")]106	only: Vec<String>,107108	/// Hosts to skip109	#[clap(long, number_of_values = 1, group = "target_hosts")]110	skip: Vec<String>,111112	/// Host, which should be threaten as current machine113	#[clap(long)]114	pub localhost: Option<String>,115}116117impl FleetOpts {118	pub fn build(mut self) -> Result<Config> {119		if self.localhost.is_none() {120			self.localhost121				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());122		}123		let directory = current_dir()?;124125		let mut fleet_data_path = directory.clone();126		fleet_data_path.push("fleet.nix");127		let bytes = std::fs::read_to_string(fleet_data_path)?;128		let data = nixlike::parse_str(&bytes)?;129130		Ok(Config(Arc::new(FleetConfigInternals {131			opts: self,132			directory,133			data,134		})))135	}136}
modifiedsrc/keys.rsdiffbeforeafterboth
--- 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<PathBuf> {
-		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<String> {
+		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<PathBuf> {
-		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<Vec<(String, PathBuf)>> {
-		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(("<unknown>".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<String> {
-		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<String> {
+		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<age::ssh::Recipient> {
+		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<Vec<String>> {
+		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)
+	}
 }
modifiedsrc/main.rsdiffbeforeafterboth
--- 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 <iam@lach.pw>")]
 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)
+		}
+	}
 }
addedsrc/nixlike.rsdiffbeforeafterboth

no changes