diff --git a/Cargo.lock b/Cargo.lock index 83d020f..37dccd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ dependencies = [ "owo-colors", "reqwest", "serde", + "serde_json", "tar", "target-lexicon", "tempdir", diff --git a/Cargo.toml b/Cargo.toml index 5a3525c..a1f1505 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ nix = { version = "0.25.0", features = ["user", "fs"], default-features = false walkdir = "2.3.2" serde = { version = "1.0.144", features = ["derive"] } url = { version = "2.3.1", features = ["serde"] } +serde_json = "1.0.85" diff --git a/src/actions/create_user.rs b/src/actions/create_user.rs index 48eaa2c..857c4bf 100644 --- a/src/actions/create_user.rs +++ b/src/actions/create_user.rs @@ -1,6 +1,6 @@ use crate::{settings::InstallSettings, HarmonicError}; -use super::{Actionable, ActionReceipt, Revertable}; +use super::{Actionable, ActionReceipt, Revertable, ActionDescription}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] @@ -23,8 +23,17 @@ impl CreateUser { #[async_trait::async_trait] impl<'a> Actionable<'a> for CreateUser { - fn description(&self) -> String { - todo!() + fn description(&self) -> Vec { + let name = &self.name; + let uid = &self.uid; + vec![ + ActionDescription::new( + format!("Create user {name} with UID {uid}"), + vec![ + format!("The nix daemon requires system users it can act as in order to build"), + ] + ) + ] } async fn execute(self) -> Result { @@ -36,7 +45,7 @@ impl<'a> Actionable<'a> for CreateUser { #[async_trait::async_trait] impl<'a> Revertable<'a> for CreateUserReceipt { - fn description(&self) -> String { + fn description(&self) -> Vec { todo!() } diff --git a/src/actions/create_users.rs b/src/actions/create_users.rs index d8680b6..b2bd0fc 100644 --- a/src/actions/create_users.rs +++ b/src/actions/create_users.rs @@ -2,11 +2,14 @@ use tokio::task::JoinSet; use crate::{settings::InstallSettings, HarmonicError}; -use super::{Actionable, CreateUser, ActionReceipt, create_user::CreateUserReceipt, Revertable}; +use super::{Actionable, CreateUser, ActionReceipt, create_user::CreateUserReceipt, Revertable, ActionDescription}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateUsers { + nix_build_user_prefix: String, + nix_build_user_id_base: usize, + daemon_user_count: usize, children: Vec, } @@ -16,24 +19,35 @@ pub struct CreateUsersReceipt { } impl CreateUsers { - pub fn plan(nix_build_user_prefix: &String, nix_build_user_id_base: usize, daemon_user_count: usize) -> Self { + pub fn plan(nix_build_user_prefix: String, nix_build_user_id_base: usize, daemon_user_count: usize) -> Self { let children = (0..daemon_user_count).map(|count| CreateUser::plan( format!("{nix_build_user_prefix}{count}"), nix_build_user_id_base + count )).collect(); - Self { children } + Self { nix_build_user_prefix, nix_build_user_id_base, daemon_user_count, children } } } #[async_trait::async_trait] impl<'a> Actionable<'a> for CreateUsers { - fn description(&self) -> String { - todo!() + fn description(&self) -> Vec { + let nix_build_user_prefix = &self.nix_build_user_prefix; + let nix_build_user_id_base = &self.nix_build_user_id_base; + let daemon_user_count = &self.daemon_user_count; + vec![ + ActionDescription::new( + format!("Create build users"), + vec![ + format!("The nix daemon requires system users it can act as in order to build"), + format!("This action will create {daemon_user_count} users with prefix `{nix_build_user_prefix}` starting at uid `{nix_build_user_id_base}`"), + ], + ) + ] } async fn execute(self) -> Result { // TODO(@hoverbear): Abstract this, it will be common - let Self { children } = self; + let Self { children, .. } = self; let mut set = JoinSet::new(); let mut successes = Vec::with_capacity(children.len()); let mut errors = Vec::default(); @@ -78,7 +92,7 @@ impl<'a> Actionable<'a> for CreateUsers { #[async_trait::async_trait] impl<'a> Revertable<'a> for CreateUsersReceipt { - fn description(&self) -> String { + fn description(&self) -> Vec { todo!() } diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 73f9573..8e2ce3d 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -7,6 +7,19 @@ pub use create_users::{CreateUsers, CreateUsersReceipt}; use crate::{HarmonicError, settings::InstallSettings}; +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] + +pub struct ActionDescription { + pub description: String, + pub explanation: Vec, +} + +impl ActionDescription { + fn new(description: String, explanation: Vec) -> Self { + Self { description, explanation } + } +} + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub enum Action { CreateUsers(CreateUsers), @@ -24,7 +37,7 @@ pub enum ActionReceipt { #[async_trait::async_trait] impl<'a> Actionable<'a> for Action { - fn description(&self) -> String { + fn description(&self) -> Vec { match self { Action::StartNixDaemonService(i) => i.description(), Action::CreateUser(i) => i.description(), @@ -43,7 +56,7 @@ impl<'a> Actionable<'a> for Action { #[async_trait::async_trait] impl<'a> Revertable<'a> for ActionReceipt { - fn description(&self) -> String { + fn description(&self) -> Vec { match self { ActionReceipt::StartNixDaemonService(i) => i.description(), ActionReceipt::CreateUser(i) => i.description(), @@ -62,12 +75,12 @@ impl<'a> Revertable<'a> for ActionReceipt { #[async_trait::async_trait] pub trait Actionable<'a>: serde::de::Deserialize<'a> + serde::Serialize { - fn description(&self) -> String; + fn description(&self) -> Vec; async fn execute(self) -> Result; } #[async_trait::async_trait] pub trait Revertable<'a>: serde::de::Deserialize<'a> + serde::Serialize { - fn description(&self) -> String; + fn description(&self) -> Vec; async fn revert(self) -> Result<(), HarmonicError>; } diff --git a/src/actions/start_nix_daemon_service.rs b/src/actions/start_nix_daemon_service.rs index 5a7cd51..a1ab34e 100644 --- a/src/actions/start_nix_daemon_service.rs +++ b/src/actions/start_nix_daemon_service.rs @@ -1,6 +1,8 @@ +use reqwest::redirect::Action; + use crate::{settings::InstallSettings, HarmonicError}; -use super::{Actionable, ActionReceipt, Revertable}; +use super::{Actionable, ActionReceipt, Revertable, ActionDescription}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] @@ -21,8 +23,15 @@ impl StartNixDaemonService { #[async_trait::async_trait] impl<'a> Actionable<'a> for StartNixDaemonService { - fn description(&self) -> String { - todo!() + fn description(&self) -> Vec { + vec![ + ActionDescription::new( + "Start the systemd Nix daemon".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + ), + ] } async fn execute(self) -> Result { @@ -33,7 +42,7 @@ impl<'a> Actionable<'a> for StartNixDaemonService { #[async_trait::async_trait] impl<'a> Revertable<'a> for StartNixDaemonServiceReceipt { - fn description(&self) -> String { + fn description(&self) -> Vec { todo!() } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9ba41c2..882e765 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,10 +1,14 @@ pub(crate) mod arg; +pub(crate) mod subcommand; use crate::{cli::arg::ChannelValue, interaction}; use clap::{ArgAction, Parser}; -use harmonic::Harmonic; +use harmonic::{Harmonic, InstallPlan, InstallSettings}; use std::process::ExitCode; +use self::subcommand::HarmonicSubcommand; + + #[async_trait::async_trait] pub(crate) trait CommandExecute { async fn execute(self) -> eyre::Result; @@ -14,14 +18,6 @@ pub(crate) trait CommandExecute { #[derive(Debug, Parser)] #[clap(version)] pub(crate) struct HarmonicCli { - // Don't actually install, just log expected actions - #[clap( - long, - action(ArgAction::SetTrue), - default_value = "false", - global = true - )] - pub(crate) dry_run: bool, #[clap(flatten)] pub(crate) instrumentation: arg::Instrumentation, /// Channel(s) to add by default, pass multiple times for multiple channels @@ -34,11 +30,25 @@ pub(crate) struct HarmonicCli { )] pub(crate) channel: Vec, /// Don't modify the user profile to automatically load nix - #[clap(long)] + #[clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + global = true + )] pub(crate) no_modify_profile: bool, /// Number of build users to create #[clap(long, default_value = "32", env = "HARMONIC_NIX_DAEMON_USER_COUNT")] pub(crate) daemon_user_count: usize, + #[clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + global = true + )] + pub(crate) explain: bool, + #[clap(subcommand)] + subcommand: Option, } #[async_trait::async_trait] @@ -47,59 +57,100 @@ impl CommandExecute for HarmonicCli { channels = %self.channel.iter().map(|ChannelValue(name, url)| format!("{name} {url}")).collect::>().join(", "), daemon_user_count = %self.daemon_user_count, no_modify_profile = %self.no_modify_profile, - dry_run = %self.dry_run, + explain = %self.explain, ))] async fn execute(self) -> eyre::Result { let Self { - dry_run, instrumentation: _, daemon_user_count, channel, no_modify_profile, + explain, + subcommand, } = self; - let mut harmonic = Harmonic::default(); + match subcommand { + Some(HarmonicSubcommand::Plan(plan)) => { + return plan.execute().await + }, + Some(HarmonicSubcommand::Execute(execute)) => { + return execute.execute().await + } + None => (), + } - harmonic.dry_run(dry_run); - harmonic.daemon_user_count(daemon_user_count); - harmonic.channels( + let mut settings = InstallSettings::default(); + + settings.explain(explain); + settings.daemon_user_count(daemon_user_count); + settings.nix_build_group_name("nixbld".to_string()); + settings.nix_build_group_id(30000); + settings.nix_build_user_prefix("nixbld".to_string()); + settings.nix_build_user_id_base(30001); + settings.channels( channel .into_iter() .map(|ChannelValue(name, url)| (name, url)), ); - harmonic.modify_profile(!no_modify_profile); + settings.modify_profile(!no_modify_profile); + + let plan = InstallPlan::new(settings).await?; + // TODO(@Hoverbear): Make this smarter if !interaction::confirm( - "\ - Ready to install nix?\n\ - \n\ - This installer will:\n\ - \n\ - * Create a `nixbld` group\n\ - * Create several `nixbld*` users\n\ - * Create several Nix related directories\n\ - * Place channel configurations\n\ - * Fetch a copy of Nix and unpack it\n\ - * Configure the shell profiles of various shells\n\ - * Place a Nix configuration\n\ - * Configure the Nix daemon to work with your init\ - ", + plan.description() ) .await? { interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; } - harmonic.create_group().await?; - harmonic.create_users().await?; - harmonic.create_directories().await?; - harmonic.place_channel_configuration().await?; - harmonic.fetch_nix().await?; - harmonic.configure_shell_profile().await?; - harmonic.setup_default_profile().await?; - harmonic.place_nix_configuration().await?; - harmonic.configure_nix_daemon_service().await?; + // let mut harmonic = Harmonic::default(); + + // harmonic.dry_run(dry_run); + // harmonic.explain(explain); + // harmonic.daemon_user_count(daemon_user_count); + // harmonic.channels( + // channel + // .into_iter() + // .map(|ChannelValue(name, url)| (name, url)), + // ); + // harmonic.modify_profile(!no_modify_profile); + + + + // // TODO(@Hoverbear): Make this smarter + // if !interaction::confirm( + // "\ + // Ready to install nix?\n\ + // \n\ + // This installer will:\n\ + // \n\ + // * Create a `nixbld` group\n\ + // * Create several `nixbld*` users\n\ + // * Create several Nix related directories\n\ + // * Place channel configurations\n\ + // * Fetch a copy of Nix and unpack it\n\ + // * Configure the shell profiles of various shells\n\ + // * Place a Nix configuration\n\ + // * Configure the Nix daemon to work with your init\ + // ", + // ) + // .await? + // { + // interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; + // } + + // harmonic.create_group().await?; + // harmonic.create_users().await?; + // harmonic.create_directories().await?; + // harmonic.place_channel_configuration().await?; + // harmonic.fetch_nix().await?; + // harmonic.configure_shell_profile().await?; + // harmonic.setup_default_profile().await?; + // harmonic.place_nix_configuration().await?; + // harmonic.configure_nix_daemon_service().await?; Ok(ExitCode::SUCCESS) } diff --git a/src/cli/subcommand/execute.rs b/src/cli/subcommand/execute.rs new file mode 100644 index 0000000..3bf3103 --- /dev/null +++ b/src/cli/subcommand/execute.rs @@ -0,0 +1,48 @@ +use std::process::ExitCode; + +use clap::{Parser, ArgAction}; +use harmonic::{InstallSettings, InstallPlan}; +use tokio::io::{AsyncWriteExt, AsyncReadExt}; + +use crate::{cli::{arg::ChannelValue, CommandExecute}, interaction}; + +/// An opinionated, experimental Nix installer +#[derive(Debug, Parser)] +pub(crate) struct Execute { + #[clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + global = true + )] + no_confirm: bool +} + +#[async_trait::async_trait] +impl CommandExecute for Execute { + #[tracing::instrument(skip_all, fields( + + ))] + async fn execute(self) -> eyre::Result { + let Self { no_confirm } = self; + + let mut stdin = tokio::io::stdin(); + let mut json = String::default(); + stdin.read_to_string(&mut json).await?; + let plan: InstallPlan = serde_json::from_str(&json)?; + + if !no_confirm { + if !interaction::confirm( + plan.description() + ) + .await? + { + interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; + } + } + + + + Ok(ExitCode::SUCCESS) + } +} \ No newline at end of file diff --git a/src/cli/subcommand/mod.rs b/src/cli/subcommand/mod.rs new file mode 100644 index 0000000..ad44c45 --- /dev/null +++ b/src/cli/subcommand/mod.rs @@ -0,0 +1,12 @@ +mod plan; +use plan::Plan; +mod execute; +use execute::Execute; + + +#[derive(Debug, clap::Subcommand)] +pub(crate) enum HarmonicSubcommand { + Plan(Plan), + Execute(Execute), +} + diff --git a/src/cli/subcommand/plan.rs b/src/cli/subcommand/plan.rs new file mode 100644 index 0000000..13c0364 --- /dev/null +++ b/src/cli/subcommand/plan.rs @@ -0,0 +1,65 @@ +use std::process::ExitCode; + +use clap::{Parser, ArgAction}; +use harmonic::{InstallSettings, InstallPlan}; +use tokio::io::AsyncWriteExt; + +use crate::cli::{arg::ChannelValue, CommandExecute}; + +/// An opinionated, experimental Nix installer +#[derive(Debug, Parser)] +pub(crate) struct Plan { + /// Channel(s) to add by default, pass multiple times for multiple channels + #[clap( + long, + value_parser, + action = clap::ArgAction::Append, + env = "HARMONIC_CHANNEL", + default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable" + )] + pub(crate) channel: Vec, + /// Don't modify the user profile to automatically load nix + #[clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + global = true + )] + pub(crate) no_modify_profile: bool, + /// Number of build users to create + #[clap(long, default_value = "32", env = "HARMONIC_NIX_DAEMON_USER_COUNT")] + pub(crate) daemon_user_count: usize, +} + +#[async_trait::async_trait] +impl CommandExecute for Plan { + #[tracing::instrument(skip_all, fields( + channels = %self.channel.iter().map(|ChannelValue(name, url)| format!("{name} {url}")).collect::>().join(", "), + daemon_user_count = %self.daemon_user_count, + no_modify_profile = %self.no_modify_profile, + ))] + async fn execute(self) -> eyre::Result { + let Self { channel, no_modify_profile, daemon_user_count } = self; + + let mut settings = InstallSettings::default(); + + settings.daemon_user_count(daemon_user_count); + settings.nix_build_group_name("nixbld".to_string()); + settings.nix_build_group_id(30000); + settings.nix_build_user_prefix("nixbld".to_string()); + settings.nix_build_user_id_base(30001); + settings.channels( + channel + .into_iter() + .map(|ChannelValue(name, url)| (name, url)), + ); + settings.modify_profile(!no_modify_profile); + + let plan = InstallPlan::new(settings).await?; + + let json = serde_json::to_string_pretty(&plan)?; + let mut stdout = tokio::io::stdout(); + stdout.write_all(json.as_bytes()).await?; + Ok(ExitCode::SUCCESS) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0e2ad74..0e5c047 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ use std::{ }; pub use error::HarmonicError; +pub use plan::InstallPlan; +pub use settings::InstallSettings; use bytes::Buf; use glob::glob; diff --git a/src/plan.rs b/src/plan.rs index ed08b67..0d7357e 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -use crate::{settings::InstallSettings, actions::{Action, StartNixDaemonService, Actionable, ActionReceipt, Revertable}, HarmonicError}; +use crate::{settings::InstallSettings, actions::{Action, StartNixDaemonService, Actionable, ActionReceipt, Revertable, CreateUsers, ActionDescription}, HarmonicError}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -struct InstallPlan { +pub struct InstallPlan { settings: InstallSettings, /** Bootstrap the install @@ -26,15 +26,47 @@ struct InstallPlan { } impl InstallPlan { - async fn plan(settings: InstallSettings) -> Result { + pub fn description(&self) -> String { + format!("\ + This Nix install is for:\n\ + Operating System: {os_type}\n\ + Init system: {init_type}\n\ + Nix channels: {nix_channels}\n\ + \n\ + The following actions will be taken:\n\ + {actions} + ", + os_type = "Linux", + init_type = "systemd", + nix_channels = self.settings.channels.iter().map(|(name,url)| format!("{name}={url}")).collect::>().join(","), + actions = self.actions.iter().flat_map(|action| action.description()).map(|desc| { + let ActionDescription { + description, + explanation, + } = desc; + + let mut buf = String::default(); + buf.push_str(&format!("* {description}\n")); + if self.settings.explain { + for line in explanation { + buf.push_str(&format!(" {line}\n")); + } + } + buf + }).collect::>().join("\n"), + ) + } + pub async fn new(settings: InstallSettings) -> Result { let start_nix_daemon_service = StartNixDaemonService::plan(); + let create_users = CreateUsers::plan(settings.nix_build_user_prefix.clone(), settings.nix_build_user_id_base, settings.daemon_user_count); let actions = vec![ + Action::CreateUsers(create_users), Action::StartNixDaemonService(start_nix_daemon_service), ]; Ok(Self { settings, actions }) } - async fn install(self) -> Result { + pub async fn install(self) -> Result { let mut receipt = Receipt::default(); // This is **deliberately sequential**. // Actions which are parallelizable are represented by "group actions" like CreateUsers @@ -64,6 +96,6 @@ impl InstallPlan { } #[derive(Default, Debug, Serialize, Deserialize)] -struct Receipt { +pub struct Receipt { actions: Vec, } diff --git a/src/settings.rs b/src/settings.rs index e690dcc..bd98d55 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,19 +1,24 @@ use url::Url; -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Default)] pub struct InstallSettings { - dry_run: bool, - daemon_user_count: usize, - channels: Vec<(String, Url)>, - modify_profile: bool, - nix_build_group_name: String, - nix_build_group_id: usize, - nix_build_user_prefix: String, - nix_build_user_id_base: usize, + pub(crate) dry_run: bool, + pub(crate) explain: bool, + pub(crate) daemon_user_count: usize, + pub(crate) channels: Vec<(String, Url)>, + pub(crate) modify_profile: bool, + pub(crate) nix_build_group_name: String, + pub(crate) nix_build_group_id: usize, + pub(crate) nix_build_user_prefix: String, + pub(crate) nix_build_user_id_base: usize, } // Builder Pattern impl InstallSettings { + pub fn explain(&mut self, explain: bool) -> &mut Self { + self.explain = explain; + self + } pub fn dry_run(&mut self, dry_run: bool) -> &mut Self { self.dry_run = dry_run; self