difftreelog
refactor move main command to subdirectory
in: trunk
21 files changed
Cargo.lockdiffbeforeafterboth--- 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",
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,28 +1,2 @@
-[package]
-name = "fleet"
-description = "NixOS configuration management"
-version = "0.1.0"
-authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
-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/*"]
cmds/fleet/Cargo.tomldiffbeforeafterboth--- /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 <iam@lach.pw>"]
+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"
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- /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<String>,
+ /// Jobs to run locally
+ #[structopt(long)]
+ jobs: Option<usize>,
+ /// Do not continue on error
+ #[structopt(long)]
+ fail_fast: bool,
+ #[structopt(long)]
+ privileged_build: bool,
+ #[structopt(subcommand)]
+ subcommand: Option<Subcommand>,
+}
+
+#[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(())
+ }
+}
cmds/fleet/src/cmds/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -0,0 +1,2 @@
+pub mod build_systems;
+pub mod secrets;
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- /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<String>,
+ /// Override secret if already present
+ #[structopt(long)]
+ force: bool,
+ #[structopt(long)]
+ public: Option<String>,
+ },
+}
+
+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::<Result<Vec<_>>>()?;
+
+ 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<dyn age::Recipient>)
+ .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(())
+ }
+}
cmds/fleet/src/command.rsdiffbeforeafterboth--- /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<T: DeserializeOwned>(&mut self) -> Result<T>;
+ fn run_string(&mut self) -> Result<String>;
+ fn inherit_stdio(&mut self) -> &mut Self;
+ fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> 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<T: DeserializeOwned>(&mut self) -> Result<T> {
+ let str = self.run_string()?;
+ serde_json::from_str(&str).with_context(|| format!("{:?}", str))
+ }
+
+ fn run_string(&mut self) -> Result<String> {
+ 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<OsStr>, command: impl AsRef<OsStr>) -> Self {
+ let mut cmd = Command::new("ssh");
+ cmd.arg(host).arg("--").arg(command);
+ cmd
+ }
+}
cmds/fleet/src/fleetdata.rsdiffbeforeafterboth--- /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<String, HostData>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+ pub secrets: BTreeMap<String, FleetSecret>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FleetSecret {
+ pub owners: Vec<String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expire_at: Option<DateTime<Utc>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub public: Option<String>,
+ #[serde(serialize_with = "as_z85", deserialize_with = "from_z85")]
+ pub secret: Vec<u8>,
+}
+
+fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ serializer.serialize_str(&z85::encode(&key))
+}
+
+fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, 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())))
+}
cmds/fleet/src/host.rsdiffbeforeafterboth--- /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<FleetData>,
+}
+
+#[derive(Clone)]
+pub struct Config(Arc<FleetConfigInternals>);
+
+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<OsStr>, 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<Vec<String>> {
+ 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<FleetData> {
+ self.data.borrow()
+ }
+ pub fn data_mut(&self) -> RefMut<FleetData> {
+ 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<String>,
+
+ /// Hosts to skip
+ #[structopt(long, number_of_values = 1, group = "target_hosts")]
+ skip: Vec<String>,
+
+ /// Host, which should be threaten as current machine
+ #[structopt(long)]
+ pub localhost: Option<String>,
+}
+
+impl FleetOpts {
+ pub fn build(mut self) -> Result<Config> {
+ 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,
+ })))
+ }
+}
cmds/fleet/src/keys.rsdiffbeforeafterboth--- /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<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()
+ }
+ 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<String> {
+ 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<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)
+ }
+}
cmds/fleet/src/main.rsdiffbeforeafterboth--- /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)
+ }
+ }
+}
cmds/fleet/src/nix.rsdiffbeforeafterboth--- /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";
src/cmds/build_systems.rsdiffbeforeafterboth--- 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<String>,
- /// Jobs to run locally
- #[structopt(long)]
- jobs: Option<usize>,
- /// Do not continue on error
- #[structopt(long)]
- fail_fast: bool,
- #[structopt(long)]
- privileged_build: bool,
- #[structopt(subcommand)]
- subcommand: Option<Subcommand>,
-}
-
-#[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(())
- }
-}
src/cmds/mod.rsdiffbeforeafterboth--- a/src/cmds/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod build_systems;
-pub mod secrets;
src/cmds/secrets/mod.rsdiffbeforeafterboth--- 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<String>,
- /// Override secret if already present
- #[structopt(long)]
- force: bool,
- #[structopt(long)]
- public: Option<String>,
- },
-}
-
-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::<Result<Vec<_>>>()?;
-
- 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<dyn age::Recipient>)
- .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(())
- }
-}
src/command.rsdiffbeforeafterboth--- 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<T: DeserializeOwned>(&mut self) -> Result<T>;
- fn run_string(&mut self) -> Result<String>;
- fn inherit_stdio(&mut self) -> &mut Self;
- fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> 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<T: DeserializeOwned>(&mut self) -> Result<T> {
- let str = self.run_string()?;
- serde_json::from_str(&str).with_context(|| format!("{:?}", str))
- }
-
- fn run_string(&mut self) -> Result<String> {
- 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<OsStr>, command: impl AsRef<OsStr>) -> Self {
- let mut cmd = Command::new("ssh");
- cmd.arg(host).arg("--").arg(command);
- cmd
- }
-}
src/fleetdata.rsdiffbeforeafterboth--- 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<String, HostData>,
- #[serde(default)]
- #[serde(skip_serializing_if = "BTreeMap::is_empty")]
- pub secrets: BTreeMap<String, FleetSecret>,
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct FleetSecret {
- pub owners: Vec<String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expire_at: Option<DateTime<Utc>>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub public: Option<String>,
- #[serde(serialize_with = "as_z85", deserialize_with = "from_z85")]
- pub secret: Vec<u8>,
-}
-
-fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- serializer.serialize_str(&z85::encode(&key))
-}
-
-fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, 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())))
-}
src/host.rsdiffbeforeafterboth--- 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<FleetData>,
-}
-
-#[derive(Clone)]
-pub struct Config(Arc<FleetConfigInternals>);
-
-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<OsStr>, 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<Vec<String>> {
- 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<FleetData> {
- self.data.borrow()
- }
- pub fn data_mut(&self) -> RefMut<FleetData> {
- 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<String>,
-
- /// Hosts to skip
- #[structopt(long, number_of_values = 1, group = "target_hosts")]
- skip: Vec<String>,
-
- /// Host, which should be threaten as current machine
- #[structopt(long)]
- pub localhost: Option<String>,
-}
-
-impl FleetOpts {
- pub fn build(mut self) -> Result<Config> {
- 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,
- })))
- }
-}
src/keys.rsdiffbeforeafterboth--- 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<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()
- }
- 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<String> {
- 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<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)
- }
-}
src/main.rsdiffbeforeafterboth--- 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 <iam@lach.pw>")]
-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)
- }
- }
-}
src/nix.rsdiffbeforeafterbothno changes