From a9e03a39aec522327daa61ad3c0774e3a5c7f32c Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 30 Sep 2021 20:42:58 +0000 Subject: [PATCH] refactor: move main command to subdirectory --- --- a/Cargo.lock +++ b/Cargo.lock @@ -456,7 +456,7 @@ "serde_json", "structopt", "tempfile", - "time 0.3.2", + "time 0.3.3", "z85", ] @@ -1521,9 +1521,9 @@ [[package]] name = "time" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0" +checksum = "cde1cf55178e0293453ba2cca0d5f8392a922e52aa958aee9c28ed02becc6d03" dependencies = [ "libc", "serde", --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,2 @@ -[package] -name = "fleet" -description = "NixOS configuration management" -version = "0.1.0" -authors = ["Yaroslav Bolyukin "] -edition = "2018" - -[dependencies] -anyhow = "1.0" -log = "0.4.14" -env_logger = "0.9.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -time = { version = "0.3.2", features = ["serde"] } -tempfile = "3.2" -once_cell = "1.5" -hostname = "0.3.1" -age-core = "0.6.0" -peg = "0.7.0" -nixlike = {path = "crates/nixlike"} -age = { version = "0.6.0", features = ["ssh", "armor"] } -base64 = "0.13.0" -chrono = { version = "0.4.19", features = ["serde"] } -z85 = "3.0.3" -structopt = "0.3.23" - [workspace] -members = ["crates/nixlike", "cmds/install-secrets"] +members = ["crates/*", "cmds/*"] --- /dev/null +++ b/cmds/fleet/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "fleet" +description = "NixOS configuration management" +version = "0.1.0" +authors = ["Yaroslav Bolyukin "] +edition = "2018" + +[dependencies] +anyhow = "1.0" +log = "0.4.14" +env_logger = "0.9.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +time = { version = "0.3.2", features = ["serde"] } +tempfile = "3.2" +once_cell = "1.5" +hostname = "0.3.1" +age-core = "0.6.0" +peg = "0.7.0" +nixlike = {path = "../../crates/nixlike"} +age = { version = "0.6.0", features = ["ssh", "armor"] } +base64 = "0.13.0" +chrono = { version = "0.4.19", features = ["serde"] } +z85 = "3.0.3" +structopt = "0.3.23" --- /dev/null +++ b/cmds/fleet/src/cmds/build_systems.rs @@ -0,0 +1,122 @@ +use std::process::Command; + +use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE}; +use anyhow::Result; +use log::info; +use structopt::StructOpt; + +#[derive(StructOpt)] +pub struct BuildSystems { + /// --builders arg for nix + #[structopt(long)] + builders: Option, + /// Jobs to run locally + #[structopt(long)] + jobs: Option, + /// Do not continue on error + #[structopt(long)] + fail_fast: bool, + #[structopt(long)] + privileged_build: bool, + #[structopt(subcommand)] + subcommand: Option, +} + +#[derive(StructOpt)] +enum Subcommand { + /// Switch to built system until reboot + Test, + /// Switch to built system after reboot + Boot, + /// test + boot + Switch, +} +impl Subcommand { + fn should_switch_profile(&self) -> bool { + matches!(self, Self::Test | Self::Switch) + } + fn name(&self) -> &'static str { + match self { + Self::Test => "test", + Self::Boot => "boot", + Self::Switch => "switch", + } + } +} + +impl BuildSystems { + pub fn run(self, config: &Config) -> Result<()> { + let hosts = config.list_hosts()?; + + for host in hosts.iter() { + if config.should_skip(host) { + continue; + } + info!("Building host {}", host); + let built = { + let dir = tempfile::tempdir()?; + dir.path().to_owned() + }; + + let mut nix_build = if self.privileged_build { + let mut out = Command::new("sudo"); + out.arg("nix"); + out + } else { + Command::new("nix") + }; + nix_build + .args(&["build", "--impure", "--no-link", "--out-link"]) + .arg(&built) + .arg(format!( + "{}.{}.config.system.build.toplevel", + SYSTEMS_ATTRIBUTE, host, + )); + + if let Some(builders) = &self.builders { + nix_build.arg("--builders").arg(builders); + } + if let Some(jobs) = &self.jobs { + nix_build.arg("--max-jobs"); + nix_build.arg(format!("{}", jobs)); + } + if !self.fail_fast { + nix_build.arg("--keep-going"); + } + + nix_build.inherit_stdio().run()?; + let built = std::fs::canonicalize(built)?; + info!("Built closure: {:?}", built); + if !config.is_local(host) { + info!("Uploading system closure"); + Command::new("nix") + .args(&["copy", "--to"]) + .arg(format!("ssh://root@{}", host)) + .arg(&built) + .inherit_stdio() + .run()?; + } + if let Some(subcommand) = &self.subcommand { + if subcommand.should_switch_profile() { + info!("Switching generation"); + config + .command_on(host, "nix-env", true) + .args(&["-p", "/nix/var/nix/profiles/system", "--set"]) + .arg(&built) + .inherit_stdio() + .run()?; + } + info!("Executing activation script"); + let mut switch_script = built.clone(); + switch_script.push("bin"); + switch_script.push("switch-to-configuration"); + config + .command_on(host, switch_script, true) + .arg(subcommand.name()) + .inherit_stdio() + .run()?; + } + } + Ok(()) + } +} --- /dev/null +++ b/cmds/fleet/src/cmds/mod.rs @@ -0,0 +1,2 @@ +pub mod build_systems; +pub mod secrets; --- /dev/null +++ b/cmds/fleet/src/cmds/secrets/mod.rs @@ -0,0 +1,80 @@ +use crate::{fleetdata::FleetSecret, host::Config}; +use anyhow::{bail, Result}; +use std::io::{self, Cursor, Read}; +use structopt::StructOpt; + +#[derive(StructOpt)] +pub enum Secrets { + /// Force load keys for all defined hosts + ForceKeys, + /// Add secret, data should be provided in stdin + Add { + /// Secret name + name: String, + /// Secret owners + machines: Vec, + /// Override secret if already present + #[structopt(long)] + force: bool, + #[structopt(long)] + public: Option, + }, +} + +impl Secrets { + pub fn run(self, config: &Config) -> Result<()> { + match self { + Secrets::ForceKeys => { + for host in config.list_hosts()? { + if config.should_skip(&host) { + continue; + } + config.key(&host)?; + } + } + Secrets::Add { + machines, + name, + force, + public, + } => { + let recipients = machines + .iter() + .map(|m| config.recipient(m)) + .collect::>>()?; + + let secret = { + let mut input = vec![]; + io::stdin().read_to_end(&mut input)?; + + let mut encrypted = vec![]; + let recipients = recipients + .iter() + .cloned() + .map(|r| Box::new(r) as Box) + .collect(); + let mut encryptor = + age::Encryptor::with_recipients(recipients).wrap_output(&mut encrypted)?; + io::copy(&mut Cursor::new(input), &mut encryptor)?; + encryptor.finish()?; + encrypted + }; + + let mut data = config.data_mut(); + if data.secrets.contains_key(&name) && !force { + bail!("secret already defined"); + } + data.secrets.insert( + name, + FleetSecret { + owners: machines, + expire_at: None, + secret, + public, + }, + ); + } + } + Ok(()) + } +} --- /dev/null +++ b/cmds/fleet/src/command.rs @@ -0,0 +1,51 @@ +use std::{ + ffi::OsStr, + process::{Command, Stdio}, +}; + +use anyhow::{Context, Result}; +use serde::de::DeserializeOwned; + +pub trait CommandExt { + fn run(&mut self) -> Result<()>; + fn run_json(&mut self) -> Result; + fn run_string(&mut self) -> Result; + fn inherit_stdio(&mut self) -> &mut Self; + fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self; +} + +impl CommandExt for Command { + fn inherit_stdio(&mut self) -> &mut Self { + self.stderr(Stdio::inherit()); + self + } + + fn run(&mut self) -> Result<()> { + self.inherit_stdio(); + let out = self.output()?; + if !out.status.success() { + anyhow::bail!("command ({:?}) failed with status {}", self, out.status); + } + Ok(()) + } + + fn run_json(&mut self) -> Result { + let str = self.run_string()?; + serde_json::from_str(&str).with_context(|| format!("{:?}", str)) + } + + fn run_string(&mut self) -> Result { + self.inherit_stdio(); + let out = self.output()?; + if !out.status.success() { + anyhow::bail!("command ({:?}) failed with status {}", self, out.status); + } + Ok(String::from_utf8(out.stdout)?) + } + + fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self { + let mut cmd = Command::new("ssh"); + cmd.arg(host).arg("--").arg(command); + cmd + } +} --- /dev/null +++ b/cmds/fleet/src/fleetdata.rs @@ -0,0 +1,49 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct HostData { + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub encryption_key: String, +} + +#[derive(Serialize, Deserialize)] +pub struct FleetData { + #[serde(default)] + pub hosts: BTreeMap, + #[serde(default)] + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub secrets: BTreeMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FleetSecret { + pub owners: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_at: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub public: Option, + #[serde(serialize_with = "as_z85", deserialize_with = "from_z85")] + pub secret: Vec, +} + +fn as_z85(key: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&z85::encode(&key)) +} + +fn from_z85<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string()))) +} --- /dev/null +++ b/cmds/fleet/src/host.rs @@ -0,0 +1,141 @@ +use std::{ + cell::{Ref, RefCell, RefMut}, + env::current_dir, + ffi::{OsStr, OsString}, + ops::Deref, + path::PathBuf, + process::Command, + sync::Arc, +}; + +use anyhow::Result; +use structopt::clap::ArgGroup; +use structopt::StructOpt; + +use crate::{command::CommandExt, fleetdata::FleetData}; + +pub struct FleetConfigInternals { + pub directory: PathBuf, + pub opts: FleetOpts, + pub data: RefCell, +} + +#[derive(Clone)] +pub struct Config(Arc); + +impl Deref for Config { + type Target = FleetConfigInternals; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +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); + str + } + + pub fn list_hosts(&self) -> Result> { + Command::new("nix") + .arg("eval") + .arg(self.full_attr_name("fleetConfigurations.default.configuredHosts")) + .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"]) + .inherit_stdio() + .run_json() + } + + pub fn data(&self) -> Ref { + self.data.borrow() + } + pub fn data_mut(&self) -> RefMut { + self.data.borrow_mut() + } + + 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, + format!( + "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n", + data + ), + )?; + Ok(()) + } +} + +#[derive(StructOpt, Clone)] +#[structopt(group = ArgGroup::with_name("target_hosts"))] +pub struct FleetOpts { + /// All hosts except those would be skipped + #[structopt(long, number_of_values = 1, group = "target_hosts")] + only: Vec, + + /// Hosts to skip + #[structopt(long, number_of_values = 1, group = "target_hosts")] + skip: Vec, + + /// Host, which should be threaten as current machine + #[structopt(long)] + pub localhost: Option, +} + +impl FleetOpts { + 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()?; + + 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, + }))) + } +} --- /dev/null +++ b/cmds/fleet/src/keys.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +use crate::{command::CommandExt, host::Config}; +use anyhow::{anyhow, Result}; +use log::warn; + +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() + } + 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 key(&self, host: &str) -> anyhow::Result { + if let Some(key) = self.cached_key(host) { + Ok(key) + } else { + warn!("Loading key for {}", host); + let key = self + .command_on(host, "cat", false) + .arg("/etc/ssh/ssh_host_ed25519_key.pub") + .run_string()?; + 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) + } +} --- /dev/null +++ b/cmds/fleet/src/main.rs @@ -0,0 +1,64 @@ +pub mod command; +pub mod host; +pub mod keys; + +pub mod cmds; +pub mod nix; + +mod fleetdata; + +use anyhow::Result; +use structopt::clap::AppSettings::*; +use structopt::StructOpt; + +use cmds::{build_systems::BuildSystems, secrets::Secrets}; +use host::{Config, FleetOpts}; + +#[derive(StructOpt)] +enum Opts { + /// Prepare systems for deployments + BuildSystems(BuildSystems), + /// Secret management + Secrets(Secrets), +} + +#[derive(StructOpt)] +#[structopt( + version = "1.0", + author, + global_setting(ColorAuto), + global_setting(ColoredHelp) +)] +struct RootOpts { + #[structopt(flatten)] + fleet_opts: FleetOpts, + #[structopt(subcommand)] + command: Opts, +} + +fn run_command(config: &Config, command: Opts) -> Result<()> { + match command { + Opts::BuildSystems(c) => c.run(config)?, + Opts::Secrets(s) => s.run(config)?, + }; + Ok(()) +} + +fn main() -> Result<()> { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .init(); + let opts = RootOpts::from_args(); + let config = opts.fleet_opts.build()?; + + match run_command(&config, opts.command) { + Ok(()) => { + config.save()?; + Ok(()) + } + Err(e) => { + let _ = config.save(); + Err(e) + } + } +} --- /dev/null +++ b/cmds/fleet/src/nix.rs @@ -0,0 +1,3 @@ +pub const HOSTS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredHosts"; +pub const SECRETS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSecrets"; +pub const SYSTEMS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSystems"; --- a/src/cmds/build_systems.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::process::Command; - -use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE}; -use anyhow::Result; -use log::info; -use structopt::StructOpt; - -#[derive(StructOpt)] -pub struct BuildSystems { - /// --builders arg for nix - #[structopt(long)] - builders: Option, - /// Jobs to run locally - #[structopt(long)] - jobs: Option, - /// Do not continue on error - #[structopt(long)] - fail_fast: bool, - #[structopt(long)] - privileged_build: bool, - #[structopt(subcommand)] - subcommand: Option, -} - -#[derive(StructOpt)] -enum Subcommand { - /// Switch to built system until reboot - Test, - /// Switch to built system after reboot - Boot, - /// test + boot - Switch, -} -impl Subcommand { - fn should_switch_profile(&self) -> bool { - matches!(self, Self::Test | Self::Switch) - } - fn name(&self) -> &'static str { - match self { - Self::Test => "test", - Self::Boot => "boot", - Self::Switch => "switch", - } - } -} - -impl BuildSystems { - pub fn run(self, config: &Config) -> Result<()> { - let hosts = config.list_hosts()?; - - for host in hosts.iter() { - if config.should_skip(host) { - continue; - } - info!("Building host {}", host); - let built = { - let dir = tempfile::tempdir()?; - dir.path().to_owned() - }; - - let mut nix_build = if self.privileged_build { - let mut out = Command::new("sudo"); - out.arg("nix"); - out - } else { - Command::new("nix") - }; - nix_build - .args(&["build", "--impure", "--no-link", "--out-link"]) - .arg(&built) - .arg(format!( - "{}.{}.config.system.build.toplevel", - SYSTEMS_ATTRIBUTE, host, - )); - - if let Some(builders) = &self.builders { - nix_build.arg("--builders").arg(builders); - } - if let Some(jobs) = &self.jobs { - nix_build.arg("--max-jobs"); - nix_build.arg(format!("{}", jobs)); - } - if !self.fail_fast { - nix_build.arg("--keep-going"); - } - - nix_build.inherit_stdio().run()?; - let built = std::fs::canonicalize(built)?; - info!("Built closure: {:?}", built); - if !config.is_local(host) { - info!("Uploading system closure"); - Command::new("nix") - .args(&["copy", "--to"]) - .arg(format!("ssh://root@{}", host)) - .arg(&built) - .inherit_stdio() - .run()?; - } - if let Some(subcommand) = &self.subcommand { - if subcommand.should_switch_profile() { - info!("Switching generation"); - config - .command_on(host, "nix-env", true) - .args(&["-p", "/nix/var/nix/profiles/system", "--set"]) - .arg(&built) - .inherit_stdio() - .run()?; - } - info!("Executing activation script"); - let mut switch_script = built.clone(); - switch_script.push("bin"); - switch_script.push("switch-to-configuration"); - config - .command_on(host, switch_script, true) - .arg(subcommand.name()) - .inherit_stdio() - .run()?; - } - } - Ok(()) - } -} --- a/src/cmds/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod build_systems; -pub mod secrets; --- a/src/cmds/secrets/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{fleetdata::FleetSecret, host::Config}; -use anyhow::{bail, Result}; -use std::io::{self, Cursor, Read}; -use structopt::StructOpt; - -#[derive(StructOpt)] -pub enum Secrets { - /// Force load keys for all defined hosts - ForceKeys, - /// Add secret, data should be provided in stdin - Add { - /// Secret name - name: String, - /// Secret owners - machines: Vec, - /// Override secret if already present - #[structopt(long)] - force: bool, - #[structopt(long)] - public: Option, - }, -} - -impl Secrets { - pub fn run(self, config: &Config) -> Result<()> { - match self { - Secrets::ForceKeys => { - for host in config.list_hosts()? { - if config.should_skip(&host) { - continue; - } - config.key(&host)?; - } - } - Secrets::Add { - machines, - name, - force, - public, - } => { - let recipients = machines - .iter() - .map(|m| config.recipient(m)) - .collect::>>()?; - - let secret = { - let mut input = vec![]; - io::stdin().read_to_end(&mut input)?; - - let mut encrypted = vec![]; - let recipients = recipients - .iter() - .cloned() - .map(|r| Box::new(r) as Box) - .collect(); - let mut encryptor = - age::Encryptor::with_recipients(recipients).wrap_output(&mut encrypted)?; - io::copy(&mut Cursor::new(input), &mut encryptor)?; - encryptor.finish()?; - encrypted - }; - - let mut data = config.data_mut(); - if data.secrets.contains_key(&name) && !force { - bail!("secret already defined"); - } - data.secrets.insert( - name, - FleetSecret { - owners: machines, - expire_at: None, - secret, - public, - }, - ); - } - } - Ok(()) - } -} --- a/src/command.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::{ - ffi::OsStr, - process::{Command, Stdio}, -}; - -use anyhow::{Context, Result}; -use serde::de::DeserializeOwned; - -pub trait CommandExt { - fn run(&mut self) -> Result<()>; - fn run_json(&mut self) -> Result; - fn run_string(&mut self) -> Result; - fn inherit_stdio(&mut self) -> &mut Self; - fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self; -} - -impl CommandExt for Command { - fn inherit_stdio(&mut self) -> &mut Self { - self.stderr(Stdio::inherit()); - self - } - - fn run(&mut self) -> Result<()> { - self.inherit_stdio(); - let out = self.output()?; - if !out.status.success() { - anyhow::bail!("command ({:?}) failed with status {}", self, out.status); - } - Ok(()) - } - - fn run_json(&mut self) -> Result { - let str = self.run_string()?; - serde_json::from_str(&str).with_context(|| format!("{:?}", str)) - } - - fn run_string(&mut self) -> Result { - self.inherit_stdio(); - let out = self.output()?; - if !out.status.success() { - anyhow::bail!("command ({:?}) failed with status {}", self, out.status); - } - Ok(String::from_utf8(out.stdout)?) - } - - fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self { - let mut cmd = Command::new("ssh"); - cmd.arg(host).arg("--").arg(command); - cmd - } -} --- a/src/fleetdata.rs +++ /dev/null @@ -1,49 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::collections::BTreeMap; - -#[derive(Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct HostData { - #[serde(default)] - #[serde(skip_serializing_if = "String::is_empty")] - pub encryption_key: String, -} - -#[derive(Serialize, Deserialize)] -pub struct FleetData { - #[serde(default)] - pub hosts: BTreeMap, - #[serde(default)] - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub secrets: BTreeMap, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FleetSecret { - pub owners: Vec, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_at: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub public: Option, - #[serde(serialize_with = "as_z85", deserialize_with = "from_z85")] - pub secret: Vec, -} - -fn as_z85(key: &[u8], serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&z85::encode(&key)) -} - -fn from_z85<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - use serde::de::Error; - String::deserialize(deserializer) - .and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string()))) -} --- a/src/host.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::{ - cell::{Ref, RefCell, RefMut}, - env::current_dir, - ffi::{OsStr, OsString}, - ops::Deref, - path::PathBuf, - process::Command, - sync::Arc, -}; - -use anyhow::Result; -use structopt::clap::ArgGroup; -use structopt::StructOpt; - -use crate::{command::CommandExt, fleetdata::FleetData}; - -pub struct FleetConfigInternals { - pub directory: PathBuf, - pub opts: FleetOpts, - pub data: RefCell, -} - -#[derive(Clone)] -pub struct Config(Arc); - -impl Deref for Config { - type Target = FleetConfigInternals; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -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); - str - } - - pub fn list_hosts(&self) -> Result> { - Command::new("nix") - .arg("eval") - .arg(self.full_attr_name("fleetConfigurations.default.configuredHosts")) - .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"]) - .inherit_stdio() - .run_json() - } - - pub fn data(&self) -> Ref { - self.data.borrow() - } - pub fn data_mut(&self) -> RefMut { - self.data.borrow_mut() - } - - 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, - format!( - "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n", - data - ), - )?; - Ok(()) - } -} - -#[derive(StructOpt, Clone)] -#[structopt(group = ArgGroup::with_name("target_hosts"))] -pub struct FleetOpts { - /// All hosts except those would be skipped - #[structopt(long, number_of_values = 1, group = "target_hosts")] - only: Vec, - - /// Hosts to skip - #[structopt(long, number_of_values = 1, group = "target_hosts")] - skip: Vec, - - /// Host, which should be threaten as current machine - #[structopt(long)] - pub localhost: Option, -} - -impl FleetOpts { - 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()?; - - 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 +++ /dev/null @@ -1,59 +0,0 @@ -use std::str::FromStr; - -use crate::{command::CommandExt, host::Config}; -use anyhow::{anyhow, Result}; -use log::warn; - -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() - } - 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 key(&self, host: &str) -> anyhow::Result { - if let Some(key) = self.cached_key(host) { - Ok(key) - } else { - warn!("Loading key for {}", host); - let key = self - .command_on(host, "cat", false) - .arg("/etc/ssh/ssh_host_ed25519_key.pub") - .run_string()?; - 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 +++ /dev/null @@ -1,58 +0,0 @@ -pub mod command; -pub mod host; -pub mod keys; - -pub mod cmds; -pub mod nix; - -mod fleetdata; - -use anyhow::Result; -use structopt::StructOpt; - -use cmds::{build_systems::BuildSystems, secrets::Secrets}; -use host::{Config, FleetOpts}; - -#[derive(StructOpt)] -#[structopt(version = "1.0", author = "CertainLach ")] -enum Opts { - /// Prepare systems for deployments - BuildSystems(BuildSystems), - /// Secret management - Secrets(Secrets), -} - -#[derive(StructOpt)] -struct RootOpts { - #[structopt(flatten)] - fleet_opts: FleetOpts, - #[structopt(subcommand)] - command: Opts, -} - -fn run_command(config: &Config, command: Opts) -> Result<()> { - match command { - Opts::BuildSystems(c) => c.run(config)?, - Opts::Secrets(s) => s.run(config)?, - }; - Ok(()) -} - -fn main() -> Result<()> { - env_logger::Builder::new() - .filter_level(log::LevelFilter::Info) - .init(); - let opts = RootOpts::from_args(); - let config = opts.fleet_opts.build()?; - - match run_command(&config, opts.command) { - Ok(()) => { - config.save()?; - Ok(()) - } - Err(e) => { - let _ = config.save(); - Err(e) - } - } -} --- a/src/nix.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub const HOSTS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredHosts"; -pub const SECRETS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSecrets"; -pub const SYSTEMS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSystems"; -- gitstuff