Uninstall shouldn't fail fast (#382)

* Uninstall shouldn't fail fast

* wip

* wip

* No longer fails fast

* Tidy up error handling

* Touchup post merge

* Refactor nix tests

* Some minor fixes

* Uninstall fail tests

* Fiddle with messaging

* nixfmt

* Tweak display a bit

* fix docs

* Fix Mac

* Revert setting I was testing

* Reflect feedback about a log level
This commit is contained in:
Ana Hobden 2023-04-05 08:12:38 -07:00 committed by GitHub
parent 5ec1d0e9b9
commit 8bb37f1bcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1401 additions and 939 deletions

2
Cargo.lock generated
View file

@ -1033,7 +1033,7 @@ dependencies = [
[[package]]
name = "nix-installer"
version = "0.7.0"
version = "0.7.1-unreleased"
dependencies = [
"async-trait",
"atty",

View file

@ -194,6 +194,7 @@
vm-test = import ./nix/tests/vm-test {
inherit forSystem;
inherit (nix.hydraJobs) binaryTarball;
inherit (nixpkgs) lib;
};
container-test = import ./nix/tests/container-test {
inherit forSystem;

View file

@ -1,5 +1,5 @@
# Largely derived from https://github.com/NixOS/nix/blob/14f7dae3e4eb0c34192d0077383a7f2a2d630129/tests/installer/default.nix
{ forSystem, binaryTarball }:
{ forSystem, binaryTarball, lib }:
let
nix-installer-install = ''
@ -18,7 +18,7 @@ let
tar xvf nix.tar.xz
./nix-*/install --no-channel-add --yes --no-daemon
'';
installScripts = rec {
installCases = rec {
install-default = {
install = nix-installer-install;
check = ''
@ -38,19 +38,25 @@ let
fi
if systemctl is-failed nix-daemon.socket; then
echo "nix-daemon.socket is failed"
systemctl status nix-daemon.socket
exit 1
fi
if systemctl is-failed nix-daemon.service; then
echo "nix-daemon.service is failed"
exit 1
fi
if !(sudo systemctl start nix-daemon.service); then
echo "nix-daemon.service failed to start"
systemctl status nix-daemon.service
exit 1
fi
if systemctl is-failed nix-daemon.service; then
echo "nix-daemon.service is failed"
systemctl status nix-daemon.service
exit 1
fi
if !(sudo systemctl stop nix-daemon.service); then
echo "nix-daemon.service failed to stop"
systemctl status nix-daemon.service
exit 1
fi
@ -65,6 +71,62 @@ let
out=$(nix-build --no-substitute -E 'derivation { name = "foo"; system = "x86_64-linux"; builder = "/bin/sh"; args = ["-c" "echo foobar > $out"]; }')
[[ $(cat $out) = foobar ]]
'';
uninstall = ''
/nix/nix-installer uninstall --no-confirm
'';
uninstallCheck = ''
if which nix; then
echo "nix existed on path after uninstall"
exit 1
fi
for i in $(seq 1 32); do
if id -u nixbld$i; then
echo "User nixbld$i exists after uninstall"
exit 1
fi
done
if grep "^nixbld:" /etc/group; then
echo "Group nixbld exists after uninstall"
exit 1
fi
if sudo -i nix store ping --store daemon; then
echo "Could run nix store ping after uninstall"
exit 1
fi
if [ -d /nix ]; then
echo "/nix exists after uninstall"
exit 1
fi
if [ -d /etc/nix/nix.conf ]; then
echo "/etc/nix/nix.conf exists after uninstall"
exit 1
fi
if [ -f /etc/systemd/system/nix-daemon.socket ]; then
echo "/etc/systemd/system/nix-daemon.socket exists after uninstall"
exit 1
fi
if [ -f /etc/systemd/system/nix-daemon.service ]; then
echo "/etc/systemd/system/nix-daemon.socket exists after uninstall"
exit 1
fi
if systemctl status nix-daemon.socket > /dev/null; then
echo "systemd unit nix-daemon.socket still exists after uninstall"
exit 1
fi
if systemctl status nix-daemon.service > /dev/null; then
echo "systemd unit nix-daemon.service still exists after uninstall"
exit 1
fi
'';
};
install-no-start-daemon = {
install = ''
@ -90,6 +152,8 @@ let
[[ $(cat $out) = foobar ]]
'';
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
install-daemonless = {
install = ''
@ -106,14 +170,20 @@ let
[[ $(cat $out) = foobar ]]
'';
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
};
cureCases = {
cure-self-linux-working = {
preinstall = ''
${nix-installer-install-quiet}
sudo mv /nix/receipt.json /nix/old-receipt.json
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-no-nix-path = {
preinstall = ''
@ -122,8 +192,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json
sudo rm -rf /nix/
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-missing-users = {
preinstall = ''
@ -133,8 +205,10 @@ let
sudo userdel nixbld3
sudo userdel nixbld16
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-missing-users-and-group = {
preinstall = ''
@ -146,8 +220,10 @@ let
done
sudo groupdel nixbld
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-daemon-disabled = {
preinstall = ''
@ -155,8 +231,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json
sudo systemctl disable --now nix-daemon.socket
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-no-etc-nix = {
preinstall = ''
@ -164,8 +242,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json
sudo rm -rf /etc/nix
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-self-linux-broken-unmodified-bashrc = {
preinstall = ''
@ -173,16 +253,20 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json
sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-self-broken-no-nix-path = {
preinstall = ''
${cure-script-multi-user}
sudo rm -rf /nix/
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-broken-missing-users = {
preinstall = ''
@ -191,44 +275,98 @@ let
sudo userdel nixbld3
sudo userdel nixbld16
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-broken-daemon-disabled = {
preinstall = ''
${cure-script-multi-user}
sudo systemctl disable --now nix-daemon.socket
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-broken-no-etc-nix = {
preinstall = ''
${cure-script-multi-user}
sudo rm -rf /etc/nix
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-broken-unmodified-bashrc = {
preinstall = ''
${cure-script-multi-user}
sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc
'';
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
cure-script-multi-working = {
preinstall = cure-script-multi-user;
install = install-default.install;
check = install-default.check;
install = installCases.install-default.install;
check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
};
# cure-script-single-working = {
# preinstall = cure-script-single-user;
# install = install-default.install;
# check = install-default.check;
# install = installCases.install-default.install;
# check = installCases.install-default.check;
# };
};
# Cases to test uninstalling is complete even in the face of errors.
uninstallCases =
let
uninstallFailExpected = ''
if /nix/nix-installer uninstall --no-confirm; then
echo "/nix/nix-installer uninstall exited with 0 during a uninstall failure test"
exit 1
else
exit 0
fi
'';
in
{
uninstall-users-and-groups-missing = {
install = installCases.install-default.install;
check = installCases.install-default.check;
preuninstall = ''
for i in $(seq 1 32); do
sudo userdel nixbld$i
done
sudo groupdel nixbld
'';
uninstall = uninstallFailExpected;
uninstallCheck = installCases.install-default.uninstallCheck;
};
uninstall-nix-conf-gone = {
install = installCases.install-default.install;
check = installCases.install-default.check;
preuninstall = ''
sudo rm -rf /etc/nix
'';
uninstall = uninstallFailExpected;
uninstallCheck = installCases.install-default.uninstallCheck;
};
uninstall-shell-profile-clobbered = {
install = installCases.install-default.install;
check = installCases.install-default.check;
preuninstall = ''
sudo rm -rf /etc/bashrc
'';
uninstall = uninstallFailExpected;
uninstallCheck = installCases.install-default.uninstallCheck;
};
};
disableSELinux = "sudo setenforce 0";
@ -333,7 +471,7 @@ let
};
makeTest = imageName: testName:
makeTest = imageName: testName: test:
let image = images.${imageName}; in
with (forSystem image.system ({ system, pkgs, ... }: pkgs));
runCommand
@ -342,9 +480,12 @@ let
buildInputs = [ qemu_kvm openssh ];
image = image.image;
postBoot = image.postBoot or "";
preinstallScript = installScripts.${testName}.preinstall or "echo \"Not Applicable\"";
installScript = installScripts.${testName}.install;
checkScript = installScripts.${testName}.check;
preinstallScript = test.preinstall or "echo \"Not Applicable\"";
installScript = test.install;
checkScript = test.check;
uninstallScript = test.uninstall;
preuninstallScript = test.preuninstall or "echo \"Not Applicable\"";
uninstallCheckScript = test.uninstallCheck;
installer = nix-installer-static;
binaryTarball = binaryTarball.${system};
}
@ -414,32 +555,38 @@ let
echo "Running installer..."
$ssh "set -eux; $installScript"
echo "Testing Nix installation..."
echo "Checking Nix installation..."
$ssh "set -eux; $checkScript"
echo "Testing Nix uninstallation..."
$ssh "set -eux; /nix/nix-installer uninstall --no-confirm"
echo "Running preuninstall..."
$ssh "set -eux; $preuninstallScript"
echo "Running Nix uninstallation..."
$ssh "set -eux; $uninstallScript"
echo "Checking Nix uninstallation..."
$ssh "set -eux; $uninstallCheckScript"
echo "Done!"
touch $out
'';
vm-tests = builtins.mapAttrs
makeTests = name: tests: builtins.mapAttrs
(imageName: image:
rec {
${image.system} = (builtins.mapAttrs
(testName: test:
makeTest imageName testName
makeTest imageName testName test
)
installScripts) // {
all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
tests) // {
"${name}" = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = name;
constituents = (
pkgs.lib.mapAttrsToList
(testName: test:
makeTest imageName testName
makeTest imageName testName test
)
installScripts
tests
);
});
};
@ -447,96 +594,35 @@ let
)
images;
allCases = lib.recursiveUpdate (lib.recursiveUpdate installCases cureCases) uninstallCases;
install-tests = makeTests "install" installCases;
cure-tests = makeTests "cure" cureCases;
uninstall-tests = makeTests "uninstall" uninstallCases;
all-tests = builtins.mapAttrs
(imageName: image: {
"x86_64-linux".all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = [
install-tests."${imageName}"."x86_64-linux".install
cure-tests."${imageName}"."x86_64-linux".cure
uninstall-tests."${imageName}"."x86_64-linux".uninstall
];
});
})
images;
joined-tests = lib.recursiveUpdate (lib.recursiveUpdate (lib.recursiveUpdate cure-tests install-tests) uninstall-tests) all-tests;
in
vm-tests // rec {
all."x86_64-linux".install-default = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests;
});
all."x86_64-linux".install-no-start-daemon = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests;
});
all."x86_64-linux".install-daemonless = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-daemonless) vm-tests;
});
all."x86_64-linux".cure-self-linux-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-working) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-no-nix-path = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-no-nix-path) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-missing-users = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-missing-users) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-missing-users-and-group = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-missing-users-and-group) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-daemon-disabled = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-daemon-disabled) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-no-etc-nix = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-no-etc-nix) vm-tests;
});
all."x86_64-linux".cure-self-linux-broken-unmodified-bashrc = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-unmodified-bashrc) vm-tests;
});
all."x86_64-linux".cure-script-multi-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-working) vm-tests;
});
# all."x86_64-linux".cure-script-multi-broken-no-nix-path = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
# name = "all";
# constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-no-nix-path) vm-tests;
# });
all."x86_64-linux".cure-script-multi-broken-missing-users = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-missing-users) vm-tests;
});
all."x86_64-linux".cure-script-multi-broken-daemon-disabled = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-daemon-disabled) vm-tests;
});
all."x86_64-linux".cure-script-multi-broken-no-etc-nix = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-no-etc-nix) vm-tests;
});
all."x86_64-linux".cure-script-multi-broken-unmodified-bashrc = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-unmodified-bashrc) vm-tests;
});
# all."x86_64-linux".cure-script-single-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
# name = "all";
# constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-single-working) vm-tests;
# });
all."x86_64-linux".all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all";
constituents = [
all."x86_64-linux".install-default
all."x86_64-linux".install-no-start-daemon
all."x86_64-linux".install-daemonless
all."x86_64-linux".cure-self-linux-working
all."x86_64-linux".cure-self-linux-broken-no-nix-path
all."x86_64-linux".cure-self-linux-broken-missing-users
all."x86_64-linux".cure-self-linux-broken-missing-users-and-group
all."x86_64-linux".cure-self-linux-broken-daemon-disabled
all."x86_64-linux".cure-self-linux-broken-no-etc-nix
all."x86_64-linux".cure-self-linux-broken-unmodified-bashrc
all."x86_64-linux".cure-script-multi-working
# all."x86_64-linux".cure-script-multi-broken-no-nix-path
all."x86_64-linux".cure-script-multi-broken-missing-users
all."x86_64-linux".cure-script-multi-broken-daemon-disabled
all."x86_64-linux".cure-script-multi-broken-no-etc-nix
all."x86_64-linux".cure-script-multi-broken-unmodified-bashrc
# all."x86_64-linux".cure-script-single-working
];
});
lib.recursiveUpdate joined-tests {
all."x86_64-linux" = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.lib.mapAttrs (caseName: case:
pkgs.releaseTools.aggregate {
name = caseName;
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux"."${caseName}") joined-tests;
}
)) (allCases // { "cure" = { }; "install" = { }; "uninstall" = { }; "all" = { }; });
}

View file

