Make plans versioned (#62)

* Make plans versioned

* Delint

* speeeeeeeeling

* remove file that was dead
This commit is contained in:
Ana Hobden 2022-11-25 11:46:38 -08:00 committed by GitHub
parent ad44b85c97
commit 7cc71f1ccd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 78 deletions

10
Cargo.lock generated
View file

@ -797,6 +797,7 @@ dependencies = [
"plist", "plist",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
"semver",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@ -1633,6 +1634,15 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "semver"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.147" version = "1.0.147"

View file

@ -49,3 +49,4 @@ erased-serde = "0.3.23"
typetag = "0.2.3" typetag = "0.2.3"
dyn-clone = "1.0.9" dyn-clone = "1.0.9"
rand = "0.8.5" rand = "0.8.5"
semver = { version = "1.0.14", features = ["serde"] }

View file

@ -23,7 +23,7 @@ pub struct ConfigureNix {
impl ConfigureNix { impl ConfigureNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
settings: CommonSettings, settings: &CommonSettings,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let channels: Vec<(String, Url)> = settings let channels: Vec<(String, Url)> = settings
.channels .channels
@ -44,8 +44,8 @@ impl ConfigureNix {
let place_channel_configuration = let place_channel_configuration =
PlaceChannelConfiguration::plan(channels, settings.force).await?; PlaceChannelConfiguration::plan(channels, settings.force).await?;
let place_nix_configuration = PlaceNixConfiguration::plan( let place_nix_configuration = PlaceNixConfiguration::plan(
settings.nix_build_group_name, settings.nix_build_group_name.clone(),
settings.extra_conf, settings.extra_conf.clone(),
settings.force, settings.force,
) )
.await?; .await?;

View file

@ -23,7 +23,7 @@ pub struct ProvisionNix {
impl ProvisionNix { impl ProvisionNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
settings: CommonSettings, settings: &CommonSettings,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let fetch_nix = FetchNix::plan( let fetch_nix = FetchNix::plan(
settings.nix_package_url.clone(), settings.nix_package_url.clone(),

View file

@ -83,7 +83,7 @@ impl CommandExecute for Install {
existing_receipt existing_receipt
} , } ,
None => { None => {
planner.plan().await.map_err(|e| eyre!(e))? InstallPlan::plan(planner.boxed()).await.map_err(|e| eyre!(e))?
}, },
} }
}, },
@ -97,7 +97,7 @@ impl CommandExecute for Install {
let builtin_planner = BuiltinPlanner::default() let builtin_planner = BuiltinPlanner::default()
.await .await
.map_err(|e| eyre::eyre!(e))?; .map_err(|e| eyre::eyre!(e))?;
builtin_planner.plan().await.map_err(|e| eyre!(e))? InstallPlan::plan(builtin_planner.boxed()).await.map_err(|e| eyre!(e))?
}, },
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")), (Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
}; };

View file

@ -1,33 +1,51 @@
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
use crossterm::style::Stylize;
use tokio::sync::broadcast::Receiver;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionImplementation}, action::{Action, ActionDescription, ActionImplementation},
planner::Planner, planner::Planner,
HarmonicError, HarmonicError,
}; };
use crossterm::style::Stylize;
use semver::{Version, VersionReq};
use serde::{de::Error, Deserialize, Deserializer};
use tokio::sync::broadcast::Receiver;
pub const RECEIPT_LOCATION: &str = "/nix/receipt.json"; 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 {
#[serde(deserialize_with = "ensure_version")]
pub(crate) version: Version,
pub(crate) actions: Vec<Box<dyn Action>>, pub(crate) actions: Vec<Box<dyn Action>>,
pub(crate) planner: Box<dyn Planner>, pub(crate) planner: Box<dyn Planner>,
} }
impl InstallPlan { impl InstallPlan {
pub async fn plan(
planner: Box<dyn Planner>,
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let actions = planner.plan().await?;
Ok(Self {
planner,
actions,
version: current_version()?,
})
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn describe_execute( pub fn describe_execute(
&self, &self,
explain: bool, explain: bool,
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
let Self { planner, actions } = self; let Self {
planner,
actions,
version,
} = self;
let buf = format!( let buf = format!(
"\ "\
Nix install plan\n\ Nix install plan (v{version})\n\
\n\ \n\
Planner: {planner}\n\ Planner: {planner}\n\
\n\ \n\
@ -82,6 +100,7 @@ impl InstallPlan {
cancel_channel: impl Into<Option<Receiver<()>>>, cancel_channel: impl Into<Option<Receiver<()>>>,
) -> Result<(), HarmonicError> { ) -> Result<(), HarmonicError> {
let Self { let Self {
version: _,
actions, actions,
planner: _, planner: _,
} = self; } = self;
@ -119,7 +138,11 @@ impl InstallPlan {
&self, &self,
explain: bool, explain: bool,
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
let Self { planner, actions } = self; let Self {
version: _,
planner,
actions,
} = self;
let buf = format!( let buf = format!(
"\ "\
Nix uninstall plan\n\ Nix uninstall plan\n\
@ -178,6 +201,7 @@ impl InstallPlan {
cancel_channel: impl Into<Option<Receiver<()>>>, cancel_channel: impl Into<Option<Receiver<()>>>,
) -> Result<(), HarmonicError> { ) -> Result<(), HarmonicError> {
let Self { let Self {
version: _,
actions, actions,
planner: _, planner: _,
} = self; } = self;
@ -222,3 +246,70 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), HarmonicError> {
.map_err(|e| HarmonicError::RecordingReceipt(install_receipt_path, e))?; .map_err(|e| HarmonicError::RecordingReceipt(install_receipt_path, e))?;
Result::<(), HarmonicError>::Ok(()) Result::<(), HarmonicError>::Ok(())
} }
fn current_version() -> Result<Version, semver::Error> {
let harmonic_version_str = env!("CARGO_PKG_VERSION");
Version::from_str(harmonic_version_str)
}
fn ensure_version<'de, D: Deserializer<'de>>(d: D) -> Result<Version, D::Error> {
let plan_version = Version::deserialize(d)?;
let req = VersionReq::parse(&plan_version.to_string()).map_err(|_e| {
D::Error::custom(&format!(
"Could not parse version `{plan_version}` as a version requirement, please report this",
))
})?;
let harmonic_version = current_version().map_err(|_e| {
D::Error::custom(&format!(
"Could not parse Harmonic's version `{}` as a valid version according to Semantic Versioning, therefore the plan version ({plan_version}) compatibility cannot be checked", env!("CARGO_PKG_VERSION")
))
})?;
if req.matches(&harmonic_version) {
Ok(plan_version)
} else {
Err(D::Error::custom(&format!(
"This version of Harmonic ({harmonic_version}) is not compatible with this plan's version ({plan_version}), please use a compatible version (according to Semantic Versioning)",
)))
}
}
#[cfg(test)]
mod test {
use semver::Version;
use crate::{planner::BuiltinPlanner, InstallPlan};
#[tokio::test]
async fn ensure_version_allows_compatible() -> eyre::Result<()> {
let planner = BuiltinPlanner::default()
.await
.map_err(|e| eyre::eyre!(e))?;
let good_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
let value = serde_json::json!({
"planner": planner.boxed(),
"version": good_version,
"actions": [],
});
let maybe_plan: Result<InstallPlan, serde_json::Error> = serde_json::from_value(value);
maybe_plan.unwrap();
Ok(())
}
#[tokio::test]
async fn ensure_version_denies_incompatible() -> eyre::Result<()> {
let planner = BuiltinPlanner::default()
.await
.map_err(|e| eyre::eyre!(e))?;
let bad_version = Version::parse("9999999999999.9999999999.99999999")?;
let value = serde_json::json!({
"planner": planner.boxed(),
"version": bad_version,
"actions": [],
});
let maybe_plan: Result<InstallPlan, serde_json::Error> = serde_json::from_value(value);
assert!(maybe_plan.is_err());
let err = maybe_plan.unwrap_err();
assert!(err.is_data());
Ok(())
}
}

View file

@ -11,7 +11,7 @@ use crate::{
execute_command, execute_command,
os::darwin::DiskUtilOutput, os::darwin::DiskUtilOutput,
planner::{BuiltinPlannerError, Planner}, planner::{BuiltinPlannerError, Planner},
BuiltinPlanner, CommonSettings, InstallPlan, Action, BuiltinPlanner, CommonSettings,
}; };
#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
@ -67,11 +67,9 @@ impl Planner for DarwinMulti {
}) })
} }
async fn plan( async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
mut self, let root_disk = match &self.root_disk {
) -> Result<crate::InstallPlan, Box<dyn std::error::Error + Sync + Send>> { root_disk @ Some(_) => root_disk.clone(),
self.root_disk = match self.root_disk {
root_disk @ Some(_) => root_disk,
None => { None => {
let buf = execute_command( let buf = execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
@ -99,29 +97,24 @@ impl Planner for DarwinMulti {
false false
}; };
Ok(InstallPlan { Ok(vec![
planner: Box::new(self.clone()), // Create Volume step:
actions: vec![ //
// Create Volume step: // setup_Synthetic -> create_synthetic_objects
// // Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership
// setup_Synthetic -> create_synthetic_objects Box::new(
// Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership CreateApfsVolume::plan(
Box::new( root_disk.unwrap(), /* We just ensured it was populated */
CreateApfsVolume::plan( self.volume_label.clone(),
self.root_disk.unwrap(), /* We just ensured it was populated */ false,
self.volume_label, encrypt,
false, )
encrypt, .await?,
) ),
.await?, Box::new(ProvisionNix::plan(&self.settings).await?),
), Box::new(ConfigureNix::plan(&self.settings).await?),
Box::new(ProvisionNix::plan(self.settings.clone()).await?), Box::new(KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).await?),
Box::new(ConfigureNix::plan(self.settings).await?), ])
Box::new(
KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).await?,
),
],
})
} }
fn settings( fn settings(

View file

@ -4,7 +4,7 @@ use crate::{
common::{ConfigureNix, ProvisionNix}, common::{ConfigureNix, ProvisionNix},
}, },
planner::Planner, planner::Planner,
BuiltinPlanner, CommonSettings, InstallPlan, Action, BuiltinPlanner, CommonSettings,
}; };
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
@ -23,7 +23,7 @@ impl Planner for LinuxMulti {
}) })
} }
async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>> { async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
// If on NixOS, running `harmonic` is pointless // If on NixOS, running `harmonic` is pointless
// NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145 // NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145
if Path::new("/etc/NIXOS").exists() { if Path::new("/etc/NIXOS").exists() {
@ -40,26 +40,23 @@ impl Planner for LinuxMulti {
return Err(Error::NixExists.into()); return Err(Error::NixExists.into());
} }
Ok(InstallPlan { Ok(vec![
planner: Box::new(self.clone()), Box::new(
actions: vec![ CreateDirectory::plan("/nix", None, None, 0o0755, true)
Box::new( .await
CreateDirectory::plan("/nix", None, None, 0o0755, true) .map_err(|v| Error::Action(v.into()))?,
.await ),
.map_err(|v| Error::Action(v.into()))?, Box::new(
), ProvisionNix::plan(&self.settings.clone())
Box::new( .await
ProvisionNix::plan(self.settings.clone()) .map_err(|v| Error::Action(v.into()))?,
.await ),
.map_err(|v| Error::Action(v.into()))?, Box::new(
), ConfigureNix::plan(&self.settings)
Box::new( .await
ConfigureNix::plan(self.settings) .map_err(|v| Error::Action(v.into()))?,
.await ),
.map_err(|v| Error::Action(v.into()))?, ])
),
],
})
} }
fn settings( fn settings(

View file

@ -4,7 +4,7 @@ pub mod specific;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{settings::InstallSettingsError, BoxableError, InstallPlan}; use crate::{settings::InstallSettingsError, Action, BoxableError};
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(tag = "planner")] #[typetag::serde(tag = "planner")]
@ -12,7 +12,7 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>>
where where
Self: Sized; Self: Sized;
async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>>; async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>>;
fn settings( 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>>;
@ -55,7 +55,9 @@ impl BuiltinPlanner {
} }
} }
pub async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>> { pub async fn plan(
self,
) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
match self { match self {
BuiltinPlanner::LinuxMulti(planner) => planner.plan().await, BuiltinPlanner::LinuxMulti(planner) => planner.plan().await,
BuiltinPlanner::DarwinMulti(planner) => planner.plan().await, BuiltinPlanner::DarwinMulti(planner) => planner.plan().await,

View file

@ -7,7 +7,7 @@ use crate::{
linux::{CreateSystemdSysext, StartSystemdUnit}, linux::{CreateSystemdSysext, StartSystemdUnit},
}, },
planner::Planner, planner::Planner,
BuiltinPlanner, CommonSettings, InstallPlan, Action, BuiltinPlanner, CommonSettings,
}; };
#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
@ -25,16 +25,13 @@ impl Planner for SteamDeck {
}) })
} }
async fn plan(self) -> Result<crate::InstallPlan, Box<dyn std::error::Error + Sync + Send>> { async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
Ok(InstallPlan { Ok(vec![
planner: Box::new(self.clone()), Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?),
actions: vec![ Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?),
Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?), Box::new(ProvisionNix::plan(&self.settings.clone()).await?),
Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?), Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?),
Box::new(ProvisionNix::plan(self.settings.clone()).await?), ])
Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?),
],
})
} }
fn settings( fn settings(