Ponder plan/revert

This commit is contained in:
Ana Hobden 2022-09-14 15:18:13 -07:00
parent 3fccbef2da
commit b04d26abf1
10 changed files with 417 additions and 17 deletions

42
Cargo.lock generated
View file

@ -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]]

View file

@ -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"] }

View file

@ -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<ActionReceipt, HarmonicError> {
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(())
}
}

View file

@ -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<CreateUser>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateUsersReceipt {
children: Vec<ActionReceipt>,
}
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<ActionReceipt, HarmonicError> {
// 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(())
}
}

73
src/actions/mod.rs Normal file
View file

@ -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<ActionReceipt, HarmonicError> {
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<ActionReceipt, HarmonicError>;
}
#[async_trait::async_trait]
pub trait Revertable<'a>: serde::de::Deserialize<'a> + serde::Serialize {
fn description(&self) -> String;
async fn revert(self) -> Result<(), HarmonicError>;
}

View file

@ -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<ActionReceipt, HarmonicError> {
todo!()
}
}
#[async_trait::async_trait]
impl<'a> Revertable<'a> for StartNixDaemonServiceReceipt {
fn description(&self) -> String {
todo!()
}
async fn revert(self) -> Result<(), HarmonicError> {
todo!();
Ok(())
}
}

View file

@ -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::<Vec<_>>().join(" & "), .1.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(" & "))]
FailedReverts(Vec<HarmonicError>, Vec<HarmonicError>),
#[error("Multiple errors: {}", .0.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(" & "))]
Multiple(Vec<HarmonicError>)
}

View file

@ -1,4 +1,8 @@
mod error;
mod actions;
mod plan;
mod settings;
use std::{
ffi::OsStr,
fs::Permissions,

69
src/plan.rs Normal file
View file

@ -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<Action>,
}
impl InstallPlan {
async fn plan(settings: InstallSettings) -> Result<Self, HarmonicError> {
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<Receipt, HarmonicError> {
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<ActionReceipt>,
}

55
src/settings.rs Normal file
View file

@ -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<Item = (String, Url)>) -> &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
}
}