lix-installer/src/action/mod.rs

312 lines
11 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, linux::SteamDeck},
action::{Action, ActionError, 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>, ActionError> {
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<(), ActionError> {
// 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<(), ActionError> {
// 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};
use std::error::Error;
use tokio::task::JoinError;
/// 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<(), ActionError>;
/// 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<(), ActionError>;
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>, ActionError>;`
}
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
}
}
/// An error occurring during an action
#[derive(thiserror::Error, Debug)]
pub enum ActionError {
/// A custom error
#[error(transparent)]
Custom(Box<dyn std::error::Error + Send + Sync>),
/// A child error
#[error(transparent)]
Child(#[from] Box<ActionError>),
/// Several child errors
#[error("Multiple errors: {}", .0.iter().map(|v| {
if let Some(source) = v.source() {
format!("{v} ({source})")
} else {
format!("{v}")
}
}).collect::<Vec<_>>().join(" & "))]
Children(Vec<Box<ActionError>>),
/// The path already exists
#[error("Path exists `{0}`")]
Exists(std::path::PathBuf),
#[error("Getting metadata for {0}`")]
GettingMetadata(std::path::PathBuf, #[source] std::io::Error),
#[error("Creating directory `{0}`")]
CreateDirectory(std::path::PathBuf, #[source] std::io::Error),
#[error("Symlinking from `{0}` to `{1}`")]
Symlink(
std::path::PathBuf,
std::path::PathBuf,
#[source] std::io::Error,
),
#[error("Set mode `{0}` on `{1}`")]
SetPermissions(u32, std::path::PathBuf, #[source] std::io::Error),
#[error("Remove file `{0}`")]
Remove(std::path::PathBuf, #[source] std::io::Error),
#[error("Copying file `{0}` to `{1}`")]
Copy(
std::path::PathBuf,
std::path::PathBuf,
#[source] std::io::Error,
),
#[error("Rename `{0}` to `{1}`")]
Rename(
std::path::PathBuf,
std::path::PathBuf,
#[source] std::io::Error,
),
#[error("Remove path `{0}`")]
Read(std::path::PathBuf, #[source] std::io::Error),
#[error("Open path `{0}`")]
Open(std::path::PathBuf, #[source] std::io::Error),
#[error("Write path `{0}`")]
Write(std::path::PathBuf, #[source] std::io::Error),
#[error("Seek path `{0}`")]
Seek(std::path::PathBuf, #[source] std::io::Error),
#[error("Getting uid for user `{0}`")]
UserId(String, #[source] nix::errno::Errno),
#[error("Getting user `{0}`")]
NoUser(String),
#[error("Getting gid for group `{0}`")]
GroupId(String, #[source] nix::errno::Errno),
#[error("Getting group `{0}`")]
NoGroup(String),
#[error("Chowning path `{0}`")]
Chown(std::path::PathBuf, #[source] nix::errno::Errno),
/// Failed to execute command
#[error("Failed to execute command")]
Command(#[source] std::io::Error),
#[error("Joining spawned async task")]
Join(
#[source]
#[from]
JoinError,
),
#[error("String from UTF-8 error")]
FromUtf8(
#[source]
#[from]
std::string::FromUtf8Error,
),
}