@ -5,7 +5,7 @@ use target_lexicon::OperatingSystem;
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::ActionError;
use crate::action::{ActionError, ActionErrorKind};
use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction};
@ -37,22 +37,23 @@ impl AddUserToGroup {
};
// Ensure user does not exists
if let Some(user) = User::from_name(name.as_str())
.map_err(|e| ActionError::GettingUserId(name.clone(), e))?
.map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e))
.map_err(Self::error)?
{
if user.uid.as_raw() != uid {
return Err(ActionError::UserUidMismatch(
return Err(Self::error(ActionErrorKind::UserUidMismatch(
name.clone(),
user.uid.as_raw(),
uid,
));
)));
}
if user.gid.as_raw() != gid {
return Err(ActionError::UserGidMismatch(
return Err(Self::error(ActionErrorKind::UserGidMismatch(
name.clone(),
user.gid.as_raw(),
gid,
));
)));
}
// See if group membership needs to be done
@ -74,7 +75,8 @@ impl AddUserToGroup {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
match output.status.code() {
Some(0) => {
// yes {user} is a member of {groupname}
@ -98,7 +100,9 @@ impl AddUserToGroup {
},
_ => {
// Some other issue
return Err(ActionError::command_output(&command, output));
return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
},
};
},
@ -109,8 +113,9 @@ impl AddUserToGroup {
.arg(&this.name)
.stdin(std::process::Stdio::null()),
)
.await?;
let output_str = String::from_utf8(output.stdout)?;
.await
.map_err(Self::error)?;
let output_str = String::from_utf8(output.stdout).map_err(Self::error)?;
let user_in_group = output_str.split(" ").any(|v| v == &this.groupname);
if user_in_group {
@ -187,7 +192,8 @@ impl Action for AddUserToGroup {
.arg(&name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/sbin/dseditgroup")
.process_group(0)
@ -199,7 +205,8 @@ impl Action for AddUserToGroup {
.arg(groupname)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
_ => {
if which::which("gpasswd").is_ok() {
@ -210,7 +217,8 @@ impl Action for AddUserToGroup {
.args([name, groupname])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("addgroup").is_ok() {
execute_command(
Command::new("addgroup")
@ -218,9 +226,12 @@ impl Action for AddUserToGroup {
.args([name, groupname])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingAddUserToGroupCommand);
return Err(Self::error(Self::error(
ActionErrorKind::MissingAddUserToGroupCommand,
)));
}
},
}
@ -264,7 +275,8 @@ impl Action for AddUserToGroup {
.arg(&name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
_ => {
if which::which("gpasswd").is_ok() {
@ -275,7 +287,8 @@ impl Action for AddUserToGroup {
.args([&name.to_string(), &groupname.to_string()])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("delgroup").is_ok() {
execute_command(
Command::new("delgroup")
@ -283,9 +296,12 @@ impl Action for AddUserToGroup {
.args([name, groupname])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingRemoveUserFromGroupCommand);
return Err(Self::error(
ActionErrorKind::MissingRemoveUserFromGroupCommand,
));
}
},
};

View file

@ -6,7 +6,7 @@ use nix::unistd::{chown, Group, User};
use tokio::fs::{create_dir, remove_dir_all};
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionState};
use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState};
use crate::action::{ActionError, StatefulAction};
/** Create a directory at the given location, optionally with an owning user, group, and mode.
@ -40,40 +40,47 @@ impl CreateDirectory {
let action_state = if path.exists() {
let metadata = tokio::fs::metadata(&path)
.await
.map_err(|e| ActionError::GettingMetadata(path.clone(), e))?;
.map_err(|e| ActionErrorKind::GettingMetadata(path.clone(), e))
.map_err(Self::error)?;
if !metadata.is_dir() {
return Err(ActionError::PathWasNotDirectory(path.to_owned()));
return Err(Self::error(ActionErrorKind::PathWasNotDirectory(
path.to_owned(),
)));
}
// Does it have the right user/group?
if let Some(user) = &user {
// If the file exists, the user must also exist to be correct.
let expected_uid = User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or_else(|| ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid;
let found_uid = metadata.uid();
if found_uid != expected_uid.as_raw() {
return Err(ActionError::PathUserMismatch(
return Err(Self::error(ActionErrorKind::PathUserMismatch(
path.clone(),
found_uid,
expected_uid.as_raw(),
));
)));
}
}
if let Some(group) = &group {
// If the file exists, the group must also exist to be correct.
let expected_gid = Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or_else(|| ActionError::NoUser(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(group.clone()))
.map_err(Self::error)?
.gid;
let found_gid = metadata.gid();
if found_gid != expected_gid.as_raw() {
return Err(ActionError::PathGroupMismatch(
return Err(Self::error(ActionErrorKind::PathGroupMismatch(
path.clone(),
found_gid,
expected_gid.as_raw(),
));
)));
}
}
@ -136,8 +143,10 @@ impl Action for CreateDirectory {
let gid = if let Some(group) = group {
Some(
Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or(ActionError::NoGroup(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoGroup(group.clone()))
.map_err(Self::error)?
.gid,
)
} else {
@ -146,8 +155,10 @@ impl Action for CreateDirectory {
let uid = if let Some(user) = user {
Some(
User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or(ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid,
)
} else {
@ -156,13 +167,17 @@ impl Action for CreateDirectory {
create_dir(path.clone())
.await
.map_err(|e| ActionError::CreateDirectory(path.clone(), e))?;
chown(path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?;
.map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e))
.map_err(Self::error)?;
chown(path, uid, gid)
.map_err(|e| ActionErrorKind::Chown(path.clone(), e))
.map_err(Self::error)?;
if let Some(mode) = mode {
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode))
.await
.map_err(|e| ActionError::SetPermissions(*mode, path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::SetPermissions(*mode, path.to_owned(), e))
.map_err(Self::error)?;
}
Ok(())
@ -202,14 +217,16 @@ impl Action for CreateDirectory {
let is_empty = path
.read_dir()
.map_err(|e| ActionError::Read(path.clone(), e))?
.map_err(|e| ActionErrorKind::Read(path.clone(), e))
.map_err(Self::error)?
.next()
.is_none();
match (is_empty, force_prune_on_revert) {
(true, _) | (false, true) => remove_dir_all(path.clone())
.await
.map_err(|e| ActionError::Remove(path.clone(), e))?,
.map_err(|e| ActionErrorKind::Remove(path.clone(), e))
.map_err(Self::error)?,
(false, false) => {
tracing::debug!("Not removing `{}`, the folder is not empty", path.display());
},

View file

@ -10,7 +10,9 @@ use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
/** Create a file at the given location with the provided `buf`,
optionally with an owning user, group, and mode.
@ -55,15 +57,17 @@ impl CreateFile {
// If the path exists, perhaps we can just skip this
let mut file = File::open(&this.path)
.await
.map_err(|e| ActionError::Open(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::Open(this.path.clone(), e))
.map_err(Self::error)?;
let metadata = file
.metadata()
.await
.map_err(|e| ActionError::GettingMetadata(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::GettingMetadata(this.path.clone(), e))
.map_err(Self::error)?;
if !metadata.is_file() {
return Err(ActionError::PathWasNotFile(this.path));
return Err(Self::error(ActionErrorKind::PathWasNotFile(this.path)));
}
if let Some(mode) = mode {
@ -73,11 +77,11 @@ impl CreateFile {
let discovered_mode = discovered_mode & 0o777;
if discovered_mode != mode {
return Err(ActionError::PathModeMismatch(
return Err(Self::error(ActionErrorKind::PathModeMismatch(
this.path.clone(),
discovered_mode,
mode,
));
)));
}
}
@ -85,31 +89,35 @@ impl CreateFile {
if let Some(user) = &this.user {
// If the file exists, the user must also exist to be correct.
let expected_uid = User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or_else(|| ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid;
let found_uid = metadata.uid();
if found_uid != expected_uid.as_raw() {
return Err(ActionError::PathUserMismatch(
return Err(Self::error(ActionErrorKind::PathUserMismatch(
this.path.clone(),
found_uid,
expected_uid.as_raw(),
));
)));
}
}
if let Some(group) = &this.group {
// If the file exists, the group must also exist to be correct.
let expected_gid = Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or_else(|| ActionError::NoUser(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(group.clone()))
.map_err(Self::error)?
.gid;
let found_gid = metadata.gid();
if found_gid != expected_gid.as_raw() {
return Err(ActionError::PathGroupMismatch(
return Err(Self::error(ActionErrorKind::PathGroupMismatch(
this.path.clone(),
found_gid,
expected_gid.as_raw(),
));
)));
}
}
@ -117,10 +125,13 @@ impl CreateFile {
let mut discovered_buf = String::new();
file.read_to_string(&mut discovered_buf)
.await
.map_err(|e| ActionError::Read(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::Read(this.path.clone(), e))
.map_err(Self::error)?;
if discovered_buf != this.buf {
return Err(ActionError::DifferentContent(this.path.clone()));
return Err(Self::error(ActionErrorKind::DifferentContent(
this.path.clone(),
)));
}
tracing::debug!("Creating file `{}` already complete", this.path.display());
@ -190,17 +201,21 @@ impl Action for CreateFile {
let mut file = options
.open(&path)
.await
.map_err(|e| ActionError::Open(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Open(path.to_owned(), e))
.map_err(Self::error)?;
file.write_all(buf.as_bytes())
.await
.map_err(|e| ActionError::Write(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Write(path.to_owned(), e))
.map_err(Self::error)?;
let gid = if let Some(group) = group {
Some(
Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or(ActionError::NoGroup(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoGroup(group.clone()))
.map_err(Self::error)?
.gid,
)
} else {
@ -209,14 +224,18 @@ impl Action for CreateFile {
let uid = if let Some(user) = user {
Some(
User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or(ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid,
)
} else {
None
};
chown(path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?;
chown(path, uid, gid)
.map_err(|e| ActionErrorKind::Chown(path.clone(), e))
.map_err(Self::error)?;
Ok(())
}
@ -250,7 +269,8 @@ impl Action for CreateFile {
remove_file(&path)
.await
.map_err(|e| ActionError::Remove(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Remove(path.to_owned(), e))
.map_err(Self::error)?;
Ok(())
}
@ -259,7 +279,7 @@ impl Action for CreateFile {
#[cfg(test)]
mod test {
use super::*;
use eyre::eyre;
use color_eyre::eyre::eyre;
use tokio::fs::write;
#[tokio::test]
@ -346,9 +366,20 @@ mod test {
)
.await
{
Err(ActionError::DifferentContent(path)) => assert_eq!(path, test_file.as_path()),
_ => return Err(eyre!("Should have returned an ActionError::Exists error")),
}
Err(error) => match error.kind() {
ActionErrorKind::DifferentContent(path) => assert_eq!(path, test_file.as_path()),
_ => {
return Err(eyre!(
"Should have returned an ActionErrorKind::Exists error"
))
},
},
_ => {
return Err(eyre!(
"Should have returned an ActionErrorKind::Exists error"
))
},
};
assert!(test_file.exists(), "File should have not been deleted");
@ -376,14 +407,21 @@ mod test {
)
.await
{
Err(ActionError::PathModeMismatch(path, got, expected)) => {
assert_eq!(path, test_file.as_path());
assert_eq!(expected, expected_mode);
assert_eq!(got, initial_mode);
Err(err) => match err.kind() {
ActionErrorKind::PathModeMismatch(path, got, expected) => {
assert_eq!(path, test_file.as_path());
assert_eq!(*expected, expected_mode);
assert_eq!(*got, initial_mode);
},
_ => {
return Err(eyre!(
"Should have returned an ActionErrorKind::PathModeMismatch error"
))
},
},
_ => {
return Err(eyre!(
"Should have returned an ActionError::PathModeMismatch error"
"Should have returned an ActionErrorKind::PathModeMismatch error"
))
},
}
@ -436,10 +474,17 @@ mod test {
)
.await
{
Err(ActionError::PathWasNotFile(path)) => assert_eq!(path, temp_dir.path()),
Err(err) => match err.kind() {
ActionErrorKind::PathWasNotFile(path) => assert_eq!(path, temp_dir.path()),
_ => {
return Err(eyre!(
"Should have returned an ActionErrorKind::PathWasNotFile error"
))
},
},
_ => {
return Err(eyre!(
"Should have returned an ActionError::PathWasNotFile error"
"Should have returned an ActionErrorKind::PathWasNotFile error"
))
},
}

View file

@ -2,7 +2,7 @@ use nix::unistd::Group;
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag};
use crate::action::{ActionError, ActionErrorKind, ActionTag};
use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction};
@ -25,14 +25,15 @@ impl CreateGroup {
};
// Ensure group does not exists
if let Some(group) = Group::from_name(name.as_str())
.map_err(|e| ActionError::GettingGroupId(name.clone(), e))?
.map_err(|e| ActionErrorKind::GettingGroupId(name.clone(), e))
.map_err(Self::error)?
{
if group.gid.as_raw() != gid {
return Err(ActionError::GroupGidMismatch(
return Err(Self::error(ActionErrorKind::GroupGidMismatch(
name.clone(),
group.gid.as_raw(),
gid,
));
)));
}
tracing::debug!("Creating group `{}` already complete", this.name);
@ -96,7 +97,8 @@ impl Action for CreateGroup {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
_ => {
if which::which("groupadd").is_ok() {
@ -106,7 +108,8 @@ impl Action for CreateGroup {
.args(["-g", &gid.to_string(), "--system", name])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("addgroup").is_ok() {
execute_command(
Command::new("addgroup")
@ -114,9 +117,10 @@ impl Action for CreateGroup {
.args(["-g", &gid.to_string(), "--system", name])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingGroupCreationCommand);
return Err(Self::error(ActionErrorKind::MissingGroupCreationCommand));
}
},
};
@ -151,7 +155,8 @@ impl Action for CreateGroup {
.args([".", "-delete", &format!("/Groups/{name}")])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
if !output.status.success() {}
},
_ => {
@ -162,7 +167,8 @@ impl Action for CreateGroup {
.arg(name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("delgroup").is_ok() {
execute_command(
Command::new("delgroup")
@ -170,9 +176,10 @@ impl Action for CreateGroup {
.arg(name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingGroupDeletionCommand);
return Err(Self::error(ActionErrorKind::MissingGroupDeletionCommand));
}
},
};

View file

@ -1,6 +1,8 @@
use nix::unistd::{chown, Group, User};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
use rand::Rng;
use std::{
io::SeekFrom,
@ -61,15 +63,17 @@ impl CreateOrInsertIntoFile {
// If the path exists, perhaps we can just skip this
let mut file = File::open(&this.path)
.await
.map_err(|e| ActionError::Open(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::Open(this.path.clone(), e))
.map_err(Self::error)?;
let metadata = file
.metadata()
.await
.map_err(|e| ActionError::GettingMetadata(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::GettingMetadata(this.path.clone(), e))
.map_err(Self::error)?;
if !metadata.is_file() {
return Err(ActionError::PathWasNotFile(this.path));
return Err(Self::error(ActionErrorKind::PathWasNotFile(this.path)));
}
if let Some(mode) = mode {
@ -92,31 +96,35 @@ impl CreateOrInsertIntoFile {
if let Some(user) = &this.user {
// If the file exists, the user must also exist to be correct.
let expected_uid = User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or_else(|| ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid;
let found_uid = metadata.uid();
if found_uid != expected_uid.as_raw() {
return Err(ActionError::PathUserMismatch(
return Err(Self::error(ActionErrorKind::PathUserMismatch(
this.path.clone(),
found_uid,
expected_uid.as_raw(),
));
)));
}
}
if let Some(group) = &this.group {
// If the file exists, the group must also exist to be correct.
let expected_gid = Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or_else(|| ActionError::NoUser(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(group.clone()))
.map_err(Self::error)?
.gid;
let found_gid = metadata.gid();
if found_gid != expected_gid.as_raw() {
return Err(ActionError::PathGroupMismatch(
return Err(Self::error(ActionErrorKind::PathGroupMismatch(
this.path.clone(),
found_gid,
expected_gid.as_raw(),
));
)));
}
}
@ -124,7 +132,8 @@ impl CreateOrInsertIntoFile {
let mut discovered_buf = String::new();
file.read_to_string(&mut discovered_buf)
.await
.map_err(|e| ActionError::Read(this.path.clone(), e))?;
.map_err(|e| ActionErrorKind::Read(this.path.clone(), e))
.map_err(Self::error)?;
if discovered_buf.contains(&this.buf) {
tracing::debug!("Inserting into `{}` already complete", this.path.display(),);
@ -185,7 +194,7 @@ impl Action for CreateOrInsertIntoFile {
let mut orig_file = match OpenOptions::new().read(true).open(&path).await {
Ok(f) => Some(f),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => return Err(ActionError::Open(path.to_owned(), e)),
Err(e) => return Err(Self::error(ActionErrorKind::Open(path.to_owned(), e))),
};
// Create a temporary file in the same directory as the one
@ -209,39 +218,44 @@ impl Action for CreateOrInsertIntoFile {
.open(&temp_file_path)
.await
.map_err(|e| {
ActionError::Open(temp_file_path.clone(), e)
})?;
ActionErrorKind::Open(temp_file_path.clone(), e)
}).map_err(Self::error)?;
if *position == Position::End {
if let Some(ref mut orig_file) = orig_file {
tokio::io::copy(orig_file, &mut temp_file)
.await
.map_err(|e| {
ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e)
})?;
ActionErrorKind::Copy(path.to_owned(), temp_file_path.to_owned(), e)
})
.map_err(Self::error)?;
}
}
temp_file
.write_all(buf.as_bytes())
.await
.map_err(|e| ActionError::Write(temp_file_path.clone(), e))?;
.map_err(|e| ActionErrorKind::Write(temp_file_path.clone(), e))
.map_err(Self::error)?;
if *position == Position::Beginning {
if let Some(ref mut orig_file) = orig_file {
tokio::io::copy(orig_file, &mut temp_file)
.await
.map_err(|e| {
ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e)
})?;
ActionErrorKind::Copy(path.to_owned(), temp_file_path.to_owned(), e)
})
.map_err(Self::error)?;
}
}
let gid = if let Some(group) = group {
Some(
Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))?
.ok_or(ActionError::NoGroup(group.clone()))?
.map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoGroup(group.clone()))
.map_err(Self::error)?
.gid,
)
} else {
@ -250,8 +264,10 @@ impl Action for CreateOrInsertIntoFile {
let uid = if let Some(user) = user {
Some(
User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))?
.ok_or(ActionError::NoUser(user.clone()))?
.map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.map_err(Self::error)?
.ok_or(ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid,
)
} else {
@ -261,17 +277,21 @@ impl Action for CreateOrInsertIntoFile {
// Change ownership _before_ applying mode, to ensure that if
// a file needs to be setuid it will never be setuid for the
// wrong user
chown(&temp_file_path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?;
chown(&temp_file_path, uid, gid)
.map_err(|e| ActionErrorKind::Chown(path.clone(), e))
.map_err(Self::error)?;
if let Some(mode) = mode {
tokio::fs::set_permissions(&temp_file_path, PermissionsExt::from_mode(*mode))
.await
.map_err(|e| ActionError::SetPermissions(*mode, path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::SetPermissions(*mode, path.to_owned(), e))
.map_err(Self::error)?;
} else if let Some(original_file) = orig_file {
let original_file_mode = original_file
.metadata()
.await
.map_err(|e| ActionError::GettingMetadata(path.to_path_buf(), e))?
.map_err(|e| ActionErrorKind::GettingMetadata(path.to_path_buf(), e))
.map_err(Self::error)?
.permissions()
.mode();
tokio::fs::set_permissions(
@ -279,12 +299,14 @@ impl Action for CreateOrInsertIntoFile {
PermissionsExt::from_mode(original_file_mode),
)
.await
.map_err(|e| ActionError::SetPermissions(original_file_mode, path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::SetPermissions(original_file_mode, path.to_owned(), e))
.map_err(Self::error)?;
}
tokio::fs::rename(&temp_file_path, &path)
.await
.map_err(|e| ActionError::Rename(path.to_owned(), temp_file_path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Rename(path.to_owned(), temp_file_path.to_owned(), e))
.map_err(Self::error)?;
Ok(())
}
@ -323,12 +345,14 @@ impl Action for CreateOrInsertIntoFile {
.read(true)
.open(&path)
.await
.map_err(|e| ActionError::Open(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Open(path.to_owned(), e))
.map_err(Self::error)?;
let mut file_contents = String::default();
file.read_to_string(&mut file_contents)
.await
.map_err(|e| ActionError::Read(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Read(path.to_owned(), e))
.map_err(Self::error)?;
if let Some(start) = file_contents.rfind(buf.as_str()) {
let end = start + buf.len();
@ -338,20 +362,25 @@ impl Action for CreateOrInsertIntoFile {
if file_contents.is_empty() {
remove_file(&path)
.await
.map_err(|e| ActionError::Remove(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Remove(path.to_owned(), e))
.map_err(Self::error)?;
} else {
file.seek(SeekFrom::Start(0))
.await
.map_err(|e| ActionError::Seek(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Seek(path.to_owned(), e))
.map_err(Self::error)?;
file.set_len(0)
.await
.map_err(|e| ActionError::Truncate(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Truncate(path.to_owned(), e))
.map_err(Self::error)?;
file.write_all(file_contents.as_bytes())
.await
.map_err(|e| ActionError::Write(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Write(path.to_owned(), e))
.map_err(Self::error)?;
file.flush()
.await
.map_err(|e| ActionError::Flush(path.to_owned(), e))?;
.map_err(|e| ActionErrorKind::Flush(path.to_owned(), e))
.map_err(Self::error)?;
}
Ok(())
}
@ -360,7 +389,7 @@ impl Action for CreateOrInsertIntoFile {
#[cfg(test)]
mod test {
use super::*;
use eyre::eyre;
use color_eyre::eyre::eyre;
use tokio::fs::{read_to_string, write};
#[tokio::test]
@ -534,10 +563,17 @@ mod test {
)
.await
{
Err(ActionError::PathWasNotFile(path)) => assert_eq!(path, temp_dir.path()),
Err(err) => match err.kind() {
ActionErrorKind::PathWasNotFile(path) => assert_eq!(path, temp_dir.path()),
_ => {
return Err(eyre!(
"Should have returned an ActionErrorKind::PathWasNotFile error"
))
},
},
_ => {
return Err(eyre!(
"Should have returned an ActionError::PathWasNotFile error"
"Should have returned an ActionErrorKind::PathWasNotFile error"
))
},
}

View file

@ -11,7 +11,9 @@ use tokio::{
};
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
/// The `nix.conf` configuration names that are safe to merge.
// FIXME(@cole-h): make configurable by downstream users?
@ -33,6 +35,12 @@ pub enum CreateOrMergeNixConfigError {
UnmergeableConfig(Vec<String>, std::path::PathBuf),
}
impl Into<ActionErrorKind> for CreateOrMergeNixConfigError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}
/// Create or merge an existing `nix.conf` at the specified path.
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateOrMergeNixConfig {
@ -132,10 +140,10 @@ impl CreateOrMergeNixConfig {
let path = path.to_path_buf();
let metadata = path
.metadata()
.map_err(|e| ActionError::GettingMetadata(path.clone(), e))?;
.map_err(|e| Self::error(ActionErrorKind::GettingMetadata(path.clone(), e)))?;
if !metadata.is_file() {
return Err(ActionError::PathWasNotFile(path));
return Err(Self::error(ActionErrorKind::PathWasNotFile(path)));
}
// Does the file have the right permissions?
@ -144,23 +152,23 @@ impl CreateOrMergeNixConfig {
let discovered_mode = discovered_mode & 0o777;
if discovered_mode != NIX_CONF_MODE {
return Err(ActionError::PathModeMismatch(
return Err(Self::error(ActionErrorKind::PathModeMismatch(
path,
discovered_mode,
NIX_CONF_MODE,
));
)));
}
let existing_nix_config = NixConfig::parse_file(&path)
.map_err(CreateOrMergeNixConfigError::ParseNixConfig)
.map_err(|e| ActionError::Custom(Box::new(e)))?;
.map_err(Self::error)?;
let (merged_nix_config, existing_nix_config) = Self::merge_pending_and_existing_nix_config(
&pending_nix_config,
&existing_nix_config,
&path,
)
.map_err(|e| ActionError::Custom(Box::new(e)))?;
.map_err(Self::error)?;
Ok((merged_nix_config, existing_nix_config))
}
@ -260,7 +268,7 @@ impl Action for CreateOrMergeNixConfig {
.open(&temp_file_path)
.await
.map_err(|e| {
ActionError::Open(temp_file_path.clone(), e)
Self::error(ActionErrorKind::Open(temp_file_path.clone(), e))
})?;
let (mut merged_nix_config, mut existing_nix_config) = if path.exists() {
@ -276,7 +284,7 @@ impl Action for CreateOrMergeNixConfig {
if let Some(existing_nix_config) = existing_nix_config.as_mut() {
let mut discovered_buf = tokio::fs::read_to_string(&path)
.await
.map_err(|e| ActionError::Read(path.to_path_buf(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Read(path.to_path_buf(), e)))?;
// We append a newline to ensure that, in the case there are comments at the end of the
// file and _NO_ trailing newline, we still preserve the entire block of comments.
@ -416,13 +424,25 @@ impl Action for CreateOrMergeNixConfig {
temp_file
.write_all(new_config.as_bytes())
.await
.map_err(|e| ActionError::Write(temp_file_path.clone(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Write(temp_file_path.clone(), e)))?;
tokio::fs::set_permissions(&temp_file_path, PermissionsExt::from_mode(NIX_CONF_MODE))
.await
.map_err(|e| ActionError::SetPermissions(NIX_CONF_MODE, path.to_owned(), e))?;
.map_err(|e| {
Self::error(ActionErrorKind::SetPermissions(
NIX_CONF_MODE,
path.to_owned(),
e,
))
})?;
tokio::fs::rename(&temp_file_path, &path)
.await
.map_err(|e| ActionError::Rename(temp_file_path.to_owned(), path.to_owned(), e))?;
.map_err(|e| {
Self::error(ActionErrorKind::Rename(
temp_file_path.to_owned(),
path.to_owned(),
e,
))
})?;
Ok(())
}
@ -448,7 +468,7 @@ impl Action for CreateOrMergeNixConfig {
remove_file(&path)
.await
.map_err(|e| ActionError::Remove(path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Remove(path.to_owned(), e)))?;
Ok(())
}
@ -457,7 +477,7 @@ impl Action for CreateOrMergeNixConfig {
#[cfg(test)]
mod test {
use super::*;
use eyre::eyre;
use color_eyre::eyre::eyre;
use tokio::fs::write;
#[tokio::test]
@ -600,15 +620,20 @@ mod test {
.settings_mut()
.insert("warn-dirty".into(), "false".into());
match CreateOrMergeNixConfig::plan(&test_file, nix_config).await {
Err(ActionError::Custom(e)) => match e.downcast_ref::<CreateOrMergeNixConfigError>() {
Some(CreateOrMergeNixConfigError::UnmergeableConfig(_, path)) => {
assert_eq!(path, test_file.as_path())
},
_ => {
return Err(eyre!(
"Should have returned CreateOrMergeNixConfigError::UnmergeableConfig"
))
Err(err) => match err.kind() {
ActionErrorKind::Custom(e) => {
match e.downcast_ref::<CreateOrMergeNixConfigError>() {
Some(CreateOrMergeNixConfigError::UnmergeableConfig(_, path)) => {
assert_eq!(path, test_file.as_path())
},
_ => {
return Err(eyre!(
"Should have returned CreateOrMergeNixConfigError::UnmergeableConfig"
))
},
}
},
_ => (),
},
_ => {
return Err(eyre!(

View file

@ -2,7 +2,7 @@ use nix::unistd::User;
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag};
use crate::action::{ActionError, ActionErrorKind, ActionTag};
use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction};
@ -37,22 +37,23 @@ impl CreateUser {
};
// Ensure user does not exists
if let Some(user) = User::from_name(name.as_str())
.map_err(|e| ActionError::GettingUserId(name.clone(), e))?
.map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e))
.map_err(Self::error)?
{
if user.uid.as_raw() != uid {
return Err(ActionError::UserUidMismatch(
return Err(Self::error(ActionErrorKind::UserUidMismatch(
name.clone(),
user.uid.as_raw(),
uid,
));
)));
}
if user.gid.as_raw() != gid {
return Err(ActionError::UserGidMismatch(
return Err(Self::error(ActionErrorKind::UserGidMismatch(
name.clone(),
user.gid.as_raw(),
gid,
));
)));
}
tracing::debug!("Creating user `{}` already complete", this.name);
@ -120,7 +121,8 @@ impl Action for CreateUser {
.args([".", "-create", &format!("/Users/{name}")])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
@ -133,7 +135,8 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
@ -146,7 +149,8 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
@ -159,7 +163,8 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
@ -172,14 +177,16 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
.args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
_ => {
if which::which("useradd").is_ok() {
@ -207,7 +214,8 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("adduser").is_ok() {
execute_command(
Command::new("adduser")
@ -229,9 +237,10 @@ impl Action for CreateUser {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingUserCreationCommand);
return Err(Self::error(ActionErrorKind::MissingUserCreationCommand));
}
},
}
@ -253,14 +262,6 @@ impl Action for CreateUser {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
name,
uid: _,
groupname: _,
gid: _,
comment: _,
} = self;
use target_lexicon::OperatingSystem;
match target_lexicon::OperatingSystem::host() {
OperatingSystem::MacOSX {
@ -274,25 +275,28 @@ impl Action for CreateUser {
// Documentation on https://it.megocollector.com/macos/cant-delete-a-macos-user-with-dscl-resolution/ and http://www.aixperts.co.uk/?p=214 suggested it was a secure token
// That is correct, however it's a bit more nuanced. It appears to be that a user must be graphically logged in for some other user on the system to be deleted.
let mut command = Command::new("/usr/bin/dscl");
command.args([".", "-delete", &format!("/Users/{name}")]);
command.args([".", "-delete", &format!("/Users/{}", self.name)]);
command.process_group(0);
command.stdin(std::process::Stdio::null());
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
let stderr = String::from_utf8_lossy(&output.stderr);
match output.status.code() {
Some(0) => (),
Some(40) if stderr.contains("-14120") => {
// The user is on an ephemeral Mac, like detsys uses
// These Macs cannot always delete users, as sometimes there is no graphical login
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{name}");
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name);
},
_ => {
// Something went wrong
return Err(ActionError::command_output(&command, output));
return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
},
}
},
@ -301,20 +305,22 @@ impl Action for CreateUser {
execute_command(
Command::new("userdel")
.process_group(0)
.arg(name)
.arg(&self.name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else if which::which("deluser").is_ok() {
execute_command(
Command::new("deluser")
.process_group(0)
.arg(name)
.arg(&self.name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
return Err(ActionError::MissingUserDeletionCommand);
return Err(Self::error(ActionErrorKind::MissingUserDeletionCommand));
}
},
};

View file

@ -5,7 +5,7 @@ use reqwest::Url;
use tracing::{span, Span};
use crate::{
action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction},
action::{Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction},
parse_ssl_cert,
};
@ -33,26 +33,18 @@ impl FetchAndUnpackNix {
match url.scheme() {
"https" | "http" | "file" => (),
_ => {
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownUrlScheme,
)))
},
_ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)),
};
if let Some(proxy) = &proxy {
match proxy.scheme() {
"https" | "http" | "socks5" => (),
_ => {
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownProxyScheme,
)))
},
_ => return Err(Self::error(FetchUrlError::UnknownProxyScheme)),
};
}
if let Some(ssl_cert_file) = &ssl_cert_file {
parse_ssl_cert(&ssl_cert_file).await?;
parse_ssl_cert(&ssl_cert_file).await.map_err(Self::error)?;
}
Ok(Self {
@ -106,41 +98,43 @@ impl Action for FetchAndUnpackNix {
"https" | "http" => {
let mut buildable_client = reqwest::Client::builder();
if let Some(proxy) = &self.proxy {
buildable_client =
buildable_client.proxy(reqwest::Proxy::all(proxy.clone()).map_err(|e| {
ActionError::Custom(Box::new(FetchUrlError::Reqwest(e)))
})?)
buildable_client = buildable_client.proxy(
reqwest::Proxy::all(proxy.clone())
.map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?,
)
}
if let Some(ssl_cert_file) = &self.ssl_cert_file {
let ssl_cert = parse_ssl_cert(&ssl_cert_file).await?;
let ssl_cert = parse_ssl_cert(&ssl_cert_file).await.map_err(Self::error)?;
buildable_client = buildable_client.add_root_certificate(ssl_cert);
}
let client = buildable_client
.build()
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?;
.map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
let req = client
.get(self.url.clone())
.build()
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?;
.map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
let res = client
.execute(req)
.await
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?;
.map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
res.bytes()
.await
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?
.map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?
},
"file" => {
let buf = tokio::fs::read(self.url.path())
.await
.map_err(|e| ActionError::Read(PathBuf::from(self.url.path()), e))?;
.map_err(|e| ActionErrorKind::Read(PathBuf::from(self.url.path()), e))
.map_err(Self::error)?;
Bytes::from(buf)
},
_ => {
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownUrlScheme,
)))
},
_ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)),
};
// TODO(@Hoverbear): Pick directory
@ -151,7 +145,8 @@ impl Action for FetchAndUnpackNix {
let mut archive = tar::Archive::new(decoder);
archive
.unpack(&dest_clone)
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Unarchive(e))))?;
.map_err(FetchUrlError::Unarchive)
.map_err(Self::error)?;
Ok(())
}
@ -182,3 +177,9 @@ pub enum FetchUrlError {
#[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")]
UnknownProxyScheme,
}
impl Into<ActionErrorKind> for FetchUrlError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -2,7 +2,9 @@ use std::path::{Path, PathBuf};
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
pub(crate) const DEST: &str = "/nix/";
@ -57,32 +59,37 @@ impl Action for MoveUnpackedNix {
// This is the `nix-$VERSION` folder which unpacks from the tarball, not a nix derivation
let found_nix_paths = glob::glob(&format!("{}/nix-*", unpacked_path.display()))
.map_err(|e| ActionError::Custom(Box::new(e)))?
.map_err(|e| Self::error(MoveUnpackedNixError::from(e)))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ActionError::Custom(Box::new(e)))?;
.map_err(|e| Self::error(MoveUnpackedNixError::from(e)))?;
if found_nix_paths.len() != 1 {
return Err(ActionError::MalformedBinaryTarball);
return Err(Self::error(ActionErrorKind::MalformedBinaryTarball));
}
let found_nix_path = found_nix_paths.into_iter().next().unwrap();
let src_store = found_nix_path.join("store");
let mut src_store_listing = tokio::fs::read_dir(src_store.clone())
.await
.map_err(|e| ActionError::ReadDir(src_store.clone(), e))?;
.map_err(|e| ActionErrorKind::ReadDir(src_store.clone(), e))
.map_err(Self::error)?;
let dest_store = Path::new(DEST).join("store");
if dest_store.exists() {
if !dest_store.is_dir() {
return Err(ActionError::PathWasNotDirectory(dest_store.clone()))?;
return Err(Self::error(ActionErrorKind::PathWasNotDirectory(
dest_store.clone(),
)))?;
}
} else {
tokio::fs::create_dir(&dest_store)
.await
.map_err(|e| ActionError::CreateDirectory(dest_store.clone(), e))?;
.map_err(|e| ActionErrorKind::CreateDirectory(dest_store.clone(), e))
.map_err(Self::error)?;
}
while let Some(entry) = src_store_listing
.next_entry()
.await
.map_err(|e| ActionError::ReadDir(src_store.clone(), e))?
.map_err(|e| ActionErrorKind::ReadDir(src_store.clone(), e))
.map_err(Self::error)?
{
let entry_dest = dest_store.join(entry.file_name());
if entry_dest.exists() {
@ -92,8 +99,9 @@ impl Action for MoveUnpackedNix {
tokio::fs::rename(&entry.path(), &entry_dest)
.await
.map_err(|e| {
ActionError::Rename(entry.path().clone(), entry_dest.to_owned(), e)
})?;
ActionErrorKind::Rename(entry.path().clone(), entry_dest.to_owned(), e)
})
.map_err(Self::error)?;
}
}
@ -127,3 +135,9 @@ pub enum MoveUnpackedNixError {
glob::GlobError,
),
}
impl Into<ActionErrorKind> for MoveUnpackedNixError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use tokio::fs::remove_dir_all;
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionState};
use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState};
use crate::action::{ActionError, StatefulAction};
/** Remove a directory, does nothing on revert.
@ -53,11 +53,13 @@ impl Action for RemoveDirectory {
async fn execute(&mut self) -> Result<(), ActionError> {
if self.path.exists() {
if !self.path.is_dir() {
return Err(ActionError::PathWasNotDirectory(self.path.clone()));
return Err(Self::error(ActionErrorKind::PathWasNotDirectory(
self.path.clone(),
)));
}
remove_dir_all(&self.path)
.await
.map_err(|e| ActionError::Remove(self.path.clone(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Remove(self.path.clone(), e)))?;
} else {
tracing::debug!("Directory `{}` not present, skipping", self.path.display(),);
};

View file

@ -1,7 +1,7 @@
use std::path::PathBuf;
use crate::{
action::{ActionError, ActionTag, StatefulAction},
action::{ActionError, ActionErrorKind, ActionTag, StatefulAction},
execute_command, set_env,
};
@ -50,9 +50,9 @@ impl Action for SetupDefaultProfile {
// Find an `nix` package
let nix_pkg_glob = "/nix/store/*-nix-*";
let mut found_nix_pkg = None;
for entry in glob(nix_pkg_glob).map_err(|e| {
ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e)))
})? {
for entry in glob(nix_pkg_glob)
.map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))?
{
match entry {
Ok(path) => {
// TODO(@Hoverbear): Should probably ensure is unique
@ -65,17 +65,15 @@ impl Action for SetupDefaultProfile {
let nix_pkg = if let Some(nix_pkg) = found_nix_pkg {
nix_pkg
} else {
return Err(ActionError::Custom(Box::new(
SetupDefaultProfileError::NoNix,
)));
return Err(Self::error(SetupDefaultProfileError::NoNix));
};
// Find an `nss-cacert` package, add it too.
let nss_ca_cert_pkg_glob = "/nix/store/*-nss-cacert-*";
let mut found_nss_ca_cert_pkg = None;
for entry in glob(nss_ca_cert_pkg_glob).map_err(|e| {
ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e)))
})? {
for entry in glob(nss_ca_cert_pkg_glob)
.map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))?
{
match entry {
Ok(path) => {
// TODO(@Hoverbear): Should probably ensure is unique
@ -88,23 +86,22 @@ impl Action for SetupDefaultProfile {
let nss_ca_cert_pkg = if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg {
nss_ca_cert_pkg
} else {
return Err(ActionError::Custom(Box::new(
SetupDefaultProfileError::NoNssCacert,
)));
return Err(Self::error(SetupDefaultProfileError::NoNssCacert));
};
let found_nix_paths = glob::glob(&format!("{}/nix-*", self.unpacked_path.display()))
.map_err(|e| ActionError::Custom(Box::new(e)))?
.map_err(|e| Self::error(SetupDefaultProfileError::from(e)))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ActionError::Custom(Box::new(e)))?;
.map_err(|e| Self::error(SetupDefaultProfileError::from(e)))?;
if found_nix_paths.len() != 1 {
return Err(ActionError::MalformedBinaryTarball);
return Err(Self::error(ActionErrorKind::MalformedBinaryTarball));
}
let found_nix_path = found_nix_paths.into_iter().next().unwrap();
let reginfo_path = PathBuf::from(found_nix_path).join(".reginfo");
let reginfo = tokio::fs::read(&reginfo_path)
.await
.map_err(|e| ActionError::Read(reginfo_path.to_path_buf(), e))?;
.map_err(|e| ActionErrorKind::Read(reginfo_path.to_path_buf(), e))
.map_err(Self::error)?;
let mut load_db_command = Command::new(nix_pkg.join("bin/nix-store"));
load_db_command.process_group(0);
load_db_command.arg("--load-db");
@ -113,9 +110,7 @@ impl Action for SetupDefaultProfile {
load_db_command.stderr(std::process::Stdio::piped());
load_db_command.env(
"HOME",
dirs::home_dir().ok_or_else(|| {
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome))
})?,
dirs::home_dir().ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
);
tracing::trace!(
"Executing `{:?}` with stdin from `{}`",
@ -124,17 +119,20 @@ impl Action for SetupDefaultProfile {
);
let mut handle = load_db_command
.spawn()
.map_err(|e| ActionError::command(&load_db_command, e))?;
.map_err(|e| ActionErrorKind::command(&load_db_command, e))
.map_err(Self::error)?;
let mut stdin = handle.stdin.take().unwrap();
stdin
.write_all(&reginfo)
.await
.map_err(|e| ActionError::Write(PathBuf::from("/dev/stdin"), e))?;
.map_err(|e| ActionErrorKind::Write(PathBuf::from("/dev/stdin"), e))
.map_err(Self::error)?;
stdin
.flush()
.await
.map_err(|e| ActionError::Write(PathBuf::from("/dev/stdin"), e))?;
.map_err(|e| ActionErrorKind::Write(PathBuf::from("/dev/stdin"), e))
.map_err(Self::error)?;
drop(stdin);
tracing::trace!(
"Wrote `{}` to stdin of `nix-store --load-db`",
@ -144,9 +142,13 @@ impl Action for SetupDefaultProfile {
let output = handle
.wait_with_output()
.await
.map_err(|e| ActionError::command(&load_db_command, e))?;
.map_err(|e| ActionErrorKind::command(&load_db_command, e))
.map_err(Self::error)?;
if !output.status.success() {
return Err(ActionError::command_output(&load_db_command, output));
return Err(Self::error(ActionErrorKind::command_output(
&load_db_command,
output,
)));
};
// Install `nix` itself into the store
@ -158,16 +160,16 @@ impl Action for SetupDefaultProfile {
.stdin(std::process::Stdio::null())
.env(
"HOME",
dirs::home_dir().ok_or_else(|| {
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome))
})?,
dirs::home_dir()
.ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
)
.env(
"NIX_SSL_CERT_FILE",
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */
)
.await?;
.await
.map_err(Self::error)?;
// Install `nix` itself into the store
execute_command(
@ -178,16 +180,16 @@ impl Action for SetupDefaultProfile {
.stdin(std::process::Stdio::null())
.env(
"HOME",
dirs::home_dir().ok_or_else(|| {
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome))
})?,
dirs::home_dir()
.ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
)
.env(
"NIX_SSL_CERT_FILE",
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */
)
.await?;
.await
.map_err(Self::error)?;
set_env(
"NIX_SSL_CERT_FILE",
@ -234,3 +236,9 @@ pub enum SetupDefaultProfileError {
#[error("No root home found to place channel configuration in")]
NoRootHome,
}
impl Into<ActionErrorKind> for SetupDefaultProfileError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -4,7 +4,7 @@ use std::path::PathBuf;
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command;
use crate::action::{Action, ActionDescription};
@ -39,7 +39,7 @@ pub struct ConfigureInitService {
impl ConfigureInitService {
#[cfg(target_os = "linux")]
async fn check_if_systemd_unit_exists(src: &str, dest: &str) -> Result<(), ActionError> {
async fn check_if_systemd_unit_exists(src: &str, dest: &str) -> Result<(), ActionErrorKind> {
// TODO: once we have a way to communicate interaction between the library and the cli,
// interactively ask for permission to remove the file
@ -50,21 +50,24 @@ impl ConfigureInitService {
if unit_dest.is_symlink() {
let link_dest = tokio::fs::read_link(&unit_dest)
.await
.map_err(|e| ActionError::ReadSymlink(unit_dest.clone(), e))?;
.map_err(|e| ActionErrorKind::ReadSymlink(unit_dest.clone(), e))?;
if link_dest != unit_src {
return Err(ActionError::SymlinkExists(unit_dest));
return Err(ActionErrorKind::SymlinkExists(unit_dest));
}
} else {
return Err(ActionError::FileExists(unit_dest));
return Err(ActionErrorKind::FileExists(unit_dest));
}
}
// NOTE: ...and if there are any overrides in the most well-known places for systemd
if Path::new(&format!("{dest}.d")).exists() {
return Err(ActionError::DirExists(PathBuf::from(format!("{dest}.d"))));
return Err(ActionErrorKind::DirExists(PathBuf::from(format!(
"{dest}.d"
))));
}
Ok(())
}
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(
init: InitSystem,
@ -75,7 +78,7 @@ impl ConfigureInitService {
Some(
ssl_cert_file
.canonicalize()
.map_err(|e| ActionError::Canonicalize(ssl_cert_file, e))?,
.map_err(|e| Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)))?,
)
} else {
None
@ -92,11 +95,15 @@ impl ConfigureInitService {
// with systemd: https://www.freedesktop.org/software/systemd/man/sd_booted.html
if !(Path::new("/run/systemd/system").exists() || which::which("systemctl").is_ok())
{
return Err(ActionError::SystemdMissing);
return Err(Self::error(ActionErrorKind::SystemdMissing));
}
Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST).await?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST).await?;
Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST)
.await
.map_err(Self::error)?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST)
.await
.map_err(Self::error)?;
},
#[cfg(target_os = "linux")]
InitSystem::None => {
@ -183,11 +190,11 @@ impl Action for ConfigureInitService {
tokio::fs::copy(src.clone(), DARWIN_NIX_DAEMON_DEST)
.await
.map_err(|e| {
ActionError::Copy(
Self::error(ActionErrorKind::Copy(
src.to_path_buf(),
PathBuf::from(DARWIN_NIX_DAEMON_DEST),
e,
)
))
})?;
execute_command(
@ -197,7 +204,8 @@ impl Action for ConfigureInitService {
.arg(DARWIN_NIX_DAEMON_DEST)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
if let Some(ssl_cert_file) = ssl_cert_file {
execute_command(
@ -208,7 +216,8 @@ impl Action for ConfigureInitService {
.arg(format!("{ssl_cert_file:?}"))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
}
if *start_daemon {
@ -220,7 +229,8 @@ impl Action for ConfigureInitService {
.arg("system/org.nixos.nix-daemon")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
}
},
#[cfg(target_os = "linux")]
@ -232,23 +242,32 @@ impl Action for ConfigureInitService {
.arg("daemon-reload")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
}
// The goal state is the `socket` enabled and active, the service not enabled and stopped (it activates via socket activation)
let socket_was_active = if is_enabled("nix-daemon.socket").await? {
disable("nix-daemon.socket", true).await?;
true
} else if is_active("nix-daemon.socket").await? {
stop("nix-daemon.socket").await?;
true
} else {
false
};
if is_enabled("nix-daemon.service").await? {
let now = is_active("nix-daemon.service").await?;
disable("nix-daemon.service", now).await?;
} else if is_active("nix-daemon.service").await? {
stop("nix-daemon.service").await?;
let socket_was_active =
if is_enabled("nix-daemon.socket").await.map_err(Self::error)? {
disable("nix-daemon.socket", true)
.await
.map_err(Self::error)?;
true
} else if is_active("nix-daemon.socket").await.map_err(Self::error)? {
stop("nix-daemon.socket").await.map_err(Self::error)?;
true
} else {
false
};
if is_enabled("nix-daemon.service")
.await
.map_err(Self::error)?
{
let now = is_active("nix-daemon.service").await.map_err(Self::error)?;
disable("nix-daemon.service", now)
.await
.map_err(Self::error)?;
} else if is_active("nix-daemon.service").await.map_err(Self::error)? {
stop("nix-daemon.service").await.map_err(Self::error)?;
};
tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking");
@ -256,12 +275,13 @@ impl Action for ConfigureInitService {
tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST)
.await
.map_err(|e| {
ActionError::Symlink(
ActionErrorKind::Symlink(
PathBuf::from(TMPFILES_SRC),
PathBuf::from(TMPFILES_DEST),
e,
)
})?;
})
.map_err(Self::error)?;
}
execute_command(
@ -271,32 +291,39 @@ impl Action for ConfigureInitService {
.arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
// TODO: once we have a way to communicate interaction between the library and the
// cli, interactively ask for permission to remove the file
Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST).await?;
Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST)
.await
.map_err(Self::error)?;
tokio::fs::symlink(SERVICE_SRC, SERVICE_DEST)
.await
.map_err(|e| {
ActionError::Symlink(
ActionErrorKind::Symlink(
PathBuf::from(SERVICE_SRC),
PathBuf::from(SERVICE_DEST),
e,
)
})?;
})
.map_err(Self::error)?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST).await?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST)
.await
.map_err(Self::error)?;
tokio::fs::symlink(SOCKET_SRC, SOCKET_DEST)
.await
.map_err(|e| {
ActionError::Symlink(
ActionErrorKind::Symlink(
PathBuf::from(SOCKET_SRC),
PathBuf::from(SOCKET_DEST),
e,
)
})?;
})
.map_err(Self::error)?;
if *start_daemon {
execute_command(
@ -305,7 +332,8 @@ impl Action for ConfigureInitService {
.arg("daemon-reload")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
}
if let Some(ssl_cert_file) = ssl_cert_file {
@ -313,8 +341,9 @@ impl Action for ConfigureInitService {
tokio::fs::create_dir(&service_conf_dir_path)
.await
.map_err(|e| {
ActionError::CreateDirectory(service_conf_dir_path.clone(), e)
})?;
ActionErrorKind::CreateDirectory(service_conf_dir_path.clone(), e)
})
.map_err(Self::error)?;
let service_conf_file_path =
service_conf_dir_path.join("nix-ssl-cert-file.conf");
tokio::fs::write(
@ -327,13 +356,14 @@ impl Action for ConfigureInitService {
),
)
.await
.map_err(|e| ActionError::Write(ssl_cert_file.clone(), e))?;
.map_err(|e| ActionErrorKind::Write(ssl_cert_file.clone(), e))
.map_err(Self::error)?;
}
if *start_daemon || socket_was_active {
enable(SOCKET_SRC, true).await?;
enable(SOCKET_SRC, true).await.map_err(Self::error)?;
} else {
enable(SOCKET_SRC, false).await?;
enable(SOCKET_SRC, false).await.map_err(Self::error)?;
}
},
#[cfg(not(target_os = "macos"))]
@ -373,6 +403,8 @@ impl Action for ConfigureInitService {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
match self.init {
#[cfg(target_os = "macos")]
InitSystem::Launchd => {
@ -382,84 +414,118 @@ impl Action for ConfigureInitService {
.arg("unload")
.arg(DARWIN_NIX_DAEMON_DEST),
)
.await?;
.await
.map_err(|e| Self::error(e))?;
},
#[cfg(target_os = "linux")]
InitSystem::Systemd => {
// We separate stop and disable (instead of using `--now`) to avoid cases where the service isn't started, but is enabled.
let socket_is_active = is_active("nix-daemon.socket").await?;
let socket_is_enabled = is_enabled("nix-daemon.socket").await?;
let service_is_active = is_active("nix-daemon.service").await?;
let service_is_enabled = is_enabled("nix-daemon.service").await?;
// These have to fail fast.
let socket_is_active = is_active("nix-daemon.socket")
.await
.map_err(|e| Self::error(e))?;
let socket_is_enabled = is_enabled("nix-daemon.socket")
.await
.map_err(|e| Self::error(e))?;
let service_is_active = is_active("nix-daemon.service")
.await
.map_err(|e| Self::error(e))?;
let service_is_enabled = is_enabled("nix-daemon.service")
.await
.map_err(|e| Self::error(e))?;
if socket_is_active {
execute_command(
if let Err(err) = execute_command(
Command::new("systemctl")
.process_group(0)
.args(["stop", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
}
if socket_is_enabled {
execute_command(
if let Err(err) = execute_command(
Command::new("systemctl")
.process_group(0)
.args(["disable", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
}
if service_is_active {
execute_command(
if let Err(err) = execute_command(
Command::new("systemctl")
.process_group(0)
.args(["stop", "nix-daemon.service"])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
}
if service_is_enabled {
execute_command(
if let Err(err) = execute_command(
Command::new("systemctl")
.process_group(0)
.args(["disable", "nix-daemon.service"])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
}
execute_command(
if let Err(err) = execute_command(
Command::new("systemd-tmpfiles")
.process_group(0)
.arg("--remove")
.arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
if self.ssl_cert_file.is_some() {
let service_conf_dir_path = PathBuf::from(format!("{SERVICE_DEST}.d"));
tokio::fs::remove_dir_all(&service_conf_dir_path)
if let Err(err) = tokio::fs::remove_dir_all(&service_conf_dir_path)
.await
.map_err(|e| ActionError::Remove(service_conf_dir_path.clone(), e))?;
.map_err(|e| ActionErrorKind::Remove(service_conf_dir_path.clone(), e))
{
errors.push(err);
}
}
tokio::fs::remove_file(TMPFILES_DEST)
if let Err(err) = tokio::fs::remove_file(TMPFILES_DEST)
.await
.map_err(|e| ActionError::Remove(PathBuf::from(TMPFILES_DEST), e))?;
.map_err(|e| ActionErrorKind::Remove(PathBuf::from(TMPFILES_DEST), e))
{
errors.push(err);
}
execute_command(
if let Err(err) = execute_command(
Command::new("systemctl")
.process_group(0)
.arg("daemon-reload")
.stdin(std::process::Stdio::null()),
)
.await?;
.await
{
errors.push(err);
}
},
#[cfg(not(target_os = "macos"))]
InitSystem::None => {
@ -467,7 +533,18 @@ impl Action for ConfigureInitService {
},
};
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(Self::error(
errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"),
))
} else {
Err(Self::error(ActionErrorKind::Multiple(errors)))
}
}
}
@ -479,25 +556,25 @@ pub enum ConfigureNixDaemonServiceError {
}
#[cfg(target_os = "linux")]
async fn stop(unit: &str) -> Result<(), ActionError> {
async fn stop(unit: &str) -> Result<(), ActionErrorKind> {
let mut command = Command::new("systemctl");
command.arg("stop");
command.arg(unit);
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() {
true => {
tracing::trace!(%unit, "Stopped");
Ok(())
},
false => Err(ActionError::command_output(&command, output)),
false => Err(ActionErrorKind::command_output(&command, output)),
}
}
#[cfg(target_os = "linux")]
async fn enable(unit: &str, now: bool) -> Result<(), ActionError> {
async fn enable(unit: &str, now: bool) -> Result<(), ActionErrorKind> {
let mut command = Command::new("systemctl");
command.arg("enable");
command.arg(unit);
@ -507,18 +584,18 @@ async fn enable(unit: &str, now: bool) -> Result<(), ActionError> {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() {
true => {
tracing::trace!(%unit, %now, "Enabled unit");
Ok(())
},
false => Err(ActionError::command_output(&command, output)),
false => Err(ActionErrorKind::command_output(&command, output)),
}
}
#[cfg(target_os = "linux")]
async fn disable(unit: &str, now: bool) -> Result<(), ActionError> {
async fn disable(unit: &str, now: bool) -> Result<(), ActionErrorKind> {
let mut command = Command::new("systemctl");
command.arg("disable");
command.arg(unit);
@ -528,25 +605,25 @@ async fn disable(unit: &str, now: bool) -> Result<(), ActionError> {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() {
true => {
tracing::trace!(%unit, %now, "Disabled unit");
Ok(())
},
false => Err(ActionError::command_output(&command, output)),
false => Err(ActionErrorKind::command_output(&command, output)),
}
}
#[cfg(target_os = "linux")]
async fn is_active(unit: &str) -> Result<bool, ActionError> {
async fn is_active(unit: &str) -> Result<bool, ActionErrorKind> {
let mut command = Command::new("systemctl");
command.arg("is-active");
command.arg(unit);
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))?;
if String::from_utf8(output.stdout)?.starts_with("active") {
tracing::trace!(%unit, "Is active");
Ok(true)
@ -557,14 +634,14 @@ async fn is_active(unit: &str) -> Result<bool, ActionError> {
}
#[cfg(target_os = "linux")]
async fn is_enabled(unit: &str) -> Result<bool, ActionError> {
async fn is_enabled(unit: &str) -> Result<bool, ActionErrorKind> {
let mut command = Command::new("systemctl");
command.arg("is-enabled");
command.arg(unit);
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))?;
let stdout = String::from_utf8(output.stdout)?;
if stdout.starts_with("enabled") || stdout.starts_with("linked") {
tracing::trace!(%unit, "Is enabled");

View file

@ -4,7 +4,7 @@ use crate::{
action::{
base::SetupDefaultProfile,
common::{ConfigureShellProfile, PlaceNixConfiguration},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
},
planner::ShellProfileLocations,
settings::{CommonSettings, SCRATCH_DIR},
@ -30,7 +30,7 @@ impl ConfigureNix {
) -> Result<StatefulAction<Self>, ActionError> {
let setup_default_profile = SetupDefaultProfile::plan(PathBuf::from(SCRATCH_DIR))
.await
.map_err(|e| ActionError::Child(SetupDefaultProfile::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let configure_shell_profile = if settings.modify_profile {
Some(
@ -39,9 +39,7 @@ impl ConfigureNix {
settings.ssl_cert_file.clone(),
)
.await
.map_err(|e| {
ActionError::Child(ConfigureShellProfile::action_tag(), Box::new(e))
})?,
.map_err(Self::error)?,
)
} else {
None
@ -52,7 +50,7 @@ impl ConfigureNix {
settings.force,
)
.await
.map_err(|e| ActionError::Child(PlaceNixConfiguration::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
Ok(Self {
place_nix_configuration,
@ -112,27 +110,21 @@ impl Action for ConfigureNix {
.try_execute()
.instrument(setup_default_profile_span)
.await
.map_err(|e| {
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
.map_err(Self::error)
},
async move {
place_nix_configuration
.try_execute()
.instrument(place_nix_configuration_span)
.await
.map_err(|e| {
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
.map_err(Self::error)
},
async move {
configure_shell_profile
.try_execute()
.instrument(configure_shell_profile_span)
.await
.map_err(|e| {
ActionError::Child(configure_shell_profile.action_tag(), Box::new(e))
})
.map_err(Self::error)
},
)?;
} else {
@ -144,18 +136,14 @@ impl Action for ConfigureNix {
.try_execute()
.instrument(setup_default_profile_span)
.await
.map_err(|e| {
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
.map_err(Self::error)
},
async move {
place_nix_configuration
.try_execute()
.instrument(place_nix_configuration_span)
.await
.map_err(|e| {
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
.map_err(Self::error)
},
)?;
};
@ -182,21 +170,28 @@ impl Action for ConfigureNix {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
if let Some(configure_shell_profile) = &mut self.configure_shell_profile {
configure_shell_profile.try_revert().await.map_err(|e| {
ActionError::Child(configure_shell_profile.action_tag(), Box::new(e))
})?;
if let Err(err) = configure_shell_profile.try_revert().await {
errors.push(err);
}
}
if let Err(err) = self.place_nix_configuration.try_revert().await {
errors.push(err);
}
if let Err(err) = self.setup_default_profile.try_revert().await {
errors.push(err);
}
self.place_nix_configuration
.try_revert()
.await
.map_err(|e| {
ActionError::Child(self.place_nix_configuration.action_tag(), Box::new(e))
})?;
self.setup_default_profile.try_revert().await.map_err(|e| {
ActionError::Child(self.setup_default_profile.action_tag(), Box::new(e))
})?;
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -1,5 +1,7 @@
use crate::action::base::{create_or_insert_into_file, CreateDirectory, CreateOrInsertIntoFile};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
use crate::planner::ShellProfileLocations;
use nix::unistd::User;
@ -32,9 +34,9 @@ impl ConfigureShellProfile {
let maybe_ssl_cert_file_setting = if let Some(ssl_cert_file) = ssl_cert_file {
format!(
"export NIX_SSL_CERT_FILE={:?}\n",
ssl_cert_file
.canonicalize()
.map_err(|e| { ActionError::Canonicalize(ssl_cert_file, e) })?
ssl_cert_file.canonicalize().map_err(|e| {
Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e))
})?
)
} else {
"".to_string()
@ -207,7 +209,7 @@ impl Action for ConfigureShellProfile {
}
let mut set = JoinSet::new();
let mut errors = Vec::default();
let mut errors = vec![];
for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate()
@ -219,12 +221,7 @@ impl Action for ConfigureShellProfile {
.try_execute()
.instrument(span)
.await
.map_err(|e| {
ActionError::Child(
create_or_insert_into_file_clone.action_tag(),
Box::new(e),
)
})?;
.map_err(Self::error)?;
Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone))
});
}
@ -235,17 +232,17 @@ impl Action for ConfigureShellProfile {
self.create_or_insert_into_files[idx] = create_or_insert_into_file
},
Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e)?,
Err(e) => return Err(Self::error(e))?,
};
}
if !errors.is_empty() {
if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap())?;
return Err(Self::error(errors.into_iter().next().unwrap()))?;
} else {
return Err(ActionError::Children(
errors.into_iter().map(|v| Box::new(v)).collect(),
));
return Err(Self::error(ActionErrorKind::MultipleChildren(
errors.into_iter().collect(),
)));
}
}
@ -262,7 +259,7 @@ impl Action for ConfigureShellProfile {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let mut set = JoinSet::new();
let mut errors: Vec<ActionError> = Vec::default();
let mut errors = vec![];
for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate()
@ -280,27 +277,26 @@ impl Action for ConfigureShellProfile {
self.create_or_insert_into_files[idx] = create_or_insert_into_file
},
Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e)?,
// This is quite rare and generally a very bad sign.
Err(e) => return Err(e).map_err(|e| Self::error(ActionErrorKind::from(e)))?,
};
}
for create_directory in self.create_directories.iter_mut() {
create_directory
.try_revert()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?;
}
if !errors.is_empty() {
if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap())?;
} else {
return Err(ActionError::Children(
errors.into_iter().map(|v| Box::new(v)).collect(),
));
if let Err(err) = create_directory.try_revert().await {
errors.push(err);
}
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -1,7 +1,9 @@
use tracing::{span, Span};
use crate::action::base::CreateDirectory;
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
const PATHS: &[&str] = &[
"/nix/var",
@ -36,7 +38,7 @@ impl CreateNixTree {
create_directories.push(
CreateDirectory::plan(path, String::from("root"), None, 0o0755, false)
.await
.map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?,
.map_err(Self::error)?,
)
}
@ -77,10 +79,7 @@ impl Action for CreateNixTree {
async fn execute(&mut self) -> Result<(), ActionError> {
// Just do sequential since parallelizing this will have little benefit
for create_directory in self.create_directories.iter_mut() {
create_directory
.try_execute()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
create_directory.try_execute().await.map_err(Self::error)?;
}
Ok(())
@ -108,14 +107,23 @@ impl Action for CreateNixTree {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
// Just do sequential since parallelizing this will have little benefit
for create_directory in self.create_directories.iter_mut().rev() {
create_directory
.try_revert()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
if let Err(err) = create_directory.try_revert().await {
errors.push(err);
}
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -1,7 +1,7 @@
use crate::{
action::{
base::{AddUserToGroup, CreateGroup, CreateUser},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
},
settings::CommonSettings,
};
@ -38,7 +38,7 @@ impl CreateUsersAndGroups {
format!("Nix build user {index}"),
)
.await
.map_err(|e| ActionError::Child(CreateUser::action_tag(), Box::new(e)))?,
.map_err(Self::error)?,
);
add_users_to_groups.push(
AddUserToGroup::plan(
@ -48,7 +48,7 @@ impl CreateUsersAndGroups {
settings.nix_build_group_id,
)
.await
.map_err(|e| ActionError::Child(AddUserToGroup::action_tag(), Box::new(e)))?,
.map_err(Self::error)?,
);
}
Ok(Self {
@ -156,18 +156,12 @@ impl Action for CreateUsersAndGroups {
}
| OperatingSystem::Darwin => {
for create_user in create_users.iter_mut() {
create_user
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
create_user.try_execute().await.map_err(Self::error)?;
}
},
_ => {
for create_user in create_users.iter_mut() {
create_user
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
create_user.try_execute().await.map_err(Self::error)?;
}
// While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04:
// ```
@ -190,7 +184,7 @@ impl Action for CreateUsersAndGroups {
// match result {
// Ok(Ok((idx, success))) => create_users[idx] = success,
// Ok(Err(e)) => errors.push(Box::new(e)),
// Err(e) => return Err(ActionError::Join(e))?,
// Err(e) => return Err(ActionErrorKind::Join(e))?,
// };
// }
@ -198,17 +192,14 @@ impl Action for CreateUsersAndGroups {
// if errors.len() == 1 {
// return Err(errors.into_iter().next().unwrap().into());
// } else {
// return Err(ActionError::Children(errors));
// return Err(ActionErrorKind::Children(errors));
// }
// }
},
};
for add_user_to_group in add_users_to_groups.iter_mut() {
add_user_to_group
.try_execute()
.await
.map_err(|e| ActionError::Child(add_user_to_group.action_tag(), Box::new(e)))?;
add_user_to_group.try_execute().await.map_err(Self::error)?;
}
Ok(())
@ -256,21 +247,11 @@ impl Action for CreateUsersAndGroups {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
create_users,
create_group,
add_users_to_groups: _,
nix_build_user_count: _,
nix_build_group_name: _,
nix_build_group_id: _,
nix_build_user_prefix: _,
nix_build_user_id_base: _,
} = self;
for create_user in create_users.iter_mut() {
create_user
.try_revert()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
let mut errors = vec![];
for create_user in self.create_users.iter_mut() {
if let Err(err) = create_user.try_revert().await {
errors.push(err);
}
}
// We don't actually need to do this, when a user is deleted they are removed from groups
@ -279,11 +260,19 @@ impl Action for CreateUsersAndGroups {
// }
// Create group
create_group
.try_revert()
.await
.map_err(|e| ActionError::Child(create_group.action_tag(), Box::new(e)))?;
if let Err(err) = self.create_group.try_revert().await {
errors.push(err);
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -2,7 +2,9 @@ use tracing::{span, Span};
use crate::action::base::create_or_merge_nix_config::CreateOrMergeNixConfigError;
use crate::action::base::{CreateDirectory, CreateOrMergeNixConfig};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
const NIX_CONF_FOLDER: &str = "/etc/nix";
const NIX_CONF: &str = "/etc/nix/nix.conf";
@ -26,7 +28,7 @@ impl PlaceNixConfiguration {
let extra_conf = extra_conf.join("\n");
let mut nix_config = nix_config_parser::NixConfig::parse_string(extra_conf, None)
.map_err(CreateOrMergeNixConfigError::ParseNixConfig)
.map_err(|e| ActionError::Custom(Box::new(e)))?;
.map_err(Self::error)?;
let settings = nix_config.settings_mut();
settings.insert("build-users-group".to_string(), nix_build_group_name);
@ -46,12 +48,10 @@ impl PlaceNixConfiguration {
let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force)
.await
.map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let create_or_merge_nix_config = CreateOrMergeNixConfig::plan(NIX_CONF, nix_config)
.await
.map_err(|e| {
ActionError::Child(CreateOrMergeNixConfig::action_tag(), Box::new(e))
})?;
.map_err(Self::error)?;
Ok(Self {
create_directory,
create_or_merge_nix_config,
@ -100,13 +100,11 @@ impl Action for PlaceNixConfiguration {
self.create_directory
.try_execute()
.await
.map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
self.create_or_merge_nix_config
.try_execute()
.await
.map_err(|e| {
ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e))
})?;
.map_err(Self::error)?;
Ok(())
}
@ -123,17 +121,23 @@ impl Action for PlaceNixConfiguration {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
self.create_or_merge_nix_config
.try_revert()
.await
.map_err(|e| {
ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e))
})?;
self.create_directory
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?;
let mut errors = vec![];
if let Err(err) = self.create_or_merge_nix_config.try_revert().await {
errors.push(err);
}
if let Err(err) = self.create_directory.try_revert().await {
errors.push(err);
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -4,7 +4,7 @@ use super::{CreateNixTree, CreateUsersAndGroups};
use crate::{
action::{
base::{FetchAndUnpackNix, MoveUnpackedNix},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
},
settings::{CommonSettings, SCRATCH_DIR},
};
@ -33,13 +33,11 @@ impl ProvisionNix {
.await?;
let create_users_and_group = CreateUsersAndGroups::plan(settings.clone())
.await
.map_err(|e| ActionError::Child(CreateUsersAndGroups::action_tag(), Box::new(e)))?;
let create_nix_tree = CreateNixTree::plan()
.await
.map_err(|e| ActionError::Child(CreateNixTree::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let create_nix_tree = CreateNixTree::plan().await.map_err(Self::error)?;
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR))
.await
.map_err(|e| ActionError::Child(MoveUnpackedNix::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
Ok(Self {
fetch_nix,
create_users_and_group,
@ -86,29 +84,27 @@ impl Action for ProvisionNix {
// We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async {
fetch_nix_clone
.try_execute()
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
fetch_nix_clone.try_execute().await.map_err(Self::error)?;
Result::<_, ActionError>::Ok(fetch_nix_clone)
});
self.create_users_and_group
.try_execute()
.await
.map_err(|e| {
ActionError::Child(self.create_users_and_group.action_tag(), Box::new(e))
})?;
.map_err(Self::error)?;
self.create_nix_tree
.try_execute()
.await
.map_err(|e| ActionError::Child(self.create_nix_tree.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
self.fetch_nix = fetch_nix_handle.await.map_err(ActionError::Join)??;
self.fetch_nix = fetch_nix_handle
.await
.map_err(ActionErrorKind::Join)
.map_err(Self::error)??;
self.move_unpacked_nix
.try_execute()
.await
.map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
Ok(())
}
@ -131,34 +127,32 @@ impl Action for ProvisionNix {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
// We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async {
fetch_nix_clone
.try_revert()
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok(fetch_nix_clone)
});
let mut errors = vec![];
if let Err(err) = self.fetch_nix.try_revert().await {
errors.push(err)
}
if let Err(err) = self.create_users_and_group.try_revert().await {
fetch_nix_handle.abort();
return Err(err);
errors.push(err)
}
if let Err(err) = self.create_nix_tree.try_revert().await {
fetch_nix_handle.abort();
return Err(err);
errors.push(err)
}
self.fetch_nix = fetch_nix_handle
.await
.map_err(ActionError::Join)?
.map_err(|e| ActionError::Child(self.fetch_nix.action_tag(), Box::new(e)))?;
self.move_unpacked_nix
.try_revert()
.await
.map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?;
if let Err(err) = self.move_unpacked_nix.try_revert().await {
errors.push(err)
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -1,7 +1,7 @@
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionState, ActionTag, StatefulAction};
use crate::action::{ActionError, ActionErrorKind, ActionState, ActionTag, StatefulAction};
use crate::execute_command;
use crate::action::{Action, ActionDescription};
@ -28,7 +28,7 @@ impl StartSystemdUnit {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
let state = if output.status.success() {
tracing::debug!("Starting systemd unit `{}` already complete", unit);
@ -84,7 +84,8 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}"))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
false => {
// TODO(@Hoverbear): Handle proxy vars
@ -95,7 +96,8 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}"))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
},
}
@ -111,30 +113,47 @@ impl Action for StartSystemdUnit {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self { unit, enable } = self;
let mut errors = vec![];
if *enable {
execute_command(
if self.enable {
if let Err(e) = execute_command(
Command::new("systemctl")
.process_group(0)
.arg("disable")
.arg(format!("{unit}"))
.arg(format!("{}", self.unit))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)
{
errors.push(e);
}
};
// We do both to avoid an error doing `disable --now` if the user did stop it already somehow.
execute_command(
if let Err(e) = execute_command(
Command::new("systemctl")
.process_group(0)
.arg("stop")
.arg(format!("{unit}"))
.arg(format!("{}", self.unit))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)
{
errors.push(e);
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command;
use crate::action::{Action, ActionDescription};
@ -40,7 +40,7 @@ impl BootstrapLaunchctlService {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
if output.status.success() || output.status.code() == Some(37) {
// We presume that success means it's found
return Ok(StatefulAction::completed(Self {
@ -102,7 +102,8 @@ impl Action for BootstrapLaunchctlService {
.arg(path)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}
@ -120,21 +121,16 @@ impl Action for BootstrapLaunchctlService {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
path,
service: _,
domain,
} = self;
execute_command(
Command::new("launchctl")
.process_group(0)
.arg("bootout")
.arg(domain)
.arg(path)
.arg(&self.domain)
.arg(&self.path)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}

View file

@ -25,9 +25,11 @@ impl CreateApfsVolume {
) -> Result<StatefulAction<Self>, ActionError> {
let output =
execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"]))
.await?;
.await
.map_err(Self::error)?;
let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?;
let parsed: DiskUtilApfsListOutput =
plist::from_bytes(&output.stdout).map_err(Self::error)?;
for container in parsed.containers {
for volume in container.volumes {
if volume.name == name {
@ -101,7 +103,8 @@ impl Action for CreateApfsVolume {
])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}
@ -119,19 +122,14 @@ impl Action for CreateApfsVolume {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
disk: _,
name,
case_sensitive: _,
} = self;
execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.args(["apfs", "deleteVolume", name])
.args(["apfs", "deleteVolume", &self.name])
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}

View file

@ -2,7 +2,7 @@ use uuid::Uuid;
use super::{get_uuid_for_label, CreateApfsVolume};
use crate::action::{
Action, ActionDescription, ActionError, ActionState, ActionTag, StatefulAction,
Action, ActionDescription, ActionError, ActionErrorKind, ActionState, ActionTag, StatefulAction,
};
use std::{io::SeekFrom, path::Path};
use tokio::{
@ -46,7 +46,7 @@ impl CreateFstabEntry {
if fstab_path.exists() {
let fstab_buf = tokio::fs::read_to_string(&fstab_path)
.await
.map_err(|e| ActionError::Read(fstab_path.to_path_buf(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_path_buf(), e)))?;
let prelude_comment = fstab_prelude_comment(&apfs_volume_label);
// See if a previous install from this crate exists, if so, invite the user to remove it (we may need to change it)
@ -122,7 +122,9 @@ impl Action for CreateFstabEntry {
existing_entry,
} = self;
let fstab_path = Path::new(FSTAB_PATH);
let uuid = get_uuid_for_label(&apfs_volume_label).await?;
let uuid = get_uuid_for_label(&apfs_volume_label)
.await
.map_err(Self::error)?;
let mut fstab = tokio::fs::OpenOptions::new()
.create(true)
@ -130,14 +132,14 @@ impl Action for CreateFstabEntry {
.read(true)
.open(fstab_path)
.await
.map_err(|e| ActionError::Open(fstab_path.to_path_buf(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_path_buf(), e)))?;
// Make sure it doesn't already exist before we write to it.
let mut fstab_buf = String::new();
fstab
.read_to_string(&mut fstab_buf)
.await
.map_err(|e| ActionError::Read(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?;
let updated_buf = match existing_entry {
ExistingFstabEntry::NixInstallerEntry => {
@ -161,9 +163,9 @@ impl Action for CreateFstabEntry {
}
}
if !(saw_prelude && updated_line) {
return Err(ActionError::Custom(Box::new(
return Err(Self::error(
CreateFstabEntryError::ExistingNixInstallerEntryDisappeared,
)));
));
}
current_fstab_lines.join("\n")
},
@ -182,9 +184,9 @@ impl Action for CreateFstabEntry {
}
}
if !updated_line {
return Err(ActionError::Custom(Box::new(
return Err(Self::error(
CreateFstabEntryError::ExistingForeignEntryDisappeared,
)));
));
}
current_fstab_lines.join("\n")
},
@ -194,15 +196,15 @@ impl Action for CreateFstabEntry {
fstab
.seek(SeekFrom::Start(0))
.await
.map_err(|e| ActionError::Seek(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?;
fstab
.set_len(0)
.await
.map_err(|e| ActionError::Truncate(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?;
fstab
.write_all(updated_buf.as_bytes())
.await
.map_err(|e| ActionError::Write(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?;
Ok(())
}
@ -223,13 +225,13 @@ impl Action for CreateFstabEntry {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
apfs_volume_label,
existing_entry: _,
} = self;
let fstab_path = Path::new(FSTAB_PATH);
let uuid = get_uuid_for_label(&apfs_volume_label).await?;
let fstab_entry = fstab_lines(&uuid, apfs_volume_label);
let uuid = get_uuid_for_label(&self.apfs_volume_label)
.await
.map_err(Self::error)?;
let fstab_entry = fstab_lines(&uuid, &self.apfs_volume_label);
let mut file = OpenOptions::new()
.create(false)
@ -237,12 +239,12 @@ impl Action for CreateFstabEntry {
.read(true)
.open(&fstab_path)
.await
.map_err(|e| ActionError::Open(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_owned(), e)))?;
let mut file_contents = String::default();
file.read_to_string(&mut file_contents)
.await
.map_err(|e| ActionError::Read(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?;
if let Some(start) = file_contents.rfind(fstab_entry.as_str()) {
let end = start + fstab_entry.len();
@ -251,16 +253,16 @@ impl Action for CreateFstabEntry {
file.seek(SeekFrom::Start(0))
.await
.map_err(|e| ActionError::Seek(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?;
file.set_len(0)
.await
.map_err(|e| ActionError::Truncate(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?;
file.write_all(file_contents.as_bytes())
.await
.map_err(|e| ActionError::Write(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?;
file.flush()
.await
.map_err(|e| ActionError::Flush(fstab_path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Flush(fstab_path.to_owned(), e)))?;
Ok(())
}
@ -288,3 +290,9 @@ pub enum CreateFstabEntryError {
#[error("The `/etc/fstab` entry (previously created by the official install scripts) detected during planning disappeared between planning and executing. Cannot update `/etc/fstab` as planned")]
ExistingForeignEntryDisappeared,
}
impl Into<ActionErrorKind> for CreateFstabEntryError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -4,7 +4,7 @@ use crate::action::{
BootstrapLaunchctlService, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership,
EncryptApfsVolume, UnmountApfsVolume,
},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
use std::{
path::{Path, PathBuf},
@ -54,23 +54,21 @@ impl CreateNixVolume {
create_or_insert_into_file::Position::End,
)
.await
.map_err(|e| ActionError::Child(CreateOrInsertIntoFile::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let create_synthetic_objects = CreateSyntheticObjects::plan()
.await
.map_err(|e| ActionError::Child(CreateSyntheticObjects::action_tag(), Box::new(e)))?;
let create_synthetic_objects = CreateSyntheticObjects::plan().await.map_err(Self::error)?;
let unmount_volume = UnmountApfsVolume::plan(disk, name.clone())
.await
.map_err(|e| ActionError::Child(UnmountApfsVolume::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive)
.await
.map_err(|e| ActionError::Child(CreateApfsVolume::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let create_fstab_entry = CreateFstabEntry::plan(name.clone(), &create_volume)
.await
.map_err(|e| ActionError::Child(CreateFstabEntry::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let encrypt_volume = if encrypt {
Some(EncryptApfsVolume::plan(disk, &name, &create_volume).await?)
@ -86,7 +84,7 @@ impl CreateNixVolume {
encrypt,
)
.await
.map_err(|e| ActionError::Child(CreateVolumeService::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let bootstrap_volume = BootstrapLaunchctlService::plan(
"system",
@ -94,14 +92,12 @@ impl CreateNixVolume {
NIX_VOLUME_MOUNTD_DEST,
)
.await
.map_err(|e| ActionError::Child(BootstrapLaunchctlService::action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let kickstart_launchctl_service =
KickstartLaunchctlService::plan("system", "org.nixos.darwin-store")
.await
.map_err(|e| {
ActionError::Child(KickstartLaunchctlService::action_tag(), Box::new(e))
})?;
let enable_ownership = EnableOwnership::plan("/nix").await?;
.map_err(Self::error)?;
let enable_ownership = EnableOwnership::plan("/nix").await.map_err(Self::error)?;
Ok(Self {
disk: disk.to_path_buf(),
@ -172,23 +168,16 @@ impl Action for CreateNixVolume {
self.create_or_append_synthetic_conf
.try_execute()
.await
.map_err(|e| {
ActionError::Child(
self.create_or_append_synthetic_conf.action_tag(),
Box::new(e),
)
})?;
.map_err(Self::error)?;
self.create_synthetic_objects
.try_execute()
.await
.map_err(|e| {
ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e))
})?;
.map_err(Self::error)?;
self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail.
self.create_volume
.try_execute()
.await
.map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
let mut retry_tokens: usize = 50;
loop {
@ -201,11 +190,14 @@ impl Action for CreateNixVolume {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
if output.status.success() {
break;
} else if retry_tokens == 0 {
return Err(ActionError::command_output(&command, output));
return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
} else {
retry_tokens = retry_tokens.saturating_sub(1);
}
@ -215,28 +207,23 @@ impl Action for CreateNixVolume {
self.create_fstab_entry
.try_execute()
.await
.map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
if let Some(encrypt_volume) = &mut self.encrypt_volume {
encrypt_volume
.try_execute()
.await
.map_err(|e| ActionError::Child(encrypt_volume.action_tag(), Box::new(e)))?
encrypt_volume.try_execute().await.map_err(Self::error)?
}
self.setup_volume_daemon
.try_execute()
.await
.map_err(|e| ActionError::Child(self.setup_volume_daemon.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
self.bootstrap_volume
.try_execute()
.await
.map_err(|e| ActionError::Child(self.bootstrap_volume.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
self.kickstart_launchctl_service
.try_execute()
.await
.map_err(|e| {
ActionError::Child(self.kickstart_launchctl_service.action_tag(), Box::new(e))
})?;
.map_err(Self::error)?;
let mut retry_tokens: usize = 50;
loop {
@ -248,11 +235,14 @@ impl Action for CreateNixVolume {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
if output.status.success() {
break;
} else if retry_tokens == 0 {
return Err(ActionError::command_output(&command, output));
return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
} else {
retry_tokens = retry_tokens.saturating_sub(1);
}
@ -262,7 +252,7 @@ impl Action for CreateNixVolume {
self.enable_ownership
.try_execute()
.await
.map_err(|e| ActionError::Child(self.enable_ownership.action_tag(), Box::new(e)))?;
.map_err(Self::error)?;
Ok(())
}
@ -296,44 +286,53 @@ impl Action for CreateNixVolume {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
self.enable_ownership.try_revert().await?;
self.kickstart_launchctl_service.try_revert().await?;
self.bootstrap_volume.try_revert().await?;
self.setup_volume_daemon.try_revert().await?;
if let Some(encrypt_volume) = &mut self.encrypt_volume {
encrypt_volume.try_revert().await?;
}
self.create_fstab_entry
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?;
let mut errors = vec![];
self.unmount_volume
.try_revert()
.await
.map_err(|e| ActionError::Child(self.unmount_volume.action_tag(), Box::new(e)))?;
self.create_volume
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?;
if let Err(err) = self.enable_ownership.try_revert().await {
errors.push(err)
};
if let Err(err) = self.kickstart_launchctl_service.try_revert().await {
errors.push(err)
};
if let Err(err) = self.bootstrap_volume.try_revert().await {
errors.push(err)
};
if let Err(err) = self.setup_volume_daemon.try_revert().await {
errors.push(err)
};
if let Some(encrypt_volume) = &mut self.encrypt_volume {
if let Err(err) = encrypt_volume.try_revert().await {
errors.push(err)
}
}
if let Err(err) = self.create_fstab_entry.try_revert().await {
errors.push(err)
}
if let Err(err) = self.unmount_volume.try_revert().await {
errors.push(err)
}
if let Err(err) = self.create_volume.try_revert().await {
errors.push(err)
}
// Purposefully not reversed
self.create_or_append_synthetic_conf
.try_revert()
.await
.map_err(|e| {
ActionError::Child(
self.create_or_append_synthetic_conf.action_tag(),
Box::new(e),
)
})?;
self.create_synthetic_objects
.try_revert()
.await
.map_err(|e| {
ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e))
})?;
if let Err(err) = self.create_or_append_synthetic_conf.try_revert().await {
errors.push(err)
}
if let Err(err) = self.create_synthetic_objects.try_revert().await {
errors.push(err)
}
Ok(())
if errors.is_empty() {
Ok(())
} else if errors.len() == 1 {
Err(errors
.into_iter()
.next()
.expect("Expected 1 len Vec to have at least 1 item"))
} else {
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
}
}
}

View file

@ -90,10 +90,3 @@ impl Action for CreateSyntheticObjects {
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum CreateSyntheticObjectsError {
#[error("Failed to execute command")]
Command(#[source] std::io::Error),
}

View file

@ -7,7 +7,9 @@ use tokio::{
io::AsyncWriteExt,
};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
use super::get_uuid_for_label;
@ -43,27 +45,27 @@ impl CreateVolumeService {
};
if this.path.exists() {
let discovered_plist: LaunchctlMountPlist = plist::from_file(&this.path)?;
let discovered_plist: LaunchctlMountPlist =
plist::from_file(&this.path).map_err(Self::error)?;
let expected_plist = generate_mount_plist(
&this.mount_service_label,
&this.apfs_volume_label,
&this.mount_point,
encrypt,
)
.await?;
.await
.map_err(Self::error)?;
if discovered_plist != expected_plist {
tracing::trace!(
?discovered_plist,
?expected_plist,
"Parsed plists not equal"
);
return Err(ActionError::Custom(Box::new(
CreateVolumeServiceError::DifferentPlist {
expected: expected_plist,
discovered: discovered_plist,
path: this.path.clone(),
},
)));
return Err(Self::error(CreateVolumeServiceError::DifferentPlist {
expected: expected_plist,
discovered: discovered_plist,
path: this.path.clone(),
}));
}
tracing::debug!("Creating file `{}` already complete", this.path.display());
@ -121,7 +123,8 @@ impl Action for CreateVolumeService {
mount_point,
*encrypt,
)
.await?;
.await
.map_err(Self::error)?;
let mut options = OpenOptions::new();
options.create_new(true).write(true).read(true);
@ -129,13 +132,13 @@ impl Action for CreateVolumeService {
let mut file = options
.open(&path)
.await
.map_err(|e| ActionError::Open(path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Open(path.to_owned(), e)))?;
let mut buf = Vec::new();
plist::to_writer_xml(&mut buf, &generated_plist)?;
plist::to_writer_xml(&mut buf, &generated_plist).map_err(Self::error)?;
file.write_all(&buf)
.await
.map_err(|e| ActionError::Write(path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Write(path.to_owned(), e)))?;
Ok(())
}
@ -151,7 +154,7 @@ impl Action for CreateVolumeService {
async fn revert(&mut self) -> Result<(), ActionError> {
remove_file(&self.path)
.await
.map_err(|e| ActionError::Remove(self.path.to_owned(), e))?;
.map_err(|e| Self::error(ActionErrorKind::Remove(self.path.to_owned(), e)))?;
Ok(())
}
@ -163,7 +166,7 @@ async fn generate_mount_plist(
apfs_volume_label: &str,
mount_point: &Path,
encrypt: bool,
) -> Result<LaunchctlMountPlist, ActionError> {
) -> Result<LaunchctlMountPlist, ActionErrorKind> {
let apfs_volume_label_with_quotes = format!("\"{apfs_volume_label}\"");
let uuid = get_uuid_for_label(&apfs_volume_label).await?;
// The official Nix scripts uppercase the UUID, so we do as well for compatibility.
@ -208,3 +211,9 @@ pub enum CreateVolumeServiceError {
path: PathBuf,
},
}
impl Into<ActionErrorKind> for CreateVolumeServiceError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command;
use crate::action::{Action, ActionDescription};
@ -62,9 +62,11 @@ impl Action for EnableOwnership {
.arg(&path)
.stdin(std::process::Stdio::null()),
)
.await?
.await
.map_err(Self::error)?
.stdout;
let the_plist: DiskUtilInfoOutput = plist::from_reader(Cursor::new(buf))?;
let the_plist: DiskUtilInfoOutput =
plist::from_reader(Cursor::new(buf)).map_err(Self::error)?;
the_plist.global_permissions_enabled
};
@ -77,7 +79,8 @@ impl Action for EnableOwnership {
.arg(path)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
}
Ok(())
@ -100,3 +103,9 @@ pub enum EnableOwnershipError {
#[error("Failed to execute command")]
Command(#[source] std::io::Error),
}
impl Into<ActionErrorKind> for EnableOwnershipError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -1,7 +1,7 @@
use crate::{
action::{
macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionState,
ActionTag, StatefulAction,
macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionErrorKind,
ActionState, ActionTag, StatefulAction,
},
execute_command,
os::darwin::DiskUtilApfsListOutput,
@ -51,7 +51,7 @@ impl EncryptApfsVolume {
if command
.status()
.await
.map_err(|e| ActionError::command(&command, e))?
.map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?
.success()
{
// The user has a password matching what we would create.
@ -61,24 +61,26 @@ impl EncryptApfsVolume {
}
// Ask the user to remove it
return Err(ActionError::Custom(Box::new(
EncryptApfsVolumeError::ExistingPasswordFound(name, disk),
return Err(Self::error(EncryptApfsVolumeError::ExistingPasswordFound(
name, disk,
)));
} else {
if planned_create_apfs_volume.state == ActionState::Completed {
// The user has a volume already created, but a password not set. This means we probably can't decrypt the volume.
return Err(ActionError::Custom(Box::new(
return Err(Self::error(
EncryptApfsVolumeError::MissingPasswordForExistingVolume(name, disk),
)));
));
}
}
// Ensure if the disk already exists, that it's encrypted
let output =
execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"]))
.await?;
.await
.map_err(Self::error)?;
let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?;
let parsed: DiskUtilApfsListOutput =
plist::from_bytes(&output.stdout).map_err(Self::error)?;
for container in parsed.containers {
for volume in container.volumes {
if volume.name == name {
@ -87,9 +89,9 @@ impl EncryptApfsVolume {
return Ok(StatefulAction::completed(Self { disk, name }));
},
false => {
return Err(ActionError::Custom(Box::new(
return Err(Self::error(
EncryptApfsVolumeError::ExistingVolumeNotEncrypted(name, disk),
)));
));
},
}
}
@ -150,7 +152,9 @@ impl Action for EncryptApfsVolume {
let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)).await?;
execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name))
.await
.map_err(Self::error)?;
// Add the password to the user keychain so they can unlock it later.
execute_command(
@ -180,7 +184,8 @@ impl Action for EncryptApfsVolume {
"/Library/Keychains/System.keychain",
]),
)
.await?;
.await
.map_err(Self::error)?;
// Encrypt the mounted volume
execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([
@ -192,7 +197,8 @@ impl Action for EncryptApfsVolume {
"-passphrase",
password.as_str(),
]))
.await?;
.await
.map_err(Self::error)?;
execute_command(
Command::new("/usr/sbin/diskutil")
@ -201,7 +207,8 @@ impl Action for EncryptApfsVolume {
.arg("force")
.arg(&name),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}
@ -220,18 +227,16 @@ impl Action for EncryptApfsVolume {
disk = %self.disk.display(),
))]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self { disk, name } = self;
let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
let disk_str = self.disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
// TODO: This seems very rough and unsafe
execute_command(
Command::new("/usr/bin/security").process_group(0).args([
"delete-generic-password",
"-a",
name.as_str(),
self.name.as_str(),
"-s",
name.as_str(),
self.name.as_str(),
"-l",
format!("{} encryption password", disk_str).as_str(),
"-D",
@ -243,7 +248,8 @@ impl Action for EncryptApfsVolume {
.as_str(),
]),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}
@ -258,3 +264,9 @@ pub enum EncryptApfsVolumeError {
#[error("The existing APFS volume \"{0}\" on disk `{1}` is not encrypted but it should be, consider removing the volume with `diskutil apfs deleteVolume \"{0}\"` (if you receive error -69888, you may need to run `launchctl bootout system/org.nixos.darwin-store` and `launchctl bootout system/org.nixos.nix-daemon` first)")]
ExistingVolumeNotEncrypted(String, PathBuf),
}
impl Into<ActionErrorKind> for EncryptApfsVolumeError {
fn into(self) -> ActionErrorKind {
ActionErrorKind::Custom(Box::new(self))
}
}

View file

@ -3,7 +3,7 @@ use std::process::Output;
use tokio::process::Command;
use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command;
use crate::action::{Action, ActionDescription};
@ -39,11 +39,11 @@ impl KickstartLaunchctlService {
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
if output.status.success() {
service_exists = true;
let output_string = String::from_utf8(output.stdout)?;
let output_string = String::from_utf8(output.stdout).map_err(|e| Self::error(e))?;
// We are looking for a line containing "state = " with some trailing content
// The output is not a JSON or a plist
// MacOS's man pages explicitly tell us not to try to parse this output
@ -102,7 +102,8 @@ impl Action for KickstartLaunchctlService {
.arg(format!("{domain}/{service}"))
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}
@ -116,26 +117,26 @@ impl Action for KickstartLaunchctlService {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self { domain, service } = self;
// MacOs doesn't offer an "ensure-stopped" like they do with Kickstart
let mut command = Command::new("launchctl");
command.process_group(0);
command.arg("stop");
command.arg(format!("{domain}/{service}"));
command.arg(format!("{}/{}", self.domain, self.service));
command.stdin(std::process::Stdio::null());
let command_str = format!("{:?}", command.as_std());
let output = command
.output()
.await
.map_err(|e| ActionError::command(&command, e))?;
.map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
// On our test Macs, a status code of `3` was reported if the service was stopped while not running.
match output.status.code() {
Some(3) | Some(0) | None => (),
_ => {
return Err(ActionError::Custom(Box::new(
return Err(Self::error(ActionErrorKind::Custom(Box::new(
KickstartLaunchctlServiceError::CannotStopService(command_str, output),
)))
))))
},
}

View file

@ -15,7 +15,7 @@ pub(crate) mod unmount_apfs_volume;
pub use bootstrap_launchctl_service::BootstrapLaunchctlService;
pub use create_apfs_volume::CreateApfsVolume;
pub use create_nix_volume::{CreateNixVolume, NIX_VOLUME_MOUNTD_DEST};
pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError};
pub use create_synthetic_objects::CreateSyntheticObjects;
pub use create_volume_service::CreateVolumeService;
pub use enable_ownership::{EnableOwnership, EnableOwnershipError};
pub use encrypt_apfs_volume::EncryptApfsVolume;
@ -27,9 +27,9 @@ use uuid::Uuid;
use crate::execute_command;
use super::ActionError;
use super::ActionErrorKind;
async fn get_uuid_for_label(apfs_volume_label: &str) -> Result<Uuid, ActionError> {
async fn get_uuid_for_label(apfs_volume_label: &str) -> Result<Uuid, ActionErrorKind> {
let output = execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)

View file

@ -65,9 +65,11 @@ impl Action for UnmountApfsVolume {
.arg(&name)
.stdin(std::process::Stdio::null()),
)
.await?
.await
.map_err(Self::error)?
.stdout;
let the_plist: DiskUtilInfoOutput = plist::from_reader(Cursor::new(buf))?;
let the_plist: DiskUtilInfoOutput =
plist::from_reader(Cursor::new(buf)).map_err(|e| Self::error(e))?;
the_plist.mount_point.is_some()
};
@ -80,7 +82,8 @@ impl Action for UnmountApfsVolume {
.arg(name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
} else {
tracing::debug!("Volume was already unmounted, can skip unmounting")
}
@ -94,16 +97,15 @@ impl Action for UnmountApfsVolume {
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
let Self { disk: _, name } = self;
execute_command(
Command::new("/usr/sbin/diskutil")
.process_group(0)
.args(["unmount", "force"])
.arg(name)
.arg(&self.name)
.stdin(std::process::Stdio::null()),
)
.await?;
.await
.map_err(Self::error)?;
Ok(())
}

View file

@ -255,6 +255,14 @@ pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
state: ActionState::Uncompleted,
}
}
fn error(kind: impl Into<ActionErrorKind>) -> ActionError
where
Self: Sized,
{
ActionError::new(Self::action_tag(), kind)
}
// They should also have an `async fn plan(args...) -> Result<StatefulAction<Self>, ActionError>;`
}
@ -299,10 +307,51 @@ impl From<&'static str> for ActionTag {
}
}
#[derive(Debug)]
pub struct ActionError {
action_tag: ActionTag,
kind: ActionErrorKind,
}
impl ActionError {
pub fn new(action_tag: ActionTag, kind: impl Into<ActionErrorKind>) -> Self {
Self {
action_tag,
kind: kind.into(),
}
}
pub fn kind(&self) -> &ActionErrorKind {
&self.kind
}
pub fn action_tag(&self) -> &ActionTag {
&self.action_tag
}
}
impl std::fmt::Display for ActionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("Action `{}` errored", self.action_tag))
}
}
impl std::error::Error for ActionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.kind)
}
}
impl From<ActionError> for ActionErrorKind {
fn from(value: ActionError) -> Self {
Self::Child(Box::new(value))
}
}
/// An error occurring during an action
#[non_exhaustive]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum ActionError {
pub enum ActionErrorKind {
/// A custom error
#[error(transparent)]
Custom(Box<dyn std::error::Error + Send + Sync>),
@ -310,17 +359,26 @@ pub enum ActionError {
#[error(transparent)]
Certificate(#[from] CertificateError),
/// A child error
#[error("Child action `{0}`")]
Child(ActionTag, #[source] Box<ActionError>),
/// Several child errors
#[error("Child action errors: {}", .0.iter().map(|v| {
if let Some(source) = v.source() {
format!("{v} ({source})")
#[error(transparent)]
Child(Box<ActionError>),
/// Several errors
#[error("Multiple child errors\n\n{}", .0.iter().map(|err| {
if let Some(source) = err.source() {
format!("{err}\n{source}\n")
} else {
format!("{v}")
format!("{err}\n")
}
}).collect::<Vec<_>>().join(" & "))]
Children(Vec<Box<ActionError>>),
}).collect::<Vec<_>>().join("\n"))]
MultipleChildren(Vec<ActionError>),
/// Several errors
#[error("Multiple errors\n\n{}", .0.iter().map(|err| {
if let Some(source) = err.source() {
format!("{err}\n{source}")
} else {
format!("{err}\n")
}
}).collect::<Vec<_>>().join("\n"))]
Multiple(Vec<ActionErrorKind>),
/// The path already exists with different content that expected
#[error(
"`{0}` exists with different content than planned, consider removing it with `rm {0}`"
@ -475,7 +533,7 @@ pub enum ActionError {
SystemdMissing,
}
impl ActionError {
impl ActionErrorKind {
pub fn command(command: &tokio::process::Command, error: std::io::Error) -> Self {
Self::Command {
#[cfg(feature = "diagnostics")]
@ -494,7 +552,7 @@ impl ActionError {
}
}
impl HasExpectedErrors for ActionError {
impl HasExpectedErrors for ActionErrorKind {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self {
Self::PathUserMismatch(_, _, _)
@ -506,11 +564,10 @@ impl HasExpectedErrors for ActionError {
}
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for ActionError {
impl crate::diagnostics::ErrorDiagnostic for ActionErrorKind {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
let context = match self {
Self::Child(action, _) => vec![action.to_string()],
Self::Read(path, _)
| Self::Open(path, _)
| Self::Write(path, _)
@ -518,7 +575,8 @@ impl crate::diagnostics::ErrorDiagnostic for ActionError {
| Self::SetPermissions(_, path, _)
| Self::GettingMetadata(path, _)
| Self::CreateDirectory(path, _)
| Self::PathWasNotFile(path) => {
| Self::PathWasNotFile(path)
| Self::Remove(path, _) => {
vec![path.to_string_lossy().to_string()]
},
Self::Rename(first_path, second_path, _)

View file

@ -15,10 +15,13 @@ use crate::{
plan::RECEIPT_LOCATION,
planner::Planner,
settings::CommonSettings,
BuiltinPlanner, InstallPlan,
BuiltinPlanner, InstallPlan, NixInstallerError,
};
use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr};
use color_eyre::{
eyre::{eyre, WrapErr},
Section,
};
use owo_colors::OwoColorize;
/// Execute an install (possibly using an existing plan)
@ -218,19 +221,30 @@ impl CommandExecute for Install {
let rx2 = tx.subscribe();
let res = install_plan.uninstall(rx2).await;
if let Err(err) = res {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
} else {
println!(
"\
{message}\n\
",
message = "Partial Nix install was uninstalled successfully!".bold(),
);
match res {
Err(NixInstallerError::ActionRevert(errs)) => {
let mut report = eyre!("Multiple errors");
for err in errs {
report = report.error(err);
}
return Err(report)?;
},
Err(err) => {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
},
_ => {
println!(
"\
{message}\n\
",
message =
"Partial Nix install was uninstalled successfully!".bold(),
);
},
}
} else {
if let Some(expected) = err.expected() {

View file

@ -8,10 +8,10 @@ use crate::{
cli::{ensure_root, interaction::PromptChoice, signal_channel},
error::HasExpectedErrors,
plan::RECEIPT_LOCATION,
InstallPlan,
InstallPlan, NixInstallerError,
};
use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr};
use color_eyre::eyre::{eyre, WrapErr};
use owo_colors::OwoColorize;
use rand::Rng;
@ -125,17 +125,21 @@ impl CommandExecute for Uninstall {
let (_tx, rx) = signal_channel().await?;
let res = plan.uninstall(rx).await;
if let Err(err) = res {
if let Some(expected) = err.expected() {
println!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
match res {
Err(err @ NixInstallerError::ActionRevert(_)) => {
tracing::error!("Uninstallation complete, some errors encountered");
return Err(err)?;
},
Err(err) => {
if let Some(expected) = err.expected() {
println!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
},
_ => (),
}
// TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going...
// However that will require being able to link error -> step and manually setting that step as `Uncompleted`.
println!(
"\
{success}\n\

View file

@ -102,11 +102,11 @@ impl DiagnosticData {
let mut walker: &dyn std::error::Error = &err;
while let Some(source) = walker.source() {
if let Some(downcasted) = source.downcast_ref::<ActionError>() {
let downcasted_diagnostic = downcasted.diagnostic();
let downcasted_diagnostic = downcasted.kind().diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<Box<ActionError>>() {
let downcasted_diagnostic = downcasted.diagnostic();
let downcasted_diagnostic = downcasted.kind().diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<PlannerError>() {

View file

@ -1,18 +1,23 @@
use std::path::PathBuf;
use std::{error::Error, path::PathBuf};
use crate::{
action::{ActionError, ActionTag},
planner::PlannerError,
settings::InstallSettingsError,
};
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError};
/// An error occurring during a call defined in this crate
#[non_exhaustive]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum NixInstallerError {
/// An error originating from an [`Action`](crate::action::Action)
#[error("Error executing action `{0}`")]
Action(ActionTag, #[source] ActionError),
#[error("Error executing action")]
Action(#[source] ActionError),
/// An error originating from an [`Action`](crate::action::Action) while reverting
#[error("Error reverting\n{}", .0.iter().map(|err| {
if let Some(source) = err.source() {
format!("{err}\n{source}\n")
} else {
format!("{err}\n")
}
}).collect::<Vec<_>>().join("\n"))]
ActionRevert(Vec<ActionError>),
/// An error while writing the [`InstallPlan`](crate::InstallPlan)
#[error("Recording install receipt")]
RecordingReceipt(PathBuf, #[source] std::io::Error),
@ -72,7 +77,8 @@ pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync {
impl HasExpectedErrors for NixInstallerError {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self {
NixInstallerError::Action(_, action_error) => action_error.expected(),
NixInstallerError::Action(action_error) => action_error.kind().expected(),
NixInstallerError::ActionRevert(_) => None,
NixInstallerError::RecordingReceipt(_, _) => None,
NixInstallerError::CopyingSelf(_) => None,
NixInstallerError::SerializingReceipt(_) => None,
@ -91,7 +97,7 @@ impl crate::diagnostics::ErrorDiagnostic for NixInstallerError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
let context = match self {
Self::Action(action, _) => vec![action.to_string()],
Self::Action(action_error) => vec![action_error.action_tag().to_string()],
_ => vec![],
};
return format!(

View file

@ -82,8 +82,6 @@ pub mod settings;
use std::{ffi::OsStr, path::Path, process::Output};
use action::{Action, ActionError};
pub use error::NixInstallerError;
pub use plan::InstallPlan;
use planner::BuiltinPlanner;
@ -91,16 +89,18 @@ use planner::BuiltinPlanner;
use reqwest::Certificate;
use tokio::process::Command;
use crate::action::{Action, ActionErrorKind};
#[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))]
async fn execute_command(command: &mut Command) -> Result<Output, ActionError> {
async fn execute_command(command: &mut Command) -> Result<Output, ActionErrorKind> {
tracing::trace!("Executing");
let output = command
.output()
.await
.map_err(|e| ActionError::command(command, e))?;
.map_err(|e| ActionErrorKind::command(command, e))?;
match output.status.success() {
true => Ok(output),
false => Err(ActionError::command_output(command, output)),
false => Err(ActionErrorKind::command_output(command, output)),
}
}

View file

@ -175,12 +175,11 @@ impl InstallPlan {
}
tracing::info!("Step: {}", action.tracing_synopsis());
let typetag_name = action.inner_typetag_name();
if let Err(err) = action.try_execute().await {
if let Err(err) = write_receipt(self.clone()).await {
tracing::error!("Error saving receipt: {:?}", err);
}
let err = NixInstallerError::Action(typetag_name.into(), err);
let err = NixInstallerError::Action(err);
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
@ -296,6 +295,7 @@ impl InstallPlan {
) -> Result<(), NixInstallerError> {
let Self { actions, .. } = self;
let mut cancel_channel = cancel_channel.into();
let mut errors = vec![];
// This is **deliberately sequential**.
// Actions which are parallelizable are represented by "group actions" like CreateUsers
@ -324,39 +324,40 @@ impl InstallPlan {
}
tracing::info!("Revert: {}", action.tracing_synopsis());
let typetag_name = action.inner_typetag_name();
if let Err(err) = action.try_revert().await {
if let Err(err) = write_receipt(self.clone()).await {
tracing::error!("Error saving receipt: {:?}", err);
}
let err = NixInstallerError::Action(typetag_name.into(), err);
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
.clone()
.failure(&err)
.send(
crate::diagnostics::DiagnosticAction::Uninstall,
crate::diagnostics::DiagnosticStatus::Failure,
)
.await?;
}
return Err(err);
if let Err(errs) = action.try_revert().await {
errors.push(errs);
}
}
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
.clone()
.send(
crate::diagnostics::DiagnosticAction::Uninstall,
crate::diagnostics::DiagnosticStatus::Success,
)
.await?;
}
if errors.is_empty() {
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
.clone()
.send(
crate::diagnostics::DiagnosticAction::Uninstall,
crate::diagnostics::DiagnosticStatus::Success,
)
.await?;
}
Ok(())
Ok(())
} else {
let error = NixInstallerError::ActionRevert(errors);
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
.clone()
.failure(&error)
.send(
crate::diagnostics::DiagnosticAction::Uninstall,
crate::diagnostics::DiagnosticStatus::Failure,
)
.await?;
}
return Err(error);
}
}
}

View file

@ -232,6 +232,7 @@ impl CommonSettings {
let url;
let nix_build_user_prefix;
let nix_build_user_id_base;
let ssl_cert_file;
use target_lexicon::{Architecture, OperatingSystem};
match (Architecture::host(), OperatingSystem::host()) {
@ -240,18 +241,21 @@ impl CommonSettings {
url = NIX_X64_64_LINUX_URL;
nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000;
ssl_cert_file = None;
},
#[cfg(target_os = "linux")]
(Architecture::X86_32(_), OperatingSystem::Linux) => {
url = NIX_I686_LINUX_URL;
nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000;
ssl_cert_file = None;
},
#[cfg(target_os = "linux")]
(Architecture::Aarch64(_), OperatingSystem::Linux) => {
url = NIX_AARCH64_LINUX_URL;
nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000;
ssl_cert_file = None;
},
#[cfg(target_os = "macos")]
(Architecture::X86_64, OperatingSystem::MacOSX { .. })
@ -259,6 +263,7 @@ impl CommonSettings {
url = NIX_X64_64_DARWIN_URL;
nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300;
ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into());
},
#[cfg(target_os = "macos")]
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
@ -266,6 +271,7 @@ impl CommonSettings {
url = NIX_AARCH64_DARWIN_URL;
nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300;
ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into());
},
_ => {
return Err(InstallSettingsError::UnsupportedArchitecture(
@ -285,7 +291,7 @@ impl CommonSettings {
proxy: Default::default(),
extra_conf: Default::default(),
force: false,
ssl_cert_file: Default::default(),
ssl_cert_file,
#[cfg(feature = "diagnostics")]
diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()),
})