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",
"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"

View file

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

View file

@ -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?;

View file

@ -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(),

View file

@ -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")),
};

View file

@ -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(())
}
}

View file

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

View file

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

View file

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

View file

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