Uninstall works

This commit is contained in:
Ana Hobden 2022-09-26 14:05:28 -07:00
parent 63a08acdea
commit 48646c7cad
26 changed files with 438 additions and 358 deletions

View file

@ -1,11 +1,12 @@
use std::path::{Path, PathBuf};
use serde::Serialize;
use tokio::fs::remove_file;
use tokio::process::Command;
use crate::{execute_command, HarmonicError};
use crate::{execute_command};
use crate::actions::{ActionDescription, Actionable, ActionState, Action, ActionError};
use crate::actions::{ActionDescription, Actionable, ActionState, Action};
const SERVICE_SRC: &str = "/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.service";
const SOCKET_SRC: &str = "/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.socket";
@ -68,9 +69,11 @@ impl Actionable for ConfigureNixDaemonService {
)
.await.map_err(Self::Error::CommandFailed)?;
execute_command(Command::new("systemctl").arg("link").arg(SOCKET_SRC)).await.map_err(Self::Error::CommandFailed)?;
execute_command(Command::new("systemctl").arg("link").arg(SOCKET_SRC)).await
.map_err(Self::Error::CommandFailed)?;
execute_command(Command::new("systemctl").arg("daemon-reload")).await.map_err(Self::Error::CommandFailed)?;
execute_command(Command::new("systemctl").arg("daemon-reload")).await
.map_err(Self::Error::CommandFailed)?;
*action_state = ActionState::Completed;
Ok(())
@ -79,8 +82,32 @@ impl Actionable for ConfigureNixDaemonService {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { action_state } = self;
tracing::info!("Unconfiguring nix daemon service");
// We don't need to do this! Systemd does it for us! (In fact, it's an error if we try to do this...)
execute_command(Command::new("systemctl").args(["disable", SOCKET_SRC])).await
.map_err(Self::Error::CommandFailed)?;
execute_command(
Command::new("systemctl").args(["disable", SERVICE_SRC]),
)
.await.map_err(Self::Error::CommandFailed)?;
execute_command(
Command::new("systemd-tmpfiles")
.arg("--remove")
.arg("--prefix=/nix/var/nix"),
)
.await.map_err(Self::Error::CommandFailed)?;
remove_file(TMPFILES_DEST).await
.map_err(|e| Self::Error::RemoveFile(PathBuf::from(TMPFILES_DEST), e))?;
execute_command(Command::new("systemctl").arg("daemon-reload")).await
.map_err(Self::Error::CommandFailed)?;
*action_state = ActionState::Reverted;
Ok(())
}
}
@ -103,12 +130,14 @@ pub enum ConfigureNixDaemonServiceError {
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error
),
#[error("Command `{0}` failed to execute")]
#[error("Command failed to execute")]
CommandFailed(
#[source]
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error
),
#[error("Remove file `{0}`")]
RemoveFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("No supported init system found")]
InitNotSupported,
}

View file

