lix-installer/src/action/mod.rs

228 lines
8.3 KiB
Rust
Raw Normal View History

/*! An executable or revertable step, possibly orcestrating sub-[`Action`]s using things like
[`JoinSet`](tokio::task::JoinSet)s
[`Action`]s should be considered an 'atom' of change. Typically they are either a 'base' or
a 'composite' [`Action`].
Base actions are things like:
* [`CreateDirectory`](base::CreateDirectory)
* [`CreateFile`](base::CreateFile)
* [`CreateUser`](base::CreateUser)
Composite actions are things like:
* [`CreateNixTree`](common::CreateNixTree)
* [`CreateUsersAndGroups`](common::CreateUsersAndGroups)
During their `plan` phase, [`Planner`](crate::planner::Planner)s call an [`Action`]s `plan` function, which may accept any
arguments. For example, several 'composite' actions accept a [`CommonSettings`](crate::settings::CommonSettings). Later, the
[`InstallPlan`](crate::InstallPlan) will call [`try_execute`](StatefulAction::try_execute) on the [`StatefulAction`].
You can manually plan, execute, then revert an [`Action`] like so:
```rust,no_run
# async fn wrapper() -> Result<(), harmonic::HarmonicError> {
use harmonic::action::base::CreateDirectory;
let mut action = CreateDirectory::plan("/nix", None, None, 0o0755, true).await?;
action.try_execute().await?;
action.try_revert().await?;
# Ok(())
# }
```
A general guidance for what determines how fine-grained an [`Action`] should be is the unit of
reversion. The [`ConfigureNixDaemonService`](linux::ConfigureNixDaemonService) action is a good
example of this,it takes several steps, such as running `systemd-tmpfiles`, and calling
`systemctl link` on some systemd units.
Where possible, tasks which could break during execution should be broken up, as uninstalling/installing
step detection is determined by the wrapping [`StatefulAction`]. If an [`Action`] is a 'composite'
its sub-[`Action`]s can be reverted piece-by-piece. So breaking up actions into faillable units is
ideal.
A custom [`Action`] can be created then used in a custom [`Planner`](crate::planner::Planner):
```rust,no_run
use std::{error::Error, collections::HashMap};
use harmonic::{
InstallPlan,
settings::{CommonSettings, InstallSettingsError},
planner::{Planner, PlannerError, specific::SteamDeck},
action::{Action, StatefulAction, ActionDescription},
};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MyAction {}
impl MyAction {
#[tracing::instrument(skip_all)]
pub async fn plan() -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self {}.into())
}
}
#[async_trait::async_trait]
#[typetag::serde(name = "my_action")]
impl Action for MyAction {
fn tracing_synopsis(&self) -> String {
"My action".to_string()
}
fn execute_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}
#[tracing::instrument(skip_all, fields(
// Tracing fields...
))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Execute steps ...
Ok(())
}
fn revert_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}
#[tracing::instrument(skip_all, fields(
// Tracing fields...
))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Revert steps...
Ok(())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MyPlanner {
pub common: CommonSettings,
}
#[async_trait::async_trait]
#[typetag::serde(name = "my-planner")]
impl Planner for MyPlanner {
async fn default() -> Result<Self, PlannerError> {
Ok(Self {
common: CommonSettings::default()?,
})
}
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
Ok(vec![
// ...
MyAction::plan()
.await
.map_err(PlannerError::Action)?.boxed(),
])
}
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
let Self { common } = self;
let mut map = std::collections::HashMap::default();
map.extend(common.settings()?.into_iter());
Ok(map)
}
}
# async fn custom_planner_install() -> color_eyre::Result<()> {
let planner = MyPlanner::default().await?;
let mut plan = InstallPlan::plan(planner).await?;
match plan.install(None).await {
Ok(()) => tracing::info!("Done"),
Err(e) => {
match e.source() {
Some(source) => tracing::error!("{e}: {}", source),
None => tracing::error!("{e}"),
};
plan.uninstall(None).await?;
},
};
# Ok(())
# }
```
*/
pub mod base;
2022-10-28 16:29:15 +00:00
pub mod common;
pub mod darwin;
pub mod linux;
mod stateful;
2022-09-15 19:11:46 +00:00
pub use stateful::{ActionState, StatefulAction};
/// An action which can be reverted or completed, with an action state
///
/// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities.
///
/// Instead of calling [`execute`][Action::execute] or [`revert`][Action::revert], you should prefer [`try_execute`][StatefulAction::try_execute] and [`try_revert`][StatefulAction::try_revert]
#[async_trait::async_trait]
#[typetag::serde(tag = "action")]
pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
/// A synopsis of the action for tracing purposes
fn tracing_synopsis(&self) -> String;
/// A description of what this action would do during execution
///
/// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_execute`] on those actions, not [`execute_description`][Action::execute_description].
///
/// This is called by [`InstallPlan::describe_install`](crate::InstallPlan::describe_install) through [`StatefulAction::describe_execute`] which will skip output if the action is completed.
fn execute_description(&self) -> Vec<ActionDescription>;
/// A description of what this action would do during revert
///
/// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_revert`] on those actions, not [`revert_description`][Action::revert_description].
///
/// This is called by [`InstallPlan::describe_uninstall`](crate::InstallPlan::describe_uninstall) through [`StatefulAction::describe_revert`] which will skip output if the action is completed.
fn revert_description(&self) -> Vec<ActionDescription>;
/// Perform any execution steps
///
/// If this action calls sub-[`Action`]s, care should be taken to call [`try_execute`][StatefulAction::try_execute], not [`execute`][Action::execute], so that [`ActionState`] is handled correctly and tracing is done.
///
/// This is called by [`InstallPlan::install`](crate::InstallPlan::install) through [`StatefulAction::try_execute`] which handles tracing as well as if the action needs to execute based on its `action_state`.
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// Perform any revert steps
///
/// If this action calls sub-[`Action`]s, care should be taken to call [`try_revert`][StatefulAction::try_revert], not [`revert`][Action::revert], so that [`ActionState`] is handled correctly and tracing is done.
///
/// /// This is called by [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) through [`StatefulAction::try_revert`] which handles tracing as well as if the action needs to revert based on its `action_state`.
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
2022-09-15 19:11:46 +00:00
fn stateful(self) -> StatefulAction<Self>
where
Self: Sized,
{
StatefulAction {
action: self,
state: ActionState::Uncompleted,
}
}
// They should also have an `async fn plan(args...) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>>;`
}
dyn_clone::clone_trait_object!(Action);
/**
A description of an [`Action`](crate::action::Action), intended for humans to review
*/
2022-09-15 17:29:22 +00:00
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ActionDescription {
pub description: String,
pub explanation: Vec<String>,
}
impl ActionDescription {
pub fn new(description: String, explanation: Vec<String>) -> Self {
2022-09-15 19:11:46 +00:00
Self {
description,
explanation,
}
2022-09-15 17:29:22 +00:00
}
}