diff --git a/Cargo.lock b/Cargo.lock index 5a21915..83d020f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,11 +437,10 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -621,6 +620,7 @@ dependencies = [ "nix", "owo-colors", "reqwest", + "serde", "tar", "target-lexicon", "tempdir", @@ -630,6 +630,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "url", "valuable", "walkdir", "xz2", @@ -729,11 +730,10 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -860,12 +860,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "memchr" version = "2.5.0" @@ -982,9 +976,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" @@ -1277,6 +1271,20 @@ name = "serde" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -1680,14 +1688,14 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 63fd5c7..5a3525c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,5 @@ bytes = "1.2.1" tar = "0.4.38" 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"] } diff --git a/src/actions/create_user.rs b/src/actions/create_user.rs new file mode 100644 index 0000000..48eaa2c --- /dev/null +++ b/src/actions/create_user.rs @@ -0,0 +1,48 @@ +use crate::{settings::InstallSettings, HarmonicError}; + +use super::{Actionable, ActionReceipt, Revertable}; + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUser { + name: String, + uid: usize, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUserReceipt { + name: String, + uid: usize, +} + +impl CreateUser { + pub fn plan(name: String, uid: usize) -> Self { + Self { name, uid } + } +} + +#[async_trait::async_trait] +impl<'a> Actionable<'a> for CreateUser { + fn description(&self) -> String { + todo!() + } + + async fn execute(self) -> Result { + let Self { name, uid } = self; + Ok(ActionReceipt::CreateUser(CreateUserReceipt { name, uid })) + } +} + + +#[async_trait::async_trait] +impl<'a> Revertable<'a> for CreateUserReceipt { + fn description(&self) -> String { + todo!() + } + + async fn revert(self) -> Result<(), HarmonicError> { + todo!(); + + Ok(()) + } +} diff --git a/src/actions/create_users.rs b/src/actions/create_users.rs new file mode 100644 index 0000000..d8680b6 --- /dev/null +++ b/src/actions/create_users.rs @@ -0,0 +1,90 @@ +use tokio::task::JoinSet; + +use crate::{settings::InstallSettings, HarmonicError}; + +use super::{Actionable, CreateUser, ActionReceipt, create_user::CreateUserReceipt, Revertable}; + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUsers { + children: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUsersReceipt { + children: Vec, +} + +impl CreateUsers { + 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 } + } +} + +#[async_trait::async_trait] +impl<'a> Actionable<'a> for CreateUsers { + fn description(&self) -> String { + todo!() + } + + async fn execute(self) -> Result { + // TODO(@hoverbear): Abstract this, it will be common + let Self { children } = self; + let mut set = JoinSet::new(); + let mut successes = Vec::with_capacity(children.len()); + let mut errors = Vec::default(); + + for child in children { + let _abort_handle = set.spawn(async move { child.execute().await }); + } + + while let Some(result) = set.join_next().await { + match result { + Ok(Ok(success)) => successes.push(success), + Ok(Err(e)) => errors.push(e), + Err(e) => errors.push(e.into()), + }; + } + + if !errors.is_empty() { + // If we got an error in a child, we need to revert the successful ones: + let mut failed_reverts = Vec::default(); + for success in successes { + match success.revert().await { + Ok(()) => (), + Err(e) => failed_reverts.push(e), + } + } + + if !failed_reverts.is_empty() { + return Err(HarmonicError::FailedReverts(errors, failed_reverts)); + } + + if errors.len() == 1 { + return Err(errors.into_iter().next().unwrap()) + } else { + return Err(HarmonicError::Multiple(errors)) + } + } + + Ok(ActionReceipt::CreateUsers(CreateUsersReceipt{ children: successes })) + } +} + + +#[async_trait::async_trait] +impl<'a> Revertable<'a> for CreateUsersReceipt { + fn description(&self) -> String { + todo!() + } + + async fn revert(self) -> Result<(), HarmonicError> { + todo!(); + + Ok(()) + } +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..73f9573 --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,73 @@ +mod start_nix_daemon_service; +mod create_users; +mod create_user; +pub use start_nix_daemon_service::{StartNixDaemonService, StartNixDaemonServiceReceipt}; +pub use create_user::{CreateUser, CreateUserReceipt}; +pub use create_users::{CreateUsers, CreateUsersReceipt}; + +use crate::{HarmonicError, settings::InstallSettings}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub enum Action { + CreateUsers(CreateUsers), + CreateUser(CreateUser), + StartNixDaemonService(StartNixDaemonService), +} + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub enum ActionReceipt { + CreateUsers(CreateUsersReceipt), + CreateUser(CreateUserReceipt), + StartNixDaemonService(StartNixDaemonServiceReceipt), +} + +#[async_trait::async_trait] +impl<'a> Actionable<'a> for Action { + fn description(&self) -> String { + match self { + Action::StartNixDaemonService(i) => i.description(), + Action::CreateUser(i) => i.description(), + Action::CreateUsers(i) => i.description(), + } + } + + async fn execute(self) -> Result { + match self { + Action::StartNixDaemonService(i) => i.execute().await, + Action::CreateUser(i) => i.execute().await, + Action::CreateUsers(i) => i.execute().await, + } + } +} + +#[async_trait::async_trait] +impl<'a> Revertable<'a> for ActionReceipt { + fn description(&self) -> String { + match self { + ActionReceipt::StartNixDaemonService(i) => i.description(), + ActionReceipt::CreateUser(i) => i.description(), + ActionReceipt::CreateUsers(i) => i.description(), + } + } + + async fn revert(self) -> Result<(), HarmonicError> { + match self { + ActionReceipt::StartNixDaemonService(i) => i.revert().await, + ActionReceipt::CreateUser(i) => i.revert().await, + ActionReceipt::CreateUsers(i) => i.revert().await, + } + } +} + +#[async_trait::async_trait] +pub trait Actionable<'a>: serde::de::Deserialize<'a> + serde::Serialize { + fn description(&self) -> String; + async fn execute(self) -> Result; +} + +#[async_trait::async_trait] +pub trait Revertable<'a>: serde::de::Deserialize<'a> + serde::Serialize { + fn description(&self) -> String; + async fn revert(self) -> Result<(), HarmonicError>; +} diff --git a/src/actions/start_nix_daemon_service.rs b/src/actions/start_nix_daemon_service.rs new file mode 100644 index 0000000..5a7cd51 --- /dev/null +++ b/src/actions/start_nix_daemon_service.rs @@ -0,0 +1,45 @@ +use crate::{settings::InstallSettings, HarmonicError}; + +use super::{Actionable, ActionReceipt, Revertable}; + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct StartNixDaemonService { + +} + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct StartNixDaemonServiceReceipt { +} + +impl StartNixDaemonService { + pub fn plan() -> Self { + Self {} + } +} + +#[async_trait::async_trait] +impl<'a> Actionable<'a> for StartNixDaemonService { + fn description(&self) -> String { + todo!() + } + + async fn execute(self) -> Result { + todo!() + } +} + + +#[async_trait::async_trait] +impl<'a> Revertable<'a> for StartNixDaemonServiceReceipt { + fn description(&self) -> String { + todo!() + } + + async fn revert(self) -> Result<(), HarmonicError> { + todo!(); + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 6f06615..3dc83a4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use serde::de::value::Error; + #[derive(thiserror::Error, Debug)] pub enum HarmonicError { #[error("Request error")] @@ -47,4 +49,8 @@ pub enum HarmonicError { GroupId(String, nix::errno::Errno), #[error("Getting group `{0}`")] NoGroup(String), + #[error("Errors with additional failures during reverts: {}\nDuring Revert:{}", .0.iter().map(|v| format!("{v}")).collect::>().join(" & "), .1.iter().map(|v| format!("{v}")).collect::>().join(" & "))] + FailedReverts(Vec, Vec), + #[error("Multiple errors: {}", .0.iter().map(|v| format!("{v}")).collect::>().join(" & "))] + Multiple(Vec) } diff --git a/src/lib.rs b/src/lib.rs index e38fb2e..0e2ad74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,8 @@ mod error; +mod actions; +mod plan; +mod settings; + use std::{ ffi::OsStr, fs::Permissions, diff --git a/src/plan.rs b/src/plan.rs new file mode 100644 index 0000000..ed08b67 --- /dev/null +++ b/src/plan.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; + +use crate::{settings::InstallSettings, actions::{Action, StartNixDaemonService, Actionable, ActionReceipt, Revertable}, HarmonicError}; + + + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +struct InstallPlan { + settings: InstallSettings, + + /** Bootstrap the install + + * There are roughly three phases: + * download_nix --------------------------------------> move_downloaded_nix + * create_group -> create_users -> create_directories -> move_downloaded_nix + * place_channel_configuration + * place_nix_configuration + * --- + * setup_default_profile + * configure_nix_daemon_service + * configure_shell_profile + * --- + * start_nix_daemon_service + */ + actions: Vec, +} + +impl InstallPlan { + async fn plan(settings: InstallSettings) -> Result { + let start_nix_daemon_service = StartNixDaemonService::plan(); + + let actions = vec![ + Action::StartNixDaemonService(start_nix_daemon_service), + ]; + Ok(Self { settings, actions }) + } + 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 + // The plan itself represents the concept of the sequence of stages. + for action in self.actions { + match action.execute().await { + Ok(action_receipt) => receipt.actions.push(action_receipt), + Err(err) => { + let mut revert_errs = Vec::default(); + + for action_receipt in receipt.actions { + if let Err(err) = action_receipt.revert().await { + revert_errs.push(err); + } + } + if !revert_errs.is_empty() { + return Err(HarmonicError::FailedReverts(vec![err], revert_errs)) + } + + return Err(err) + + }, + }; + } + Ok(receipt) + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +struct Receipt { + actions: Vec, +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..e690dcc --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,55 @@ +use url::Url; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +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, +} + +// Builder Pattern +impl InstallSettings { + pub fn dry_run(&mut self, dry_run: bool) -> &mut Self { + self.dry_run = dry_run; + self + } + pub fn daemon_user_count(&mut self, count: usize) -> &mut Self { + self.daemon_user_count = count; + self + } + + pub fn channels(&mut self, channels: impl IntoIterator) -> &mut Self { + self.channels = channels.into_iter().collect(); + self + } + + pub fn modify_profile(&mut self, toggle: bool) -> &mut Self { + self.modify_profile = toggle; + self + } + + pub fn nix_build_group_name(&mut self, val: String) -> &mut Self { + self.nix_build_group_name = val; + self + } + + pub fn nix_build_group_id(&mut self, count: usize) -> &mut Self { + self.nix_build_group_id = count; + self + } + + pub fn nix_build_user_prefix(&mut self, val: String) -> &mut Self { + self.nix_build_user_prefix = val; + self + } + + pub fn nix_build_user_id_base(&mut self, count: usize) -> &mut Self { + self.nix_build_user_id_base = count; + self + } +} \ No newline at end of file