Make plans versioned (#62)
* Make plans versioned * Delint * speeeeeeeeling * remove file that was dead
This commit is contained in:
parent
ad44b85c97
commit
7cc71f1ccd
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -797,6 +797,7 @@ dependencies = [
|
|||
"plist",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
|
@ -1633,6 +1634,15 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.147"
|
||||
|
|
|
@ -49,3 +49,4 @@ erased-serde = "0.3.23"
|
|||
typetag = "0.2.3"
|
||||
dyn-clone = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
semver = { version = "1.0.14", features = ["serde"] }
|
||||
|
|
|
@ -23,7 +23,7 @@ pub struct ConfigureNix {
|
|||
impl ConfigureNix {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(
|
||||
settings: CommonSettings,
|
||||
settings: &CommonSettings,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let channels: Vec<(String, Url)> = settings
|
||||
.channels
|
||||
|
@ -44,8 +44,8 @@ impl ConfigureNix {
|
|||
let place_channel_configuration =
|
||||
PlaceChannelConfiguration::plan(channels, settings.force).await?;
|
||||
let place_nix_configuration = PlaceNixConfiguration::plan(
|
||||
settings.nix_build_group_name,
|
||||
settings.extra_conf,
|
||||
settings.nix_build_group_name.clone(),
|
||||
settings.extra_conf.clone(),
|
||||
settings.force,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -23,7 +23,7 @@ pub struct ProvisionNix {
|
|||
impl ProvisionNix {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(
|
||||
settings: CommonSettings,
|
||||
settings: &CommonSettings,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let fetch_nix = FetchNix::plan(
|
||||
settings.nix_package_url.clone(),
|
||||
|
|
|
@ -83,7 +83,7 @@ impl CommandExecute for Install {
|
|||
existing_receipt
|
||||
} ,
|
||||
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()
|
||||
.await
|
||||
.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")),
|
||||
};
|
||||
|
|
105
src/plan.rs
105
src/plan.rs
|
@ -1,33 +1,51 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crossterm::style::Stylize;
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
action::{Action, ActionDescription, ActionImplementation},
|
||||
planner::Planner,
|
||||
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";
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct InstallPlan {
|
||||
#[serde(deserialize_with = "ensure_version")]
|
||||
pub(crate) version: Version,
|
||||
|
||||
pub(crate) actions: Vec<Box<dyn Action>>,
|
||||
|
||||
pub(crate) planner: Box<dyn Planner>,
|
||||
}
|
||||
|
||||
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)]
|
||||
pub fn describe_execute(
|
||||
&self,
|
||||
explain: bool,
|
||||
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let Self { planner, actions } = self;
|
||||
let Self {
|
||||
planner,
|
||||
actions,
|
||||
version,
|
||||
} = self;
|
||||
let buf = format!(
|
||||
"\
|
||||
Nix install plan\n\
|
||||
Nix install plan (v{version})\n\
|
||||
\n\
|
||||
Planner: {planner}\n\
|
||||
\n\
|
||||
|
@ -82,6 +100,7 @@ impl InstallPlan {
|
|||
cancel_channel: impl Into<Option<Receiver<()>>>,
|
||||
) -> Result<(), HarmonicError> {
|
||||
let Self {
|
||||
version: _,
|
||||
actions,
|
||||
planner: _,
|
||||
} = self;
|
||||
|
@ -119,7 +138,11 @@ impl InstallPlan {
|
|||
&self,
|
||||
explain: bool,
|
||||
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let Self { planner, actions } = self;
|
||||
let Self {
|
||||
version: _,
|
||||
planner,
|
||||
actions,
|
||||
} = self;
|
||||
let buf = format!(
|
||||
"\
|
||||
Nix uninstall plan\n\
|
||||
|
@ -178,6 +201,7 @@ impl InstallPlan {
|
|||
cancel_channel: impl Into<Option<Receiver<()>>>,
|
||||
) -> Result<(), HarmonicError> {
|
||||
let Self {
|
||||
version: _,
|
||||
actions,
|
||||
planner: _,
|
||||
} = self;
|
||||
|
@ -222,3 +246,70 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), HarmonicError> {
|
|||
.map_err(|e| HarmonicError::RecordingReceipt(install_receipt_path, e))?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
execute_command,
|
||||
os::darwin::DiskUtilOutput,
|
||||
planner::{BuiltinPlannerError, Planner},
|
||||
BuiltinPlanner, CommonSettings, InstallPlan,
|
||||
Action, BuiltinPlanner, CommonSettings,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
|
||||
|
@ -67,11 +67,9 @@ impl Planner for DarwinMulti {
|
|||
})
|
||||
}
|
||||
|
||||
async fn plan(
|
||||
mut self,
|
||||
) -> Result<crate::InstallPlan, Box<dyn std::error::Error + Sync + Send>> {
|
||||
self.root_disk = match self.root_disk {
|
||||
root_disk @ Some(_) => root_disk,
|
||||
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let root_disk = match &self.root_disk {
|
||||
root_disk @ Some(_) => root_disk.clone(),
|
||||
None => {
|
||||
let buf = execute_command(
|
||||
Command::new("/usr/sbin/diskutil")
|
||||
|
@ -99,29 +97,24 @@ impl Planner for DarwinMulti {
|
|||
false
|
||||
};
|
||||
|
||||
Ok(InstallPlan {
|
||||
planner: Box::new(self.clone()),
|
||||
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
|
||||
Box::new(
|
||||
CreateApfsVolume::plan(
|
||||
self.root_disk.unwrap(), /* We just ensured it was populated */
|
||||
self.volume_label,
|
||||
false,
|
||||
encrypt,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
Box::new(ProvisionNix::plan(self.settings.clone()).await?),
|
||||
Box::new(ConfigureNix::plan(self.settings).await?),
|
||||
Box::new(
|
||||
KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).await?,
|
||||
),
|
||||
],
|
||||
})
|
||||
Ok(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
|
||||
Box::new(
|
||||
CreateApfsVolume::plan(
|
||||
root_disk.unwrap(), /* We just ensured it was populated */
|
||||
self.volume_label.clone(),
|
||||
false,
|
||||
encrypt,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
Box::new(ProvisionNix::plan(&self.settings).await?),
|
||||
Box::new(ConfigureNix::plan(&self.settings).await?),
|
||||
Box::new(KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).await?),
|
||||
])
|
||||
}
|
||||
|
||||
fn settings(
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
common::{ConfigureNix, ProvisionNix},
|
||||
},
|
||||
planner::Planner,
|
||||
BuiltinPlanner, CommonSettings, InstallPlan,
|
||||
Action, BuiltinPlanner, CommonSettings,
|
||||
};
|
||||
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
|
||||
// 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() {
|
||||
|
@ -40,26 +40,23 @@ impl Planner for LinuxMulti {
|
|||
return Err(Error::NixExists.into());
|
||||
}
|
||||
|
||||
Ok(InstallPlan {
|
||||
planner: Box::new(self.clone()),
|
||||
actions: vec![
|
||||
Box::new(
|
||||
CreateDirectory::plan("/nix", None, None, 0o0755, true)
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
Box::new(
|
||||
ProvisionNix::plan(self.settings.clone())
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
Box::new(
|
||||
ConfigureNix::plan(self.settings)
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
],
|
||||
})
|
||||
Ok(vec![
|
||||
Box::new(
|
||||
CreateDirectory::plan("/nix", None, None, 0o0755, true)
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
Box::new(
|
||||
ProvisionNix::plan(&self.settings.clone())
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
Box::new(
|
||||
ConfigureNix::plan(&self.settings)
|
||||
.await
|
||||
.map_err(|v| Error::Action(v.into()))?,
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn settings(
|
||||
|
|
|
@ -4,7 +4,7 @@ pub mod specific;
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{settings::InstallSettingsError, BoxableError, InstallPlan};
|
||||
use crate::{settings::InstallSettingsError, Action, BoxableError};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[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>>
|
||||
where
|
||||
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(
|
||||
&self,
|
||||
) -> 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 {
|
||||
BuiltinPlanner::LinuxMulti(planner) => planner.plan().await,
|
||||
BuiltinPlanner::DarwinMulti(planner) => planner.plan().await,
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
linux::{CreateSystemdSysext, StartSystemdUnit},
|
||||
},
|
||||
planner::Planner,
|
||||
BuiltinPlanner, CommonSettings, InstallPlan,
|
||||
Action, BuiltinPlanner, CommonSettings,
|
||||
};
|
||||
|
||||
#[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>> {
|
||||
Ok(InstallPlan {
|
||||
planner: Box::new(self.clone()),
|
||||
actions: vec![
|
||||
Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?),
|
||||
Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?),
|
||||
Box::new(ProvisionNix::plan(self.settings.clone()).await?),
|
||||
Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?),
|
||||
],
|
||||
})
|
||||
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
Ok(vec![
|
||||
Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?),
|
||||
Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?),
|
||||
Box::new(ProvisionNix::plan(&self.settings.clone()).await?),
|
||||
Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?),
|
||||
])
|
||||
}
|
||||
|
||||
fn settings(
|
||||
|
|
Loading…
Reference in a new issue