Merge pull request #39 from DeterminateSystems/hoverbear/ds-411-install-should-detect-existing

Install can detect existing /nix/receipt.json
This commit is contained in:
Ana Hobden 2022-11-09 15:43:15 -08:00 committed by GitHub
commit 460c0ba8c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 186 additions and 21 deletions

View file

@ -184,4 +184,8 @@ impl Action for ConfigureNix {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }

View file

@ -221,6 +221,10 @@ impl Action for ConfigureNixDaemonService {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -181,6 +181,10 @@ impl Action for ConfigureShellProfile {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -214,6 +214,10 @@ impl Action for CreateDirectory {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -188,6 +188,10 @@ impl Action for CreateFile {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -160,6 +160,10 @@ impl Action for CreateGroup {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -134,6 +134,10 @@ impl Action for CreateNixTree {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -223,6 +223,10 @@ impl Action for CreateOrAppendFile {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -254,6 +254,10 @@ impl Action for CreateUser {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -229,6 +229,10 @@ impl Action for CreateUsersAndGroup {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -115,6 +115,10 @@ impl Action for FetchNix {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -106,6 +106,10 @@ impl Action for MoveUnpackedNix {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -130,6 +130,10 @@ impl Action for PlaceChannelConfiguration {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -115,6 +115,10 @@ impl Action for PlaceNixConfiguration {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -168,6 +168,10 @@ impl Action for ProvisionNix {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -181,6 +181,10 @@ impl Action for SetupDefaultProfile {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -106,6 +106,10 @@ impl Action for BootstrapVolume {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -289,6 +289,10 @@ impl Action for CreateApfsVolume {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -98,6 +98,10 @@ impl Action for CreateSyntheticObjects {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -130,6 +130,10 @@ impl Action for CreateVolume {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -109,6 +109,10 @@ impl Action for EnableOwnership {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -86,6 +86,10 @@ impl Action for EncryptVolume {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -96,6 +96,10 @@ impl Action for KickstartLaunchctlService {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -117,6 +117,10 @@ impl Action for UnmountVolume {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -185,6 +185,10 @@ impl Action for CreateSystemdSysext {
*action_state = ActionState::Uncompleted; *action_state = ActionState::Uncompleted;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -102,6 +102,10 @@ impl Action for StartSystemdUnit {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -102,6 +102,10 @@ impl Action for SystemdSysextMerge {
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View file

@ -13,11 +13,12 @@ pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
// They should also have an `async fn plan(args...) -> Result<ActionState<Self>, Box<dyn std::error::Error + Send + Sync>>;` // They should also have an `async fn plan(args...) -> Result<ActionState<Self>, Box<dyn std::error::Error + Send + Sync>>;`
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>; async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>; async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn action_state(&self) -> ActionState;
} }
dyn_clone::clone_trait_object!(Action); dyn_clone::clone_trait_object!(Action);
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)]
pub enum ActionState { pub enum ActionState {
Completed, Completed,
// Only applicable to meta-actions that start multiple sub-actions. // Only applicable to meta-actions that start multiple sub-actions.

View file

@ -1,6 +1,9 @@
use std::{path::PathBuf, process::ExitCode}; use std::{
path::{Path, PathBuf},
process::ExitCode,
};
use crate::BuiltinPlanner; use crate::{action::ActionState, plan::RECEIPT_LOCATION, BuiltinPlanner, InstallPlan, Planner};
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr}; use eyre::{eyre, WrapErr};
@ -8,7 +11,7 @@ use crate::{cli::CommandExecute, interaction};
/// Execute an install (possibly using an existing plan) /// Execute an install (possibly using an existing plan)
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(args_conflicts_with_subcommands = true)] #[command(args_conflicts_with_subcommands = true, arg_required_else_help = true)]
pub struct Install { pub struct Install {
#[clap( #[clap(
long, long,
@ -29,7 +32,7 @@ pub struct Install {
pub plan: Option<PathBuf>, pub plan: Option<PathBuf>,
#[clap(subcommand)] #[clap(subcommand)]
pub planner: BuiltinPlanner, pub planner: Option<BuiltinPlanner>,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -43,30 +46,68 @@ impl CommandExecute for Install {
explain, explain,
} = self; } = self;
let mut plan = match &plan { let existing_receipt: Option<InstallPlan> = match Path::new(RECEIPT_LOCATION).exists() {
Some(plan_path) => { true => {
let install_plan_string = tokio::fs::read_to_string(&plan_path) let install_plan_string = tokio::fs::read_to_string(&RECEIPT_LOCATION)
.await .await
.wrap_err("Reading plan")?; .wrap_err("Reading plan")?;
Some(serde_json::from_str(&install_plan_string)?)
},
false => None,
};
let mut install_plan = match (planner, plan) {
(Some(planner), None) => {
let chosen_planner: Box<dyn Planner> = planner.clone().boxed();
match existing_receipt {
Some(existing_receipt) => {
if existing_receipt.planner.typetag_name() != chosen_planner.typetag_name() {
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install"))
}
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != chosen_planner.settings().map_err(|e| eyre!(e))? {
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install"))
}
if existing_receipt.actions.iter().all(|v| v.action_state() == ActionState::Completed) {
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling and reinstalling if Nix isn't working"))
}
existing_receipt
} ,
None => {
planner.plan().await.map_err(|e| eyre!(e))?
},
}
},
(None, Some(plan_path)) => {
let install_plan_string = tokio::fs::read_to_string(&plan_path)
.await
.wrap_err("Reading plan")?;
serde_json::from_str(&install_plan_string)? serde_json::from_str(&install_plan_string)?
}, },
None => planner.plan().await.map_err(|e| eyre!(e))?, (None, None) => return Err(eyre!("`--plan` or a planner is required")),
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
}; };
if !no_confirm { if !no_confirm {
if !interaction::confirm(plan.describe_execute(explain).map_err(|e| eyre!(e))?).await? { if !interaction::confirm(
install_plan
.describe_execute(explain)
.map_err(|e| eyre!(e))?,
)
.await?
{
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
} }
} }
if let Err(err) = plan.install().await { if let Err(err) = install_plan.install().await {
let error = eyre!(err).wrap_err("Install failure"); let error = eyre!(err).wrap_err("Install failure");
if !no_confirm { if !no_confirm {
tracing::error!("{:?}", error); tracing::error!("{:?}", error);
if !interaction::confirm(plan.describe_revert(explain)).await? { if !interaction::confirm(install_plan.describe_revert(explain)).await? {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
} }
plan.revert().await? install_plan.revert().await?
} else { } else {
return Err(error); return Err(error);
} }

View file

@ -1,6 +1,6 @@
use std::{path::PathBuf, process::ExitCode}; use std::{path::PathBuf, process::ExitCode};
use crate::InstallPlan; use crate::{plan::RECEIPT_LOCATION, InstallPlan};
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::WrapErr; use eyre::WrapErr;
@ -23,7 +23,7 @@ pub struct Uninstall {
global = true global = true
)] )]
pub explain: bool, pub explain: bool,
#[clap(default_value = "/nix/receipt.json")] #[clap(default_value = RECEIPT_LOCATION)]
pub receipt: PathBuf, pub receipt: PathBuf,
} }

View file

@ -8,6 +8,8 @@ use crate::{
HarmonicError, HarmonicError,
}; };
pub const RECEIPT_LOCATION: &str = "/nix/receipt.json";
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct InstallPlan { pub struct InstallPlan {
pub(crate) actions: Vec<Box<dyn Action>>, pub(crate) actions: Vec<Box<dyn Action>>,
@ -43,7 +45,7 @@ impl InstallPlan {
}, },
planner = planner.typetag_name(), planner = planner.typetag_name(),
plan_settings = planner plan_settings = planner
.describe()? .settings()?
.into_iter() .into_iter()
.map(|(k, v)| format!("* {k}: {v}", k = k.bold().white())) .map(|(k, v)| format!("* {k}: {v}", k = k.bold().white()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -166,7 +168,7 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), HarmonicError> {
tokio::fs::create_dir_all("/nix") tokio::fs::create_dir_all("/nix")
.await .await
.map_err(|e| HarmonicError::RecordingReceipt(PathBuf::from("/nix"), e))?; .map_err(|e| HarmonicError::RecordingReceipt(PathBuf::from("/nix"), e))?;
let install_receipt_path = PathBuf::from("/nix/receipt.json"); let install_receipt_path = PathBuf::from(RECEIPT_LOCATION);
let self_json = let self_json =
serde_json::to_string_pretty(&plan).map_err(HarmonicError::SerializingReceipt)?; serde_json::to_string_pretty(&plan).map_err(HarmonicError::SerializingReceipt)?;
tokio::fs::write(&install_receipt_path, self_json) tokio::fs::write(&install_receipt_path, self_json)

View file

@ -98,7 +98,7 @@ impl Planner for DarwinMulti {
}) })
} }
fn describe( fn settings(
&self, &self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {

View file

@ -35,7 +35,7 @@ impl Planner for LinuxMulti {
}) })
} }
fn describe( fn settings(
&self, &self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { settings } = self; let Self { settings } = self;

View file

@ -13,9 +13,15 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
where where
Self: Sized; Self: Sized;
async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>>; async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>>;
fn describe( fn settings(
&self, &self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>>; ) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>>;
fn boxed(self) -> Box<dyn Planner>
where
Self: Sized + 'static,
{
Box::new(self)
}
} }
dyn_clone::clone_trait_object!(Planner); dyn_clone::clone_trait_object!(Planner);
@ -56,6 +62,13 @@ impl BuiltinPlanner {
BuiltinPlanner::SteamDeck(planner) => planner.plan().await, BuiltinPlanner::SteamDeck(planner) => planner.plan().await,
} }
} }
pub fn boxed(self) -> Box<dyn Planner> {
match self {
BuiltinPlanner::LinuxMulti(i) => i.boxed(),
BuiltinPlanner::DarwinMulti(i) => i.boxed(),
BuiltinPlanner::SteamDeck(i) => i.boxed(),
}
}
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]

View file

@ -36,7 +36,7 @@ impl Planner for SteamDeck {
}) })
} }
fn describe( fn settings(
&self, &self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { settings } = self; let Self { settings } = self;