@ -1,12 +1,14 @@
use std::os::unix::prelude::PermissionsExt;
use std::path::{Path, PathBuf};
use nix::unistd::{chown, Group, User};
use serde::Serialize;
use tokio::fs::create_dir;
use tokio::fs::{create_dir, remove_dir_all};
use crate::HarmonicError;
use crate::actions::{ActionDescription, Actionable, ActionState, Action, ActionError};
use crate::actions::{ActionDescription, Actionable, ActionState, Action};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateDirectory {
@ -57,7 +59,7 @@ impl Actionable for CreateDirectory {
user,
group,
mode,
action_state,
action_state: _,
} = &self;
vec![ActionDescription::new(
format!("Create the directory `{}`", path.display()),
@ -93,6 +95,12 @@ impl Actionable for CreateDirectory {
.map_err(|e| Self::Error::Creating(path.clone(), e))?;
chown(path, Some(uid), Some(gid)).map_err(|e| Self::Error::Chown(path.clone(), e))?;
tracing::trace!(path = %path.display(), "Changing permissions on directory");
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode))
.await
.map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?;
*action_state = ActionState::Completed;
Ok(())
}
@ -100,8 +108,20 @@ impl Actionable for CreateDirectory {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
path,
user: _,
group: _,
mode: _,
action_state,
} = self;
tracing::trace!(path = %path.display(), "Removing directory");
remove_dir_all(path.clone())
.await
.map_err(|e| Self::Error::Removing(path.clone(), e))?;
*action_state = ActionState::Reverted;
Ok(())
}
}
@ -120,6 +140,10 @@ pub enum CreateDirectoryError {
Exists(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Creating directory `{0}`")]
Creating(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Removing directory `{0}`")]
Removing(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Set mode `{0}` on `{1}`")]
SetPermissions(u32, std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Chowning directory `{0}`")]
Chown(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] nix::errno::Errno),
#[error("Getting uid for user `{0}`")]

View file

@ -2,11 +2,11 @@ use nix::unistd::{chown, Group, User};
use serde::Serialize;
use std::path::{Path, PathBuf};
use tokio::{
fs::{create_dir_all, OpenOptions},
fs::{OpenOptions, remove_file},
io::AsyncWriteExt,
};
use crate::{HarmonicError, actions::{ActionState, Action, ActionError}};
use crate::{actions::{ActionState, Action, ActionError}};
use crate::actions::{ActionDescription, Actionable};
@ -59,7 +59,7 @@ impl Actionable for CreateFile {
group,
mode,
buf,
force,
force: _,
action_state: _,
} = &self;
vec![ActionDescription::new(
@ -84,6 +84,7 @@ impl Actionable for CreateFile {
tracing::trace!(path = %path.display(), "Creating file");
let mut file = OpenOptions::new()
.create_new(true)
.mode(*mode)
.write(true)
.read(true)
.open(&path)
@ -113,8 +114,22 @@ impl Actionable for CreateFile {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
path,
user: _,
group: _,
mode: _,
buf: _,
force: _,
action_state,
} = self;
tracing::trace!(path = %path.display(), "Deleting file");
remove_file(&path).await
.map_err(|e| Self::Error::RemoveFile(path.to_owned(), e))?;
*action_state = ActionState::Reverted;
Ok(())
}
}
@ -129,6 +144,8 @@ impl From<CreateFile> for Action {
pub enum CreateFileError {
#[error("File exists `{0}`")]
Exists(std::path::PathBuf),
#[error("Remove file `{0}`")]
RemoveFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Open file `{0}`")]
OpenFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Write file `{0}`")]

View file

@ -47,8 +47,13 @@ impl Actionable for CreateGroup {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { name, gid: _, action_state } = self;
execute_command(
Command::new("groupdel").arg(&name),
).await.map_err(CreateGroupError::Command)?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -2,11 +2,11 @@ use nix::unistd::{chown, Group, User};
use serde::Serialize;
use std::{
io::SeekFrom,
path::{Path, PathBuf},
path::{Path, PathBuf}, os::unix::prelude::PermissionsExt, f32::consts::E,
};
use tokio::{
fs::{create_dir_all, OpenOptions},
io::{AsyncSeekExt, AsyncWriteExt},
fs::{create_dir_all, OpenOptions, remove_file},
io::{AsyncSeekExt, AsyncWriteExt, AsyncReadExt},
};
use crate::{HarmonicError, actions::{ActionState, Action, ActionError}};
@ -88,6 +88,7 @@ impl Actionable for CreateOrAppendFile {
file.seek(SeekFrom::End(0))
.await
.map_err(|e| Self::Error::SeekFile(path.to_owned(), e))?;
file.write_all(buf.as_bytes())
.await
.map_err(|e| Self::Error::WriteFile(path.to_owned(), e))?;
@ -101,9 +102,15 @@ impl Actionable for CreateOrAppendFile {
.ok_or(Self::Error::NoUser(user.clone()))?
.uid;
tracing::trace!(path = %path.display(), "Chowning");
tracing::trace!(path = %path.display(), "Changing permissions on file");
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode))
.await
.map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?;
tracing::trace!(path = %path.display(), "Chowning");
chown(path, Some(uid), Some(gid)).map_err(|e| Self::Error::Chown(path.clone(), e))?;
*action_state = ActionState::Completed;
Ok(())
}
@ -111,8 +118,45 @@ impl Actionable for CreateOrAppendFile {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
path,
user: _,
group: _,
mode: _,
buf,
action_state,
} = self;
tracing::trace!(path = %path.display(), "Deleting or trimming content from file");
let mut file = OpenOptions::new()
.create(false)
.write(true)
.read(true)
.open(&path)
.await
.map_err(|e| Self::Error::ReadFile(path.to_owned(), e))?;
let mut file_contents = String::default();
file.read_to_string(&mut file_contents).await
.map_err(|e| Self::Error::SeekFile(path.to_owned(), e))?;
if let Some(start) = file_contents.rfind(buf.as_str()) {
let end = start + buf.len();
file_contents.replace_range(start..end, "")
}
if buf.is_empty() {
remove_file(&path).await.map_err(|e| Self::Error::RemoveFile(path.to_owned(), e))?;
} else {
file.seek(SeekFrom::Start(0))
.await
.map_err(|e| Self::Error::SeekFile(path.to_owned(), e))?;
file.write_all(file_contents.as_bytes())
.await
.map_err(|e| Self::Error::WriteFile(path.to_owned(), e))?;
}
*action_state = ActionState::Reverted;
Ok(())
}
}
@ -126,6 +170,10 @@ impl From<CreateOrAppendFile> for Action {
#[derive(Debug, thiserror::Error, Serialize)]
pub enum CreateOrAppendFileError {
#[error("Remove file `{0}`")]
RemoveFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Remove file `{0}`")]
ReadFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Open file `{0}`")]
OpenFile(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Write file `{0}`")]
@ -140,6 +188,8 @@ pub enum CreateOrAppendFileError {
GroupId(String, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] nix::errno::Errno),
#[error("Getting group `{0}`")]
NoGroup(String),
#[error("Set mode `{0}` on `{1}`")]
SetPermissions(u32, std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error),
#[error("Chowning directory `{0}`")]
Chown(std::path::PathBuf, #[source] #[serde(serialize_with = "crate::serialize_error_to_display")] nix::errno::Errno),
}

View file

@ -65,8 +65,13 @@ impl Actionable for CreateUser {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { name, uid: _, gid: _, action_state } = self;
execute_command(Command::new("userdel").args([
&name.to_string(),
])).await.map_err(Self::Error::Command)?;
*action_state = ActionState::Completed;
Ok(())
}
}

View file

@ -70,8 +70,11 @@ impl Actionable for FetchNix {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { url: _, destination: _, action_state } = self;
tracing::trace!("Nothing to do for `FetchNix` revert");
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -61,8 +61,11 @@ impl Actionable for MoveUnpackedNix {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { source: _, action_state } = self;
tracing::trace!("Nothing to do for `MoveUnpackedNix` revert");
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -39,9 +39,9 @@ impl Actionable for PlaceChannelConfiguration {
type Error = PlaceChannelConfigurationError;
fn description(&self) -> Vec<ActionDescription> {
let Self {
channels,
create_file,
action_state: _,
channels: _,
create_file: _,
action_state: _,
} = self;
vec![ActionDescription::new(
"Place a channel configuration".to_string(),
@ -53,7 +53,7 @@ impl Actionable for PlaceChannelConfiguration {
async fn execute(&mut self) -> Result<(), Self::Error> {
let Self {
create_file,
channels,
channels: _,
action_state,
} = self;
@ -66,8 +66,15 @@ impl Actionable for PlaceChannelConfiguration {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
create_file,
channels: _,
action_state,
} = self;
create_file.revert().await?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -66,8 +66,12 @@ impl Actionable for PlaceNixConfiguration {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { create_file, create_directory, action_state } = self;
create_file.revert().await?;
create_directory.revert().await?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -9,12 +9,13 @@ use crate::actions::{ActionDescription, Actionable};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct SetupDefaultProfile {
channels: Vec<String>,
action_state: ActionState,
}
impl SetupDefaultProfile {
#[tracing::instrument(skip_all)]
pub async fn plan(channels: Vec<String>) -> Result<Self, SetupDefaultProfileError> {
Ok(Self { channels })
Ok(Self { channels, action_state: ActionState::Planned })
}
}
@ -30,7 +31,7 @@ impl Actionable for SetupDefaultProfile {
#[tracing::instrument(skip_all)]
async fn execute(&mut self) -> Result<(), Self::Error> {
let Self { channels } = self;
let Self { channels, action_state } = self;
tracing::info!("Setting up default profile");
// Find an `nix` package
@ -105,14 +106,18 @@ impl Actionable for SetupDefaultProfile {
execute_command(&mut command).await.map_err(SetupDefaultProfileError::Command)?;
}
*action_state = ActionState::Completed;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { channels: _, action_state } = self;
std::env::remove_var("NIX_SSL_CERT_FILE");
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -70,8 +70,17 @@ impl Actionable for StartSystemdUnit {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { unit, action_state } = self;
// TODO(@Hoverbear): Handle proxy vars
execute_command(
Command::new("systemctl")
.arg("stop")
.arg(format!("{unit}")),
)
.await.map_err(StartSystemdUnitError::Command)?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -89,28 +89,21 @@ impl Actionable for ConfigureNix {
action_state,
} = self;
let (
setup_default_profile,
place_nix_configuration,
place_channel_configuration,
configure_shell_profile,
) = if let Some(configure_shell_profile) = configure_shell_profile {
let (a, b, c, d) = tokio::try_join!(
if let Some(configure_shell_profile) = configure_shell_profile {
tokio::try_join!(
async move { setup_default_profile.execute().await.map_err(|e| ConfigureNixError::from(e)) },
async move { place_nix_configuration.execute().await.map_err(|e| ConfigureNixError::from(e)) },
async move { place_channel_configuration.execute().await.map_err(|e| ConfigureNixError::from(e)) },
async move { configure_shell_profile.execute().await.map_err(|e| ConfigureNixError::from(e)) },
)?;
(a, b, c, Some(d))
} else {
let (a, b, c) = tokio::try_join!(
tokio::try_join!(
async move { setup_default_profile.execute().await.map_err(|e| ConfigureNixError::from(e)) },
async move { place_nix_configuration.execute().await.map_err(|e| ConfigureNixError::from(e)) },
async move { place_channel_configuration.execute().await.map_err(|e| ConfigureNixError::from(e)) },
)?;
(a, b, c, None)
};
let configure_nix_daemon_service = configure_nix_daemon_service.execute().await?;
configure_nix_daemon_service.execute().await?;
*action_state = ActionState::Completed;
Ok(())
@ -119,8 +112,24 @@ impl Actionable for ConfigureNix {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
setup_default_profile,
configure_nix_daemon_service,
place_nix_configuration,
place_channel_configuration,
configure_shell_profile,
action_state,
} = self;
configure_nix_daemon_service.revert().await?;
if let Some(configure_shell_profile) = configure_shell_profile {
configure_shell_profile.revert().await?;
}
place_channel_configuration.revert().await?;
place_nix_configuration.revert().await?;
setup_default_profile.revert().await?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -105,8 +105,37 @@ impl Actionable for ConfigureShellProfile {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
create_or_append_files,
action_state,
} = self;
tracing::info!("Configuring shell profile");
let mut set = JoinSet::new();
let mut errors = Vec::default();
for (idx, create_or_append_file) in create_or_append_files.iter().enumerate() {
let mut create_or_append_file_clone = create_or_append_file.clone();
let _abort_handle = set.spawn(async move { create_or_append_file_clone.revert().await?; Result::<_, CreateOrAppendFileError>::Ok((idx, create_or_append_file_clone)) });
}
while let Some(result) = set.join_next().await {
match result {
Ok(Ok((idx, create_or_append_file))) => create_or_append_files[idx] = create_or_append_file,
Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e.into()),
};
}
if !errors.is_empty() {
if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into());
} else {
return Err(ConfigureShellProfileError::MultipleCreateOrAppendFile(errors));
}
}
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -72,8 +72,14 @@ impl Actionable for CreateNixTree {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self { create_directories, action_state } = self;
// Just do sequential since parallizing this will have little benefit
for create_directory in create_directories.iter_mut().rev() {
create_directory.revert().await?
}
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -81,17 +81,17 @@ impl Actionable for CreateUsersAndGroup {
let Self {
create_users,
create_group,
daemon_user_count,
nix_build_group_name,
nix_build_group_id,
nix_build_user_prefix,
nix_build_user_id_base,
action_state
daemon_user_count: _,
nix_build_group_name: _,
nix_build_group_id: _,
nix_build_user_prefix: _,
nix_build_user_id_base: _,
action_state,
} = self;
// Create group
let create_group = create_group.execute().await?;
create_group.execute().await?;
// Create users
// TODO(@hoverbear): Abstract this, it will be common
@ -127,8 +127,48 @@ impl Actionable for CreateUsersAndGroup {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
create_users,
create_group,
daemon_user_count: _,
nix_build_group_name: _,
nix_build_group_id: _,
nix_build_user_prefix: _,
nix_build_user_id_base: _,
action_state,
} = self;
// Create users
// TODO(@hoverbear): Abstract this, it will be common
let mut set = JoinSet::new();
let mut errors = Vec::default();
for (idx, create_user) in create_users.iter().enumerate() {
let mut create_user_clone = create_user.clone();
let _abort_handle = set.spawn(async move { create_user_clone.revert().await?; Result::<_, CreateUserError>::Ok((idx, create_user_clone)) });
}
while let Some(result) = set.join_next().await {
match result {
Ok(Ok((idx, success))) => create_users[idx] = success,
Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e)?,
};
}
if !errors.is_empty() {
if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into());
} else {
return Err(CreateUsersAndGroupError::CreateUsers(errors));
}
}
// Create group
create_group.revert().await?;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -90,8 +90,25 @@ impl Actionable for ProvisionNix {
#[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Self::Error> {
todo!();
let Self {
fetch_nix,
create_nix_tree,
create_users_and_group,
move_unpacked_nix,
action_state,
} = self;
// We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async { fetch_nix_clone.revert().await?; Result::<_, Self::Error>::Ok(fetch_nix_clone) });
create_users_and_group.revert().await?;
create_nix_tree.revert().await.map_err(ProvisionNixError::from)?;
*fetch_nix = fetch_nix_handle.await.map_err(ProvisionNixError::from)??;
move_unpacked_nix.revert().await?;
*action_state = ActionState::Completed;
Ok(())
}
}

View file

@ -37,6 +37,7 @@ impl Actionable for StartNixDaemon {
start_systemd_socket.execute().await?;
*action_state = ActionState::Completed;
Ok(())
}
@ -46,7 +47,7 @@ impl Actionable for StartNixDaemon {
start_systemd_socket.revert().await?;
*action_state = ActionState::Completed;
*action_state = ActionState::Reverted;
Ok(())
}
}

View file

@ -79,6 +79,7 @@ impl CommandExecute for HarmonicCli {
match subcommand {
Some(HarmonicSubcommand::Plan(plan)) => plan.execute().await,
Some(HarmonicSubcommand::Execute(execute)) => execute.execute().await,
Some(HarmonicSubcommand::Revert(revert)) => revert.execute().await,
None => {
let mut settings = InstallSettings::default();

View file

@ -1,8 +1,8 @@
use std::process::ExitCode;
use std::{process::ExitCode, path::PathBuf};
use clap::{ArgAction, Parser};
use harmonic::InstallPlan;
use tokio::io::AsyncReadExt;
use eyre::WrapErr;
use crate::{
cli::CommandExecute,
@ -19,18 +19,18 @@ pub(crate) struct Execute {
global = true
)]
no_confirm: bool,
#[clap(default_value = "/dev/stdin")]
plan: PathBuf,
}
#[async_trait::async_trait]
impl CommandExecute for Execute {
#[tracing::instrument(skip_all, fields())]
async fn execute(self) -> eyre::Result<ExitCode> {
let Self { no_confirm } = self;
let Self { no_confirm, plan } = self;
let mut stdin = tokio::io::stdin();
let mut json = String::default();
stdin.read_to_string(&mut json).await?;
let plan: InstallPlan = serde_json::from_str(&json)?;
let install_plan_string = tokio::fs::read_to_string(plan).await.wrap_err("Reading plan")?;
let mut plan: InstallPlan = serde_json::from_str(&install_plan_string)?;
if !no_confirm {
if !interaction::confirm(plan.description()).await? {
@ -38,6 +38,8 @@ impl CommandExecute for Execute {
}
}
plan.install().await?;
Ok(ExitCode::SUCCESS)
}
}

View file

@ -2,9 +2,12 @@ mod plan;
use plan::Plan;
mod execute;
use execute::Execute;
mod revert;
use revert::Revert;
#[derive(Debug, clap::Subcommand)]
pub(crate) enum HarmonicSubcommand {
Plan(Plan),
Execute(Execute),
Revert(Revert),
}

View file

@ -1,8 +1,9 @@
use std::process::ExitCode;
use std::{process::ExitCode, path::PathBuf};
use clap::{ArgAction, Parser};
use harmonic::{InstallPlan, InstallSettings};
use tokio::io::AsyncWriteExt;
use eyre::WrapErr;
use crate::cli::{arg::ChannelValue, CommandExecute};
@ -43,6 +44,8 @@ pub(crate) struct Plan {
global = true
)]
pub(crate) force: bool,
#[clap(default_value = "/dev/stdout")]
plan: PathBuf,
}
#[async_trait::async_trait]
@ -59,6 +62,7 @@ impl CommandExecute for Plan {
daemon_user_count,
explain,
force,
plan,
} = self;
let mut settings = InstallSettings::default();
@ -73,11 +77,11 @@ impl CommandExecute for Plan {
);
settings.modify_profile(!no_modify_profile);
let plan = InstallPlan::new(settings).await?;
let install_plan = InstallPlan::new(settings).await?;
let json = serde_json::to_string_pretty(&install_plan)?;
tokio::fs::write(plan, json).await.wrap_err("Writing plan")?;
let json = serde_json::to_string_pretty(&plan)?;
let mut stdout = tokio::io::stdout();
stdout.write_all(json.as_bytes()).await?;
Ok(ExitCode::SUCCESS)
}
}

View file

@ -0,0 +1,45 @@
use std::{process::ExitCode, path::PathBuf};
use clap::{ArgAction, Parser};
use harmonic::InstallPlan;
use eyre::WrapErr;
use crate::{
cli::CommandExecute,
interaction,
};
/// An opinionated, experimental Nix installer
#[derive(Debug, Parser)]
pub(crate) struct Revert {
#[clap(
long,
action(ArgAction::SetTrue),
default_value = "false",
global = true
)]
no_confirm: bool,
#[clap(default_value = "/nix/receipt.json")]
receipt: PathBuf,
}
#[async_trait::async_trait]
impl CommandExecute for Revert {
#[tracing::instrument(skip_all, fields())]
async fn execute(self) -> eyre::Result<ExitCode> {
let Self { no_confirm, receipt } = self;
let install_receipt_string = tokio::fs::read_to_string(receipt).await.wrap_err("Reading receipt")?;
let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?;
if !no_confirm {
if !interaction::confirm(plan.description()).await? {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
}
}
plan.revert().await?;
Ok(ExitCode::SUCCESS)
}
}

View file

@ -1,58 +1,13 @@
use serde::de::value::Error;
use std::path::PathBuf;
use crate::actions::ActionError;
#[derive(thiserror::Error, Debug)]
pub enum HarmonicError {
#[error("Request error")]
Reqwest(#[from] reqwest::Error),
#[error("Unarchiving error")]
Unarchive(std::io::Error),
#[error("Getting temporary directory")]
TempDir(std::io::Error),
#[error("Glob pattern error")]
GlobPatternError(#[from] glob::PatternError),
#[error("Glob globbing error")]
GlobGlobError(#[from] glob::GlobError),
#[error("Symlinking from `{0}` to `{1}`")]
Symlink(std::path::PathBuf, std::path::PathBuf, std::io::Error),
#[error("Renaming from `{0}` to `{1}`")]
Rename(std::path::PathBuf, std::path::PathBuf, std::io::Error),
#[error("Unarchived Nix store did not appear to include a `nss-cacert` location")]
NoNssCacert,
#[error("No supported init system found")]
InitNotSupported,
#[error("Creating file `{0}`: {1}")]
CreateFile(std::path::PathBuf, std::io::Error),
#[error("Creating directory `{0}`: {1}")]
CreateDirectory(std::path::PathBuf, std::io::Error),
#[error("Walking directory `{0}`")]
WalkDirectory(std::path::PathBuf, walkdir::Error),
#[error("Setting permissions `{0}`")]
SetPermissions(std::path::PathBuf, std::io::Error),
#[error("Command `{0}` failed to execute")]
CommandFailedExec(String, std::io::Error),
// TODO(@Hoverbear): This should capture the stdout.
#[error("Command `{0}` did not to return a success status")]
CommandFailedStatus(String),
#[error("Join error")]
JoinError(#[from] tokio::task::JoinError),
#[error("Opening file `{0}` for writing")]
OpenFile(std::path::PathBuf, std::io::Error),
#[error("Opening file `{0}` for writing")]
WriteFile(std::path::PathBuf, std::io::Error),
#[error("Seeking file `{0}` for writing")]
SeekFile(std::path::PathBuf, std::io::Error),
#[error("Changing ownership of `{0}`")]
Chown(std::path::PathBuf, nix::errno::Errno),
#[error("Getting uid for user `{0}`")]
UserId(String, nix::errno::Errno),
#[error("Getting user `{0}`")]
NoUser(String),
#[error("Getting gid for group `{0}`")]
GroupId(String, nix::errno::Errno),
#[error("Getting group `{0}`")]
NoGroup(String),
#[error("Errors with additional failures during reverts: {}\nDuring Revert:{}", .0.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(" & "), .1.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(" & "))]
FailedReverts(Vec<HarmonicError>, Vec<HarmonicError>),
#[error("Multiple errors: {}", .0.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(" & "))]
Multiple(Vec<HarmonicError>),
#[error("Error executing action")]
ActionError(#[source] #[from] ActionError),
#[error("Recording install receipt")]
RecordingReceipt(PathBuf, #[source] std::io::Error),
#[error(transparent)]
SerializingReceipt(serde_json::Error),
}

View file

@ -28,77 +28,6 @@ use tokio::{
};
#[tracing::instrument(skip_all, fields(
path = %path.as_ref().display(),
permissions = tracing::field::valuable(&permissions.clone().map(|v| format!("{:#o}", v.mode()))),
owner = tracing::field::valuable(&owner),
group = tracing::field::valuable(&group),
))]
async fn set_permissions(
path: impl AsRef<Path>,
permissions: Option<Permissions>,
owner: Option<String>,
group: Option<String>,
dry_run: bool,
) -> Result<(), HarmonicError> {
use nix::unistd::{chown, Group, User};
use walkdir::WalkDir;
if !dry_run {
tracing::trace!("Setting permissions");
let path = path.as_ref();
let uid = if let Some(owner) = owner {
let uid = User::from_name(owner.as_str())
.map_err(|e| HarmonicError::UserId(owner.clone(), e))?
.ok_or(HarmonicError::NoUser(owner))?
.uid;
Some(uid)
} else {
None
};
let gid = if let Some(group) = group {
let gid = Group::from_name(group.as_str())
.map_err(|e| HarmonicError::GroupId(group.clone(), e))?
.ok_or(HarmonicError::NoGroup(group))?
.gid;
Some(gid)
} else {
None
};
for child in WalkDir::new(path) {
let entry = child.map_err(|e| HarmonicError::WalkDirectory(path.to_owned(), e))?;
if let Some(ref perms) = permissions {
tokio::fs::set_permissions(path, perms.clone())
.await
.map_err(|e| HarmonicError::SetPermissions(path.to_owned(), e))?;
}
chown(entry.path(), uid, gid)
.map_err(|e| HarmonicError::Chown(entry.path().to_owned(), e))?;
}
} else {
tracing::info!("Dry run: Would recursively set permissions/ownership");
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
path = %path.as_ref().display(),
))]
async fn create_directory(path: impl AsRef<Path>, dry_run: bool) -> Result<(), HarmonicError> {
use tokio::fs::create_dir;
if !dry_run {
tracing::trace!("Creating directory");
let path = path.as_ref();
create_dir(path)
.await
.map_err(|e| HarmonicError::CreateDirectory(path.to_owned(), e))?;
} else {
tracing::info!("Dry run: Would create directory");
}
Ok(())
}
#[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))]
async fn execute_command(
command: &mut Command,
@ -110,163 +39,10 @@ async fn execute_command(
.await?;
match status.success() {
true => Ok(status),
false => Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed status")),
false => Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Command `{command_str}` failed status"))),
}
}
#[tracing::instrument(skip_all, fields(
path = %path.as_ref().display(),
buf = %format!("```{}```", buf.as_ref()),
))]
async fn create_or_append_file(
path: impl AsRef<Path>,
buf: impl AsRef<str>,
dry_run: bool,
) -> Result<(), HarmonicError> {
use tokio::fs::{create_dir_all, OpenOptions};
let path = path.as_ref();
let buf = buf.as_ref();
if !dry_run {
tracing::trace!("Creating or appending");
if let Some(parent) = path.parent() {
create_dir_all(parent)
.await
.map_err(|e| HarmonicError::CreateDirectory(parent.to_owned(), e))?;
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open(&path)
.await
.map_err(|e| HarmonicError::OpenFile(path.to_owned(), e))?;
file.seek(SeekFrom::End(0))
.await
.map_err(|e| HarmonicError::SeekFile(path.to_owned(), e))?;
file.write_all(buf.as_bytes())
.await
.map_err(|e| HarmonicError::WriteFile(path.to_owned(), e))?;
} else {
tracing::info!("Dry run: Would create or append");
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
path = %path.as_ref().display(),
buf = %format!("```{}```", buf.as_ref()),
))]
async fn create_file_if_not_exists(
path: impl AsRef<Path>,
buf: impl AsRef<str>,
dry_run: bool,
) -> Result<(), HarmonicError> {
use tokio::fs::{create_dir_all, OpenOptions};
let path = path.as_ref();
let buf = buf.as_ref();
if !dry_run {
tracing::trace!("Creating if not exists");
if let Some(parent) = path.parent() {
create_dir_all(parent)
.await
.map_err(|e| HarmonicError::CreateDirectory(parent.to_owned(), e))?;
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open(&path)
.await
.map_err(|e| HarmonicError::OpenFile(path.to_owned(), e))?;
file.write_all(buf.as_bytes())
.await
.map_err(|e| HarmonicError::WriteFile(path.to_owned(), e))?;
} else {
tracing::info!("Dry run: Would create (or error if exists)");
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
src = %src.as_ref().display(),
dest = %dest.as_ref().display(),
))]
async fn symlink(
src: impl AsRef<Path>,
dest: impl AsRef<Path>,
dry_run: bool,
) -> Result<(), HarmonicError> {
let src = src.as_ref();
let dest = dest.as_ref();
if !dry_run {
tracing::trace!("Symlinking");
tokio::fs::symlink(src, dest)
.await
.map_err(|e| HarmonicError::Symlink(src.to_owned(), dest.to_owned(), e))?;
} else {
tracing::info!("Dry run: Would symlink",);
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
src = %src.as_ref().display(),
dest = %dest.as_ref().display(),
))]
async fn rename(
src: impl AsRef<Path>,
dest: impl AsRef<Path>,
dry_run: bool,
) -> Result<(), HarmonicError> {
let src = src.as_ref();
let dest = dest.as_ref();
if !dry_run {
tracing::trace!("Renaming");
tokio::fs::rename(src, dest)
.await
.map_err(|e| HarmonicError::Rename(src.to_owned(), dest.to_owned(), e))?;
} else {
tracing::info!("Dry run: Would rename",);
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
url = %url.as_ref(),
dest = %dest.as_ref().display(),
))]
async fn fetch_url_and_unpack_xz(
url: impl AsRef<str>,
dest: impl AsRef<Path>,
dry_run: bool,
) -> Result<(), HarmonicError> {
let url = url.as_ref();
let dest = dest.as_ref().to_owned();
if !dry_run {
tracing::trace!("Fetching url");
let res = reqwest::get(url).await.map_err(HarmonicError::Reqwest)?;
let bytes = res.bytes().await.map_err(HarmonicError::Reqwest)?;
// TODO(@Hoverbear): Pick directory
tracing::trace!("Unpacking tar.xz");
let handle: Result<(), HarmonicError> = spawn_blocking(move || {
let decoder = xz2::read::XzDecoder::new(bytes.reader());
let mut archive = tar::Archive::new(decoder);
archive.unpack(&dest).map_err(HarmonicError::Unarchive)?;
tracing::debug!(dest = %dest.display(), "Downloaded & extracted Nix");
Ok(())
})
.await?;
handle?;
} else {
tracing::info!("Dry run: Would fetch and unpack xz tarball");
}
Ok(())
}
#[tracing::instrument(skip_all, fields(
k = %k.as_ref().to_string_lossy(),
v = %v.as_ref().to_string_lossy(),

View file

@ -1,4 +1,7 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use crate::{
actions::{
@ -82,23 +85,51 @@ impl InstallPlan {
},
)
}
pub async fn new(settings: InstallSettings) -> Result<Self, ActionError> {
pub async fn new(settings: InstallSettings) -> Result<Self, HarmonicError> {
Ok(Self {
settings: settings.clone(),
provision_nix: ProvisionNix::plan(settings.clone()).await?,
configure_nix: ConfigureNix::plan(settings).await?,
start_nix_daemon: StartNixDaemon::plan().await?,
provision_nix: ProvisionNix::plan(settings.clone()).await
.map_err(|e| ActionError::from(e))?,
configure_nix: ConfigureNix::plan(settings).await
.map_err(|e| ActionError::from(e))?,
start_nix_daemon: StartNixDaemon::plan().await
.map_err(|e| ActionError::from(e))?,
})
}
#[tracing::instrument(skip_all)]
pub async fn install(&mut self) -> Result<(), ActionError> {
pub async fn install(&mut self) -> Result<(), HarmonicError> {
// This is **deliberately sequential**.
// Actions which are parallelizable are represented by "group actions" like CreateUsers
// The plan itself represents the concept of the sequence of stages.
self.provision_nix.execute().await?;
self.configure_nix.execute().await?;
self.start_nix_daemon.execute().await?;
self.provision_nix.execute().await
.map_err(|e| ActionError::from(e))?;
self.configure_nix.execute().await
.map_err(|e| ActionError::from(e))?;
self.start_nix_daemon.execute().await
.map_err(|e| ActionError::from(e))?;
let install_receipt_path = PathBuf::from("/nix/receipt.json");
let self_json = serde_json::to_string_pretty(&self)
.map_err(HarmonicError::SerializingReceipt)?;
tokio::fs::write(&install_receipt_path, self_json).await
.map_err(|e| HarmonicError::RecordingReceipt(install_receipt_path, e))?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn revert(&mut self) -> Result<(), HarmonicError> {
// This is **deliberately sequential**.
// Actions which are parallelizable are represented by "group actions" like CreateUsers
// The plan itself represents the concept of the sequence of stages.
self.start_nix_daemon.revert().await
.map_err(|e| ActionError::from(e))?;
self.configure_nix.revert().await
.map_err(|e| ActionError::from(e))?;
self.provision_nix.revert().await
.map_err(|e| ActionError::from(e))?;
Ok(())
}
}