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]] [[package]]
name = "nix-installer" name = "nix-installer"
version = "0.7.0" version = "0.7.1-unreleased"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"atty", "atty",

View file

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

View file

@ -1,5 +1,5 @@
# Largely derived from https://github.com/NixOS/nix/blob/14f7dae3e4eb0c34192d0077383a7f2a2d630129/tests/installer/default.nix # Largely derived from https://github.com/NixOS/nix/blob/14f7dae3e4eb0c34192d0077383a7f2a2d630129/tests/installer/default.nix
{ forSystem, binaryTarball }: { forSystem, binaryTarball, lib }:
let let
nix-installer-install = '' nix-installer-install = ''
@ -18,7 +18,7 @@ let
tar xvf nix.tar.xz tar xvf nix.tar.xz
./nix-*/install --no-channel-add --yes --no-daemon ./nix-*/install --no-channel-add --yes --no-daemon
''; '';
installScripts = rec { installCases = rec {
install-default = { install-default = {
install = nix-installer-install; install = nix-installer-install;
check = '' check = ''
@ -38,19 +38,25 @@ let
fi fi
if systemctl is-failed nix-daemon.socket; then if systemctl is-failed nix-daemon.socket; then
echo "nix-daemon.socket is failed" echo "nix-daemon.socket is failed"
systemctl status nix-daemon.socket
exit 1 exit 1
fi 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 if !(sudo systemctl start nix-daemon.service); then
echo "nix-daemon.service failed to start" 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 exit 1
fi fi
if !(sudo systemctl stop nix-daemon.service); then if !(sudo systemctl stop nix-daemon.service); then
echo "nix-daemon.service failed to stop" echo "nix-daemon.service failed to stop"
systemctl status nix-daemon.service
exit 1 exit 1
fi 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"]; }') out=$(nix-build --no-substitute -E 'derivation { name = "foo"; system = "x86_64-linux"; builder = "/bin/sh"; args = ["-c" "echo foobar > $out"]; }')
[[ $(cat $out) = foobar ]] [[ $(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-no-start-daemon = {
install = '' install = ''
@ -90,6 +152,8 @@ let
[[ $(cat $out) = foobar ]] [[ $(cat $out) = foobar ]]
''; '';
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
install-daemonless = { install-daemonless = {
install = '' install = ''
@ -106,14 +170,20 @@ let
[[ $(cat $out) = foobar ]] [[ $(cat $out) = foobar ]]
''; '';
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
};
cureCases = {
cure-self-linux-working = { cure-self-linux-working = {
preinstall = '' preinstall = ''
${nix-installer-install-quiet} ${nix-installer-install-quiet}
sudo mv /nix/receipt.json /nix/old-receipt.json sudo mv /nix/receipt.json /nix/old-receipt.json
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-no-nix-path = { cure-self-linux-broken-no-nix-path = {
preinstall = '' preinstall = ''
@ -122,8 +192,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json sudo mv /nix/receipt.json /nix/old-receipt.json
sudo rm -rf /nix/ sudo rm -rf /nix/
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-missing-users = { cure-self-linux-broken-missing-users = {
preinstall = '' preinstall = ''
@ -133,8 +205,10 @@ let
sudo userdel nixbld3 sudo userdel nixbld3
sudo userdel nixbld16 sudo userdel nixbld16
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-missing-users-and-group = { cure-self-linux-broken-missing-users-and-group = {
preinstall = '' preinstall = ''
@ -146,8 +220,10 @@ let
done done
sudo groupdel nixbld sudo groupdel nixbld
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-daemon-disabled = { cure-self-linux-broken-daemon-disabled = {
preinstall = '' preinstall = ''
@ -155,8 +231,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json sudo mv /nix/receipt.json /nix/old-receipt.json
sudo systemctl disable --now nix-daemon.socket sudo systemctl disable --now nix-daemon.socket
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-no-etc-nix = { cure-self-linux-broken-no-etc-nix = {
preinstall = '' preinstall = ''
@ -164,8 +242,10 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json sudo mv /nix/receipt.json /nix/old-receipt.json
sudo rm -rf /etc/nix sudo rm -rf /etc/nix
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-self-linux-broken-unmodified-bashrc = { cure-self-linux-broken-unmodified-bashrc = {
preinstall = '' preinstall = ''
@ -173,16 +253,20 @@ let
sudo mv /nix/receipt.json /nix/old-receipt.json sudo mv /nix/receipt.json /nix/old-receipt.json
sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-self-broken-no-nix-path = { cure-script-multi-self-broken-no-nix-path = {
preinstall = '' preinstall = ''
${cure-script-multi-user} ${cure-script-multi-user}
sudo rm -rf /nix/ sudo rm -rf /nix/
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-broken-missing-users = { cure-script-multi-broken-missing-users = {
preinstall = '' preinstall = ''
@ -191,44 +275,98 @@ let
sudo userdel nixbld3 sudo userdel nixbld3
sudo userdel nixbld16 sudo userdel nixbld16
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-broken-daemon-disabled = { cure-script-multi-broken-daemon-disabled = {
preinstall = '' preinstall = ''
${cure-script-multi-user} ${cure-script-multi-user}
sudo systemctl disable --now nix-daemon.socket sudo systemctl disable --now nix-daemon.socket
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-broken-no-etc-nix = { cure-script-multi-broken-no-etc-nix = {
preinstall = '' preinstall = ''
${cure-script-multi-user} ${cure-script-multi-user}
sudo rm -rf /etc/nix sudo rm -rf /etc/nix
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-broken-unmodified-bashrc = { cure-script-multi-broken-unmodified-bashrc = {
preinstall = '' preinstall = ''
${cure-script-multi-user} ${cure-script-multi-user}
sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc
''; '';
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
cure-script-multi-working = { cure-script-multi-working = {
preinstall = cure-script-multi-user; preinstall = cure-script-multi-user;
install = install-default.install; install = installCases.install-default.install;
check = install-default.check; check = installCases.install-default.check;
uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck;
}; };
# cure-script-single-working = { # cure-script-single-working = {
# preinstall = cure-script-single-user; # preinstall = cure-script-single-user;
# install = install-default.install; # install = installCases.install-default.install;
# check = install-default.check; # 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"; disableSELinux = "sudo setenforce 0";
@ -333,7 +471,7 @@ let
}; };
makeTest = imageName: testName: makeTest = imageName: testName: test:
let image = images.${imageName}; in let image = images.${imageName}; in
with (forSystem image.system ({ system, pkgs, ... }: pkgs)); with (forSystem image.system ({ system, pkgs, ... }: pkgs));
runCommand runCommand
@ -342,9 +480,12 @@ let
buildInputs = [ qemu_kvm openssh ]; buildInputs = [ qemu_kvm openssh ];
image = image.image; image = image.image;
postBoot = image.postBoot or ""; postBoot = image.postBoot or "";
preinstallScript = installScripts.${testName}.preinstall or "echo \"Not Applicable\""; preinstallScript = test.preinstall or "echo \"Not Applicable\"";
installScript = installScripts.${testName}.install; installScript = test.install;
checkScript = installScripts.${testName}.check; checkScript = test.check;
uninstallScript = test.uninstall;
preuninstallScript = test.preuninstall or "echo \"Not Applicable\"";
uninstallCheckScript = test.uninstallCheck;
installer = nix-installer-static; installer = nix-installer-static;
binaryTarball = binaryTarball.${system}; binaryTarball = binaryTarball.${system};
} }
@ -414,32 +555,38 @@ let
echo "Running installer..." echo "Running installer..."
$ssh "set -eux; $installScript" $ssh "set -eux; $installScript"
echo "Testing Nix installation..." echo "Checking Nix installation..."
$ssh "set -eux; $checkScript" $ssh "set -eux; $checkScript"
echo "Testing Nix uninstallation..." echo "Running preuninstall..."
$ssh "set -eux; /nix/nix-installer uninstall --no-confirm" $ssh "set -eux; $preuninstallScript"
echo "Running Nix uninstallation..."
$ssh "set -eux; $uninstallScript"
echo "Checking Nix uninstallation..."
$ssh "set -eux; $uninstallCheckScript"
echo "Done!" echo "Done!"
touch $out touch $out
''; '';
vm-tests = builtins.mapAttrs makeTests = name: tests: builtins.mapAttrs
(imageName: image: (imageName: image:
rec { rec {
${image.system} = (builtins.mapAttrs ${image.system} = (builtins.mapAttrs
(testName: test: (testName: test:
makeTest imageName testName makeTest imageName testName test
) )
installScripts) // { tests) // {
all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { "${name}" = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate {
name = "all"; name = name;
constituents = ( constituents = (
pkgs.lib.mapAttrsToList pkgs.lib.mapAttrsToList
(testName: test: (testName: test:
makeTest imageName testName makeTest imageName testName test
) )
installScripts tests
); );
}); });
}; };
@ -447,96 +594,35 @@ let
) )
images; images;
in allCases = lib.recursiveUpdate (lib.recursiveUpdate installCases cureCases) uninstallCases;
vm-tests // rec {
all."x86_64-linux".install-default = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { install-tests = makeTests "install" installCases;
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests; cure-tests = makeTests "cure" cureCases;
});
all."x86_64-linux".install-no-start-daemon = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { uninstall-tests = makeTests "uninstall" uninstallCases;
name = "all";
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests; all-tests = builtins.mapAttrs
}); (imageName: image: {
all."x86_64-linux".install-daemonless = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { "x86_64-linux".all = (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"; name = "all";
constituents = [ constituents = [
all."x86_64-linux".install-default install-tests."${imageName}"."x86_64-linux".install
all."x86_64-linux".install-no-start-daemon cure-tests."${imageName}"."x86_64-linux".cure
all."x86_64-linux".install-daemonless uninstall-tests."${imageName}"."x86_64-linux".uninstall
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
]; ];
}); });
})
images;
joined-tests = lib.recursiveUpdate (lib.recursiveUpdate (lib.recursiveUpdate cure-tests install-tests) uninstall-tests) all-tests;
in
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 tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::ActionError; use crate::action::{ActionError, ActionErrorKind};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction}; use crate::action::{Action, ActionDescription, StatefulAction};
@ -37,22 +37,23 @@ impl AddUserToGroup {
}; };
// Ensure user does not exists // Ensure user does not exists
if let Some(user) = User::from_name(name.as_str()) 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 { if user.uid.as_raw() != uid {
return Err(ActionError::UserUidMismatch( return Err(Self::error(ActionErrorKind::UserUidMismatch(
name.clone(), name.clone(),
user.uid.as_raw(), user.uid.as_raw(),
uid, uid,
)); )));
} }
if user.gid.as_raw() != gid { if user.gid.as_raw() != gid {
return Err(ActionError::UserGidMismatch( return Err(Self::error(ActionErrorKind::UserGidMismatch(
name.clone(), name.clone(),
user.gid.as_raw(), user.gid.as_raw(),
gid, gid,
)); )));
} }
// See if group membership needs to be done // See if group membership needs to be done
@ -74,7 +75,8 @@ impl AddUserToGroup {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
match output.status.code() { match output.status.code() {
Some(0) => { Some(0) => {
// yes {user} is a member of {groupname} // yes {user} is a member of {groupname}
@ -98,7 +100,9 @@ impl AddUserToGroup {
}, },
_ => { _ => {
// Some other issue // 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) .arg(&this.name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
let output_str = String::from_utf8(output.stdout)?; .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); let user_in_group = output_str.split(" ").any(|v| v == &this.groupname);
if user_in_group { if user_in_group {
@ -187,7 +192,8 @@ impl Action for AddUserToGroup {
.arg(&name) .arg(&name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/sbin/dseditgroup") Command::new("/usr/sbin/dseditgroup")
.process_group(0) .process_group(0)
@ -199,7 +205,8 @@ impl Action for AddUserToGroup {
.arg(groupname) .arg(groupname)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
}, },
_ => { _ => {
if which::which("gpasswd").is_ok() { if which::which("gpasswd").is_ok() {
@ -210,7 +217,8 @@ impl Action for AddUserToGroup {
.args([name, groupname]) .args([name, groupname])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else if which::which("addgroup").is_ok() { } else if which::which("addgroup").is_ok() {
execute_command( execute_command(
Command::new("addgroup") Command::new("addgroup")
@ -218,9 +226,12 @@ impl Action for AddUserToGroup {
.args([name, groupname]) .args([name, groupname])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else { } else {
return Err(ActionError::MissingAddUserToGroupCommand); return Err(Self::error(Self::error(
ActionErrorKind::MissingAddUserToGroupCommand,
)));
} }
}, },
} }
@ -264,7 +275,8 @@ impl Action for AddUserToGroup {
.arg(&name) .arg(&name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
}, },
_ => { _ => {
if which::which("gpasswd").is_ok() { if which::which("gpasswd").is_ok() {
@ -275,7 +287,8 @@ impl Action for AddUserToGroup {
.args([&name.to_string(), &groupname.to_string()]) .args([&name.to_string(), &groupname.to_string()])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else if which::which("delgroup").is_ok() { } else if which::which("delgroup").is_ok() {
execute_command( execute_command(
Command::new("delgroup") Command::new("delgroup")
@ -283,9 +296,12 @@ impl Action for AddUserToGroup {
.args([name, groupname]) .args([name, groupname])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else { } 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 tokio::fs::{create_dir, remove_dir_all};
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionState}; use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, StatefulAction};
/** Create a directory at the given location, optionally with an owning user, group, and mode. /** 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 action_state = if path.exists() {
let metadata = tokio::fs::metadata(&path) let metadata = tokio::fs::metadata(&path)
.await .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() { 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? // Does it have the right user/group?
if let Some(user) = &user { if let Some(user) = &user {
// If the file exists, the user must also exist to be correct. // If the file exists, the user must also exist to be correct.
let expected_uid = User::from_name(user.as_str()) let expected_uid = User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))? .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.ok_or_else(|| ActionError::NoUser(user.clone()))? .map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid; .uid;
let found_uid = metadata.uid(); let found_uid = metadata.uid();
if found_uid != expected_uid.as_raw() { if found_uid != expected_uid.as_raw() {
return Err(ActionError::PathUserMismatch( return Err(Self::error(ActionErrorKind::PathUserMismatch(
path.clone(), path.clone(),
found_uid, found_uid,
expected_uid.as_raw(), expected_uid.as_raw(),
)); )));
} }
} }
if let Some(group) = &group { if let Some(group) = &group {
// If the file exists, the group must also exist to be correct. // If the file exists, the group must also exist to be correct.
let expected_gid = Group::from_name(group.as_str()) let expected_gid = Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))? .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.ok_or_else(|| ActionError::NoUser(group.clone()))? .map_err(Self::error)?
.ok_or_else(|| ActionErrorKind::NoUser(group.clone()))
.map_err(Self::error)?
.gid; .gid;
let found_gid = metadata.gid(); let found_gid = metadata.gid();
if found_gid != expected_gid.as_raw() { if found_gid != expected_gid.as_raw() {
return Err(ActionError::PathGroupMismatch( return Err(Self::error(ActionErrorKind::PathGroupMismatch(
path.clone(), path.clone(),
found_gid, found_gid,
expected_gid.as_raw(), expected_gid.as_raw(),
)); )));
} }
} }
@ -136,8 +143,10 @@ impl Action for CreateDirectory {
let gid = if let Some(group) = group { let gid = if let Some(group) = group {
Some( Some(
Group::from_name(group.as_str()) Group::from_name(group.as_str())
.map_err(|e| ActionError::GettingGroupId(group.clone(), e))? .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e))
.ok_or(ActionError::NoGroup(group.clone()))? .map_err(Self::error)?
.ok_or(ActionErrorKind::NoGroup(group.clone()))
.map_err(Self::error)?
.gid, .gid,
) )
} else { } else {
@ -146,8 +155,10 @@ impl Action for CreateDirectory {
let uid = if let Some(user) = user { let uid = if let Some(user) = user {
Some( Some(
User::from_name(user.as_str()) User::from_name(user.as_str())
.map_err(|e| ActionError::GettingUserId(user.clone(), e))? .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e))
.ok_or(ActionError::NoUser(user.clone()))? .map_err(Self::error)?
.ok_or(ActionErrorKind::NoUser(user.clone()))
.map_err(Self::error)?
.uid, .uid,
) )
} else { } else {
@ -156,13 +167,17 @@ impl Action for CreateDirectory {
create_dir(path.clone()) create_dir(path.clone())
.await .await
.map_err(|e| ActionError::CreateDirectory(path.clone(), e))?; .map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e))
chown(path, uid, gid).map_err(|e| ActionError::Chown(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 { if let Some(mode) = mode {
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode))
.await .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(()) Ok(())
@ -202,14 +217,16 @@ impl Action for CreateDirectory {
let is_empty = path let is_empty = path
.read_dir() .read_dir()
.map_err(|e| ActionError::Read(path.clone(), e))? .map_err(|e| ActionErrorKind::Read(path.clone(), e))
.map_err(Self::error)?
.next() .next()
.is_none(); .is_none();
match (is_empty, force_prune_on_revert) { match (is_empty, force_prune_on_revert) {
(true, _) | (false, true) => remove_dir_all(path.clone()) (true, _) | (false, true) => remove_dir_all(path.clone())
.await .await
.map_err(|e| ActionError::Remove(path.clone(), e))?, .map_err(|e| ActionErrorKind::Remove(path.clone(), e))
.map_err(Self::error)?,
(false, false) => { (false, false) => {
tracing::debug!("Not removing `{}`, the folder is not empty", path.display()); tracing::debug!("Not removing `{}`, the folder is not empty", path.display());
}, },

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use nix::unistd::User;
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionTag}; use crate::action::{ActionError, ActionErrorKind, ActionTag};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction}; use crate::action::{Action, ActionDescription, StatefulAction};
@ -37,22 +37,23 @@ impl CreateUser {
}; };
// Ensure user does not exists // Ensure user does not exists
if let Some(user) = User::from_name(name.as_str()) 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 { if user.uid.as_raw() != uid {
return Err(ActionError::UserUidMismatch( return Err(Self::error(ActionErrorKind::UserUidMismatch(
name.clone(), name.clone(),
user.uid.as_raw(), user.uid.as_raw(),
uid, uid,
)); )));
} }
if user.gid.as_raw() != gid { if user.gid.as_raw() != gid {
return Err(ActionError::UserGidMismatch( return Err(Self::error(ActionErrorKind::UserGidMismatch(
name.clone(), name.clone(),
user.gid.as_raw(), user.gid.as_raw(),
gid, gid,
)); )));
} }
tracing::debug!("Creating user `{}` already complete", this.name); tracing::debug!("Creating user `{}` already complete", this.name);
@ -120,7 +121,8 @@ impl Action for CreateUser {
.args([".", "-create", &format!("/Users/{name}")]) .args([".", "-create", &format!("/Users/{name}")])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -133,7 +135,8 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -146,7 +149,8 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -159,7 +163,8 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -172,14 +177,16 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
.args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"]) .args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
}, },
_ => { _ => {
if which::which("useradd").is_ok() { if which::which("useradd").is_ok() {
@ -207,7 +214,8 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else if which::which("adduser").is_ok() { } else if which::which("adduser").is_ok() {
execute_command( execute_command(
Command::new("adduser") Command::new("adduser")
@ -229,9 +237,10 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else { } 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)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
name,
uid: _,
groupname: _,
gid: _,
comment: _,
} = self;
use target_lexicon::OperatingSystem; use target_lexicon::OperatingSystem;
match target_lexicon::OperatingSystem::host() { match target_lexicon::OperatingSystem::host() {
OperatingSystem::MacOSX { 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 // 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. // 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"); 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.process_group(0);
command.stdin(std::process::Stdio::null()); command.stdin(std::process::Stdio::null());
let output = command let output = command
.output() .output()
.await .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); let stderr = String::from_utf8_lossy(&output.stderr);
match output.status.code() { match output.status.code() {
Some(0) => (), Some(0) => (),
Some(40) if stderr.contains("-14120") => { Some(40) if stderr.contains("-14120") => {
// The user is on an ephemeral Mac, like detsys uses // The user is on an ephemeral Mac, like detsys uses
// These Macs cannot always delete users, as sometimes there is no graphical login // 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 // 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( execute_command(
Command::new("userdel") Command::new("userdel")
.process_group(0) .process_group(0)
.arg(name) .arg(&self.name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else if which::which("deluser").is_ok() { } else if which::which("deluser").is_ok() {
execute_command( execute_command(
Command::new("deluser") Command::new("deluser")
.process_group(0) .process_group(0)
.arg(name) .arg(&self.name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} else { } 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 tracing::{span, Span};
use crate::{ use crate::{
action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}, action::{Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction},
parse_ssl_cert, parse_ssl_cert,
}; };
@ -33,26 +33,18 @@ impl FetchAndUnpackNix {
match url.scheme() { match url.scheme() {
"https" | "http" | "file" => (), "https" | "http" | "file" => (),
_ => { _ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)),
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownUrlScheme,
)))
},
}; };
if let Some(proxy) = &proxy { if let Some(proxy) = &proxy {
match proxy.scheme() { match proxy.scheme() {
"https" | "http" | "socks5" => (), "https" | "http" | "socks5" => (),
_ => { _ => return Err(Self::error(FetchUrlError::UnknownProxyScheme)),
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownProxyScheme,
)))
},
}; };
} }
if let Some(ssl_cert_file) = &ssl_cert_file { 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 { Ok(Self {
@ -106,41 +98,43 @@ impl Action for FetchAndUnpackNix {
"https" | "http" => { "https" | "http" => {
let mut buildable_client = reqwest::Client::builder(); let mut buildable_client = reqwest::Client::builder();
if let Some(proxy) = &self.proxy { if let Some(proxy) = &self.proxy {
buildable_client = buildable_client = buildable_client.proxy(
buildable_client.proxy(reqwest::Proxy::all(proxy.clone()).map_err(|e| { reqwest::Proxy::all(proxy.clone())
ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))) .map_err(FetchUrlError::Reqwest)
})?) .map_err(Self::error)?,
)
} }
if let Some(ssl_cert_file) = &self.ssl_cert_file { 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); buildable_client = buildable_client.add_root_certificate(ssl_cert);
} }
let client = buildable_client let client = buildable_client
.build() .build()
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; .map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
let req = client let req = client
.get(self.url.clone()) .get(self.url.clone())
.build() .build()
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; .map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
let res = client let res = client
.execute(req) .execute(req)
.await .await
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; .map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?;
res.bytes() res.bytes()
.await .await
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))? .map_err(FetchUrlError::Reqwest)
.map_err(Self::error)?
}, },
"file" => { "file" => {
let buf = tokio::fs::read(self.url.path()) let buf = tokio::fs::read(self.url.path())
.await .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) Bytes::from(buf)
}, },
_ => { _ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)),
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownUrlScheme,
)))
},
}; };
// TODO(@Hoverbear): Pick directory // TODO(@Hoverbear): Pick directory
@ -151,7 +145,8 @@ impl Action for FetchAndUnpackNix {
let mut archive = tar::Archive::new(decoder); let mut archive = tar::Archive::new(decoder);
archive archive
.unpack(&dest_clone) .unpack(&dest_clone)
.map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Unarchive(e))))?; .map_err(FetchUrlError::Unarchive)
.map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -182,3 +177,9 @@ pub enum FetchUrlError {
#[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")] #[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")]
UnknownProxyScheme, 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 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/"; 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 // 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())) 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<_>, _>>() .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 { 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 found_nix_path = found_nix_paths.into_iter().next().unwrap();
let src_store = found_nix_path.join("store"); let src_store = found_nix_path.join("store");
let mut src_store_listing = tokio::fs::read_dir(src_store.clone()) let mut src_store_listing = tokio::fs::read_dir(src_store.clone())
.await .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"); let dest_store = Path::new(DEST).join("store");
if dest_store.exists() { if dest_store.exists() {
if !dest_store.is_dir() { if !dest_store.is_dir() {
return Err(ActionError::PathWasNotDirectory(dest_store.clone()))?; return Err(Self::error(ActionErrorKind::PathWasNotDirectory(
dest_store.clone(),
)))?;
} }
} else { } else {
tokio::fs::create_dir(&dest_store) tokio::fs::create_dir(&dest_store)
.await .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 while let Some(entry) = src_store_listing
.next_entry() .next_entry()
.await .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()); let entry_dest = dest_store.join(entry.file_name());
if entry_dest.exists() { if entry_dest.exists() {
@ -92,8 +99,9 @@ impl Action for MoveUnpackedNix {
tokio::fs::rename(&entry.path(), &entry_dest) tokio::fs::rename(&entry.path(), &entry_dest)
.await .await
.map_err(|e| { .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, 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 tokio::fs::remove_dir_all;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionState}; use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, StatefulAction};
/** Remove a directory, does nothing on revert. /** Remove a directory, does nothing on revert.
@ -53,11 +53,13 @@ impl Action for RemoveDirectory {
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
if self.path.exists() { if self.path.exists() {
if !self.path.is_dir() { 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) remove_dir_all(&self.path)
.await .await
.map_err(|e| ActionError::Remove(self.path.clone(), e))?; .map_err(|e| Self::error(ActionErrorKind::Remove(self.path.clone(), e)))?;
} else { } else {
tracing::debug!("Directory `{}` not present, skipping", self.path.display(),); tracing::debug!("Directory `{}` not present, skipping", self.path.display(),);
}; };

View file

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::{ use crate::{
action::{ActionError, ActionTag, StatefulAction}, action::{ActionError, ActionErrorKind, ActionTag, StatefulAction},
execute_command, set_env, execute_command, set_env,
}; };
@ -50,9 +50,9 @@ impl Action for SetupDefaultProfile {
// Find an `nix` package // Find an `nix` package
let nix_pkg_glob = "/nix/store/*-nix-*"; let nix_pkg_glob = "/nix/store/*-nix-*";
let mut found_nix_pkg = None; let mut found_nix_pkg = None;
for entry in glob(nix_pkg_glob).map_err(|e| { for entry in glob(nix_pkg_glob)
ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e))) .map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))?
})? { {
match entry { match entry {
Ok(path) => { Ok(path) => {
// TODO(@Hoverbear): Should probably ensure is unique // 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 { let nix_pkg = if let Some(nix_pkg) = found_nix_pkg {
nix_pkg nix_pkg
} else { } else {
return Err(ActionError::Custom(Box::new( return Err(Self::error(SetupDefaultProfileError::NoNix));
SetupDefaultProfileError::NoNix,
)));
}; };
// Find an `nss-cacert` package, add it too. // Find an `nss-cacert` package, add it too.
let nss_ca_cert_pkg_glob = "/nix/store/*-nss-cacert-*"; let nss_ca_cert_pkg_glob = "/nix/store/*-nss-cacert-*";
let mut found_nss_ca_cert_pkg = None; let mut found_nss_ca_cert_pkg = None;
for entry in glob(nss_ca_cert_pkg_glob).map_err(|e| { for entry in glob(nss_ca_cert_pkg_glob)
ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e))) .map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))?
})? { {
match entry { match entry {
Ok(path) => { Ok(path) => {
// TODO(@Hoverbear): Should probably ensure is unique // 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 { let nss_ca_cert_pkg = if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg {
nss_ca_cert_pkg nss_ca_cert_pkg
} else { } else {
return Err(ActionError::Custom(Box::new( return Err(Self::error(SetupDefaultProfileError::NoNssCacert));
SetupDefaultProfileError::NoNssCacert,
)));
}; };
let found_nix_paths = glob::glob(&format!("{}/nix-*", self.unpacked_path.display())) 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<_>, _>>() .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 { 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 found_nix_path = found_nix_paths.into_iter().next().unwrap();
let reginfo_path = PathBuf::from(found_nix_path).join(".reginfo"); let reginfo_path = PathBuf::from(found_nix_path).join(".reginfo");
let reginfo = tokio::fs::read(&reginfo_path) let reginfo = tokio::fs::read(&reginfo_path)
.await .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")); let mut load_db_command = Command::new(nix_pkg.join("bin/nix-store"));
load_db_command.process_group(0); load_db_command.process_group(0);
load_db_command.arg("--load-db"); 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.stderr(std::process::Stdio::piped());
load_db_command.env( load_db_command.env(
"HOME", "HOME",
dirs::home_dir().ok_or_else(|| { dirs::home_dir().ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome))
})?,
); );
tracing::trace!( tracing::trace!(
"Executing `{:?}` with stdin from `{}`", "Executing `{:?}` with stdin from `{}`",
@ -124,17 +119,20 @@ impl Action for SetupDefaultProfile {
); );
let mut handle = load_db_command let mut handle = load_db_command
.spawn() .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(); let mut stdin = handle.stdin.take().unwrap();
stdin stdin
.write_all(&reginfo) .write_all(&reginfo)
.await .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 stdin
.flush() .flush()
.await .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); drop(stdin);
tracing::trace!( tracing::trace!(
"Wrote `{}` to stdin of `nix-store --load-db`", "Wrote `{}` to stdin of `nix-store --load-db`",
@ -144,9 +142,13 @@ impl Action for SetupDefaultProfile {
let output = handle let output = handle
.wait_with_output() .wait_with_output()
.await .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() { 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 // Install `nix` itself into the store
@ -158,16 +160,16 @@ impl Action for SetupDefaultProfile {
.stdin(std::process::Stdio::null()) .stdin(std::process::Stdio::null())
.env( .env(
"HOME", "HOME",
dirs::home_dir().ok_or_else(|| { dirs::home_dir()
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) .ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
})?,
) )
.env( .env(
"NIX_SSL_CERT_FILE", "NIX_SSL_CERT_FILE",
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */ ), /* This is apparently load bearing... */
) )
.await?; .await
.map_err(Self::error)?;
// Install `nix` itself into the store // Install `nix` itself into the store
execute_command( execute_command(
@ -178,16 +180,16 @@ impl Action for SetupDefaultProfile {
.stdin(std::process::Stdio::null()) .stdin(std::process::Stdio::null())
.env( .env(
"HOME", "HOME",
dirs::home_dir().ok_or_else(|| { dirs::home_dir()
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) .ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?,
})?,
) )
.env( .env(
"NIX_SSL_CERT_FILE", "NIX_SSL_CERT_FILE",
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */ ), /* This is apparently load bearing... */
) )
.await?; .await
.map_err(Self::error)?;
set_env( set_env(
"NIX_SSL_CERT_FILE", "NIX_SSL_CERT_FILE",
@ -234,3 +236,9 @@ pub enum SetupDefaultProfileError {
#[error("No root home found to place channel configuration in")] #[error("No root home found to place channel configuration in")]
NoRootHome, 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 tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction}; use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -39,7 +39,7 @@ pub struct ConfigureInitService {
impl ConfigureInitService { impl ConfigureInitService {
#[cfg(target_os = "linux")] #[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, // TODO: once we have a way to communicate interaction between the library and the cli,
// interactively ask for permission to remove the file // interactively ask for permission to remove the file
@ -50,21 +50,24 @@ impl ConfigureInitService {
if unit_dest.is_symlink() { if unit_dest.is_symlink() {
let link_dest = tokio::fs::read_link(&unit_dest) let link_dest = tokio::fs::read_link(&unit_dest)
.await .await
.map_err(|e| ActionError::ReadSymlink(unit_dest.clone(), e))?; .map_err(|e| ActionErrorKind::ReadSymlink(unit_dest.clone(), e))?;
if link_dest != unit_src { if link_dest != unit_src {
return Err(ActionError::SymlinkExists(unit_dest)); return Err(ActionErrorKind::SymlinkExists(unit_dest));
} }
} else { } 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 // NOTE: ...and if there are any overrides in the most well-known places for systemd
if Path::new(&format!("{dest}.d")).exists() { 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(()) Ok(())
} }
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
pub async fn plan( pub async fn plan(
init: InitSystem, init: InitSystem,
@ -75,7 +78,7 @@ impl ConfigureInitService {
Some( Some(
ssl_cert_file ssl_cert_file
.canonicalize() .canonicalize()
.map_err(|e| ActionError::Canonicalize(ssl_cert_file, e))?, .map_err(|e| Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)))?,
) )
} else { } else {
None None
@ -92,11 +95,15 @@ impl ConfigureInitService {
// with systemd: https://www.freedesktop.org/software/systemd/man/sd_booted.html // with systemd: https://www.freedesktop.org/software/systemd/man/sd_booted.html
if !(Path::new("/run/systemd/system").exists() || which::which("systemctl").is_ok()) 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(SERVICE_SRC, SERVICE_DEST)
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST).await?; .await
.map_err(Self::error)?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST)
.await
.map_err(Self::error)?;
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
InitSystem::None => { InitSystem::None => {
@ -183,11 +190,11 @@ impl Action for ConfigureInitService {
tokio::fs::copy(src.clone(), DARWIN_NIX_DAEMON_DEST) tokio::fs::copy(src.clone(), DARWIN_NIX_DAEMON_DEST)
.await .await
.map_err(|e| { .map_err(|e| {
ActionError::Copy( Self::error(ActionErrorKind::Copy(
src.to_path_buf(), src.to_path_buf(),
PathBuf::from(DARWIN_NIX_DAEMON_DEST), PathBuf::from(DARWIN_NIX_DAEMON_DEST),
e, e,
) ))
})?; })?;
execute_command( execute_command(
@ -197,7 +204,8 @@ impl Action for ConfigureInitService {
.arg(DARWIN_NIX_DAEMON_DEST) .arg(DARWIN_NIX_DAEMON_DEST)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
if let Some(ssl_cert_file) = ssl_cert_file { if let Some(ssl_cert_file) = ssl_cert_file {
execute_command( execute_command(
@ -208,7 +216,8 @@ impl Action for ConfigureInitService {
.arg(format!("{ssl_cert_file:?}")) .arg(format!("{ssl_cert_file:?}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} }
if *start_daemon { if *start_daemon {
@ -220,7 +229,8 @@ impl Action for ConfigureInitService {
.arg("system/org.nixos.nix-daemon") .arg("system/org.nixos.nix-daemon")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} }
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -232,23 +242,32 @@ impl Action for ConfigureInitService {
.arg("daemon-reload") .arg("daemon-reload")
.stdin(std::process::Stdio::null()), .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) // 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? { let socket_was_active =
disable("nix-daemon.socket", true).await?; if is_enabled("nix-daemon.socket").await.map_err(Self::error)? {
disable("nix-daemon.socket", true)
.await
.map_err(Self::error)?;
true true
} else if is_active("nix-daemon.socket").await? { } else if is_active("nix-daemon.socket").await.map_err(Self::error)? {
stop("nix-daemon.socket").await?; stop("nix-daemon.socket").await.map_err(Self::error)?;
true true
} else { } else {
false false
}; };
if is_enabled("nix-daemon.service").await? { if is_enabled("nix-daemon.service")
let now = is_active("nix-daemon.service").await?; .await
disable("nix-daemon.service", now).await?; .map_err(Self::error)?
} else if is_active("nix-daemon.service").await? { {
stop("nix-daemon.service").await?; 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"); tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking");
@ -256,12 +275,13 @@ impl Action for ConfigureInitService {
tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST) tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST)
.await .await
.map_err(|e| { .map_err(|e| {
ActionError::Symlink( ActionErrorKind::Symlink(
PathBuf::from(TMPFILES_SRC), PathBuf::from(TMPFILES_SRC),
PathBuf::from(TMPFILES_DEST), PathBuf::from(TMPFILES_DEST),
e, e,
) )
})?; })
.map_err(Self::error)?;
} }
execute_command( execute_command(
@ -271,32 +291,39 @@ impl Action for ConfigureInitService {
.arg("--prefix=/nix/var/nix") .arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()), .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 // TODO: once we have a way to communicate interaction between the library and the
// cli, interactively ask for permission to remove the file // 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) tokio::fs::symlink(SERVICE_SRC, SERVICE_DEST)
.await .await
.map_err(|e| { .map_err(|e| {
ActionError::Symlink( ActionErrorKind::Symlink(
PathBuf::from(SERVICE_SRC), PathBuf::from(SERVICE_SRC),
PathBuf::from(SERVICE_DEST), PathBuf::from(SERVICE_DEST),
e, 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) tokio::fs::symlink(SOCKET_SRC, SOCKET_DEST)
.await .await
.map_err(|e| { .map_err(|e| {
ActionError::Symlink( ActionErrorKind::Symlink(
PathBuf::from(SOCKET_SRC), PathBuf::from(SOCKET_SRC),
PathBuf::from(SOCKET_DEST), PathBuf::from(SOCKET_DEST),
e, e,
) )
})?; })
.map_err(Self::error)?;
if *start_daemon { if *start_daemon {
execute_command( execute_command(
@ -305,7 +332,8 @@ impl Action for ConfigureInitService {
.arg("daemon-reload") .arg("daemon-reload")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} }
if let Some(ssl_cert_file) = ssl_cert_file { 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) tokio::fs::create_dir(&service_conf_dir_path)
.await .await
.map_err(|e| { .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 = let service_conf_file_path =
service_conf_dir_path.join("nix-ssl-cert-file.conf"); service_conf_dir_path.join("nix-ssl-cert-file.conf");
tokio::fs::write( tokio::fs::write(
@ -327,13 +356,14 @@ impl Action for ConfigureInitService {
), ),
) )
.await .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 { if *start_daemon || socket_was_active {
enable(SOCKET_SRC, true).await?; enable(SOCKET_SRC, true).await.map_err(Self::error)?;
} else { } else {
enable(SOCKET_SRC, false).await?; enable(SOCKET_SRC, false).await.map_err(Self::error)?;
} }
}, },
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@ -373,6 +403,8 @@ impl Action for ConfigureInitService {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
match self.init { match self.init {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
InitSystem::Launchd => { InitSystem::Launchd => {
@ -382,84 +414,118 @@ impl Action for ConfigureInitService {
.arg("unload") .arg("unload")
.arg(DARWIN_NIX_DAEMON_DEST), .arg(DARWIN_NIX_DAEMON_DEST),
) )
.await?; .await
.map_err(|e| Self::error(e))?;
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
InitSystem::Systemd => { InitSystem::Systemd => {
// We separate stop and disable (instead of using `--now`) to avoid cases where the service isn't started, but is enabled. // 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?; // These have to fail fast.
let socket_is_enabled = is_enabled("nix-daemon.socket").await?; let socket_is_active = is_active("nix-daemon.socket")
let service_is_active = is_active("nix-daemon.service").await?; .await
let service_is_enabled = is_enabled("nix-daemon.service").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 { if socket_is_active {
execute_command( if let Err(err) = execute_command(
Command::new("systemctl") Command::new("systemctl")
.process_group(0) .process_group(0)
.args(["stop", "nix-daemon.socket"]) .args(["stop", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
} }
if socket_is_enabled { if socket_is_enabled {
execute_command( if let Err(err) = execute_command(
Command::new("systemctl") Command::new("systemctl")
.process_group(0) .process_group(0)
.args(["disable", "nix-daemon.socket"]) .args(["disable", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
} }
if service_is_active { if service_is_active {
execute_command( if let Err(err) = execute_command(
Command::new("systemctl") Command::new("systemctl")
.process_group(0) .process_group(0)
.args(["stop", "nix-daemon.service"]) .args(["stop", "nix-daemon.service"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
} }
if service_is_enabled { if service_is_enabled {
execute_command( if let Err(err) = execute_command(
Command::new("systemctl") Command::new("systemctl")
.process_group(0) .process_group(0)
.args(["disable", "nix-daemon.service"]) .args(["disable", "nix-daemon.service"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
} }
execute_command( if let Err(err) = execute_command(
Command::new("systemd-tmpfiles") Command::new("systemd-tmpfiles")
.process_group(0) .process_group(0)
.arg("--remove") .arg("--remove")
.arg("--prefix=/nix/var/nix") .arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
if self.ssl_cert_file.is_some() { if self.ssl_cert_file.is_some() {
let service_conf_dir_path = PathBuf::from(format!("{SERVICE_DEST}.d")); 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 .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 .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") Command::new("systemctl")
.process_group(0) .process_group(0)
.arg("daemon-reload") .arg("daemon-reload")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
{
errors.push(err);
}
}, },
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
InitSystem::None => { InitSystem::None => {
@ -467,7 +533,18 @@ impl Action for ConfigureInitService {
}, },
}; };
if errors.is_empty() {
Ok(()) 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")] #[cfg(target_os = "linux")]
async fn stop(unit: &str) -> Result<(), ActionError> { async fn stop(unit: &str) -> Result<(), ActionErrorKind> {
let mut command = Command::new("systemctl"); let mut command = Command::new("systemctl");
command.arg("stop"); command.arg("stop");
command.arg(unit); command.arg(unit);
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() { match output.status.success() {
true => { true => {
tracing::trace!(%unit, "Stopped"); tracing::trace!(%unit, "Stopped");
Ok(()) Ok(())
}, },
false => Err(ActionError::command_output(&command, output)), false => Err(ActionErrorKind::command_output(&command, output)),
} }
} }
#[cfg(target_os = "linux")] #[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"); let mut command = Command::new("systemctl");
command.arg("enable"); command.arg("enable");
command.arg(unit); command.arg(unit);
@ -507,18 +584,18 @@ async fn enable(unit: &str, now: bool) -> Result<(), ActionError> {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() { match output.status.success() {
true => { true => {
tracing::trace!(%unit, %now, "Enabled unit"); tracing::trace!(%unit, %now, "Enabled unit");
Ok(()) Ok(())
}, },
false => Err(ActionError::command_output(&command, output)), false => Err(ActionErrorKind::command_output(&command, output)),
} }
} }
#[cfg(target_os = "linux")] #[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"); let mut command = Command::new("systemctl");
command.arg("disable"); command.arg("disable");
command.arg(unit); command.arg(unit);
@ -528,25 +605,25 @@ async fn disable(unit: &str, now: bool) -> Result<(), ActionError> {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))?;
match output.status.success() { match output.status.success() {
true => { true => {
tracing::trace!(%unit, %now, "Disabled unit"); tracing::trace!(%unit, %now, "Disabled unit");
Ok(()) Ok(())
}, },
false => Err(ActionError::command_output(&command, output)), false => Err(ActionErrorKind::command_output(&command, output)),
} }
} }
#[cfg(target_os = "linux")] #[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"); let mut command = Command::new("systemctl");
command.arg("is-active"); command.arg("is-active");
command.arg(unit); command.arg(unit);
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))?;
if String::from_utf8(output.stdout)?.starts_with("active") { if String::from_utf8(output.stdout)?.starts_with("active") {
tracing::trace!(%unit, "Is active"); tracing::trace!(%unit, "Is active");
Ok(true) Ok(true)
@ -557,14 +634,14 @@ async fn is_active(unit: &str) -> Result<bool, ActionError> {
} }
#[cfg(target_os = "linux")] #[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"); let mut command = Command::new("systemctl");
command.arg("is-enabled"); command.arg("is-enabled");
command.arg(unit); command.arg(unit);
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))?;
let stdout = String::from_utf8(output.stdout)?; let stdout = String::from_utf8(output.stdout)?;
if stdout.starts_with("enabled") || stdout.starts_with("linked") { if stdout.starts_with("enabled") || stdout.starts_with("linked") {
tracing::trace!(%unit, "Is enabled"); tracing::trace!(%unit, "Is enabled");

View file

@ -4,7 +4,7 @@ use crate::{
action::{ action::{
base::SetupDefaultProfile, base::SetupDefaultProfile,
common::{ConfigureShellProfile, PlaceNixConfiguration}, common::{ConfigureShellProfile, PlaceNixConfiguration},
Action, ActionDescription, ActionError, ActionTag, StatefulAction, Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
}, },
planner::ShellProfileLocations, planner::ShellProfileLocations,
settings::{CommonSettings, SCRATCH_DIR}, settings::{CommonSettings, SCRATCH_DIR},
@ -30,7 +30,7 @@ impl ConfigureNix {
) -> Result<StatefulAction<Self>, ActionError> { ) -> Result<StatefulAction<Self>, ActionError> {
let setup_default_profile = SetupDefaultProfile::plan(PathBuf::from(SCRATCH_DIR)) let setup_default_profile = SetupDefaultProfile::plan(PathBuf::from(SCRATCH_DIR))
.await .await
.map_err(|e| ActionError::Child(SetupDefaultProfile::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let configure_shell_profile = if settings.modify_profile { let configure_shell_profile = if settings.modify_profile {
Some( Some(
@ -39,9 +39,7 @@ impl ConfigureNix {
settings.ssl_cert_file.clone(), settings.ssl_cert_file.clone(),
) )
.await .await
.map_err(|e| { .map_err(Self::error)?,
ActionError::Child(ConfigureShellProfile::action_tag(), Box::new(e))
})?,
) )
} else { } else {
None None
@ -52,7 +50,7 @@ impl ConfigureNix {
settings.force, settings.force,
) )
.await .await
.map_err(|e| ActionError::Child(PlaceNixConfiguration::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
Ok(Self { Ok(Self {
place_nix_configuration, place_nix_configuration,
@ -112,27 +110,21 @@ impl Action for ConfigureNix {
.try_execute() .try_execute()
.instrument(setup_default_profile_span) .instrument(setup_default_profile_span)
.await .await
.map_err(|e| { .map_err(Self::error)
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_nix_configuration place_nix_configuration
.try_execute() .try_execute()
.instrument(place_nix_configuration_span) .instrument(place_nix_configuration_span)
.await .await
.map_err(|e| { .map_err(Self::error)
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
}, },
async move { async move {
configure_shell_profile configure_shell_profile
.try_execute() .try_execute()
.instrument(configure_shell_profile_span) .instrument(configure_shell_profile_span)
.await .await
.map_err(|e| { .map_err(Self::error)
ActionError::Child(configure_shell_profile.action_tag(), Box::new(e))
})
}, },
)?; )?;
} else { } else {
@ -144,18 +136,14 @@ impl Action for ConfigureNix {
.try_execute() .try_execute()
.instrument(setup_default_profile_span) .instrument(setup_default_profile_span)
.await .await
.map_err(|e| { .map_err(Self::error)
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_nix_configuration place_nix_configuration
.try_execute() .try_execute()
.instrument(place_nix_configuration_span) .instrument(place_nix_configuration_span)
.await .await
.map_err(|e| { .map_err(Self::error)
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
}, },
)?; )?;
}; };
@ -182,21 +170,28 @@ impl Action for ConfigureNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
if let Some(configure_shell_profile) = &mut self.configure_shell_profile { if let Some(configure_shell_profile) = &mut self.configure_shell_profile {
configure_shell_profile.try_revert().await.map_err(|e| { if let Err(err) = configure_shell_profile.try_revert().await {
ActionError::Child(configure_shell_profile.action_tag(), Box::new(e)) 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))
})?;
if errors.is_empty() {
Ok(()) 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::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 crate::planner::ShellProfileLocations;
use nix::unistd::User; 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 { let maybe_ssl_cert_file_setting = if let Some(ssl_cert_file) = ssl_cert_file {
format!( format!(
"export NIX_SSL_CERT_FILE={:?}\n", "export NIX_SSL_CERT_FILE={:?}\n",
ssl_cert_file ssl_cert_file.canonicalize().map_err(|e| {
.canonicalize() Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e))
.map_err(|e| { ActionError::Canonicalize(ssl_cert_file, e) })? })?
) )
} else { } else {
"".to_string() "".to_string()
@ -207,7 +209,7 @@ impl Action for ConfigureShellProfile {
} }
let mut set = JoinSet::new(); let mut set = JoinSet::new();
let mut errors = Vec::default(); let mut errors = vec![];
for (idx, create_or_insert_into_file) in for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate() self.create_or_insert_into_files.iter_mut().enumerate()
@ -219,12 +221,7 @@ impl Action for ConfigureShellProfile {
.try_execute() .try_execute()
.instrument(span) .instrument(span)
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(
create_or_insert_into_file_clone.action_tag(),
Box::new(e),
)
})?;
Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone)) 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 self.create_or_insert_into_files[idx] = create_or_insert_into_file
}, },
Ok(Err(e)) => errors.push(e), Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e)?, Err(e) => return Err(Self::error(e))?,
}; };
} }
if !errors.is_empty() { if !errors.is_empty() {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap())?; return Err(Self::error(errors.into_iter().next().unwrap()))?;
} else { } else {
return Err(ActionError::Children( return Err(Self::error(ActionErrorKind::MultipleChildren(
errors.into_iter().map(|v| Box::new(v)).collect(), errors.into_iter().collect(),
)); )));
} }
} }
@ -262,7 +259,7 @@ impl Action for ConfigureShellProfile {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let mut set = JoinSet::new(); let mut set = JoinSet::new();
let mut errors: Vec<ActionError> = Vec::default(); let mut errors = vec![];
for (idx, create_or_insert_into_file) in for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate() 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 self.create_or_insert_into_files[idx] = create_or_insert_into_file
}, },
Ok(Err(e)) => errors.push(e), 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() { for create_directory in self.create_directories.iter_mut() {
create_directory if let Err(err) = create_directory.try_revert().await {
.try_revert() errors.push(err);
.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 errors.is_empty() {
Ok(()) 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 tracing::{span, Span};
use crate::action::base::CreateDirectory; 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] = &[ const PATHS: &[&str] = &[
"/nix/var", "/nix/var",
@ -36,7 +38,7 @@ impl CreateNixTree {
create_directories.push( create_directories.push(
CreateDirectory::plan(path, String::from("root"), None, 0o0755, false) CreateDirectory::plan(path, String::from("root"), None, 0o0755, false)
.await .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> { async fn execute(&mut self) -> Result<(), ActionError> {
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in self.create_directories.iter_mut() { for create_directory in self.create_directories.iter_mut() {
create_directory create_directory.try_execute().await.map_err(Self::error)?;
.try_execute()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
} }
Ok(()) Ok(())
@ -108,14 +107,23 @@ impl Action for CreateNixTree {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in self.create_directories.iter_mut().rev() { for create_directory in self.create_directories.iter_mut().rev() {
create_directory if let Err(err) = create_directory.try_revert().await {
.try_revert() errors.push(err);
.await }
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
} }
if errors.is_empty() {
Ok(()) 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::{ use crate::{
action::{ action::{
base::{AddUserToGroup, CreateGroup, CreateUser}, base::{AddUserToGroup, CreateGroup, CreateUser},
Action, ActionDescription, ActionError, ActionTag, StatefulAction, Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
}, },
settings::CommonSettings, settings::CommonSettings,
}; };
@ -38,7 +38,7 @@ impl CreateUsersAndGroups {
format!("Nix build user {index}"), format!("Nix build user {index}"),
) )
.await .await
.map_err(|e| ActionError::Child(CreateUser::action_tag(), Box::new(e)))?, .map_err(Self::error)?,
); );
add_users_to_groups.push( add_users_to_groups.push(
AddUserToGroup::plan( AddUserToGroup::plan(
@ -48,7 +48,7 @@ impl CreateUsersAndGroups {
settings.nix_build_group_id, settings.nix_build_group_id,
) )
.await .await
.map_err(|e| ActionError::Child(AddUserToGroup::action_tag(), Box::new(e)))?, .map_err(Self::error)?,
); );
} }
Ok(Self { Ok(Self {
@ -156,18 +156,12 @@ impl Action for CreateUsersAndGroups {
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
for create_user in create_users.iter_mut() { for create_user in create_users.iter_mut() {
create_user create_user.try_execute().await.map_err(Self::error)?;
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
} }
}, },
_ => { _ => {
for create_user in create_users.iter_mut() { for create_user in create_users.iter_mut() {
create_user create_user.try_execute().await.map_err(Self::error)?;
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
} }
// While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04: // 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 { // match result {
// Ok(Ok((idx, success))) => create_users[idx] = success, // Ok(Ok((idx, success))) => create_users[idx] = success,
// Ok(Err(e)) => errors.push(Box::new(e)), // 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 { // if errors.len() == 1 {
// return Err(errors.into_iter().next().unwrap().into()); // return Err(errors.into_iter().next().unwrap().into());
// } else { // } else {
// return Err(ActionError::Children(errors)); // return Err(ActionErrorKind::Children(errors));
// } // }
// } // }
}, },
}; };
for add_user_to_group in add_users_to_groups.iter_mut() { for add_user_to_group in add_users_to_groups.iter_mut() {
add_user_to_group add_user_to_group.try_execute().await.map_err(Self::error)?;
.try_execute()
.await
.map_err(|e| ActionError::Child(add_user_to_group.action_tag(), Box::new(e)))?;
} }
Ok(()) Ok(())
@ -256,21 +247,11 @@ impl Action for CreateUsersAndGroups {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { let mut errors = vec![];
create_users, for create_user in self.create_users.iter_mut() {
create_group, if let Err(err) = create_user.try_revert().await {
add_users_to_groups: _, errors.push(err);
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)))?;
} }
// We don't actually need to do this, when a user is deleted they are removed from groups // 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
create_group if let Err(err) = self.create_group.try_revert().await {
.try_revert() errors.push(err);
.await }
.map_err(|e| ActionError::Child(create_group.action_tag(), Box::new(e)))?;
if errors.is_empty() {
Ok(()) 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::create_or_merge_nix_config::CreateOrMergeNixConfigError;
use crate::action::base::{CreateDirectory, CreateOrMergeNixConfig}; 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_FOLDER: &str = "/etc/nix";
const NIX_CONF: &str = "/etc/nix/nix.conf"; const NIX_CONF: &str = "/etc/nix/nix.conf";
@ -26,7 +28,7 @@ impl PlaceNixConfiguration {
let extra_conf = extra_conf.join("\n"); let extra_conf = extra_conf.join("\n");
let mut nix_config = nix_config_parser::NixConfig::parse_string(extra_conf, None) let mut nix_config = nix_config_parser::NixConfig::parse_string(extra_conf, None)
.map_err(CreateOrMergeNixConfigError::ParseNixConfig) .map_err(CreateOrMergeNixConfigError::ParseNixConfig)
.map_err(|e| ActionError::Custom(Box::new(e)))?; .map_err(Self::error)?;
let settings = nix_config.settings_mut(); let settings = nix_config.settings_mut();
settings.insert("build-users-group".to_string(), nix_build_group_name); 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) let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force)
.await .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) let create_or_merge_nix_config = CreateOrMergeNixConfig::plan(NIX_CONF, nix_config)
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(CreateOrMergeNixConfig::action_tag(), Box::new(e))
})?;
Ok(Self { Ok(Self {
create_directory, create_directory,
create_or_merge_nix_config, create_or_merge_nix_config,
@ -100,13 +100,11 @@ impl Action for PlaceNixConfiguration {
self.create_directory self.create_directory
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
self.create_or_merge_nix_config self.create_or_merge_nix_config
.try_execute() .try_execute()
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e))
})?;
Ok(()) Ok(())
} }
@ -123,17 +121,23 @@ impl Action for PlaceNixConfiguration {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
self.create_or_merge_nix_config let mut errors = vec![];
.try_revert() if let Err(err) = self.create_or_merge_nix_config.try_revert().await {
.await errors.push(err);
.map_err(|e| { }
ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e)) if let Err(err) = self.create_directory.try_revert().await {
})?; errors.push(err);
self.create_directory }
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?;
if errors.is_empty() {
Ok(()) 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::{ use crate::{
action::{ action::{
base::{FetchAndUnpackNix, MoveUnpackedNix}, base::{FetchAndUnpackNix, MoveUnpackedNix},
Action, ActionDescription, ActionError, ActionTag, StatefulAction, Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
}, },
settings::{CommonSettings, SCRATCH_DIR}, settings::{CommonSettings, SCRATCH_DIR},
}; };
@ -33,13 +33,11 @@ impl ProvisionNix {
.await?; .await?;
let create_users_and_group = CreateUsersAndGroups::plan(settings.clone()) let create_users_and_group = CreateUsersAndGroups::plan(settings.clone())
.await .await
.map_err(|e| ActionError::Child(CreateUsersAndGroups::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let create_nix_tree = CreateNixTree::plan() let create_nix_tree = CreateNixTree::plan().await.map_err(Self::error)?;
.await
.map_err(|e| ActionError::Child(CreateNixTree::action_tag(), Box::new(e)))?;
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR)) let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR))
.await .await
.map_err(|e| ActionError::Child(MoveUnpackedNix::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
Ok(Self { Ok(Self {
fetch_nix, fetch_nix,
create_users_and_group, create_users_and_group,
@ -86,29 +84,27 @@ impl Action for ProvisionNix {
// We fetch nix while doing the rest, then move it over. // We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = self.fetch_nix.clone(); let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async { let fetch_nix_handle = tokio::task::spawn(async {
fetch_nix_clone fetch_nix_clone.try_execute().await.map_err(Self::error)?;
.try_execute()
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok(fetch_nix_clone) Result::<_, ActionError>::Ok(fetch_nix_clone)
}); });
self.create_users_and_group self.create_users_and_group
.try_execute() .try_execute()
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(self.create_users_and_group.action_tag(), Box::new(e))
})?;
self.create_nix_tree self.create_nix_tree
.try_execute() .try_execute()
.await .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 self.move_unpacked_nix
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -131,34 +127,32 @@ impl Action for ProvisionNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
// We fetch nix while doing the rest, then move it over. let mut errors = vec![];
let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async { if let Err(err) = self.fetch_nix.try_revert().await {
fetch_nix_clone errors.push(err)
.try_revert() }
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok(fetch_nix_clone)
});
if let Err(err) = self.create_users_and_group.try_revert().await { if let Err(err) = self.create_users_and_group.try_revert().await {
fetch_nix_handle.abort(); errors.push(err)
return Err(err);
} }
if let Err(err) = self.create_nix_tree.try_revert().await { if let Err(err) = self.create_nix_tree.try_revert().await {
fetch_nix_handle.abort(); errors.push(err)
return Err(err);
} }
self.fetch_nix = fetch_nix_handle if let Err(err) = self.move_unpacked_nix.try_revert().await {
.await errors.push(err)
.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 errors.is_empty() {
Ok(()) 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 tokio::process::Command;
use tracing::{span, Span}; 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::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -28,7 +28,7 @@ impl StartSystemdUnit {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
let state = if output.status.success() { let state = if output.status.success() {
tracing::debug!("Starting systemd unit `{}` already complete", unit); tracing::debug!("Starting systemd unit `{}` already complete", unit);
@ -84,7 +84,8 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
}, },
false => { false => {
// TODO(@Hoverbear): Handle proxy vars // TODO(@Hoverbear): Handle proxy vars
@ -95,7 +96,8 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .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)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { unit, enable } = self; let mut errors = vec![];
if *enable { if self.enable {
execute_command( if let Err(e) = execute_command(
Command::new("systemctl") Command::new("systemctl")
.process_group(0) .process_group(0)
.arg("disable") .arg("disable")
.arg(format!("{unit}")) .arg(format!("{}", self.unit))
.stdin(std::process::Stdio::null()), .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. // 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") Command::new("systemctl")
.process_group(0) .process_group(0)
.arg("stop") .arg("stop")
.arg(format!("{unit}")) .arg(format!("{}", self.unit))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)
{
errors.push(e);
}
if errors.is_empty() {
Ok(()) 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 tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction}; use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -40,7 +40,7 @@ impl BootstrapLaunchctlService {
let output = command let output = command
.output() .output()
.await .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) { if output.status.success() || output.status.code() == Some(37) {
// We presume that success means it's found // We presume that success means it's found
return Ok(StatefulAction::completed(Self { return Ok(StatefulAction::completed(Self {
@ -102,7 +102,8 @@ impl Action for BootstrapLaunchctlService {
.arg(path) .arg(path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -120,21 +121,16 @@ impl Action for BootstrapLaunchctlService {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
path,
service: _,
domain,
} = self;
execute_command( execute_command(
Command::new("launchctl") Command::new("launchctl")
.process_group(0) .process_group(0)
.arg("bootout") .arg("bootout")
.arg(domain) .arg(&self.domain)
.arg(path) .arg(&self.path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
Ok(()) Ok(())
} }

View file

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

View file

@ -2,7 +2,7 @@ use uuid::Uuid;
use super::{get_uuid_for_label, CreateApfsVolume}; use super::{get_uuid_for_label, CreateApfsVolume};
use crate::action::{ use crate::action::{
Action, ActionDescription, ActionError, ActionState, ActionTag, StatefulAction, Action, ActionDescription, ActionError, ActionErrorKind, ActionState, ActionTag, StatefulAction,
}; };
use std::{io::SeekFrom, path::Path}; use std::{io::SeekFrom, path::Path};
use tokio::{ use tokio::{
@ -46,7 +46,7 @@ impl CreateFstabEntry {
if fstab_path.exists() { if fstab_path.exists() {
let fstab_buf = tokio::fs::read_to_string(&fstab_path) let fstab_buf = tokio::fs::read_to_string(&fstab_path)
.await .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); 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) // 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, existing_entry,
} = self; } = self;
let fstab_path = Path::new(FSTAB_PATH); 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() let mut fstab = tokio::fs::OpenOptions::new()
.create(true) .create(true)
@ -130,14 +132,14 @@ impl Action for CreateFstabEntry {
.read(true) .read(true)
.open(fstab_path) .open(fstab_path)
.await .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. // Make sure it doesn't already exist before we write to it.
let mut fstab_buf = String::new(); let mut fstab_buf = String::new();
fstab fstab
.read_to_string(&mut fstab_buf) .read_to_string(&mut fstab_buf)
.await .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 { let updated_buf = match existing_entry {
ExistingFstabEntry::NixInstallerEntry => { ExistingFstabEntry::NixInstallerEntry => {
@ -161,9 +163,9 @@ impl Action for CreateFstabEntry {
} }
} }
if !(saw_prelude && updated_line) { if !(saw_prelude && updated_line) {
return Err(ActionError::Custom(Box::new( return Err(Self::error(
CreateFstabEntryError::ExistingNixInstallerEntryDisappeared, CreateFstabEntryError::ExistingNixInstallerEntryDisappeared,
))); ));
} }
current_fstab_lines.join("\n") current_fstab_lines.join("\n")
}, },
@ -182,9 +184,9 @@ impl Action for CreateFstabEntry {
} }
} }
if !updated_line { if !updated_line {
return Err(ActionError::Custom(Box::new( return Err(Self::error(
CreateFstabEntryError::ExistingForeignEntryDisappeared, CreateFstabEntryError::ExistingForeignEntryDisappeared,
))); ));
} }
current_fstab_lines.join("\n") current_fstab_lines.join("\n")
}, },
@ -194,15 +196,15 @@ impl Action for CreateFstabEntry {
fstab fstab
.seek(SeekFrom::Start(0)) .seek(SeekFrom::Start(0))
.await .await
.map_err(|e| ActionError::Seek(fstab_path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?;
fstab fstab
.set_len(0) .set_len(0)
.await .await
.map_err(|e| ActionError::Truncate(fstab_path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?;
fstab fstab
.write_all(updated_buf.as_bytes()) .write_all(updated_buf.as_bytes())
.await .await
.map_err(|e| ActionError::Write(fstab_path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?;
Ok(()) Ok(())
} }
@ -223,13 +225,13 @@ impl Action for CreateFstabEntry {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
apfs_volume_label,
existing_entry: _,
} = self;
let fstab_path = Path::new(FSTAB_PATH); 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() let mut file = OpenOptions::new()
.create(false) .create(false)
@ -237,12 +239,12 @@ impl Action for CreateFstabEntry {
.read(true) .read(true)
.open(&fstab_path) .open(&fstab_path)
.await .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(); let mut file_contents = String::default();
file.read_to_string(&mut file_contents) file.read_to_string(&mut file_contents)
.await .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()) { if let Some(start) = file_contents.rfind(fstab_entry.as_str()) {
let end = start + fstab_entry.len(); let end = start + fstab_entry.len();
@ -251,16 +253,16 @@ impl Action for CreateFstabEntry {
file.seek(SeekFrom::Start(0)) file.seek(SeekFrom::Start(0))
.await .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) file.set_len(0)
.await .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()) file.write_all(file_contents.as_bytes())
.await .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() file.flush()
.await .await
.map_err(|e| ActionError::Flush(fstab_path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Flush(fstab_path.to_owned(), e)))?;
Ok(()) 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")] #[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, 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, BootstrapLaunchctlService, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership,
EncryptApfsVolume, UnmountApfsVolume, EncryptApfsVolume, UnmountApfsVolume,
}, },
Action, ActionDescription, ActionError, ActionTag, StatefulAction, Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
}; };
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -54,23 +54,21 @@ impl CreateNixVolume {
create_or_insert_into_file::Position::End, create_or_insert_into_file::Position::End,
) )
.await .await
.map_err(|e| ActionError::Child(CreateOrInsertIntoFile::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let create_synthetic_objects = CreateSyntheticObjects::plan() let create_synthetic_objects = CreateSyntheticObjects::plan().await.map_err(Self::error)?;
.await
.map_err(|e| ActionError::Child(CreateSyntheticObjects::action_tag(), Box::new(e)))?;
let unmount_volume = UnmountApfsVolume::plan(disk, name.clone()) let unmount_volume = UnmountApfsVolume::plan(disk, name.clone())
.await .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) let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive)
.await .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) let create_fstab_entry = CreateFstabEntry::plan(name.clone(), &create_volume)
.await .await
.map_err(|e| ActionError::Child(CreateFstabEntry::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let encrypt_volume = if encrypt { let encrypt_volume = if encrypt {
Some(EncryptApfsVolume::plan(disk, &name, &create_volume).await?) Some(EncryptApfsVolume::plan(disk, &name, &create_volume).await?)
@ -86,7 +84,7 @@ impl CreateNixVolume {
encrypt, encrypt,
) )
.await .await
.map_err(|e| ActionError::Child(CreateVolumeService::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let bootstrap_volume = BootstrapLaunchctlService::plan( let bootstrap_volume = BootstrapLaunchctlService::plan(
"system", "system",
@ -94,14 +92,12 @@ impl CreateNixVolume {
NIX_VOLUME_MOUNTD_DEST, NIX_VOLUME_MOUNTD_DEST,
) )
.await .await
.map_err(|e| ActionError::Child(BootstrapLaunchctlService::action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let kickstart_launchctl_service = let kickstart_launchctl_service =
KickstartLaunchctlService::plan("system", "org.nixos.darwin-store") KickstartLaunchctlService::plan("system", "org.nixos.darwin-store")
.await .await
.map_err(|e| { .map_err(Self::error)?;
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?;
Ok(Self { Ok(Self {
disk: disk.to_path_buf(), disk: disk.to_path_buf(),
@ -172,23 +168,16 @@ impl Action for CreateNixVolume {
self.create_or_append_synthetic_conf self.create_or_append_synthetic_conf
.try_execute() .try_execute()
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(
self.create_or_append_synthetic_conf.action_tag(),
Box::new(e),
)
})?;
self.create_synthetic_objects self.create_synthetic_objects
.try_execute() .try_execute()
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e))
})?;
self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail. self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail.
self.create_volume self.create_volume
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
let mut retry_tokens: usize = 50; let mut retry_tokens: usize = 50;
loop { loop {
@ -201,11 +190,14 @@ impl Action for CreateNixVolume {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
if output.status.success() { if output.status.success() {
break; break;
} else if retry_tokens == 0 { } else if retry_tokens == 0 {
return Err(ActionError::command_output(&command, output)); return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
} else { } else {
retry_tokens = retry_tokens.saturating_sub(1); retry_tokens = retry_tokens.saturating_sub(1);
} }
@ -215,28 +207,23 @@ impl Action for CreateNixVolume {
self.create_fstab_entry self.create_fstab_entry
.try_execute() .try_execute()
.await .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 { if let Some(encrypt_volume) = &mut self.encrypt_volume {
encrypt_volume encrypt_volume.try_execute().await.map_err(Self::error)?
.try_execute()
.await
.map_err(|e| ActionError::Child(encrypt_volume.action_tag(), Box::new(e)))?
} }
self.setup_volume_daemon self.setup_volume_daemon
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.setup_volume_daemon.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
self.bootstrap_volume self.bootstrap_volume
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.bootstrap_volume.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
self.kickstart_launchctl_service self.kickstart_launchctl_service
.try_execute() .try_execute()
.await .await
.map_err(|e| { .map_err(Self::error)?;
ActionError::Child(self.kickstart_launchctl_service.action_tag(), Box::new(e))
})?;
let mut retry_tokens: usize = 50; let mut retry_tokens: usize = 50;
loop { loop {
@ -248,11 +235,14 @@ impl Action for CreateNixVolume {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| ActionErrorKind::command(&command, e))
.map_err(Self::error)?;
if output.status.success() { if output.status.success() {
break; break;
} else if retry_tokens == 0 { } else if retry_tokens == 0 {
return Err(ActionError::command_output(&command, output)); return Err(Self::error(ActionErrorKind::command_output(
&command, output,
)));
} else { } else {
retry_tokens = retry_tokens.saturating_sub(1); retry_tokens = retry_tokens.saturating_sub(1);
} }
@ -262,7 +252,7 @@ impl Action for CreateNixVolume {
self.enable_ownership self.enable_ownership
.try_execute() .try_execute()
.await .await
.map_err(|e| ActionError::Child(self.enable_ownership.action_tag(), Box::new(e)))?; .map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -296,44 +286,53 @@ impl Action for CreateNixVolume {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
self.enable_ownership.try_revert().await?; let mut errors = vec![];
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)))?;
self.unmount_volume if let Err(err) = self.enable_ownership.try_revert().await {
.try_revert() errors.push(err)
.await };
.map_err(|e| ActionError::Child(self.unmount_volume.action_tag(), Box::new(e)))?; if let Err(err) = self.kickstart_launchctl_service.try_revert().await {
self.create_volume errors.push(err)
.try_revert() };
.await if let Err(err) = self.bootstrap_volume.try_revert().await {
.map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?; 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 // Purposefully not reversed
self.create_or_append_synthetic_conf if let Err(err) = self.create_or_append_synthetic_conf.try_revert().await {
.try_revert() errors.push(err)
.await }
.map_err(|e| { if let Err(err) = self.create_synthetic_objects.try_revert().await {
ActionError::Child( errors.push(err)
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 errors.is_empty() {
Ok(()) 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(()) 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, 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; use super::get_uuid_for_label;
@ -43,27 +45,27 @@ impl CreateVolumeService {
}; };
if this.path.exists() { 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( let expected_plist = generate_mount_plist(
&this.mount_service_label, &this.mount_service_label,
&this.apfs_volume_label, &this.apfs_volume_label,
&this.mount_point, &this.mount_point,
encrypt, encrypt,
) )
.await?; .await
.map_err(Self::error)?;
if discovered_plist != expected_plist { if discovered_plist != expected_plist {
tracing::trace!( tracing::trace!(
?discovered_plist, ?discovered_plist,
?expected_plist, ?expected_plist,
"Parsed plists not equal" "Parsed plists not equal"
); );
return Err(ActionError::Custom(Box::new( return Err(Self::error(CreateVolumeServiceError::DifferentPlist {
CreateVolumeServiceError::DifferentPlist {
expected: expected_plist, expected: expected_plist,
discovered: discovered_plist, discovered: discovered_plist,
path: this.path.clone(), path: this.path.clone(),
}, }));
)));
} }
tracing::debug!("Creating file `{}` already complete", this.path.display()); tracing::debug!("Creating file `{}` already complete", this.path.display());
@ -121,7 +123,8 @@ impl Action for CreateVolumeService {
mount_point, mount_point,
*encrypt, *encrypt,
) )
.await?; .await
.map_err(Self::error)?;
let mut options = OpenOptions::new(); let mut options = OpenOptions::new();
options.create_new(true).write(true).read(true); options.create_new(true).write(true).read(true);
@ -129,13 +132,13 @@ impl Action for CreateVolumeService {
let mut file = options let mut file = options
.open(&path) .open(&path)
.await .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(); 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) file.write_all(&buf)
.await .await
.map_err(|e| ActionError::Write(path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Write(path.to_owned(), e)))?;
Ok(()) Ok(())
} }
@ -151,7 +154,7 @@ impl Action for CreateVolumeService {
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
remove_file(&self.path) remove_file(&self.path)
.await .await
.map_err(|e| ActionError::Remove(self.path.to_owned(), e))?; .map_err(|e| Self::error(ActionErrorKind::Remove(self.path.to_owned(), e)))?;
Ok(()) Ok(())
} }
@ -163,7 +166,7 @@ async fn generate_mount_plist(
apfs_volume_label: &str, apfs_volume_label: &str,
mount_point: &Path, mount_point: &Path,
encrypt: bool, encrypt: bool,
) -> Result<LaunchctlMountPlist, ActionError> { ) -> Result<LaunchctlMountPlist, ActionErrorKind> {
let apfs_volume_label_with_quotes = format!("\"{apfs_volume_label}\""); let apfs_volume_label_with_quotes = format!("\"{apfs_volume_label}\"");
let uuid = get_uuid_for_label(&apfs_volume_label).await?; let uuid = get_uuid_for_label(&apfs_volume_label).await?;
// The official Nix scripts uppercase the UUID, so we do as well for compatibility. // The official Nix scripts uppercase the UUID, so we do as well for compatibility.
@ -208,3 +211,9 @@ pub enum CreateVolumeServiceError {
path: PathBuf, 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 tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction}; use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -62,9 +62,11 @@ impl Action for EnableOwnership {
.arg(&path) .arg(&path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await? .await
.map_err(Self::error)?
.stdout; .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 the_plist.global_permissions_enabled
}; };
@ -77,7 +79,8 @@ impl Action for EnableOwnership {
.arg(path) .arg(path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
} }
Ok(()) Ok(())
@ -100,3 +103,9 @@ pub enum EnableOwnershipError {
#[error("Failed to execute command")] #[error("Failed to execute command")]
Command(#[source] std::io::Error), 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::{ use crate::{
action::{ action::{
macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionState, macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionErrorKind,
ActionTag, StatefulAction, ActionState, ActionTag, StatefulAction,
}, },
execute_command, execute_command,
os::darwin::DiskUtilApfsListOutput, os::darwin::DiskUtilApfsListOutput,
@ -51,7 +51,7 @@ impl EncryptApfsVolume {
if command if command
.status() .status()
.await .await
.map_err(|e| ActionError::command(&command, e))? .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?
.success() .success()
{ {
// The user has a password matching what we would create. // The user has a password matching what we would create.
@ -61,24 +61,26 @@ impl EncryptApfsVolume {
} }
// Ask the user to remove it // Ask the user to remove it
return Err(ActionError::Custom(Box::new( return Err(Self::error(EncryptApfsVolumeError::ExistingPasswordFound(
EncryptApfsVolumeError::ExistingPasswordFound(name, disk), name, disk,
))); )));
} else { } else {
if planned_create_apfs_volume.state == ActionState::Completed { 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. // 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), EncryptApfsVolumeError::MissingPasswordForExistingVolume(name, disk),
))); ));
} }
} }
// Ensure if the disk already exists, that it's encrypted // Ensure if the disk already exists, that it's encrypted
let output = let output =
execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"])) 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 container in parsed.containers {
for volume in container.volumes { for volume in container.volumes {
if volume.name == name { if volume.name == name {
@ -87,9 +89,9 @@ impl EncryptApfsVolume {
return Ok(StatefulAction::completed(Self { disk, name })); return Ok(StatefulAction::completed(Self { disk, name }));
}, },
false => { false => {
return Err(ActionError::Custom(Box::new( return Err(Self::error(
EncryptApfsVolumeError::ExistingVolumeNotEncrypted(name, disk), 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 */ 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. // Add the password to the user keychain so they can unlock it later.
execute_command( execute_command(
@ -180,7 +184,8 @@ impl Action for EncryptApfsVolume {
"/Library/Keychains/System.keychain", "/Library/Keychains/System.keychain",
]), ]),
) )
.await?; .await
.map_err(Self::error)?;
// Encrypt the mounted volume // Encrypt the mounted volume
execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([ execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([
@ -192,7 +197,8 @@ impl Action for EncryptApfsVolume {
"-passphrase", "-passphrase",
password.as_str(), password.as_str(),
])) ]))
.await?; .await
.map_err(Self::error)?;
execute_command( execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
@ -201,7 +207,8 @@ impl Action for EncryptApfsVolume {
.arg("force") .arg("force")
.arg(&name), .arg(&name),
) )
.await?; .await
.map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -220,18 +227,16 @@ impl Action for EncryptApfsVolume {
disk = %self.disk.display(), disk = %self.disk.display(),
))] ))]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { disk, name } = self; let disk_str = self.disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
// TODO: This seems very rough and unsafe // TODO: This seems very rough and unsafe
execute_command( execute_command(
Command::new("/usr/bin/security").process_group(0).args([ Command::new("/usr/bin/security").process_group(0).args([
"delete-generic-password", "delete-generic-password",
"-a", "-a",
name.as_str(), self.name.as_str(),
"-s", "-s",
name.as_str(), self.name.as_str(),
"-l", "-l",
format!("{} encryption password", disk_str).as_str(), format!("{} encryption password", disk_str).as_str(),
"-D", "-D",
@ -243,7 +248,8 @@ impl Action for EncryptApfsVolume {
.as_str(), .as_str(),
]), ]),
) )
.await?; .await
.map_err(Self::error)?;
Ok(()) 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)")] #[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), 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 tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionTag, StatefulAction}; use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -39,11 +39,11 @@ impl KickstartLaunchctlService {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(&command, e))?; .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?;
if output.status.success() { if output.status.success() {
service_exists = true; 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 // We are looking for a line containing "state = " with some trailing content
// The output is not a JSON or a plist // The output is not a JSON or a plist
// MacOS's man pages explicitly tell us not to try to parse this output // 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}")) .arg(format!("{domain}/{service}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await?; .await
.map_err(Self::error)?;
Ok(()) Ok(())
} }
@ -116,26 +117,26 @@ impl Action for KickstartLaunchctlService {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { domain, service } = self;
// MacOs doesn't offer an "ensure-stopped" like they do with Kickstart // MacOs doesn't offer an "ensure-stopped" like they do with Kickstart
let mut command = Command::new("launchctl"); let mut command = Command::new("launchctl");
command.process_group(0); command.process_group(0);
command.arg("stop"); command.arg("stop");
command.arg(format!("{domain}/{service}")); command.arg(format!("{}/{}", self.domain, self.service));
command.stdin(std::process::Stdio::null()); command.stdin(std::process::Stdio::null());
let command_str = format!("{:?}", command.as_std()); let command_str = format!("{:?}", command.as_std());
let output = command let output = command
.output() .output()
.await .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. // On our test Macs, a status code of `3` was reported if the service was stopped while not running.
match output.status.code() { match output.status.code() {
Some(3) | Some(0) | None => (), Some(3) | Some(0) | None => (),
_ => { _ => {
return Err(ActionError::Custom(Box::new( return Err(Self::error(ActionErrorKind::Custom(Box::new(
KickstartLaunchctlServiceError::CannotStopService(command_str, output), 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 bootstrap_launchctl_service::BootstrapLaunchctlService;
pub use create_apfs_volume::CreateApfsVolume; pub use create_apfs_volume::CreateApfsVolume;
pub use create_nix_volume::{CreateNixVolume, NIX_VOLUME_MOUNTD_DEST}; 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 create_volume_service::CreateVolumeService;
pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; pub use enable_ownership::{EnableOwnership, EnableOwnershipError};
pub use encrypt_apfs_volume::EncryptApfsVolume; pub use encrypt_apfs_volume::EncryptApfsVolume;
@ -27,9 +27,9 @@ use uuid::Uuid;
use crate::execute_command; 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( let output = execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
.process_group(0) .process_group(0)

View file

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

View file

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

View file

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

View file

@ -8,10 +8,10 @@ use crate::{
cli::{ensure_root, interaction::PromptChoice, signal_channel}, cli::{ensure_root, interaction::PromptChoice, signal_channel},
error::HasExpectedErrors, error::HasExpectedErrors,
plan::RECEIPT_LOCATION, plan::RECEIPT_LOCATION,
InstallPlan, InstallPlan, NixInstallerError,
}; };
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr}; use color_eyre::eyre::{eyre, WrapErr};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rand::Rng; use rand::Rng;
@ -125,17 +125,21 @@ impl CommandExecute for Uninstall {
let (_tx, rx) = signal_channel().await?; let (_tx, rx) = signal_channel().await?;
let res = plan.uninstall(rx).await; let res = plan.uninstall(rx).await;
if let Err(err) = res { match res {
Err(err @ NixInstallerError::ActionRevert(_)) => {
tracing::error!("Uninstallation complete, some errors encountered");
return Err(err)?;
},
Err(err) => {
if let Some(expected) = err.expected() { if let Some(expected) = err.expected() {
println!("{}", expected.red()); println!("{}", expected.red());
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
return Err(err)?; 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!( println!(
"\ "\
{success}\n\ {success}\n\

View file

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

View file

@ -82,8 +82,6 @@ pub mod settings;
use std::{ffi::OsStr, path::Path, process::Output}; use std::{ffi::OsStr, path::Path, process::Output};
use action::{Action, ActionError};
pub use error::NixInstallerError; pub use error::NixInstallerError;
pub use plan::InstallPlan; pub use plan::InstallPlan;
use planner::BuiltinPlanner; use planner::BuiltinPlanner;
@ -91,16 +89,18 @@ use planner::BuiltinPlanner;
use reqwest::Certificate; use reqwest::Certificate;
use tokio::process::Command; use tokio::process::Command;
use crate::action::{Action, ActionErrorKind};
#[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))] #[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"); tracing::trace!("Executing");
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::command(command, e))?; .map_err(|e| ActionErrorKind::command(command, e))?;
match output.status.success() { match output.status.success() {
true => Ok(output), 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()); 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) = action.try_execute().await {
if let Err(err) = write_receipt(self.clone()).await { if let Err(err) = write_receipt(self.clone()).await {
tracing::error!("Error saving receipt: {:?}", err); tracing::error!("Error saving receipt: {:?}", err);
} }
let err = NixInstallerError::Action(typetag_name.into(), err); let err = NixInstallerError::Action(err);
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data { if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data diagnostic_data
@ -296,6 +295,7 @@ impl InstallPlan {
) -> Result<(), NixInstallerError> { ) -> Result<(), NixInstallerError> {
let Self { actions, .. } = self; let Self { actions, .. } = self;
let mut cancel_channel = cancel_channel.into(); let mut cancel_channel = cancel_channel.into();
let mut errors = vec![];
// This is **deliberately sequential**. // This is **deliberately sequential**.
// Actions which are parallelizable are represented by "group actions" like CreateUsers // Actions which are parallelizable are represented by "group actions" like CreateUsers
@ -324,27 +324,12 @@ impl InstallPlan {
} }
tracing::info!("Revert: {}", action.tracing_synopsis()); tracing::info!("Revert: {}", action.tracing_synopsis());
let typetag_name = action.inner_typetag_name(); if let Err(errs) = action.try_revert().await {
if let Err(err) = action.try_revert().await { errors.push(errs);
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 errors.is_empty() {
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data { if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data diagnostic_data
@ -357,6 +342,22 @@ impl InstallPlan {
} }
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 url;
let nix_build_user_prefix; let nix_build_user_prefix;
let nix_build_user_id_base; let nix_build_user_id_base;
let ssl_cert_file;
use target_lexicon::{Architecture, OperatingSystem}; use target_lexicon::{Architecture, OperatingSystem};
match (Architecture::host(), OperatingSystem::host()) { match (Architecture::host(), OperatingSystem::host()) {
@ -240,18 +241,21 @@ impl CommonSettings {
url = NIX_X64_64_LINUX_URL; url = NIX_X64_64_LINUX_URL;
nix_build_user_prefix = "nixbld"; nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000; nix_build_user_id_base = 30000;
ssl_cert_file = None;
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
(Architecture::X86_32(_), OperatingSystem::Linux) => { (Architecture::X86_32(_), OperatingSystem::Linux) => {
url = NIX_I686_LINUX_URL; url = NIX_I686_LINUX_URL;
nix_build_user_prefix = "nixbld"; nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000; nix_build_user_id_base = 30000;
ssl_cert_file = None;
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
(Architecture::Aarch64(_), OperatingSystem::Linux) => { (Architecture::Aarch64(_), OperatingSystem::Linux) => {
url = NIX_AARCH64_LINUX_URL; url = NIX_AARCH64_LINUX_URL;
nix_build_user_prefix = "nixbld"; nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 30000; nix_build_user_id_base = 30000;
ssl_cert_file = None;
}, },
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
(Architecture::X86_64, OperatingSystem::MacOSX { .. }) (Architecture::X86_64, OperatingSystem::MacOSX { .. })
@ -259,6 +263,7 @@ impl CommonSettings {
url = NIX_X64_64_DARWIN_URL; url = NIX_X64_64_DARWIN_URL;
nix_build_user_prefix = "_nixbld"; nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300; nix_build_user_id_base = 300;
ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into());
}, },
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
@ -266,6 +271,7 @@ impl CommonSettings {
url = NIX_AARCH64_DARWIN_URL; url = NIX_AARCH64_DARWIN_URL;
nix_build_user_prefix = "_nixbld"; nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300; nix_build_user_id_base = 300;
ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into());
}, },
_ => { _ => {
return Err(InstallSettingsError::UnsupportedArchitecture( return Err(InstallSettingsError::UnsupportedArchitecture(
@ -285,7 +291,7 @@ impl CommonSettings {
proxy: Default::default(), proxy: Default::default(),
extra_conf: Default::default(), extra_conf: Default::default(),
force: false, force: false,
ssl_cert_file: Default::default(), ssl_cert_file,
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()), diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()),
}) })