diff --git a/Cargo.lock b/Cargo.lock index 9fecf8a..25da3f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "nix-installer" -version = "0.7.0" +version = "0.7.1-unreleased" dependencies = [ "async-trait", "atty", diff --git a/flake.nix b/flake.nix index 0b93cc5..3d4a87a 100644 --- a/flake.nix +++ b/flake.nix @@ -194,6 +194,7 @@ vm-test = import ./nix/tests/vm-test { inherit forSystem; inherit (nix.hydraJobs) binaryTarball; + inherit (nixpkgs) lib; }; container-test = import ./nix/tests/container-test { inherit forSystem; diff --git a/nix/tests/vm-test/default.nix b/nix/tests/vm-test/default.nix index 9148e65..d4c9908 100644 --- a/nix/tests/vm-test/default.nix +++ b/nix/tests/vm-test/default.nix @@ -1,5 +1,5 @@ # Largely derived from https://github.com/NixOS/nix/blob/14f7dae3e4eb0c34192d0077383a7f2a2d630129/tests/installer/default.nix -{ forSystem, binaryTarball }: +{ forSystem, binaryTarball, lib }: let nix-installer-install = '' @@ -18,7 +18,7 @@ let tar xvf nix.tar.xz ./nix-*/install --no-channel-add --yes --no-daemon ''; - installScripts = rec { + installCases = rec { install-default = { install = nix-installer-install; check = '' @@ -38,19 +38,25 @@ let fi if systemctl is-failed nix-daemon.socket; then echo "nix-daemon.socket is failed" + systemctl status nix-daemon.socket exit 1 fi - if systemctl is-failed nix-daemon.service; then - echo "nix-daemon.service is failed" - exit 1 - fi + if !(sudo systemctl start nix-daemon.service); then echo "nix-daemon.service failed to start" + systemctl status nix-daemon.service + exit 1 + fi + + if systemctl is-failed nix-daemon.service; then + echo "nix-daemon.service is failed" + systemctl status nix-daemon.service exit 1 fi if !(sudo systemctl stop nix-daemon.service); then echo "nix-daemon.service failed to stop" + systemctl status nix-daemon.service exit 1 fi @@ -65,6 +71,62 @@ let out=$(nix-build --no-substitute -E 'derivation { name = "foo"; system = "x86_64-linux"; builder = "/bin/sh"; args = ["-c" "echo foobar > $out"]; }') [[ $(cat $out) = foobar ]] ''; + uninstall = '' + /nix/nix-installer uninstall --no-confirm + ''; + uninstallCheck = '' + if which nix; then + echo "nix existed on path after uninstall" + exit 1 + fi + + for i in $(seq 1 32); do + if id -u nixbld$i; then + echo "User nixbld$i exists after uninstall" + exit 1 + fi + done + if grep "^nixbld:" /etc/group; then + echo "Group nixbld exists after uninstall" + exit 1 + fi + + if sudo -i nix store ping --store daemon; then + echo "Could run nix store ping after uninstall" + exit 1 + fi + + if [ -d /nix ]; then + echo "/nix exists after uninstall" + exit 1 + fi + + if [ -d /etc/nix/nix.conf ]; then + echo "/etc/nix/nix.conf exists after uninstall" + exit 1 + fi + + if [ -f /etc/systemd/system/nix-daemon.socket ]; then + echo "/etc/systemd/system/nix-daemon.socket exists after uninstall" + exit 1 + fi + + if [ -f /etc/systemd/system/nix-daemon.service ]; then + echo "/etc/systemd/system/nix-daemon.socket exists after uninstall" + exit 1 + fi + + + if systemctl status nix-daemon.socket > /dev/null; then + echo "systemd unit nix-daemon.socket still exists after uninstall" + exit 1 + fi + + if systemctl status nix-daemon.service > /dev/null; then + echo "systemd unit nix-daemon.service still exists after uninstall" + exit 1 + fi + ''; }; install-no-start-daemon = { install = '' @@ -90,6 +152,8 @@ let [[ $(cat $out) = foobar ]] ''; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; install-daemonless = { install = '' @@ -106,14 +170,20 @@ let [[ $(cat $out) = foobar ]] ''; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; + }; + cureCases = { cure-self-linux-working = { preinstall = '' ${nix-installer-install-quiet} sudo mv /nix/receipt.json /nix/old-receipt.json ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-no-nix-path = { preinstall = '' @@ -122,8 +192,10 @@ let sudo mv /nix/receipt.json /nix/old-receipt.json sudo rm -rf /nix/ ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-missing-users = { preinstall = '' @@ -133,8 +205,10 @@ let sudo userdel nixbld3 sudo userdel nixbld16 ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-missing-users-and-group = { preinstall = '' @@ -146,8 +220,10 @@ let done sudo groupdel nixbld ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-daemon-disabled = { preinstall = '' @@ -155,8 +231,10 @@ let sudo mv /nix/receipt.json /nix/old-receipt.json sudo systemctl disable --now nix-daemon.socket ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-no-etc-nix = { preinstall = '' @@ -164,8 +242,10 @@ let sudo mv /nix/receipt.json /nix/old-receipt.json sudo rm -rf /etc/nix ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-self-linux-broken-unmodified-bashrc = { preinstall = '' @@ -173,16 +253,20 @@ let sudo mv /nix/receipt.json /nix/old-receipt.json sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-self-broken-no-nix-path = { preinstall = '' ${cure-script-multi-user} sudo rm -rf /nix/ ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-broken-missing-users = { preinstall = '' @@ -191,44 +275,98 @@ let sudo userdel nixbld3 sudo userdel nixbld16 ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-broken-daemon-disabled = { preinstall = '' ${cure-script-multi-user} sudo systemctl disable --now nix-daemon.socket ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-broken-no-etc-nix = { preinstall = '' ${cure-script-multi-user} sudo rm -rf /etc/nix ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-broken-unmodified-bashrc = { preinstall = '' ${cure-script-multi-user} sudo sed -i '/# Nix/,/# End Nix/d' /etc/bash.bashrc ''; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; cure-script-multi-working = { preinstall = cure-script-multi-user; - install = install-default.install; - check = install-default.check; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; }; # cure-script-single-working = { # preinstall = cure-script-single-user; - # install = install-default.install; - # check = install-default.check; + # install = installCases.install-default.install; + # check = installCases.install-default.check; # }; }; + # Cases to test uninstalling is complete even in the face of errors. + uninstallCases = + let + uninstallFailExpected = '' + if /nix/nix-installer uninstall --no-confirm; then + echo "/nix/nix-installer uninstall exited with 0 during a uninstall failure test" + exit 1 + else + exit 0 + fi + ''; + in + { + uninstall-users-and-groups-missing = { + install = installCases.install-default.install; + check = installCases.install-default.check; + preuninstall = '' + for i in $(seq 1 32); do + sudo userdel nixbld$i + done + sudo groupdel nixbld + ''; + uninstall = uninstallFailExpected; + uninstallCheck = installCases.install-default.uninstallCheck; + }; + uninstall-nix-conf-gone = { + install = installCases.install-default.install; + check = installCases.install-default.check; + preuninstall = '' + sudo rm -rf /etc/nix + ''; + uninstall = uninstallFailExpected; + uninstallCheck = installCases.install-default.uninstallCheck; + }; + uninstall-shell-profile-clobbered = { + install = installCases.install-default.install; + check = installCases.install-default.check; + preuninstall = '' + sudo rm -rf /etc/bashrc + ''; + uninstall = uninstallFailExpected; + uninstallCheck = installCases.install-default.uninstallCheck; + }; + }; disableSELinux = "sudo setenforce 0"; @@ -333,7 +471,7 @@ let }; - makeTest = imageName: testName: + makeTest = imageName: testName: test: let image = images.${imageName}; in with (forSystem image.system ({ system, pkgs, ... }: pkgs)); runCommand @@ -342,9 +480,12 @@ let buildInputs = [ qemu_kvm openssh ]; image = image.image; postBoot = image.postBoot or ""; - preinstallScript = installScripts.${testName}.preinstall or "echo \"Not Applicable\""; - installScript = installScripts.${testName}.install; - checkScript = installScripts.${testName}.check; + preinstallScript = test.preinstall or "echo \"Not Applicable\""; + installScript = test.install; + checkScript = test.check; + uninstallScript = test.uninstall; + preuninstallScript = test.preuninstall or "echo \"Not Applicable\""; + uninstallCheckScript = test.uninstallCheck; installer = nix-installer-static; binaryTarball = binaryTarball.${system}; } @@ -414,32 +555,38 @@ let echo "Running installer..." $ssh "set -eux; $installScript" - echo "Testing Nix installation..." + echo "Checking Nix installation..." $ssh "set -eux; $checkScript" - echo "Testing Nix uninstallation..." - $ssh "set -eux; /nix/nix-installer uninstall --no-confirm" + echo "Running preuninstall..." + $ssh "set -eux; $preuninstallScript" + + echo "Running Nix uninstallation..." + $ssh "set -eux; $uninstallScript" + + echo "Checking Nix uninstallation..." + $ssh "set -eux; $uninstallCheckScript" echo "Done!" touch $out ''; - vm-tests = builtins.mapAttrs + makeTests = name: tests: builtins.mapAttrs (imageName: image: rec { ${image.system} = (builtins.mapAttrs (testName: test: - makeTest imageName testName + makeTest imageName testName test ) - installScripts) // { - all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; + tests) // { + "${name}" = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { + name = name; constituents = ( pkgs.lib.mapAttrsToList (testName: test: - makeTest imageName testName + makeTest imageName testName test ) - installScripts + tests ); }); }; @@ -447,96 +594,35 @@ let ) images; + allCases = lib.recursiveUpdate (lib.recursiveUpdate installCases cureCases) uninstallCases; + + install-tests = makeTests "install" installCases; + + cure-tests = makeTests "cure" cureCases; + + uninstall-tests = makeTests "uninstall" uninstallCases; + + all-tests = builtins.mapAttrs + (imageName: image: { + "x86_64-linux".all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { + name = "all"; + constituents = [ + install-tests."${imageName}"."x86_64-linux".install + cure-tests."${imageName}"."x86_64-linux".cure + uninstall-tests."${imageName}"."x86_64-linux".uninstall + ]; + }); + }) + images; + + joined-tests = lib.recursiveUpdate (lib.recursiveUpdate (lib.recursiveUpdate cure-tests install-tests) uninstall-tests) all-tests; + in -vm-tests // rec { - all."x86_64-linux".install-default = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests; - }); - all."x86_64-linux".install-no-start-daemon = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-default) vm-tests; - }); - all."x86_64-linux".install-daemonless = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".install-daemonless) vm-tests; - }); - all."x86_64-linux".cure-self-linux-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-working) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-no-nix-path = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-no-nix-path) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-missing-users = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-missing-users) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-missing-users-and-group = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-missing-users-and-group) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-daemon-disabled = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-daemon-disabled) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-no-etc-nix = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-no-etc-nix) vm-tests; - }); - all."x86_64-linux".cure-self-linux-broken-unmodified-bashrc = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-self-linux-broken-unmodified-bashrc) vm-tests; - }); - all."x86_64-linux".cure-script-multi-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-working) vm-tests; - }); - # all."x86_64-linux".cure-script-multi-broken-no-nix-path = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - # name = "all"; - # constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-no-nix-path) vm-tests; - # }); - all."x86_64-linux".cure-script-multi-broken-missing-users = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-missing-users) vm-tests; - }); - all."x86_64-linux".cure-script-multi-broken-daemon-disabled = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-daemon-disabled) vm-tests; - }); - all."x86_64-linux".cure-script-multi-broken-no-etc-nix = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-no-etc-nix) vm-tests; - }); - all."x86_64-linux".cure-script-multi-broken-unmodified-bashrc = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-multi-broken-unmodified-bashrc) vm-tests; - }); - # all."x86_64-linux".cure-script-single-working = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - # name = "all"; - # constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux".cure-script-single-working) vm-tests; - # }); - all."x86_64-linux".all = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.releaseTools.aggregate { - name = "all"; - constituents = [ - all."x86_64-linux".install-default - all."x86_64-linux".install-no-start-daemon - all."x86_64-linux".install-daemonless - all."x86_64-linux".cure-self-linux-working - all."x86_64-linux".cure-self-linux-broken-no-nix-path - all."x86_64-linux".cure-self-linux-broken-missing-users - all."x86_64-linux".cure-self-linux-broken-missing-users-and-group - all."x86_64-linux".cure-self-linux-broken-daemon-disabled - all."x86_64-linux".cure-self-linux-broken-no-etc-nix - all."x86_64-linux".cure-self-linux-broken-unmodified-bashrc - all."x86_64-linux".cure-script-multi-working - # all."x86_64-linux".cure-script-multi-broken-no-nix-path - all."x86_64-linux".cure-script-multi-broken-missing-users - all."x86_64-linux".cure-script-multi-broken-daemon-disabled - all."x86_64-linux".cure-script-multi-broken-no-etc-nix - all."x86_64-linux".cure-script-multi-broken-unmodified-bashrc - # all."x86_64-linux".cure-script-single-working - ]; - }); +lib.recursiveUpdate joined-tests { + all."x86_64-linux" = (with (forSystem "x86_64-linux" ({ system, pkgs, ... }: pkgs)); pkgs.lib.mapAttrs (caseName: case: + pkgs.releaseTools.aggregate { + name = caseName; + constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux"."${caseName}") joined-tests; + } + )) (allCases // { "cure" = { }; "install" = { }; "uninstall" = { }; "all" = { }; }); } diff --git a/src/action/base/add_user_to_group.rs b/src/action/base/add_user_to_group.rs index 9c7b8a5..9ec2b6b 100644 --- a/src/action/base/add_user_to_group.rs +++ b/src/action/base/add_user_to_group.rs @@ -5,7 +5,7 @@ use target_lexicon::OperatingSystem; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::ActionError; +use crate::action::{ActionError, ActionErrorKind}; use crate::execute_command; use crate::action::{Action, ActionDescription, StatefulAction}; @@ -37,22 +37,23 @@ impl AddUserToGroup { }; // Ensure user does not exists if let Some(user) = User::from_name(name.as_str()) - .map_err(|e| ActionError::GettingUserId(name.clone(), e))? + .map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e)) + .map_err(Self::error)? { if user.uid.as_raw() != uid { - return Err(ActionError::UserUidMismatch( + return Err(Self::error(ActionErrorKind::UserUidMismatch( name.clone(), user.uid.as_raw(), uid, - )); + ))); } if user.gid.as_raw() != gid { - return Err(ActionError::UserGidMismatch( + return Err(Self::error(ActionErrorKind::UserGidMismatch( name.clone(), user.gid.as_raw(), gid, - )); + ))); } // See if group membership needs to be done @@ -74,7 +75,8 @@ impl AddUserToGroup { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; match output.status.code() { Some(0) => { // yes {user} is a member of {groupname} @@ -98,7 +100,9 @@ impl AddUserToGroup { }, _ => { // Some other issue - return Err(ActionError::command_output(&command, output)); + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); }, }; }, @@ -109,8 +113,9 @@ impl AddUserToGroup { .arg(&this.name) .stdin(std::process::Stdio::null()), ) - .await?; - let output_str = String::from_utf8(output.stdout)?; + .await + .map_err(Self::error)?; + let output_str = String::from_utf8(output.stdout).map_err(Self::error)?; let user_in_group = output_str.split(" ").any(|v| v == &this.groupname); if user_in_group { @@ -187,7 +192,8 @@ impl Action for AddUserToGroup { .arg(&name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/sbin/dseditgroup") .process_group(0) @@ -199,7 +205,8 @@ impl Action for AddUserToGroup { .arg(groupname) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, _ => { if which::which("gpasswd").is_ok() { @@ -210,7 +217,8 @@ impl Action for AddUserToGroup { .args([name, groupname]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("addgroup").is_ok() { execute_command( Command::new("addgroup") @@ -218,9 +226,12 @@ impl Action for AddUserToGroup { .args([name, groupname]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingAddUserToGroupCommand); + return Err(Self::error(Self::error( + ActionErrorKind::MissingAddUserToGroupCommand, + ))); } }, } @@ -264,7 +275,8 @@ impl Action for AddUserToGroup { .arg(&name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, _ => { if which::which("gpasswd").is_ok() { @@ -275,7 +287,8 @@ impl Action for AddUserToGroup { .args([&name.to_string(), &groupname.to_string()]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("delgroup").is_ok() { execute_command( Command::new("delgroup") @@ -283,9 +296,12 @@ impl Action for AddUserToGroup { .args([name, groupname]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingRemoveUserFromGroupCommand); + return Err(Self::error( + ActionErrorKind::MissingRemoveUserFromGroupCommand, + )); } }, }; diff --git a/src/action/base/create_directory.rs b/src/action/base/create_directory.rs index 97b96c8..6fd7e9e 100644 --- a/src/action/base/create_directory.rs +++ b/src/action/base/create_directory.rs @@ -6,7 +6,7 @@ use nix::unistd::{chown, Group, User}; use tokio::fs::{create_dir, remove_dir_all}; use tracing::{span, Span}; -use crate::action::{Action, ActionDescription, ActionState}; +use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState}; use crate::action::{ActionError, StatefulAction}; /** Create a directory at the given location, optionally with an owning user, group, and mode. @@ -40,40 +40,47 @@ impl CreateDirectory { let action_state = if path.exists() { let metadata = tokio::fs::metadata(&path) .await - .map_err(|e| ActionError::GettingMetadata(path.clone(), e))?; + .map_err(|e| ActionErrorKind::GettingMetadata(path.clone(), e)) + .map_err(Self::error)?; if !metadata.is_dir() { - return Err(ActionError::PathWasNotDirectory(path.to_owned())); + return Err(Self::error(ActionErrorKind::PathWasNotDirectory( + path.to_owned(), + ))); } // Does it have the right user/group? if let Some(user) = &user { // If the file exists, the user must also exist to be correct. let expected_uid = User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or_else(|| ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid; let found_uid = metadata.uid(); if found_uid != expected_uid.as_raw() { - return Err(ActionError::PathUserMismatch( + return Err(Self::error(ActionErrorKind::PathUserMismatch( path.clone(), found_uid, expected_uid.as_raw(), - )); + ))); } } if let Some(group) = &group { // If the file exists, the group must also exist to be correct. let expected_gid = Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or_else(|| ActionError::NoUser(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(group.clone())) + .map_err(Self::error)? .gid; let found_gid = metadata.gid(); if found_gid != expected_gid.as_raw() { - return Err(ActionError::PathGroupMismatch( + return Err(Self::error(ActionErrorKind::PathGroupMismatch( path.clone(), found_gid, expected_gid.as_raw(), - )); + ))); } } @@ -136,8 +143,10 @@ impl Action for CreateDirectory { let gid = if let Some(group) = group { Some( Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or(ActionError::NoGroup(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoGroup(group.clone())) + .map_err(Self::error)? .gid, ) } else { @@ -146,8 +155,10 @@ impl Action for CreateDirectory { let uid = if let Some(user) = user { Some( User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or(ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid, ) } else { @@ -156,13 +167,17 @@ impl Action for CreateDirectory { create_dir(path.clone()) .await - .map_err(|e| ActionError::CreateDirectory(path.clone(), e))?; - chown(path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?; + .map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e)) + .map_err(Self::error)?; + chown(path, uid, gid) + .map_err(|e| ActionErrorKind::Chown(path.clone(), e)) + .map_err(Self::error)?; if let Some(mode) = mode { tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) .await - .map_err(|e| ActionError::SetPermissions(*mode, path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::SetPermissions(*mode, path.to_owned(), e)) + .map_err(Self::error)?; } Ok(()) @@ -202,14 +217,16 @@ impl Action for CreateDirectory { let is_empty = path .read_dir() - .map_err(|e| ActionError::Read(path.clone(), e))? + .map_err(|e| ActionErrorKind::Read(path.clone(), e)) + .map_err(Self::error)? .next() .is_none(); match (is_empty, force_prune_on_revert) { (true, _) | (false, true) => remove_dir_all(path.clone()) .await - .map_err(|e| ActionError::Remove(path.clone(), e))?, + .map_err(|e| ActionErrorKind::Remove(path.clone(), e)) + .map_err(Self::error)?, (false, false) => { tracing::debug!("Not removing `{}`, the folder is not empty", path.display()); }, diff --git a/src/action/base/create_file.rs b/src/action/base/create_file.rs index 9bdec86..31cf16d 100644 --- a/src/action/base/create_file.rs +++ b/src/action/base/create_file.rs @@ -10,7 +10,9 @@ use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, }; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; /** Create a file at the given location with the provided `buf`, optionally with an owning user, group, and mode. @@ -55,15 +57,17 @@ impl CreateFile { // If the path exists, perhaps we can just skip this let mut file = File::open(&this.path) .await - .map_err(|e| ActionError::Open(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::Open(this.path.clone(), e)) + .map_err(Self::error)?; let metadata = file .metadata() .await - .map_err(|e| ActionError::GettingMetadata(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::GettingMetadata(this.path.clone(), e)) + .map_err(Self::error)?; if !metadata.is_file() { - return Err(ActionError::PathWasNotFile(this.path)); + return Err(Self::error(ActionErrorKind::PathWasNotFile(this.path))); } if let Some(mode) = mode { @@ -73,11 +77,11 @@ impl CreateFile { let discovered_mode = discovered_mode & 0o777; if discovered_mode != mode { - return Err(ActionError::PathModeMismatch( + return Err(Self::error(ActionErrorKind::PathModeMismatch( this.path.clone(), discovered_mode, mode, - )); + ))); } } @@ -85,31 +89,35 @@ impl CreateFile { if let Some(user) = &this.user { // If the file exists, the user must also exist to be correct. let expected_uid = User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or_else(|| ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid; let found_uid = metadata.uid(); if found_uid != expected_uid.as_raw() { - return Err(ActionError::PathUserMismatch( + return Err(Self::error(ActionErrorKind::PathUserMismatch( this.path.clone(), found_uid, expected_uid.as_raw(), - )); + ))); } } if let Some(group) = &this.group { // If the file exists, the group must also exist to be correct. let expected_gid = Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or_else(|| ActionError::NoUser(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(group.clone())) + .map_err(Self::error)? .gid; let found_gid = metadata.gid(); if found_gid != expected_gid.as_raw() { - return Err(ActionError::PathGroupMismatch( + return Err(Self::error(ActionErrorKind::PathGroupMismatch( this.path.clone(), found_gid, expected_gid.as_raw(), - )); + ))); } } @@ -117,10 +125,13 @@ impl CreateFile { let mut discovered_buf = String::new(); file.read_to_string(&mut discovered_buf) .await - .map_err(|e| ActionError::Read(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::Read(this.path.clone(), e)) + .map_err(Self::error)?; if discovered_buf != this.buf { - return Err(ActionError::DifferentContent(this.path.clone())); + return Err(Self::error(ActionErrorKind::DifferentContent( + this.path.clone(), + ))); } tracing::debug!("Creating file `{}` already complete", this.path.display()); @@ -190,17 +201,21 @@ impl Action for CreateFile { let mut file = options .open(&path) .await - .map_err(|e| ActionError::Open(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Open(path.to_owned(), e)) + .map_err(Self::error)?; file.write_all(buf.as_bytes()) .await - .map_err(|e| ActionError::Write(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Write(path.to_owned(), e)) + .map_err(Self::error)?; let gid = if let Some(group) = group { Some( Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or(ActionError::NoGroup(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoGroup(group.clone())) + .map_err(Self::error)? .gid, ) } else { @@ -209,14 +224,18 @@ impl Action for CreateFile { let uid = if let Some(user) = user { Some( User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or(ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid, ) } else { None }; - chown(path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?; + chown(path, uid, gid) + .map_err(|e| ActionErrorKind::Chown(path.clone(), e)) + .map_err(Self::error)?; Ok(()) } @@ -250,7 +269,8 @@ impl Action for CreateFile { remove_file(&path) .await - .map_err(|e| ActionError::Remove(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Remove(path.to_owned(), e)) + .map_err(Self::error)?; Ok(()) } @@ -259,7 +279,7 @@ impl Action for CreateFile { #[cfg(test)] mod test { use super::*; - use eyre::eyre; + use color_eyre::eyre::eyre; use tokio::fs::write; #[tokio::test] @@ -346,9 +366,20 @@ mod test { ) .await { - Err(ActionError::DifferentContent(path)) => assert_eq!(path, test_file.as_path()), - _ => return Err(eyre!("Should have returned an ActionError::Exists error")), - } + Err(error) => match error.kind() { + ActionErrorKind::DifferentContent(path) => assert_eq!(path, test_file.as_path()), + _ => { + return Err(eyre!( + "Should have returned an ActionErrorKind::Exists error" + )) + }, + }, + _ => { + return Err(eyre!( + "Should have returned an ActionErrorKind::Exists error" + )) + }, + }; assert!(test_file.exists(), "File should have not been deleted"); @@ -376,14 +407,21 @@ mod test { ) .await { - Err(ActionError::PathModeMismatch(path, got, expected)) => { - assert_eq!(path, test_file.as_path()); - assert_eq!(expected, expected_mode); - assert_eq!(got, initial_mode); + Err(err) => match err.kind() { + ActionErrorKind::PathModeMismatch(path, got, expected) => { + assert_eq!(path, test_file.as_path()); + assert_eq!(*expected, expected_mode); + assert_eq!(*got, initial_mode); + }, + _ => { + return Err(eyre!( + "Should have returned an ActionErrorKind::PathModeMismatch error" + )) + }, }, _ => { return Err(eyre!( - "Should have returned an ActionError::PathModeMismatch error" + "Should have returned an ActionErrorKind::PathModeMismatch error" )) }, } @@ -436,10 +474,17 @@ mod test { ) .await { - Err(ActionError::PathWasNotFile(path)) => assert_eq!(path, temp_dir.path()), + Err(err) => match err.kind() { + ActionErrorKind::PathWasNotFile(path) => assert_eq!(path, temp_dir.path()), + _ => { + return Err(eyre!( + "Should have returned an ActionErrorKind::PathWasNotFile error" + )) + }, + }, _ => { return Err(eyre!( - "Should have returned an ActionError::PathWasNotFile error" + "Should have returned an ActionErrorKind::PathWasNotFile error" )) }, } diff --git a/src/action/base/create_group.rs b/src/action/base/create_group.rs index cc4b941..25971e2 100644 --- a/src/action/base/create_group.rs +++ b/src/action/base/create_group.rs @@ -2,7 +2,7 @@ use nix::unistd::Group; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag}; +use crate::action::{ActionError, ActionErrorKind, ActionTag}; use crate::execute_command; use crate::action::{Action, ActionDescription, StatefulAction}; @@ -25,14 +25,15 @@ impl CreateGroup { }; // Ensure group does not exists if let Some(group) = Group::from_name(name.as_str()) - .map_err(|e| ActionError::GettingGroupId(name.clone(), e))? + .map_err(|e| ActionErrorKind::GettingGroupId(name.clone(), e)) + .map_err(Self::error)? { if group.gid.as_raw() != gid { - return Err(ActionError::GroupGidMismatch( + return Err(Self::error(ActionErrorKind::GroupGidMismatch( name.clone(), group.gid.as_raw(), gid, - )); + ))); } tracing::debug!("Creating group `{}` already complete", this.name); @@ -96,7 +97,8 @@ impl Action for CreateGroup { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, _ => { if which::which("groupadd").is_ok() { @@ -106,7 +108,8 @@ impl Action for CreateGroup { .args(["-g", &gid.to_string(), "--system", name]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("addgroup").is_ok() { execute_command( Command::new("addgroup") @@ -114,9 +117,10 @@ impl Action for CreateGroup { .args(["-g", &gid.to_string(), "--system", name]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingGroupCreationCommand); + return Err(Self::error(ActionErrorKind::MissingGroupCreationCommand)); } }, }; @@ -151,7 +155,8 @@ impl Action for CreateGroup { .args([".", "-delete", &format!("/Groups/{name}")]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; if !output.status.success() {} }, _ => { @@ -162,7 +167,8 @@ impl Action for CreateGroup { .arg(name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("delgroup").is_ok() { execute_command( Command::new("delgroup") @@ -170,9 +176,10 @@ impl Action for CreateGroup { .arg(name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingGroupDeletionCommand); + return Err(Self::error(ActionErrorKind::MissingGroupDeletionCommand)); } }, }; diff --git a/src/action/base/create_or_insert_into_file.rs b/src/action/base/create_or_insert_into_file.rs index 323e71f..3deedd2 100644 --- a/src/action/base/create_or_insert_into_file.rs +++ b/src/action/base/create_or_insert_into_file.rs @@ -1,6 +1,8 @@ use nix::unistd::{chown, Group, User}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; use rand::Rng; use std::{ io::SeekFrom, @@ -61,15 +63,17 @@ impl CreateOrInsertIntoFile { // If the path exists, perhaps we can just skip this let mut file = File::open(&this.path) .await - .map_err(|e| ActionError::Open(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::Open(this.path.clone(), e)) + .map_err(Self::error)?; let metadata = file .metadata() .await - .map_err(|e| ActionError::GettingMetadata(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::GettingMetadata(this.path.clone(), e)) + .map_err(Self::error)?; if !metadata.is_file() { - return Err(ActionError::PathWasNotFile(this.path)); + return Err(Self::error(ActionErrorKind::PathWasNotFile(this.path))); } if let Some(mode) = mode { @@ -92,31 +96,35 @@ impl CreateOrInsertIntoFile { if let Some(user) = &this.user { // If the file exists, the user must also exist to be correct. let expected_uid = User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or_else(|| ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid; let found_uid = metadata.uid(); if found_uid != expected_uid.as_raw() { - return Err(ActionError::PathUserMismatch( + return Err(Self::error(ActionErrorKind::PathUserMismatch( this.path.clone(), found_uid, expected_uid.as_raw(), - )); + ))); } } if let Some(group) = &this.group { // If the file exists, the group must also exist to be correct. let expected_gid = Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or_else(|| ActionError::NoUser(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or_else(|| ActionErrorKind::NoUser(group.clone())) + .map_err(Self::error)? .gid; let found_gid = metadata.gid(); if found_gid != expected_gid.as_raw() { - return Err(ActionError::PathGroupMismatch( + return Err(Self::error(ActionErrorKind::PathGroupMismatch( this.path.clone(), found_gid, expected_gid.as_raw(), - )); + ))); } } @@ -124,7 +132,8 @@ impl CreateOrInsertIntoFile { let mut discovered_buf = String::new(); file.read_to_string(&mut discovered_buf) .await - .map_err(|e| ActionError::Read(this.path.clone(), e))?; + .map_err(|e| ActionErrorKind::Read(this.path.clone(), e)) + .map_err(Self::error)?; if discovered_buf.contains(&this.buf) { tracing::debug!("Inserting into `{}` already complete", this.path.display(),); @@ -185,7 +194,7 @@ impl Action for CreateOrInsertIntoFile { let mut orig_file = match OpenOptions::new().read(true).open(&path).await { Ok(f) => Some(f), Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, - Err(e) => return Err(ActionError::Open(path.to_owned(), e)), + Err(e) => return Err(Self::error(ActionErrorKind::Open(path.to_owned(), e))), }; // Create a temporary file in the same directory as the one @@ -209,39 +218,44 @@ impl Action for CreateOrInsertIntoFile { .open(&temp_file_path) .await .map_err(|e| { - ActionError::Open(temp_file_path.clone(), e) - })?; + ActionErrorKind::Open(temp_file_path.clone(), e) + }).map_err(Self::error)?; if *position == Position::End { if let Some(ref mut orig_file) = orig_file { tokio::io::copy(orig_file, &mut temp_file) .await .map_err(|e| { - ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e) - })?; + ActionErrorKind::Copy(path.to_owned(), temp_file_path.to_owned(), e) + }) + .map_err(Self::error)?; } } temp_file .write_all(buf.as_bytes()) .await - .map_err(|e| ActionError::Write(temp_file_path.clone(), e))?; + .map_err(|e| ActionErrorKind::Write(temp_file_path.clone(), e)) + .map_err(Self::error)?; if *position == Position::Beginning { if let Some(ref mut orig_file) = orig_file { tokio::io::copy(orig_file, &mut temp_file) .await .map_err(|e| { - ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e) - })?; + ActionErrorKind::Copy(path.to_owned(), temp_file_path.to_owned(), e) + }) + .map_err(Self::error)?; } } let gid = if let Some(group) = group { Some( Group::from_name(group.as_str()) - .map_err(|e| ActionError::GettingGroupId(group.clone(), e))? - .ok_or(ActionError::NoGroup(group.clone()))? + .map_err(|e| ActionErrorKind::GettingGroupId(group.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoGroup(group.clone())) + .map_err(Self::error)? .gid, ) } else { @@ -250,8 +264,10 @@ impl Action for CreateOrInsertIntoFile { let uid = if let Some(user) = user { Some( User::from_name(user.as_str()) - .map_err(|e| ActionError::GettingUserId(user.clone(), e))? - .ok_or(ActionError::NoUser(user.clone()))? + .map_err(|e| ActionErrorKind::GettingUserId(user.clone(), e)) + .map_err(Self::error)? + .ok_or(ActionErrorKind::NoUser(user.clone())) + .map_err(Self::error)? .uid, ) } else { @@ -261,17 +277,21 @@ impl Action for CreateOrInsertIntoFile { // Change ownership _before_ applying mode, to ensure that if // a file needs to be setuid it will never be setuid for the // wrong user - chown(&temp_file_path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?; + chown(&temp_file_path, uid, gid) + .map_err(|e| ActionErrorKind::Chown(path.clone(), e)) + .map_err(Self::error)?; if let Some(mode) = mode { tokio::fs::set_permissions(&temp_file_path, PermissionsExt::from_mode(*mode)) .await - .map_err(|e| ActionError::SetPermissions(*mode, path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::SetPermissions(*mode, path.to_owned(), e)) + .map_err(Self::error)?; } else if let Some(original_file) = orig_file { let original_file_mode = original_file .metadata() .await - .map_err(|e| ActionError::GettingMetadata(path.to_path_buf(), e))? + .map_err(|e| ActionErrorKind::GettingMetadata(path.to_path_buf(), e)) + .map_err(Self::error)? .permissions() .mode(); tokio::fs::set_permissions( @@ -279,12 +299,14 @@ impl Action for CreateOrInsertIntoFile { PermissionsExt::from_mode(original_file_mode), ) .await - .map_err(|e| ActionError::SetPermissions(original_file_mode, path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::SetPermissions(original_file_mode, path.to_owned(), e)) + .map_err(Self::error)?; } tokio::fs::rename(&temp_file_path, &path) .await - .map_err(|e| ActionError::Rename(path.to_owned(), temp_file_path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Rename(path.to_owned(), temp_file_path.to_owned(), e)) + .map_err(Self::error)?; Ok(()) } @@ -323,12 +345,14 @@ impl Action for CreateOrInsertIntoFile { .read(true) .open(&path) .await - .map_err(|e| ActionError::Open(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Open(path.to_owned(), e)) + .map_err(Self::error)?; let mut file_contents = String::default(); file.read_to_string(&mut file_contents) .await - .map_err(|e| ActionError::Read(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Read(path.to_owned(), e)) + .map_err(Self::error)?; if let Some(start) = file_contents.rfind(buf.as_str()) { let end = start + buf.len(); @@ -338,20 +362,25 @@ impl Action for CreateOrInsertIntoFile { if file_contents.is_empty() { remove_file(&path) .await - .map_err(|e| ActionError::Remove(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Remove(path.to_owned(), e)) + .map_err(Self::error)?; } else { file.seek(SeekFrom::Start(0)) .await - .map_err(|e| ActionError::Seek(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Seek(path.to_owned(), e)) + .map_err(Self::error)?; file.set_len(0) .await - .map_err(|e| ActionError::Truncate(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Truncate(path.to_owned(), e)) + .map_err(Self::error)?; file.write_all(file_contents.as_bytes()) .await - .map_err(|e| ActionError::Write(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Write(path.to_owned(), e)) + .map_err(Self::error)?; file.flush() .await - .map_err(|e| ActionError::Flush(path.to_owned(), e))?; + .map_err(|e| ActionErrorKind::Flush(path.to_owned(), e)) + .map_err(Self::error)?; } Ok(()) } @@ -360,7 +389,7 @@ impl Action for CreateOrInsertIntoFile { #[cfg(test)] mod test { use super::*; - use eyre::eyre; + use color_eyre::eyre::eyre; use tokio::fs::{read_to_string, write}; #[tokio::test] @@ -534,10 +563,17 @@ mod test { ) .await { - Err(ActionError::PathWasNotFile(path)) => assert_eq!(path, temp_dir.path()), + Err(err) => match err.kind() { + ActionErrorKind::PathWasNotFile(path) => assert_eq!(path, temp_dir.path()), + _ => { + return Err(eyre!( + "Should have returned an ActionErrorKind::PathWasNotFile error" + )) + }, + }, _ => { return Err(eyre!( - "Should have returned an ActionError::PathWasNotFile error" + "Should have returned an ActionErrorKind::PathWasNotFile error" )) }, } diff --git a/src/action/base/create_or_merge_nix_config.rs b/src/action/base/create_or_merge_nix_config.rs index 4f43faa..1729e71 100644 --- a/src/action/base/create_or_merge_nix_config.rs +++ b/src/action/base/create_or_merge_nix_config.rs @@ -11,7 +11,9 @@ use tokio::{ }; use tracing::{span, Span}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; /// The `nix.conf` configuration names that are safe to merge. // FIXME(@cole-h): make configurable by downstream users? @@ -33,6 +35,12 @@ pub enum CreateOrMergeNixConfigError { UnmergeableConfig(Vec, std::path::PathBuf), } +impl Into for CreateOrMergeNixConfigError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} + /// Create or merge an existing `nix.conf` at the specified path. #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateOrMergeNixConfig { @@ -132,10 +140,10 @@ impl CreateOrMergeNixConfig { let path = path.to_path_buf(); let metadata = path .metadata() - .map_err(|e| ActionError::GettingMetadata(path.clone(), e))?; + .map_err(|e| Self::error(ActionErrorKind::GettingMetadata(path.clone(), e)))?; if !metadata.is_file() { - return Err(ActionError::PathWasNotFile(path)); + return Err(Self::error(ActionErrorKind::PathWasNotFile(path))); } // Does the file have the right permissions? @@ -144,23 +152,23 @@ impl CreateOrMergeNixConfig { let discovered_mode = discovered_mode & 0o777; if discovered_mode != NIX_CONF_MODE { - return Err(ActionError::PathModeMismatch( + return Err(Self::error(ActionErrorKind::PathModeMismatch( path, discovered_mode, NIX_CONF_MODE, - )); + ))); } let existing_nix_config = NixConfig::parse_file(&path) .map_err(CreateOrMergeNixConfigError::ParseNixConfig) - .map_err(|e| ActionError::Custom(Box::new(e)))?; + .map_err(Self::error)?; let (merged_nix_config, existing_nix_config) = Self::merge_pending_and_existing_nix_config( &pending_nix_config, &existing_nix_config, &path, ) - .map_err(|e| ActionError::Custom(Box::new(e)))?; + .map_err(Self::error)?; Ok((merged_nix_config, existing_nix_config)) } @@ -260,7 +268,7 @@ impl Action for CreateOrMergeNixConfig { .open(&temp_file_path) .await .map_err(|e| { - ActionError::Open(temp_file_path.clone(), e) + Self::error(ActionErrorKind::Open(temp_file_path.clone(), e)) })?; let (mut merged_nix_config, mut existing_nix_config) = if path.exists() { @@ -276,7 +284,7 @@ impl Action for CreateOrMergeNixConfig { if let Some(existing_nix_config) = existing_nix_config.as_mut() { let mut discovered_buf = tokio::fs::read_to_string(&path) .await - .map_err(|e| ActionError::Read(path.to_path_buf(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Read(path.to_path_buf(), e)))?; // We append a newline to ensure that, in the case there are comments at the end of the // file and _NO_ trailing newline, we still preserve the entire block of comments. @@ -416,13 +424,25 @@ impl Action for CreateOrMergeNixConfig { temp_file .write_all(new_config.as_bytes()) .await - .map_err(|e| ActionError::Write(temp_file_path.clone(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Write(temp_file_path.clone(), e)))?; tokio::fs::set_permissions(&temp_file_path, PermissionsExt::from_mode(NIX_CONF_MODE)) .await - .map_err(|e| ActionError::SetPermissions(NIX_CONF_MODE, path.to_owned(), e))?; + .map_err(|e| { + Self::error(ActionErrorKind::SetPermissions( + NIX_CONF_MODE, + path.to_owned(), + e, + )) + })?; tokio::fs::rename(&temp_file_path, &path) .await - .map_err(|e| ActionError::Rename(temp_file_path.to_owned(), path.to_owned(), e))?; + .map_err(|e| { + Self::error(ActionErrorKind::Rename( + temp_file_path.to_owned(), + path.to_owned(), + e, + )) + })?; Ok(()) } @@ -448,7 +468,7 @@ impl Action for CreateOrMergeNixConfig { remove_file(&path) .await - .map_err(|e| ActionError::Remove(path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Remove(path.to_owned(), e)))?; Ok(()) } @@ -457,7 +477,7 @@ impl Action for CreateOrMergeNixConfig { #[cfg(test)] mod test { use super::*; - use eyre::eyre; + use color_eyre::eyre::eyre; use tokio::fs::write; #[tokio::test] @@ -600,15 +620,20 @@ mod test { .settings_mut() .insert("warn-dirty".into(), "false".into()); match CreateOrMergeNixConfig::plan(&test_file, nix_config).await { - Err(ActionError::Custom(e)) => match e.downcast_ref::() { - Some(CreateOrMergeNixConfigError::UnmergeableConfig(_, path)) => { - assert_eq!(path, test_file.as_path()) - }, - _ => { - return Err(eyre!( - "Should have returned CreateOrMergeNixConfigError::UnmergeableConfig" - )) + Err(err) => match err.kind() { + ActionErrorKind::Custom(e) => { + match e.downcast_ref::() { + Some(CreateOrMergeNixConfigError::UnmergeableConfig(_, path)) => { + assert_eq!(path, test_file.as_path()) + }, + _ => { + return Err(eyre!( + "Should have returned CreateOrMergeNixConfigError::UnmergeableConfig" + )) + }, + } }, + _ => (), }, _ => { return Err(eyre!( diff --git a/src/action/base/create_user.rs b/src/action/base/create_user.rs index 7407e62..548b4e6 100644 --- a/src/action/base/create_user.rs +++ b/src/action/base/create_user.rs @@ -2,7 +2,7 @@ use nix::unistd::User; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag}; +use crate::action::{ActionError, ActionErrorKind, ActionTag}; use crate::execute_command; use crate::action::{Action, ActionDescription, StatefulAction}; @@ -37,22 +37,23 @@ impl CreateUser { }; // Ensure user does not exists if let Some(user) = User::from_name(name.as_str()) - .map_err(|e| ActionError::GettingUserId(name.clone(), e))? + .map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e)) + .map_err(Self::error)? { if user.uid.as_raw() != uid { - return Err(ActionError::UserUidMismatch( + return Err(Self::error(ActionErrorKind::UserUidMismatch( name.clone(), user.uid.as_raw(), uid, - )); + ))); } if user.gid.as_raw() != gid { - return Err(ActionError::UserGidMismatch( + return Err(Self::error(ActionErrorKind::UserGidMismatch( name.clone(), user.gid.as_raw(), gid, - )); + ))); } tracing::debug!("Creating user `{}` already complete", this.name); @@ -120,7 +121,8 @@ impl Action for CreateUser { .args([".", "-create", &format!("/Users/{name}")]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/bin/dscl") .process_group(0) @@ -133,7 +135,8 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/bin/dscl") .process_group(0) @@ -146,7 +149,8 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/bin/dscl") .process_group(0) @@ -159,7 +163,8 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/bin/dscl") .process_group(0) @@ -172,14 +177,16 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/bin/dscl") .process_group(0) .args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, _ => { if which::which("useradd").is_ok() { @@ -207,7 +214,8 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("adduser").is_ok() { execute_command( Command::new("adduser") @@ -229,9 +237,10 @@ impl Action for CreateUser { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingUserCreationCommand); + return Err(Self::error(ActionErrorKind::MissingUserCreationCommand)); } }, } @@ -253,14 +262,6 @@ impl Action for CreateUser { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { - name, - uid: _, - groupname: _, - gid: _, - comment: _, - } = self; - use target_lexicon::OperatingSystem; match target_lexicon::OperatingSystem::host() { OperatingSystem::MacOSX { @@ -274,25 +275,28 @@ impl Action for CreateUser { // Documentation on https://it.megocollector.com/macos/cant-delete-a-macos-user-with-dscl-resolution/ and http://www.aixperts.co.uk/?p=214 suggested it was a secure token // That is correct, however it's a bit more nuanced. It appears to be that a user must be graphically logged in for some other user on the system to be deleted. let mut command = Command::new("/usr/bin/dscl"); - command.args([".", "-delete", &format!("/Users/{name}")]); + command.args([".", "-delete", &format!("/Users/{}", self.name)]); command.process_group(0); command.stdin(std::process::Stdio::null()); let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; let stderr = String::from_utf8_lossy(&output.stderr); match output.status.code() { Some(0) => (), Some(40) if stderr.contains("-14120") => { // The user is on an ephemeral Mac, like detsys uses // These Macs cannot always delete users, as sometimes there is no graphical login - tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{name}"); + tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name); }, _ => { // Something went wrong - return Err(ActionError::command_output(&command, output)); + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); }, } }, @@ -301,20 +305,22 @@ impl Action for CreateUser { execute_command( Command::new("userdel") .process_group(0) - .arg(name) + .arg(&self.name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else if which::which("deluser").is_ok() { execute_command( Command::new("deluser") .process_group(0) - .arg(name) + .arg(&self.name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { - return Err(ActionError::MissingUserDeletionCommand); + return Err(Self::error(ActionErrorKind::MissingUserDeletionCommand)); } }, }; diff --git a/src/action/base/fetch_and_unpack_nix.rs b/src/action/base/fetch_and_unpack_nix.rs index c2b6bb0..bc5ff70 100644 --- a/src/action/base/fetch_and_unpack_nix.rs +++ b/src/action/base/fetch_and_unpack_nix.rs @@ -5,7 +5,7 @@ use reqwest::Url; use tracing::{span, Span}; use crate::{ - action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}, + action::{Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction}, parse_ssl_cert, }; @@ -33,26 +33,18 @@ impl FetchAndUnpackNix { match url.scheme() { "https" | "http" | "file" => (), - _ => { - return Err(ActionError::Custom(Box::new( - FetchUrlError::UnknownUrlScheme, - ))) - }, + _ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)), }; if let Some(proxy) = &proxy { match proxy.scheme() { "https" | "http" | "socks5" => (), - _ => { - return Err(ActionError::Custom(Box::new( - FetchUrlError::UnknownProxyScheme, - ))) - }, + _ => return Err(Self::error(FetchUrlError::UnknownProxyScheme)), }; } if let Some(ssl_cert_file) = &ssl_cert_file { - parse_ssl_cert(&ssl_cert_file).await?; + parse_ssl_cert(&ssl_cert_file).await.map_err(Self::error)?; } Ok(Self { @@ -106,41 +98,43 @@ impl Action for FetchAndUnpackNix { "https" | "http" => { let mut buildable_client = reqwest::Client::builder(); if let Some(proxy) = &self.proxy { - buildable_client = - buildable_client.proxy(reqwest::Proxy::all(proxy.clone()).map_err(|e| { - ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))) - })?) + buildable_client = buildable_client.proxy( + reqwest::Proxy::all(proxy.clone()) + .map_err(FetchUrlError::Reqwest) + .map_err(Self::error)?, + ) } if let Some(ssl_cert_file) = &self.ssl_cert_file { - let ssl_cert = parse_ssl_cert(&ssl_cert_file).await?; + let ssl_cert = parse_ssl_cert(&ssl_cert_file).await.map_err(Self::error)?; buildable_client = buildable_client.add_root_certificate(ssl_cert); } let client = buildable_client .build() - .map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; + .map_err(FetchUrlError::Reqwest) + .map_err(Self::error)?; let req = client .get(self.url.clone()) .build() - .map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; + .map_err(FetchUrlError::Reqwest) + .map_err(Self::error)?; let res = client .execute(req) .await - .map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))?; + .map_err(FetchUrlError::Reqwest) + .map_err(Self::error)?; res.bytes() .await - .map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Reqwest(e))))? + .map_err(FetchUrlError::Reqwest) + .map_err(Self::error)? }, "file" => { let buf = tokio::fs::read(self.url.path()) .await - .map_err(|e| ActionError::Read(PathBuf::from(self.url.path()), e))?; + .map_err(|e| ActionErrorKind::Read(PathBuf::from(self.url.path()), e)) + .map_err(Self::error)?; Bytes::from(buf) }, - _ => { - return Err(ActionError::Custom(Box::new( - FetchUrlError::UnknownUrlScheme, - ))) - }, + _ => return Err(Self::error(FetchUrlError::UnknownUrlScheme)), }; // TODO(@Hoverbear): Pick directory @@ -151,7 +145,8 @@ impl Action for FetchAndUnpackNix { let mut archive = tar::Archive::new(decoder); archive .unpack(&dest_clone) - .map_err(|e| ActionError::Custom(Box::new(FetchUrlError::Unarchive(e))))?; + .map_err(FetchUrlError::Unarchive) + .map_err(Self::error)?; Ok(()) } @@ -182,3 +177,9 @@ pub enum FetchUrlError { #[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")] UnknownProxyScheme, } + +impl Into for FetchUrlError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/base/move_unpacked_nix.rs b/src/action/base/move_unpacked_nix.rs index 1ca3b75..3bdf719 100644 --- a/src/action/base/move_unpacked_nix.rs +++ b/src/action/base/move_unpacked_nix.rs @@ -2,7 +2,9 @@ use std::path::{Path, PathBuf}; use tracing::{span, Span}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; pub(crate) const DEST: &str = "/nix/"; @@ -57,32 +59,37 @@ impl Action for MoveUnpackedNix { // This is the `nix-$VERSION` folder which unpacks from the tarball, not a nix derivation let found_nix_paths = glob::glob(&format!("{}/nix-*", unpacked_path.display())) - .map_err(|e| ActionError::Custom(Box::new(e)))? + .map_err(|e| Self::error(MoveUnpackedNixError::from(e)))? .collect::, _>>() - .map_err(|e| ActionError::Custom(Box::new(e)))?; + .map_err(|e| Self::error(MoveUnpackedNixError::from(e)))?; if found_nix_paths.len() != 1 { - return Err(ActionError::MalformedBinaryTarball); + return Err(Self::error(ActionErrorKind::MalformedBinaryTarball)); } let found_nix_path = found_nix_paths.into_iter().next().unwrap(); let src_store = found_nix_path.join("store"); let mut src_store_listing = tokio::fs::read_dir(src_store.clone()) .await - .map_err(|e| ActionError::ReadDir(src_store.clone(), e))?; + .map_err(|e| ActionErrorKind::ReadDir(src_store.clone(), e)) + .map_err(Self::error)?; let dest_store = Path::new(DEST).join("store"); if dest_store.exists() { if !dest_store.is_dir() { - return Err(ActionError::PathWasNotDirectory(dest_store.clone()))?; + return Err(Self::error(ActionErrorKind::PathWasNotDirectory( + dest_store.clone(), + )))?; } } else { tokio::fs::create_dir(&dest_store) .await - .map_err(|e| ActionError::CreateDirectory(dest_store.clone(), e))?; + .map_err(|e| ActionErrorKind::CreateDirectory(dest_store.clone(), e)) + .map_err(Self::error)?; } while let Some(entry) = src_store_listing .next_entry() .await - .map_err(|e| ActionError::ReadDir(src_store.clone(), e))? + .map_err(|e| ActionErrorKind::ReadDir(src_store.clone(), e)) + .map_err(Self::error)? { let entry_dest = dest_store.join(entry.file_name()); if entry_dest.exists() { @@ -92,8 +99,9 @@ impl Action for MoveUnpackedNix { tokio::fs::rename(&entry.path(), &entry_dest) .await .map_err(|e| { - ActionError::Rename(entry.path().clone(), entry_dest.to_owned(), e) - })?; + ActionErrorKind::Rename(entry.path().clone(), entry_dest.to_owned(), e) + }) + .map_err(Self::error)?; } } @@ -127,3 +135,9 @@ pub enum MoveUnpackedNixError { glob::GlobError, ), } + +impl Into for MoveUnpackedNixError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/base/remove_directory.rs b/src/action/base/remove_directory.rs index 57ca1a6..382aaf1 100644 --- a/src/action/base/remove_directory.rs +++ b/src/action/base/remove_directory.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use tokio::fs::remove_dir_all; use tracing::{span, Span}; -use crate::action::{Action, ActionDescription, ActionState}; +use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState}; use crate::action::{ActionError, StatefulAction}; /** Remove a directory, does nothing on revert. @@ -53,11 +53,13 @@ impl Action for RemoveDirectory { async fn execute(&mut self) -> Result<(), ActionError> { if self.path.exists() { if !self.path.is_dir() { - return Err(ActionError::PathWasNotDirectory(self.path.clone())); + return Err(Self::error(ActionErrorKind::PathWasNotDirectory( + self.path.clone(), + ))); } remove_dir_all(&self.path) .await - .map_err(|e| ActionError::Remove(self.path.clone(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Remove(self.path.clone(), e)))?; } else { tracing::debug!("Directory `{}` not present, skipping", self.path.display(),); }; diff --git a/src/action/base/setup_default_profile.rs b/src/action/base/setup_default_profile.rs index c10e064..62febef 100644 --- a/src/action/base/setup_default_profile.rs +++ b/src/action/base/setup_default_profile.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use crate::{ - action::{ActionError, ActionTag, StatefulAction}, + action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}, execute_command, set_env, }; @@ -50,9 +50,9 @@ impl Action for SetupDefaultProfile { // Find an `nix` package let nix_pkg_glob = "/nix/store/*-nix-*"; let mut found_nix_pkg = None; - for entry in glob(nix_pkg_glob).map_err(|e| { - ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e))) - })? { + for entry in glob(nix_pkg_glob) + .map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))? + { match entry { Ok(path) => { // TODO(@Hoverbear): Should probably ensure is unique @@ -65,17 +65,15 @@ impl Action for SetupDefaultProfile { let nix_pkg = if let Some(nix_pkg) = found_nix_pkg { nix_pkg } else { - return Err(ActionError::Custom(Box::new( - SetupDefaultProfileError::NoNix, - ))); + return Err(Self::error(SetupDefaultProfileError::NoNix)); }; // Find an `nss-cacert` package, add it too. let nss_ca_cert_pkg_glob = "/nix/store/*-nss-cacert-*"; let mut found_nss_ca_cert_pkg = None; - for entry in glob(nss_ca_cert_pkg_glob).map_err(|e| { - ActionError::Custom(Box::new(SetupDefaultProfileError::GlobPatternError(e))) - })? { + for entry in glob(nss_ca_cert_pkg_glob) + .map_err(|e| Self::error(SetupDefaultProfileError::GlobPatternError(e)))? + { match entry { Ok(path) => { // TODO(@Hoverbear): Should probably ensure is unique @@ -88,23 +86,22 @@ impl Action for SetupDefaultProfile { let nss_ca_cert_pkg = if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg { nss_ca_cert_pkg } else { - return Err(ActionError::Custom(Box::new( - SetupDefaultProfileError::NoNssCacert, - ))); + return Err(Self::error(SetupDefaultProfileError::NoNssCacert)); }; let found_nix_paths = glob::glob(&format!("{}/nix-*", self.unpacked_path.display())) - .map_err(|e| ActionError::Custom(Box::new(e)))? + .map_err(|e| Self::error(SetupDefaultProfileError::from(e)))? .collect::, _>>() - .map_err(|e| ActionError::Custom(Box::new(e)))?; + .map_err(|e| Self::error(SetupDefaultProfileError::from(e)))?; if found_nix_paths.len() != 1 { - return Err(ActionError::MalformedBinaryTarball); + return Err(Self::error(ActionErrorKind::MalformedBinaryTarball)); } let found_nix_path = found_nix_paths.into_iter().next().unwrap(); let reginfo_path = PathBuf::from(found_nix_path).join(".reginfo"); let reginfo = tokio::fs::read(®info_path) .await - .map_err(|e| ActionError::Read(reginfo_path.to_path_buf(), e))?; + .map_err(|e| ActionErrorKind::Read(reginfo_path.to_path_buf(), e)) + .map_err(Self::error)?; let mut load_db_command = Command::new(nix_pkg.join("bin/nix-store")); load_db_command.process_group(0); load_db_command.arg("--load-db"); @@ -113,9 +110,7 @@ impl Action for SetupDefaultProfile { load_db_command.stderr(std::process::Stdio::piped()); load_db_command.env( "HOME", - dirs::home_dir().ok_or_else(|| { - ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) - })?, + dirs::home_dir().ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?, ); tracing::trace!( "Executing `{:?}` with stdin from `{}`", @@ -124,17 +119,20 @@ impl Action for SetupDefaultProfile { ); let mut handle = load_db_command .spawn() - .map_err(|e| ActionError::command(&load_db_command, e))?; + .map_err(|e| ActionErrorKind::command(&load_db_command, e)) + .map_err(Self::error)?; let mut stdin = handle.stdin.take().unwrap(); stdin .write_all(®info) .await - .map_err(|e| ActionError::Write(PathBuf::from("/dev/stdin"), e))?; + .map_err(|e| ActionErrorKind::Write(PathBuf::from("/dev/stdin"), e)) + .map_err(Self::error)?; stdin .flush() .await - .map_err(|e| ActionError::Write(PathBuf::from("/dev/stdin"), e))?; + .map_err(|e| ActionErrorKind::Write(PathBuf::from("/dev/stdin"), e)) + .map_err(Self::error)?; drop(stdin); tracing::trace!( "Wrote `{}` to stdin of `nix-store --load-db`", @@ -144,9 +142,13 @@ impl Action for SetupDefaultProfile { let output = handle .wait_with_output() .await - .map_err(|e| ActionError::command(&load_db_command, e))?; + .map_err(|e| ActionErrorKind::command(&load_db_command, e)) + .map_err(Self::error)?; if !output.status.success() { - return Err(ActionError::command_output(&load_db_command, output)); + return Err(Self::error(ActionErrorKind::command_output( + &load_db_command, + output, + ))); }; // Install `nix` itself into the store @@ -158,16 +160,16 @@ impl Action for SetupDefaultProfile { .stdin(std::process::Stdio::null()) .env( "HOME", - dirs::home_dir().ok_or_else(|| { - ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) - })?, + dirs::home_dir() + .ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?, ) .env( "NIX_SSL_CERT_FILE", nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), ), /* This is apparently load bearing... */ ) - .await?; + .await + .map_err(Self::error)?; // Install `nix` itself into the store execute_command( @@ -178,16 +180,16 @@ impl Action for SetupDefaultProfile { .stdin(std::process::Stdio::null()) .env( "HOME", - dirs::home_dir().ok_or_else(|| { - ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) - })?, + dirs::home_dir() + .ok_or_else(|| Self::error(SetupDefaultProfileError::NoRootHome))?, ) .env( "NIX_SSL_CERT_FILE", nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), ), /* This is apparently load bearing... */ ) - .await?; + .await + .map_err(Self::error)?; set_env( "NIX_SSL_CERT_FILE", @@ -234,3 +236,9 @@ pub enum SetupDefaultProfileError { #[error("No root home found to place channel configuration in")] NoRootHome, } + +impl Into for SetupDefaultProfileError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/common/configure_init_service.rs b/src/action/common/configure_init_service.rs index 551c19a..a6dcdd9 100644 --- a/src/action/common/configure_init_service.rs +++ b/src/action/common/configure_init_service.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag, StatefulAction}; +use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}; use crate::execute_command; use crate::action::{Action, ActionDescription}; @@ -39,7 +39,7 @@ pub struct ConfigureInitService { impl ConfigureInitService { #[cfg(target_os = "linux")] - async fn check_if_systemd_unit_exists(src: &str, dest: &str) -> Result<(), ActionError> { + async fn check_if_systemd_unit_exists(src: &str, dest: &str) -> Result<(), ActionErrorKind> { // TODO: once we have a way to communicate interaction between the library and the cli, // interactively ask for permission to remove the file @@ -50,21 +50,24 @@ impl ConfigureInitService { if unit_dest.is_symlink() { let link_dest = tokio::fs::read_link(&unit_dest) .await - .map_err(|e| ActionError::ReadSymlink(unit_dest.clone(), e))?; + .map_err(|e| ActionErrorKind::ReadSymlink(unit_dest.clone(), e))?; if link_dest != unit_src { - return Err(ActionError::SymlinkExists(unit_dest)); + return Err(ActionErrorKind::SymlinkExists(unit_dest)); } } else { - return Err(ActionError::FileExists(unit_dest)); + return Err(ActionErrorKind::FileExists(unit_dest)); } } // NOTE: ...and if there are any overrides in the most well-known places for systemd if Path::new(&format!("{dest}.d")).exists() { - return Err(ActionError::DirExists(PathBuf::from(format!("{dest}.d")))); + return Err(ActionErrorKind::DirExists(PathBuf::from(format!( + "{dest}.d" + )))); } Ok(()) } + #[tracing::instrument(level = "debug", skip_all)] pub async fn plan( init: InitSystem, @@ -75,7 +78,7 @@ impl ConfigureInitService { Some( ssl_cert_file .canonicalize() - .map_err(|e| ActionError::Canonicalize(ssl_cert_file, e))?, + .map_err(|e| Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)))?, ) } else { None @@ -92,11 +95,15 @@ impl ConfigureInitService { // with systemd: https://www.freedesktop.org/software/systemd/man/sd_booted.html if !(Path::new("/run/systemd/system").exists() || which::which("systemctl").is_ok()) { - return Err(ActionError::SystemdMissing); + return Err(Self::error(ActionErrorKind::SystemdMissing)); } - Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST).await?; - Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST).await?; + Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST) + .await + .map_err(Self::error)?; + Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST) + .await + .map_err(Self::error)?; }, #[cfg(target_os = "linux")] InitSystem::None => { @@ -183,11 +190,11 @@ impl Action for ConfigureInitService { tokio::fs::copy(src.clone(), DARWIN_NIX_DAEMON_DEST) .await .map_err(|e| { - ActionError::Copy( + Self::error(ActionErrorKind::Copy( src.to_path_buf(), PathBuf::from(DARWIN_NIX_DAEMON_DEST), e, - ) + )) })?; execute_command( @@ -197,7 +204,8 @@ impl Action for ConfigureInitService { .arg(DARWIN_NIX_DAEMON_DEST) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; if let Some(ssl_cert_file) = ssl_cert_file { execute_command( @@ -208,7 +216,8 @@ impl Action for ConfigureInitService { .arg(format!("{ssl_cert_file:?}")) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } if *start_daemon { @@ -220,7 +229,8 @@ impl Action for ConfigureInitService { .arg("system/org.nixos.nix-daemon") .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } }, #[cfg(target_os = "linux")] @@ -232,23 +242,32 @@ impl Action for ConfigureInitService { .arg("daemon-reload") .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } // The goal state is the `socket` enabled and active, the service not enabled and stopped (it activates via socket activation) - let socket_was_active = if is_enabled("nix-daemon.socket").await? { - disable("nix-daemon.socket", true).await?; - true - } else if is_active("nix-daemon.socket").await? { - stop("nix-daemon.socket").await?; - true - } else { - false - }; - if is_enabled("nix-daemon.service").await? { - let now = is_active("nix-daemon.service").await?; - disable("nix-daemon.service", now).await?; - } else if is_active("nix-daemon.service").await? { - stop("nix-daemon.service").await?; + let socket_was_active = + if is_enabled("nix-daemon.socket").await.map_err(Self::error)? { + disable("nix-daemon.socket", true) + .await + .map_err(Self::error)?; + true + } else if is_active("nix-daemon.socket").await.map_err(Self::error)? { + stop("nix-daemon.socket").await.map_err(Self::error)?; + true + } else { + false + }; + if is_enabled("nix-daemon.service") + .await + .map_err(Self::error)? + { + let now = is_active("nix-daemon.service").await.map_err(Self::error)?; + disable("nix-daemon.service", now) + .await + .map_err(Self::error)?; + } else if is_active("nix-daemon.service").await.map_err(Self::error)? { + stop("nix-daemon.service").await.map_err(Self::error)?; }; tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking"); @@ -256,12 +275,13 @@ impl Action for ConfigureInitService { tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST) .await .map_err(|e| { - ActionError::Symlink( + ActionErrorKind::Symlink( PathBuf::from(TMPFILES_SRC), PathBuf::from(TMPFILES_DEST), e, ) - })?; + }) + .map_err(Self::error)?; } execute_command( @@ -271,32 +291,39 @@ impl Action for ConfigureInitService { .arg("--prefix=/nix/var/nix") .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; // TODO: once we have a way to communicate interaction between the library and the // cli, interactively ask for permission to remove the file - Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST).await?; + Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST) + .await + .map_err(Self::error)?; tokio::fs::symlink(SERVICE_SRC, SERVICE_DEST) .await .map_err(|e| { - ActionError::Symlink( + ActionErrorKind::Symlink( PathBuf::from(SERVICE_SRC), PathBuf::from(SERVICE_DEST), e, ) - })?; + }) + .map_err(Self::error)?; - Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST).await?; + Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST) + .await + .map_err(Self::error)?; tokio::fs::symlink(SOCKET_SRC, SOCKET_DEST) .await .map_err(|e| { - ActionError::Symlink( + ActionErrorKind::Symlink( PathBuf::from(SOCKET_SRC), PathBuf::from(SOCKET_DEST), e, ) - })?; + }) + .map_err(Self::error)?; if *start_daemon { execute_command( @@ -305,7 +332,8 @@ impl Action for ConfigureInitService { .arg("daemon-reload") .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } if let Some(ssl_cert_file) = ssl_cert_file { @@ -313,8 +341,9 @@ impl Action for ConfigureInitService { tokio::fs::create_dir(&service_conf_dir_path) .await .map_err(|e| { - ActionError::CreateDirectory(service_conf_dir_path.clone(), e) - })?; + ActionErrorKind::CreateDirectory(service_conf_dir_path.clone(), e) + }) + .map_err(Self::error)?; let service_conf_file_path = service_conf_dir_path.join("nix-ssl-cert-file.conf"); tokio::fs::write( @@ -327,13 +356,14 @@ impl Action for ConfigureInitService { ), ) .await - .map_err(|e| ActionError::Write(ssl_cert_file.clone(), e))?; + .map_err(|e| ActionErrorKind::Write(ssl_cert_file.clone(), e)) + .map_err(Self::error)?; } if *start_daemon || socket_was_active { - enable(SOCKET_SRC, true).await?; + enable(SOCKET_SRC, true).await.map_err(Self::error)?; } else { - enable(SOCKET_SRC, false).await?; + enable(SOCKET_SRC, false).await.map_err(Self::error)?; } }, #[cfg(not(target_os = "macos"))] @@ -373,6 +403,8 @@ impl Action for ConfigureInitService { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { + let mut errors = vec![]; + match self.init { #[cfg(target_os = "macos")] InitSystem::Launchd => { @@ -382,84 +414,118 @@ impl Action for ConfigureInitService { .arg("unload") .arg(DARWIN_NIX_DAEMON_DEST), ) - .await?; + .await + .map_err(|e| Self::error(e))?; }, #[cfg(target_os = "linux")] InitSystem::Systemd => { // We separate stop and disable (instead of using `--now`) to avoid cases where the service isn't started, but is enabled. - let socket_is_active = is_active("nix-daemon.socket").await?; - let socket_is_enabled = is_enabled("nix-daemon.socket").await?; - let service_is_active = is_active("nix-daemon.service").await?; - let service_is_enabled = is_enabled("nix-daemon.service").await?; + // These have to fail fast. + let socket_is_active = is_active("nix-daemon.socket") + .await + .map_err(|e| Self::error(e))?; + let socket_is_enabled = is_enabled("nix-daemon.socket") + .await + .map_err(|e| Self::error(e))?; + let service_is_active = is_active("nix-daemon.service") + .await + .map_err(|e| Self::error(e))?; + let service_is_enabled = is_enabled("nix-daemon.service") + .await + .map_err(|e| Self::error(e))?; if socket_is_active { - execute_command( + if let Err(err) = execute_command( Command::new("systemctl") .process_group(0) .args(["stop", "nix-daemon.socket"]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } } if socket_is_enabled { - execute_command( + if let Err(err) = execute_command( Command::new("systemctl") .process_group(0) .args(["disable", "nix-daemon.socket"]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } } if service_is_active { - execute_command( + if let Err(err) = execute_command( Command::new("systemctl") .process_group(0) .args(["stop", "nix-daemon.service"]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } } if service_is_enabled { - execute_command( + if let Err(err) = execute_command( Command::new("systemctl") .process_group(0) .args(["disable", "nix-daemon.service"]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } } - execute_command( + if let Err(err) = execute_command( Command::new("systemd-tmpfiles") .process_group(0) .arg("--remove") .arg("--prefix=/nix/var/nix") .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } if self.ssl_cert_file.is_some() { let service_conf_dir_path = PathBuf::from(format!("{SERVICE_DEST}.d")); - tokio::fs::remove_dir_all(&service_conf_dir_path) + if let Err(err) = tokio::fs::remove_dir_all(&service_conf_dir_path) .await - .map_err(|e| ActionError::Remove(service_conf_dir_path.clone(), e))?; + .map_err(|e| ActionErrorKind::Remove(service_conf_dir_path.clone(), e)) + { + errors.push(err); + } } - tokio::fs::remove_file(TMPFILES_DEST) + if let Err(err) = tokio::fs::remove_file(TMPFILES_DEST) .await - .map_err(|e| ActionError::Remove(PathBuf::from(TMPFILES_DEST), e))?; + .map_err(|e| ActionErrorKind::Remove(PathBuf::from(TMPFILES_DEST), e)) + { + errors.push(err); + } - execute_command( + if let Err(err) = execute_command( Command::new("systemctl") .process_group(0) .arg("daemon-reload") .stdin(std::process::Stdio::null()), ) - .await?; + .await + { + errors.push(err); + } }, #[cfg(not(target_os = "macos"))] InitSystem::None => { @@ -467,7 +533,18 @@ impl Action for ConfigureInitService { }, }; - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(Self::error( + errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item"), + )) + } else { + Err(Self::error(ActionErrorKind::Multiple(errors))) + } } } @@ -479,25 +556,25 @@ pub enum ConfigureNixDaemonServiceError { } #[cfg(target_os = "linux")] -async fn stop(unit: &str) -> Result<(), ActionError> { +async fn stop(unit: &str) -> Result<(), ActionErrorKind> { let mut command = Command::new("systemctl"); command.arg("stop"); command.arg(unit); let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e))?; match output.status.success() { true => { tracing::trace!(%unit, "Stopped"); Ok(()) }, - false => Err(ActionError::command_output(&command, output)), + false => Err(ActionErrorKind::command_output(&command, output)), } } #[cfg(target_os = "linux")] -async fn enable(unit: &str, now: bool) -> Result<(), ActionError> { +async fn enable(unit: &str, now: bool) -> Result<(), ActionErrorKind> { let mut command = Command::new("systemctl"); command.arg("enable"); command.arg(unit); @@ -507,18 +584,18 @@ async fn enable(unit: &str, now: bool) -> Result<(), ActionError> { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e))?; match output.status.success() { true => { tracing::trace!(%unit, %now, "Enabled unit"); Ok(()) }, - false => Err(ActionError::command_output(&command, output)), + false => Err(ActionErrorKind::command_output(&command, output)), } } #[cfg(target_os = "linux")] -async fn disable(unit: &str, now: bool) -> Result<(), ActionError> { +async fn disable(unit: &str, now: bool) -> Result<(), ActionErrorKind> { let mut command = Command::new("systemctl"); command.arg("disable"); command.arg(unit); @@ -528,25 +605,25 @@ async fn disable(unit: &str, now: bool) -> Result<(), ActionError> { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e))?; match output.status.success() { true => { tracing::trace!(%unit, %now, "Disabled unit"); Ok(()) }, - false => Err(ActionError::command_output(&command, output)), + false => Err(ActionErrorKind::command_output(&command, output)), } } #[cfg(target_os = "linux")] -async fn is_active(unit: &str) -> Result { +async fn is_active(unit: &str) -> Result { let mut command = Command::new("systemctl"); command.arg("is-active"); command.arg(unit); let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e))?; if String::from_utf8(output.stdout)?.starts_with("active") { tracing::trace!(%unit, "Is active"); Ok(true) @@ -557,14 +634,14 @@ async fn is_active(unit: &str) -> Result { } #[cfg(target_os = "linux")] -async fn is_enabled(unit: &str) -> Result { +async fn is_enabled(unit: &str) -> Result { let mut command = Command::new("systemctl"); command.arg("is-enabled"); command.arg(unit); let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e))?; let stdout = String::from_utf8(output.stdout)?; if stdout.starts_with("enabled") || stdout.starts_with("linked") { tracing::trace!(%unit, "Is enabled"); diff --git a/src/action/common/configure_nix.rs b/src/action/common/configure_nix.rs index b8a571d..e427dfe 100644 --- a/src/action/common/configure_nix.rs +++ b/src/action/common/configure_nix.rs @@ -4,7 +4,7 @@ use crate::{ action::{ base::SetupDefaultProfile, common::{ConfigureShellProfile, PlaceNixConfiguration}, - Action, ActionDescription, ActionError, ActionTag, StatefulAction, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, }, planner::ShellProfileLocations, settings::{CommonSettings, SCRATCH_DIR}, @@ -30,7 +30,7 @@ impl ConfigureNix { ) -> Result, ActionError> { let setup_default_profile = SetupDefaultProfile::plan(PathBuf::from(SCRATCH_DIR)) .await - .map_err(|e| ActionError::Child(SetupDefaultProfile::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let configure_shell_profile = if settings.modify_profile { Some( @@ -39,9 +39,7 @@ impl ConfigureNix { settings.ssl_cert_file.clone(), ) .await - .map_err(|e| { - ActionError::Child(ConfigureShellProfile::action_tag(), Box::new(e)) - })?, + .map_err(Self::error)?, ) } else { None @@ -52,7 +50,7 @@ impl ConfigureNix { settings.force, ) .await - .map_err(|e| ActionError::Child(PlaceNixConfiguration::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; Ok(Self { place_nix_configuration, @@ -112,27 +110,21 @@ impl Action for ConfigureNix { .try_execute() .instrument(setup_default_profile_span) .await - .map_err(|e| { - ActionError::Child(setup_default_profile.action_tag(), Box::new(e)) - }) + .map_err(Self::error) }, async move { place_nix_configuration .try_execute() .instrument(place_nix_configuration_span) .await - .map_err(|e| { - ActionError::Child(place_nix_configuration.action_tag(), Box::new(e)) - }) + .map_err(Self::error) }, async move { configure_shell_profile .try_execute() .instrument(configure_shell_profile_span) .await - .map_err(|e| { - ActionError::Child(configure_shell_profile.action_tag(), Box::new(e)) - }) + .map_err(Self::error) }, )?; } else { @@ -144,18 +136,14 @@ impl Action for ConfigureNix { .try_execute() .instrument(setup_default_profile_span) .await - .map_err(|e| { - ActionError::Child(setup_default_profile.action_tag(), Box::new(e)) - }) + .map_err(Self::error) }, async move { place_nix_configuration .try_execute() .instrument(place_nix_configuration_span) .await - .map_err(|e| { - ActionError::Child(place_nix_configuration.action_tag(), Box::new(e)) - }) + .map_err(Self::error) }, )?; }; @@ -182,21 +170,28 @@ impl Action for ConfigureNix { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { + let mut errors = vec![]; if let Some(configure_shell_profile) = &mut self.configure_shell_profile { - configure_shell_profile.try_revert().await.map_err(|e| { - ActionError::Child(configure_shell_profile.action_tag(), Box::new(e)) - })?; + if let Err(err) = configure_shell_profile.try_revert().await { + errors.push(err); + } + } + if let Err(err) = self.place_nix_configuration.try_revert().await { + errors.push(err); + } + if let Err(err) = self.setup_default_profile.try_revert().await { + errors.push(err); } - self.place_nix_configuration - .try_revert() - .await - .map_err(|e| { - ActionError::Child(self.place_nix_configuration.action_tag(), Box::new(e)) - })?; - self.setup_default_profile.try_revert().await.map_err(|e| { - ActionError::Child(self.setup_default_profile.action_tag(), Box::new(e)) - })?; - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/common/configure_shell_profile.rs b/src/action/common/configure_shell_profile.rs index 96688ac..4ae92d1 100644 --- a/src/action/common/configure_shell_profile.rs +++ b/src/action/common/configure_shell_profile.rs @@ -1,5 +1,7 @@ use crate::action::base::{create_or_insert_into_file, CreateDirectory, CreateOrInsertIntoFile}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; use crate::planner::ShellProfileLocations; use nix::unistd::User; @@ -32,9 +34,9 @@ impl ConfigureShellProfile { let maybe_ssl_cert_file_setting = if let Some(ssl_cert_file) = ssl_cert_file { format!( "export NIX_SSL_CERT_FILE={:?}\n", - ssl_cert_file - .canonicalize() - .map_err(|e| { ActionError::Canonicalize(ssl_cert_file, e) })? + ssl_cert_file.canonicalize().map_err(|e| { + Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)) + })? ) } else { "".to_string() @@ -207,7 +209,7 @@ impl Action for ConfigureShellProfile { } let mut set = JoinSet::new(); - let mut errors = Vec::default(); + let mut errors = vec![]; for (idx, create_or_insert_into_file) in self.create_or_insert_into_files.iter_mut().enumerate() @@ -219,12 +221,7 @@ impl Action for ConfigureShellProfile { .try_execute() .instrument(span) .await - .map_err(|e| { - ActionError::Child( - create_or_insert_into_file_clone.action_tag(), - Box::new(e), - ) - })?; + .map_err(Self::error)?; Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone)) }); } @@ -235,17 +232,17 @@ impl Action for ConfigureShellProfile { self.create_or_insert_into_files[idx] = create_or_insert_into_file }, Ok(Err(e)) => errors.push(e), - Err(e) => return Err(e)?, + Err(e) => return Err(Self::error(e))?, }; } if !errors.is_empty() { if errors.len() == 1 { - return Err(errors.into_iter().next().unwrap())?; + return Err(Self::error(errors.into_iter().next().unwrap()))?; } else { - return Err(ActionError::Children( - errors.into_iter().map(|v| Box::new(v)).collect(), - )); + return Err(Self::error(ActionErrorKind::MultipleChildren( + errors.into_iter().collect(), + ))); } } @@ -262,7 +259,7 @@ impl Action for ConfigureShellProfile { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { let mut set = JoinSet::new(); - let mut errors: Vec = Vec::default(); + let mut errors = vec![]; for (idx, create_or_insert_into_file) in self.create_or_insert_into_files.iter_mut().enumerate() @@ -280,27 +277,26 @@ impl Action for ConfigureShellProfile { self.create_or_insert_into_files[idx] = create_or_insert_into_file }, Ok(Err(e)) => errors.push(e), - Err(e) => return Err(e)?, + // This is quite rare and generally a very bad sign. + Err(e) => return Err(e).map_err(|e| Self::error(ActionErrorKind::from(e)))?, }; } for create_directory in self.create_directories.iter_mut() { - create_directory - .try_revert() - .await - .map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?; - } - - if !errors.is_empty() { - if errors.len() == 1 { - return Err(errors.into_iter().next().unwrap())?; - } else { - return Err(ActionError::Children( - errors.into_iter().map(|v| Box::new(v)).collect(), - )); + if let Err(err) = create_directory.try_revert().await { + errors.push(err); } } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/common/create_nix_tree.rs b/src/action/common/create_nix_tree.rs index 91b2de0..ecc0945 100644 --- a/src/action/common/create_nix_tree.rs +++ b/src/action/common/create_nix_tree.rs @@ -1,7 +1,9 @@ use tracing::{span, Span}; use crate::action::base::CreateDirectory; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; const PATHS: &[&str] = &[ "/nix/var", @@ -36,7 +38,7 @@ impl CreateNixTree { create_directories.push( CreateDirectory::plan(path, String::from("root"), None, 0o0755, false) .await - .map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?, + .map_err(Self::error)?, ) } @@ -77,10 +79,7 @@ impl Action for CreateNixTree { async fn execute(&mut self) -> Result<(), ActionError> { // Just do sequential since parallelizing this will have little benefit for create_directory in self.create_directories.iter_mut() { - create_directory - .try_execute() - .await - .map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))? + create_directory.try_execute().await.map_err(Self::error)?; } Ok(()) @@ -108,14 +107,23 @@ impl Action for CreateNixTree { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { + let mut errors = vec![]; // Just do sequential since parallelizing this will have little benefit for create_directory in self.create_directories.iter_mut().rev() { - create_directory - .try_revert() - .await - .map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))? + if let Err(err) = create_directory.try_revert().await { + errors.push(err); + } } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/common/create_users_and_groups.rs b/src/action/common/create_users_and_groups.rs index e4795f3..e460734 100644 --- a/src/action/common/create_users_and_groups.rs +++ b/src/action/common/create_users_and_groups.rs @@ -1,7 +1,7 @@ use crate::{ action::{ base::{AddUserToGroup, CreateGroup, CreateUser}, - Action, ActionDescription, ActionError, ActionTag, StatefulAction, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, }, settings::CommonSettings, }; @@ -38,7 +38,7 @@ impl CreateUsersAndGroups { format!("Nix build user {index}"), ) .await - .map_err(|e| ActionError::Child(CreateUser::action_tag(), Box::new(e)))?, + .map_err(Self::error)?, ); add_users_to_groups.push( AddUserToGroup::plan( @@ -48,7 +48,7 @@ impl CreateUsersAndGroups { settings.nix_build_group_id, ) .await - .map_err(|e| ActionError::Child(AddUserToGroup::action_tag(), Box::new(e)))?, + .map_err(Self::error)?, ); } Ok(Self { @@ -156,18 +156,12 @@ impl Action for CreateUsersAndGroups { } | OperatingSystem::Darwin => { for create_user in create_users.iter_mut() { - create_user - .try_execute() - .await - .map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?; + create_user.try_execute().await.map_err(Self::error)?; } }, _ => { for create_user in create_users.iter_mut() { - create_user - .try_execute() - .await - .map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?; + create_user.try_execute().await.map_err(Self::error)?; } // While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04: // ``` @@ -190,7 +184,7 @@ impl Action for CreateUsersAndGroups { // match result { // Ok(Ok((idx, success))) => create_users[idx] = success, // Ok(Err(e)) => errors.push(Box::new(e)), - // Err(e) => return Err(ActionError::Join(e))?, + // Err(e) => return Err(ActionErrorKind::Join(e))?, // }; // } @@ -198,17 +192,14 @@ impl Action for CreateUsersAndGroups { // if errors.len() == 1 { // return Err(errors.into_iter().next().unwrap().into()); // } else { - // return Err(ActionError::Children(errors)); + // return Err(ActionErrorKind::Children(errors)); // } // } }, }; for add_user_to_group in add_users_to_groups.iter_mut() { - add_user_to_group - .try_execute() - .await - .map_err(|e| ActionError::Child(add_user_to_group.action_tag(), Box::new(e)))?; + add_user_to_group.try_execute().await.map_err(Self::error)?; } Ok(()) @@ -256,21 +247,11 @@ impl Action for CreateUsersAndGroups { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { - create_users, - create_group, - add_users_to_groups: _, - nix_build_user_count: _, - nix_build_group_name: _, - nix_build_group_id: _, - nix_build_user_prefix: _, - nix_build_user_id_base: _, - } = self; - for create_user in create_users.iter_mut() { - create_user - .try_revert() - .await - .map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?; + let mut errors = vec![]; + for create_user in self.create_users.iter_mut() { + if let Err(err) = create_user.try_revert().await { + errors.push(err); + } } // We don't actually need to do this, when a user is deleted they are removed from groups @@ -279,11 +260,19 @@ impl Action for CreateUsersAndGroups { // } // Create group - create_group - .try_revert() - .await - .map_err(|e| ActionError::Child(create_group.action_tag(), Box::new(e)))?; + if let Err(err) = self.create_group.try_revert().await { + errors.push(err); + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/common/place_nix_configuration.rs b/src/action/common/place_nix_configuration.rs index 2fe447c..395159e 100644 --- a/src/action/common/place_nix_configuration.rs +++ b/src/action/common/place_nix_configuration.rs @@ -2,7 +2,9 @@ use tracing::{span, Span}; use crate::action::base::create_or_merge_nix_config::CreateOrMergeNixConfigError; use crate::action::base::{CreateDirectory, CreateOrMergeNixConfig}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; const NIX_CONF_FOLDER: &str = "/etc/nix"; const NIX_CONF: &str = "/etc/nix/nix.conf"; @@ -26,7 +28,7 @@ impl PlaceNixConfiguration { let extra_conf = extra_conf.join("\n"); let mut nix_config = nix_config_parser::NixConfig::parse_string(extra_conf, None) .map_err(CreateOrMergeNixConfigError::ParseNixConfig) - .map_err(|e| ActionError::Custom(Box::new(e)))?; + .map_err(Self::error)?; let settings = nix_config.settings_mut(); settings.insert("build-users-group".to_string(), nix_build_group_name); @@ -46,12 +48,10 @@ impl PlaceNixConfiguration { let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force) .await - .map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let create_or_merge_nix_config = CreateOrMergeNixConfig::plan(NIX_CONF, nix_config) .await - .map_err(|e| { - ActionError::Child(CreateOrMergeNixConfig::action_tag(), Box::new(e)) - })?; + .map_err(Self::error)?; Ok(Self { create_directory, create_or_merge_nix_config, @@ -100,13 +100,11 @@ impl Action for PlaceNixConfiguration { self.create_directory .try_execute() .await - .map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; self.create_or_merge_nix_config .try_execute() .await - .map_err(|e| { - ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e)) - })?; + .map_err(Self::error)?; Ok(()) } @@ -123,17 +121,23 @@ impl Action for PlaceNixConfiguration { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - self.create_or_merge_nix_config - .try_revert() - .await - .map_err(|e| { - ActionError::Child(self.create_or_merge_nix_config.action_tag(), Box::new(e)) - })?; - self.create_directory - .try_revert() - .await - .map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?; + let mut errors = vec![]; + if let Err(err) = self.create_or_merge_nix_config.try_revert().await { + errors.push(err); + } + if let Err(err) = self.create_directory.try_revert().await { + errors.push(err); + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/common/provision_nix.rs b/src/action/common/provision_nix.rs index 6239a44..63d23a4 100644 --- a/src/action/common/provision_nix.rs +++ b/src/action/common/provision_nix.rs @@ -4,7 +4,7 @@ use super::{CreateNixTree, CreateUsersAndGroups}; use crate::{ action::{ base::{FetchAndUnpackNix, MoveUnpackedNix}, - Action, ActionDescription, ActionError, ActionTag, StatefulAction, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, }, settings::{CommonSettings, SCRATCH_DIR}, }; @@ -33,13 +33,11 @@ impl ProvisionNix { .await?; let create_users_and_group = CreateUsersAndGroups::plan(settings.clone()) .await - .map_err(|e| ActionError::Child(CreateUsersAndGroups::action_tag(), Box::new(e)))?; - let create_nix_tree = CreateNixTree::plan() - .await - .map_err(|e| ActionError::Child(CreateNixTree::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; + let create_nix_tree = CreateNixTree::plan().await.map_err(Self::error)?; let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR)) .await - .map_err(|e| ActionError::Child(MoveUnpackedNix::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; Ok(Self { fetch_nix, create_users_and_group, @@ -86,29 +84,27 @@ impl Action for ProvisionNix { // We fetch nix while doing the rest, then move it over. let mut fetch_nix_clone = self.fetch_nix.clone(); let fetch_nix_handle = tokio::task::spawn(async { - fetch_nix_clone - .try_execute() - .await - .map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?; + fetch_nix_clone.try_execute().await.map_err(Self::error)?; Result::<_, ActionError>::Ok(fetch_nix_clone) }); self.create_users_and_group .try_execute() .await - .map_err(|e| { - ActionError::Child(self.create_users_and_group.action_tag(), Box::new(e)) - })?; + .map_err(Self::error)?; self.create_nix_tree .try_execute() .await - .map_err(|e| ActionError::Child(self.create_nix_tree.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; - self.fetch_nix = fetch_nix_handle.await.map_err(ActionError::Join)??; + self.fetch_nix = fetch_nix_handle + .await + .map_err(ActionErrorKind::Join) + .map_err(Self::error)??; self.move_unpacked_nix .try_execute() .await - .map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; Ok(()) } @@ -131,34 +127,32 @@ impl Action for ProvisionNix { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - // We fetch nix while doing the rest, then move it over. - let mut fetch_nix_clone = self.fetch_nix.clone(); - let fetch_nix_handle = tokio::task::spawn(async { - fetch_nix_clone - .try_revert() - .await - .map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?; - Result::<_, ActionError>::Ok(fetch_nix_clone) - }); + let mut errors = vec![]; + + if let Err(err) = self.fetch_nix.try_revert().await { + errors.push(err) + } if let Err(err) = self.create_users_and_group.try_revert().await { - fetch_nix_handle.abort(); - return Err(err); + errors.push(err) } if let Err(err) = self.create_nix_tree.try_revert().await { - fetch_nix_handle.abort(); - return Err(err); + errors.push(err) } - self.fetch_nix = fetch_nix_handle - .await - .map_err(ActionError::Join)? - .map_err(|e| ActionError::Child(self.fetch_nix.action_tag(), Box::new(e)))?; - self.move_unpacked_nix - .try_revert() - .await - .map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?; + if let Err(err) = self.move_unpacked_nix.try_revert().await { + errors.push(err) + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/linux/start_systemd_unit.rs b/src/action/linux/start_systemd_unit.rs index a2f059b..70ca769 100644 --- a/src/action/linux/start_systemd_unit.rs +++ b/src/action/linux/start_systemd_unit.rs @@ -1,7 +1,7 @@ use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionState, ActionTag, StatefulAction}; +use crate::action::{ActionError, ActionErrorKind, ActionState, ActionTag, StatefulAction}; use crate::execute_command; use crate::action::{Action, ActionDescription}; @@ -28,7 +28,7 @@ impl StartSystemdUnit { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?; let state = if output.status.success() { tracing::debug!("Starting systemd unit `{}` already complete", unit); @@ -84,7 +84,8 @@ impl Action for StartSystemdUnit { .arg(format!("{unit}")) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, false => { // TODO(@Hoverbear): Handle proxy vars @@ -95,7 +96,8 @@ impl Action for StartSystemdUnit { .arg(format!("{unit}")) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; }, } @@ -111,30 +113,47 @@ impl Action for StartSystemdUnit { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { unit, enable } = self; + let mut errors = vec![]; - if *enable { - execute_command( + if self.enable { + if let Err(e) = execute_command( Command::new("systemctl") .process_group(0) .arg("disable") - .arg(format!("{unit}")) + .arg(format!("{}", self.unit)) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error) + { + errors.push(e); + } }; // We do both to avoid an error doing `disable --now` if the user did stop it already somehow. - execute_command( + if let Err(e) = execute_command( Command::new("systemctl") .process_group(0) .arg("stop") - .arg(format!("{unit}")) + .arg(format!("{}", self.unit)) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error) + { + errors.push(e); + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/macos/bootstrap_launchctl_service.rs b/src/action/macos/bootstrap_launchctl_service.rs index b49d301..a90a170 100644 --- a/src/action/macos/bootstrap_launchctl_service.rs +++ b/src/action/macos/bootstrap_launchctl_service.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag, StatefulAction}; +use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}; use crate::execute_command; use crate::action::{Action, ActionDescription}; @@ -40,7 +40,7 @@ impl BootstrapLaunchctlService { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?; if output.status.success() || output.status.code() == Some(37) { // We presume that success means it's found return Ok(StatefulAction::completed(Self { @@ -102,7 +102,8 @@ impl Action for BootstrapLaunchctlService { .arg(path) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } @@ -120,21 +121,16 @@ impl Action for BootstrapLaunchctlService { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { - path, - service: _, - domain, - } = self; - execute_command( Command::new("launchctl") .process_group(0) .arg("bootout") - .arg(domain) - .arg(path) + .arg(&self.domain) + .arg(&self.path) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } diff --git a/src/action/macos/create_apfs_volume.rs b/src/action/macos/create_apfs_volume.rs index 4a2649f..10fc93f 100644 --- a/src/action/macos/create_apfs_volume.rs +++ b/src/action/macos/create_apfs_volume.rs @@ -25,9 +25,11 @@ impl CreateApfsVolume { ) -> Result, ActionError> { let output = execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"])) - .await?; + .await + .map_err(Self::error)?; - let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?; + let parsed: DiskUtilApfsListOutput = + plist::from_bytes(&output.stdout).map_err(Self::error)?; for container in parsed.containers { for volume in container.volumes { if volume.name == name { @@ -101,7 +103,8 @@ impl Action for CreateApfsVolume { ]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } @@ -119,19 +122,14 @@ impl Action for CreateApfsVolume { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { - disk: _, - name, - case_sensitive: _, - } = self; - execute_command( Command::new("/usr/sbin/diskutil") .process_group(0) - .args(["apfs", "deleteVolume", name]) + .args(["apfs", "deleteVolume", &self.name]) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } diff --git a/src/action/macos/create_fstab_entry.rs b/src/action/macos/create_fstab_entry.rs index 30acbfe..0202bdf 100644 --- a/src/action/macos/create_fstab_entry.rs +++ b/src/action/macos/create_fstab_entry.rs @@ -2,7 +2,7 @@ use uuid::Uuid; use super::{get_uuid_for_label, CreateApfsVolume}; use crate::action::{ - Action, ActionDescription, ActionError, ActionState, ActionTag, StatefulAction, + Action, ActionDescription, ActionError, ActionErrorKind, ActionState, ActionTag, StatefulAction, }; use std::{io::SeekFrom, path::Path}; use tokio::{ @@ -46,7 +46,7 @@ impl CreateFstabEntry { if fstab_path.exists() { let fstab_buf = tokio::fs::read_to_string(&fstab_path) .await - .map_err(|e| ActionError::Read(fstab_path.to_path_buf(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_path_buf(), e)))?; let prelude_comment = fstab_prelude_comment(&apfs_volume_label); // See if a previous install from this crate exists, if so, invite the user to remove it (we may need to change it) @@ -122,7 +122,9 @@ impl Action for CreateFstabEntry { existing_entry, } = self; let fstab_path = Path::new(FSTAB_PATH); - let uuid = get_uuid_for_label(&apfs_volume_label).await?; + let uuid = get_uuid_for_label(&apfs_volume_label) + .await + .map_err(Self::error)?; let mut fstab = tokio::fs::OpenOptions::new() .create(true) @@ -130,14 +132,14 @@ impl Action for CreateFstabEntry { .read(true) .open(fstab_path) .await - .map_err(|e| ActionError::Open(fstab_path.to_path_buf(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_path_buf(), e)))?; // Make sure it doesn't already exist before we write to it. let mut fstab_buf = String::new(); fstab .read_to_string(&mut fstab_buf) .await - .map_err(|e| ActionError::Read(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?; let updated_buf = match existing_entry { ExistingFstabEntry::NixInstallerEntry => { @@ -161,9 +163,9 @@ impl Action for CreateFstabEntry { } } if !(saw_prelude && updated_line) { - return Err(ActionError::Custom(Box::new( + return Err(Self::error( CreateFstabEntryError::ExistingNixInstallerEntryDisappeared, - ))); + )); } current_fstab_lines.join("\n") }, @@ -182,9 +184,9 @@ impl Action for CreateFstabEntry { } } if !updated_line { - return Err(ActionError::Custom(Box::new( + return Err(Self::error( CreateFstabEntryError::ExistingForeignEntryDisappeared, - ))); + )); } current_fstab_lines.join("\n") }, @@ -194,15 +196,15 @@ impl Action for CreateFstabEntry { fstab .seek(SeekFrom::Start(0)) .await - .map_err(|e| ActionError::Seek(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?; fstab .set_len(0) .await - .map_err(|e| ActionError::Truncate(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?; fstab .write_all(updated_buf.as_bytes()) .await - .map_err(|e| ActionError::Write(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?; Ok(()) } @@ -223,13 +225,13 @@ impl Action for CreateFstabEntry { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { - apfs_volume_label, - existing_entry: _, - } = self; let fstab_path = Path::new(FSTAB_PATH); - let uuid = get_uuid_for_label(&apfs_volume_label).await?; - let fstab_entry = fstab_lines(&uuid, apfs_volume_label); + + let uuid = get_uuid_for_label(&self.apfs_volume_label) + .await + .map_err(Self::error)?; + + let fstab_entry = fstab_lines(&uuid, &self.apfs_volume_label); let mut file = OpenOptions::new() .create(false) @@ -237,12 +239,12 @@ impl Action for CreateFstabEntry { .read(true) .open(&fstab_path) .await - .map_err(|e| ActionError::Open(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Open(fstab_path.to_owned(), e)))?; let mut file_contents = String::default(); file.read_to_string(&mut file_contents) .await - .map_err(|e| ActionError::Read(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Read(fstab_path.to_owned(), e)))?; if let Some(start) = file_contents.rfind(fstab_entry.as_str()) { let end = start + fstab_entry.len(); @@ -251,16 +253,16 @@ impl Action for CreateFstabEntry { file.seek(SeekFrom::Start(0)) .await - .map_err(|e| ActionError::Seek(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Seek(fstab_path.to_owned(), e)))?; file.set_len(0) .await - .map_err(|e| ActionError::Truncate(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Truncate(fstab_path.to_owned(), e)))?; file.write_all(file_contents.as_bytes()) .await - .map_err(|e| ActionError::Write(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Write(fstab_path.to_owned(), e)))?; file.flush() .await - .map_err(|e| ActionError::Flush(fstab_path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Flush(fstab_path.to_owned(), e)))?; Ok(()) } @@ -288,3 +290,9 @@ pub enum CreateFstabEntryError { #[error("The `/etc/fstab` entry (previously created by the official install scripts) detected during planning disappeared between planning and executing. Cannot update `/etc/fstab` as planned")] ExistingForeignEntryDisappeared, } + +impl Into for CreateFstabEntryError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/macos/create_nix_volume.rs b/src/action/macos/create_nix_volume.rs index e0749aa..9c4fec5 100644 --- a/src/action/macos/create_nix_volume.rs +++ b/src/action/macos/create_nix_volume.rs @@ -4,7 +4,7 @@ use crate::action::{ BootstrapLaunchctlService, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership, EncryptApfsVolume, UnmountApfsVolume, }, - Action, ActionDescription, ActionError, ActionTag, StatefulAction, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, }; use std::{ path::{Path, PathBuf}, @@ -54,23 +54,21 @@ impl CreateNixVolume { create_or_insert_into_file::Position::End, ) .await - .map_err(|e| ActionError::Child(CreateOrInsertIntoFile::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; - let create_synthetic_objects = CreateSyntheticObjects::plan() - .await - .map_err(|e| ActionError::Child(CreateSyntheticObjects::action_tag(), Box::new(e)))?; + let create_synthetic_objects = CreateSyntheticObjects::plan().await.map_err(Self::error)?; let unmount_volume = UnmountApfsVolume::plan(disk, name.clone()) .await - .map_err(|e| ActionError::Child(UnmountApfsVolume::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive) .await - .map_err(|e| ActionError::Child(CreateApfsVolume::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let create_fstab_entry = CreateFstabEntry::plan(name.clone(), &create_volume) .await - .map_err(|e| ActionError::Child(CreateFstabEntry::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let encrypt_volume = if encrypt { Some(EncryptApfsVolume::plan(disk, &name, &create_volume).await?) @@ -86,7 +84,7 @@ impl CreateNixVolume { encrypt, ) .await - .map_err(|e| ActionError::Child(CreateVolumeService::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let bootstrap_volume = BootstrapLaunchctlService::plan( "system", @@ -94,14 +92,12 @@ impl CreateNixVolume { NIX_VOLUME_MOUNTD_DEST, ) .await - .map_err(|e| ActionError::Child(BootstrapLaunchctlService::action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let kickstart_launchctl_service = KickstartLaunchctlService::plan("system", "org.nixos.darwin-store") .await - .map_err(|e| { - ActionError::Child(KickstartLaunchctlService::action_tag(), Box::new(e)) - })?; - let enable_ownership = EnableOwnership::plan("/nix").await?; + .map_err(Self::error)?; + let enable_ownership = EnableOwnership::plan("/nix").await.map_err(Self::error)?; Ok(Self { disk: disk.to_path_buf(), @@ -172,23 +168,16 @@ impl Action for CreateNixVolume { self.create_or_append_synthetic_conf .try_execute() .await - .map_err(|e| { - ActionError::Child( - self.create_or_append_synthetic_conf.action_tag(), - Box::new(e), - ) - })?; + .map_err(Self::error)?; self.create_synthetic_objects .try_execute() .await - .map_err(|e| { - ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e)) - })?; + .map_err(Self::error)?; self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail. self.create_volume .try_execute() .await - .map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; let mut retry_tokens: usize = 50; loop { @@ -201,11 +190,14 @@ impl Action for CreateNixVolume { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; if output.status.success() { break; } else if retry_tokens == 0 { - return Err(ActionError::command_output(&command, output)); + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); } else { retry_tokens = retry_tokens.saturating_sub(1); } @@ -215,28 +207,23 @@ impl Action for CreateNixVolume { self.create_fstab_entry .try_execute() .await - .map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; if let Some(encrypt_volume) = &mut self.encrypt_volume { - encrypt_volume - .try_execute() - .await - .map_err(|e| ActionError::Child(encrypt_volume.action_tag(), Box::new(e)))? + encrypt_volume.try_execute().await.map_err(Self::error)? } self.setup_volume_daemon .try_execute() .await - .map_err(|e| ActionError::Child(self.setup_volume_daemon.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; self.bootstrap_volume .try_execute() .await - .map_err(|e| ActionError::Child(self.bootstrap_volume.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; self.kickstart_launchctl_service .try_execute() .await - .map_err(|e| { - ActionError::Child(self.kickstart_launchctl_service.action_tag(), Box::new(e)) - })?; + .map_err(Self::error)?; let mut retry_tokens: usize = 50; loop { @@ -248,11 +235,14 @@ impl Action for CreateNixVolume { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; if output.status.success() { break; } else if retry_tokens == 0 { - return Err(ActionError::command_output(&command, output)); + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); } else { retry_tokens = retry_tokens.saturating_sub(1); } @@ -262,7 +252,7 @@ impl Action for CreateNixVolume { self.enable_ownership .try_execute() .await - .map_err(|e| ActionError::Child(self.enable_ownership.action_tag(), Box::new(e)))?; + .map_err(Self::error)?; Ok(()) } @@ -296,44 +286,53 @@ impl Action for CreateNixVolume { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - self.enable_ownership.try_revert().await?; - self.kickstart_launchctl_service.try_revert().await?; - self.bootstrap_volume.try_revert().await?; - self.setup_volume_daemon.try_revert().await?; - if let Some(encrypt_volume) = &mut self.encrypt_volume { - encrypt_volume.try_revert().await?; - } - self.create_fstab_entry - .try_revert() - .await - .map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?; + let mut errors = vec![]; - self.unmount_volume - .try_revert() - .await - .map_err(|e| ActionError::Child(self.unmount_volume.action_tag(), Box::new(e)))?; - self.create_volume - .try_revert() - .await - .map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?; + if let Err(err) = self.enable_ownership.try_revert().await { + errors.push(err) + }; + if let Err(err) = self.kickstart_launchctl_service.try_revert().await { + errors.push(err) + }; + if let Err(err) = self.bootstrap_volume.try_revert().await { + errors.push(err) + }; + if let Err(err) = self.setup_volume_daemon.try_revert().await { + errors.push(err) + }; + if let Some(encrypt_volume) = &mut self.encrypt_volume { + if let Err(err) = encrypt_volume.try_revert().await { + errors.push(err) + } + } + if let Err(err) = self.create_fstab_entry.try_revert().await { + errors.push(err) + } + + if let Err(err) = self.unmount_volume.try_revert().await { + errors.push(err) + } + if let Err(err) = self.create_volume.try_revert().await { + errors.push(err) + } // Purposefully not reversed - self.create_or_append_synthetic_conf - .try_revert() - .await - .map_err(|e| { - ActionError::Child( - self.create_or_append_synthetic_conf.action_tag(), - Box::new(e), - ) - })?; - self.create_synthetic_objects - .try_revert() - .await - .map_err(|e| { - ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e)) - })?; + if let Err(err) = self.create_or_append_synthetic_conf.try_revert().await { + errors.push(err) + } + if let Err(err) = self.create_synthetic_objects.try_revert().await { + errors.push(err) + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } } } diff --git a/src/action/macos/create_synthetic_objects.rs b/src/action/macos/create_synthetic_objects.rs index 56f31ae..4c4e3e3 100644 --- a/src/action/macos/create_synthetic_objects.rs +++ b/src/action/macos/create_synthetic_objects.rs @@ -90,10 +90,3 @@ impl Action for CreateSyntheticObjects { Ok(()) } } - -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum CreateSyntheticObjectsError { - #[error("Failed to execute command")] - Command(#[source] std::io::Error), -} diff --git a/src/action/macos/create_volume_service.rs b/src/action/macos/create_volume_service.rs index d22c157..82a6b40 100644 --- a/src/action/macos/create_volume_service.rs +++ b/src/action/macos/create_volume_service.rs @@ -7,7 +7,9 @@ use tokio::{ io::AsyncWriteExt, }; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; +use crate::action::{ + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; use super::get_uuid_for_label; @@ -43,27 +45,27 @@ impl CreateVolumeService { }; if this.path.exists() { - let discovered_plist: LaunchctlMountPlist = plist::from_file(&this.path)?; + let discovered_plist: LaunchctlMountPlist = + plist::from_file(&this.path).map_err(Self::error)?; let expected_plist = generate_mount_plist( &this.mount_service_label, &this.apfs_volume_label, &this.mount_point, encrypt, ) - .await?; + .await + .map_err(Self::error)?; if discovered_plist != expected_plist { tracing::trace!( ?discovered_plist, ?expected_plist, "Parsed plists not equal" ); - return Err(ActionError::Custom(Box::new( - CreateVolumeServiceError::DifferentPlist { - expected: expected_plist, - discovered: discovered_plist, - path: this.path.clone(), - }, - ))); + return Err(Self::error(CreateVolumeServiceError::DifferentPlist { + expected: expected_plist, + discovered: discovered_plist, + path: this.path.clone(), + })); } tracing::debug!("Creating file `{}` already complete", this.path.display()); @@ -121,7 +123,8 @@ impl Action for CreateVolumeService { mount_point, *encrypt, ) - .await?; + .await + .map_err(Self::error)?; let mut options = OpenOptions::new(); options.create_new(true).write(true).read(true); @@ -129,13 +132,13 @@ impl Action for CreateVolumeService { let mut file = options .open(&path) .await - .map_err(|e| ActionError::Open(path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Open(path.to_owned(), e)))?; let mut buf = Vec::new(); - plist::to_writer_xml(&mut buf, &generated_plist)?; + plist::to_writer_xml(&mut buf, &generated_plist).map_err(Self::error)?; file.write_all(&buf) .await - .map_err(|e| ActionError::Write(path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Write(path.to_owned(), e)))?; Ok(()) } @@ -151,7 +154,7 @@ impl Action for CreateVolumeService { async fn revert(&mut self) -> Result<(), ActionError> { remove_file(&self.path) .await - .map_err(|e| ActionError::Remove(self.path.to_owned(), e))?; + .map_err(|e| Self::error(ActionErrorKind::Remove(self.path.to_owned(), e)))?; Ok(()) } @@ -163,7 +166,7 @@ async fn generate_mount_plist( apfs_volume_label: &str, mount_point: &Path, encrypt: bool, -) -> Result { +) -> Result { let apfs_volume_label_with_quotes = format!("\"{apfs_volume_label}\""); let uuid = get_uuid_for_label(&apfs_volume_label).await?; // The official Nix scripts uppercase the UUID, so we do as well for compatibility. @@ -208,3 +211,9 @@ pub enum CreateVolumeServiceError { path: PathBuf, }, } + +impl Into for CreateVolumeServiceError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/macos/enable_ownership.rs b/src/action/macos/enable_ownership.rs index 5427c2c..00353ab 100644 --- a/src/action/macos/enable_ownership.rs +++ b/src/action/macos/enable_ownership.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag, StatefulAction}; +use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}; use crate::execute_command; use crate::action::{Action, ActionDescription}; @@ -62,9 +62,11 @@ impl Action for EnableOwnership { .arg(&path) .stdin(std::process::Stdio::null()), ) - .await? + .await + .map_err(Self::error)? .stdout; - let the_plist: DiskUtilInfoOutput = plist::from_reader(Cursor::new(buf))?; + let the_plist: DiskUtilInfoOutput = + plist::from_reader(Cursor::new(buf)).map_err(Self::error)?; the_plist.global_permissions_enabled }; @@ -77,7 +79,8 @@ impl Action for EnableOwnership { .arg(path) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } Ok(()) @@ -100,3 +103,9 @@ pub enum EnableOwnershipError { #[error("Failed to execute command")] Command(#[source] std::io::Error), } + +impl Into for EnableOwnershipError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/macos/encrypt_apfs_volume.rs b/src/action/macos/encrypt_apfs_volume.rs index b3c6808..3aa69b2 100644 --- a/src/action/macos/encrypt_apfs_volume.rs +++ b/src/action/macos/encrypt_apfs_volume.rs @@ -1,7 +1,7 @@ use crate::{ action::{ - macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionState, - ActionTag, StatefulAction, + macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionErrorKind, + ActionState, ActionTag, StatefulAction, }, execute_command, os::darwin::DiskUtilApfsListOutput, @@ -51,7 +51,7 @@ impl EncryptApfsVolume { if command .status() .await - .map_err(|e| ActionError::command(&command, e))? + .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))? .success() { // The user has a password matching what we would create. @@ -61,24 +61,26 @@ impl EncryptApfsVolume { } // Ask the user to remove it - return Err(ActionError::Custom(Box::new( - EncryptApfsVolumeError::ExistingPasswordFound(name, disk), + return Err(Self::error(EncryptApfsVolumeError::ExistingPasswordFound( + name, disk, ))); } else { if planned_create_apfs_volume.state == ActionState::Completed { // The user has a volume already created, but a password not set. This means we probably can't decrypt the volume. - return Err(ActionError::Custom(Box::new( + return Err(Self::error( EncryptApfsVolumeError::MissingPasswordForExistingVolume(name, disk), - ))); + )); } } // Ensure if the disk already exists, that it's encrypted let output = execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"])) - .await?; + .await + .map_err(Self::error)?; - let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?; + let parsed: DiskUtilApfsListOutput = + plist::from_bytes(&output.stdout).map_err(Self::error)?; for container in parsed.containers { for volume in container.volumes { if volume.name == name { @@ -87,9 +89,9 @@ impl EncryptApfsVolume { return Ok(StatefulAction::completed(Self { disk, name })); }, false => { - return Err(ActionError::Custom(Box::new( + return Err(Self::error( EncryptApfsVolumeError::ExistingVolumeNotEncrypted(name, disk), - ))); + )); }, } } @@ -150,7 +152,9 @@ impl Action for EncryptApfsVolume { let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ - execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)).await?; + execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)) + .await + .map_err(Self::error)?; // Add the password to the user keychain so they can unlock it later. execute_command( @@ -180,7 +184,8 @@ impl Action for EncryptApfsVolume { "/Library/Keychains/System.keychain", ]), ) - .await?; + .await + .map_err(Self::error)?; // Encrypt the mounted volume execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([ @@ -192,7 +197,8 @@ impl Action for EncryptApfsVolume { "-passphrase", password.as_str(), ])) - .await?; + .await + .map_err(Self::error)?; execute_command( Command::new("/usr/sbin/diskutil") @@ -201,7 +207,8 @@ impl Action for EncryptApfsVolume { .arg("force") .arg(&name), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } @@ -220,18 +227,16 @@ impl Action for EncryptApfsVolume { disk = %self.disk.display(), ))] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { disk, name } = self; - - let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ + let disk_str = self.disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ // TODO: This seems very rough and unsafe execute_command( Command::new("/usr/bin/security").process_group(0).args([ "delete-generic-password", "-a", - name.as_str(), + self.name.as_str(), "-s", - name.as_str(), + self.name.as_str(), "-l", format!("{} encryption password", disk_str).as_str(), "-D", @@ -243,7 +248,8 @@ impl Action for EncryptApfsVolume { .as_str(), ]), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } @@ -258,3 +264,9 @@ pub enum EncryptApfsVolumeError { #[error("The existing APFS volume \"{0}\" on disk `{1}` is not encrypted but it should be, consider removing the volume with `diskutil apfs deleteVolume \"{0}\"` (if you receive error -69888, you may need to run `launchctl bootout system/org.nixos.darwin-store` and `launchctl bootout system/org.nixos.nix-daemon` first)")] ExistingVolumeNotEncrypted(String, PathBuf), } + +impl Into for EncryptApfsVolumeError { + fn into(self) -> ActionErrorKind { + ActionErrorKind::Custom(Box::new(self)) + } +} diff --git a/src/action/macos/kickstart_launchctl_service.rs b/src/action/macos/kickstart_launchctl_service.rs index b477474..459ba86 100644 --- a/src/action/macos/kickstart_launchctl_service.rs +++ b/src/action/macos/kickstart_launchctl_service.rs @@ -3,7 +3,7 @@ use std::process::Output; use tokio::process::Command; use tracing::{span, Span}; -use crate::action::{ActionError, ActionTag, StatefulAction}; +use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}; use crate::execute_command; use crate::action::{Action, ActionDescription}; @@ -39,11 +39,11 @@ impl KickstartLaunchctlService { let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?; if output.status.success() { service_exists = true; - let output_string = String::from_utf8(output.stdout)?; + let output_string = String::from_utf8(output.stdout).map_err(|e| Self::error(e))?; // We are looking for a line containing "state = " with some trailing content // The output is not a JSON or a plist // MacOS's man pages explicitly tell us not to try to parse this output @@ -102,7 +102,8 @@ impl Action for KickstartLaunchctlService { .arg(format!("{domain}/{service}")) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } @@ -116,26 +117,26 @@ impl Action for KickstartLaunchctlService { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { domain, service } = self; - // MacOs doesn't offer an "ensure-stopped" like they do with Kickstart let mut command = Command::new("launchctl"); command.process_group(0); command.arg("stop"); - command.arg(format!("{domain}/{service}")); + command.arg(format!("{}/{}", self.domain, self.service)); command.stdin(std::process::Stdio::null()); let command_str = format!("{:?}", command.as_std()); + let output = command .output() .await - .map_err(|e| ActionError::command(&command, e))?; + .map_err(|e| Self::error(ActionErrorKind::command(&command, e)))?; + // On our test Macs, a status code of `3` was reported if the service was stopped while not running. match output.status.code() { Some(3) | Some(0) | None => (), _ => { - return Err(ActionError::Custom(Box::new( + return Err(Self::error(ActionErrorKind::Custom(Box::new( KickstartLaunchctlServiceError::CannotStopService(command_str, output), - ))) + )))) }, } diff --git a/src/action/macos/mod.rs b/src/action/macos/mod.rs index ef0aed2..a994ee1 100644 --- a/src/action/macos/mod.rs +++ b/src/action/macos/mod.rs @@ -15,7 +15,7 @@ pub(crate) mod unmount_apfs_volume; pub use bootstrap_launchctl_service::BootstrapLaunchctlService; pub use create_apfs_volume::CreateApfsVolume; pub use create_nix_volume::{CreateNixVolume, NIX_VOLUME_MOUNTD_DEST}; -pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError}; +pub use create_synthetic_objects::CreateSyntheticObjects; pub use create_volume_service::CreateVolumeService; pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; pub use encrypt_apfs_volume::EncryptApfsVolume; @@ -27,9 +27,9 @@ use uuid::Uuid; use crate::execute_command; -use super::ActionError; +use super::ActionErrorKind; -async fn get_uuid_for_label(apfs_volume_label: &str) -> Result { +async fn get_uuid_for_label(apfs_volume_label: &str) -> Result { let output = execute_command( Command::new("/usr/sbin/diskutil") .process_group(0) diff --git a/src/action/macos/unmount_apfs_volume.rs b/src/action/macos/unmount_apfs_volume.rs index 89605a5..0d8d494 100644 --- a/src/action/macos/unmount_apfs_volume.rs +++ b/src/action/macos/unmount_apfs_volume.rs @@ -65,9 +65,11 @@ impl Action for UnmountApfsVolume { .arg(&name) .stdin(std::process::Stdio::null()), ) - .await? + .await + .map_err(Self::error)? .stdout; - let the_plist: DiskUtilInfoOutput = plist::from_reader(Cursor::new(buf))?; + let the_plist: DiskUtilInfoOutput = + plist::from_reader(Cursor::new(buf)).map_err(|e| Self::error(e))?; the_plist.mount_point.is_some() }; @@ -80,7 +82,8 @@ impl Action for UnmountApfsVolume { .arg(name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; } else { tracing::debug!("Volume was already unmounted, can skip unmounting") } @@ -94,16 +97,15 @@ impl Action for UnmountApfsVolume { #[tracing::instrument(level = "debug", skip_all)] async fn revert(&mut self) -> Result<(), ActionError> { - let Self { disk: _, name } = self; - execute_command( Command::new("/usr/sbin/diskutil") .process_group(0) .args(["unmount", "force"]) - .arg(name) + .arg(&self.name) .stdin(std::process::Stdio::null()), ) - .await?; + .await + .map_err(Self::error)?; Ok(()) } diff --git a/src/action/mod.rs b/src/action/mod.rs index c21d555..33edf1b 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -255,6 +255,14 @@ pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone { state: ActionState::Uncompleted, } } + + fn error(kind: impl Into) -> ActionError + where + Self: Sized, + { + ActionError::new(Self::action_tag(), kind) + } + // They should also have an `async fn plan(args...) -> Result, 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) -> 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 for ActionErrorKind { + fn from(value: ActionError) -> Self { + Self::Child(Box::new(value)) + } +} + /// An error occurring during an action #[non_exhaustive] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)] -pub enum ActionError { +pub enum ActionErrorKind { /// A custom error #[error(transparent)] Custom(Box), @@ -310,17 +359,26 @@ pub enum ActionError { #[error(transparent)] Certificate(#[from] CertificateError), /// A child error - #[error("Child action `{0}`")] - Child(ActionTag, #[source] Box), - /// Several child errors - #[error("Child action errors: {}", .0.iter().map(|v| { - if let Some(source) = v.source() { - format!("{v} ({source})") + #[error(transparent)] + Child(Box), + /// Several errors + #[error("Multiple child errors\n\n{}", .0.iter().map(|err| { + if let Some(source) = err.source() { + format!("{err}\n{source}\n") } else { - format!("{v}") + format!("{err}\n") } - }).collect::>().join(" & "))] - Children(Vec>), + }).collect::>().join("\n"))] + MultipleChildren(Vec), + /// 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::>().join("\n"))] + Multiple(Vec), /// The path already exists with different content that expected #[error( "`{0}` exists with different content than planned, consider removing it with `rm {0}`" @@ -475,7 +533,7 @@ pub enum ActionError { SystemdMissing, } -impl ActionError { +impl ActionErrorKind { pub fn command(command: &tokio::process::Command, error: std::io::Error) -> Self { Self::Command { #[cfg(feature = "diagnostics")] @@ -494,7 +552,7 @@ impl ActionError { } } -impl HasExpectedErrors for ActionError { +impl HasExpectedErrors for ActionErrorKind { fn expected<'a>(&'a self) -> Option> { match self { Self::PathUserMismatch(_, _, _) @@ -506,11 +564,10 @@ impl HasExpectedErrors for ActionError { } #[cfg(feature = "diagnostics")] -impl crate::diagnostics::ErrorDiagnostic for ActionError { +impl crate::diagnostics::ErrorDiagnostic for ActionErrorKind { fn diagnostic(&self) -> String { let static_str: &'static str = (self).into(); let context = match self { - Self::Child(action, _) => vec![action.to_string()], Self::Read(path, _) | Self::Open(path, _) | Self::Write(path, _) @@ -518,7 +575,8 @@ impl crate::diagnostics::ErrorDiagnostic for ActionError { | Self::SetPermissions(_, path, _) | Self::GettingMetadata(path, _) | Self::CreateDirectory(path, _) - | Self::PathWasNotFile(path) => { + | Self::PathWasNotFile(path) + | Self::Remove(path, _) => { vec![path.to_string_lossy().to_string()] }, Self::Rename(first_path, second_path, _) diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index 354b37c..b0fdc7b 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -15,10 +15,13 @@ use crate::{ plan::RECEIPT_LOCATION, planner::Planner, settings::CommonSettings, - BuiltinPlanner, InstallPlan, + BuiltinPlanner, InstallPlan, NixInstallerError, }; use clap::{ArgAction, Parser}; -use eyre::{eyre, WrapErr}; +use color_eyre::{ + eyre::{eyre, WrapErr}, + Section, +}; use owo_colors::OwoColorize; /// Execute an install (possibly using an existing plan) @@ -218,19 +221,30 @@ impl CommandExecute for Install { let rx2 = tx.subscribe(); let res = install_plan.uninstall(rx2).await; - if let Err(err) = res { - if let Some(expected) = err.expected() { - eprintln!("{}", expected.red()); - return Ok(ExitCode::FAILURE); - } - return Err(err)?; - } else { - println!( - "\ - {message}\n\ - ", - message = "Partial Nix install was uninstalled successfully!".bold(), - ); + match res { + Err(NixInstallerError::ActionRevert(errs)) => { + let mut report = eyre!("Multiple errors"); + for err in errs { + report = report.error(err); + } + return Err(report)?; + }, + Err(err) => { + if let Some(expected) = err.expected() { + eprintln!("{}", expected.red()); + return Ok(ExitCode::FAILURE); + } + return Err(err)?; + }, + _ => { + println!( + "\ + {message}\n\ + ", + message = + "Partial Nix install was uninstalled successfully!".bold(), + ); + }, } } else { if let Some(expected) = err.expected() { diff --git a/src/cli/subcommand/uninstall.rs b/src/cli/subcommand/uninstall.rs index ba0b262..2a7bd7e 100644 --- a/src/cli/subcommand/uninstall.rs +++ b/src/cli/subcommand/uninstall.rs @@ -8,10 +8,10 @@ use crate::{ cli::{ensure_root, interaction::PromptChoice, signal_channel}, error::HasExpectedErrors, plan::RECEIPT_LOCATION, - InstallPlan, + InstallPlan, NixInstallerError, }; use clap::{ArgAction, Parser}; -use eyre::{eyre, WrapErr}; +use color_eyre::eyre::{eyre, WrapErr}; use owo_colors::OwoColorize; use rand::Rng; @@ -125,17 +125,21 @@ impl CommandExecute for Uninstall { let (_tx, rx) = signal_channel().await?; let res = plan.uninstall(rx).await; - if let Err(err) = res { - if let Some(expected) = err.expected() { - println!("{}", expected.red()); - return Ok(ExitCode::FAILURE); - } - return Err(err)?; + match res { + Err(err @ NixInstallerError::ActionRevert(_)) => { + tracing::error!("Uninstallation complete, some errors encountered"); + return Err(err)?; + }, + Err(err) => { + if let Some(expected) = err.expected() { + println!("{}", expected.red()); + return Ok(ExitCode::FAILURE); + } + return Err(err)?; + }, + _ => (), } - // TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going... - // However that will require being able to link error -> step and manually setting that step as `Uncompleted`. - println!( "\ {success}\n\ diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 5d71220..4b4e8ba 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -102,11 +102,11 @@ impl DiagnosticData { let mut walker: &dyn std::error::Error = &err; while let Some(source) = walker.source() { if let Some(downcasted) = source.downcast_ref::() { - let downcasted_diagnostic = downcasted.diagnostic(); + let downcasted_diagnostic = downcasted.kind().diagnostic(); failure_chain.push(downcasted_diagnostic); } if let Some(downcasted) = source.downcast_ref::>() { - let downcasted_diagnostic = downcasted.diagnostic(); + let downcasted_diagnostic = downcasted.kind().diagnostic(); failure_chain.push(downcasted_diagnostic); } if let Some(downcasted) = source.downcast_ref::() { diff --git a/src/error.rs b/src/error.rs index 3070374..698a058 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,18 +1,23 @@ -use std::path::PathBuf; +use std::{error::Error, path::PathBuf}; -use crate::{ - action::{ActionError, ActionTag}, - planner::PlannerError, - settings::InstallSettingsError, -}; +use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError}; /// An error occurring during a call defined in this crate #[non_exhaustive] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)] pub enum NixInstallerError { /// An error originating from an [`Action`](crate::action::Action) - #[error("Error executing action `{0}`")] - Action(ActionTag, #[source] ActionError), + #[error("Error executing action")] + Action(#[source] ActionError), + /// An error originating from an [`Action`](crate::action::Action) while reverting + #[error("Error reverting\n{}", .0.iter().map(|err| { + if let Some(source) = err.source() { + format!("{err}\n{source}\n") + } else { + format!("{err}\n") + } + }).collect::>().join("\n"))] + ActionRevert(Vec), /// An error while writing the [`InstallPlan`](crate::InstallPlan) #[error("Recording install receipt")] RecordingReceipt(PathBuf, #[source] std::io::Error), @@ -72,7 +77,8 @@ pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync { impl HasExpectedErrors for NixInstallerError { fn expected<'a>(&'a self) -> Option> { match self { - NixInstallerError::Action(_, action_error) => action_error.expected(), + NixInstallerError::Action(action_error) => action_error.kind().expected(), + NixInstallerError::ActionRevert(_) => None, NixInstallerError::RecordingReceipt(_, _) => None, NixInstallerError::CopyingSelf(_) => None, NixInstallerError::SerializingReceipt(_) => None, @@ -91,7 +97,7 @@ impl crate::diagnostics::ErrorDiagnostic for NixInstallerError { fn diagnostic(&self) -> String { let static_str: &'static str = (self).into(); let context = match self { - Self::Action(action, _) => vec![action.to_string()], + Self::Action(action_error) => vec![action_error.action_tag().to_string()], _ => vec![], }; return format!( diff --git a/src/lib.rs b/src/lib.rs index 9224bff..4f5feda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,8 +82,6 @@ pub mod settings; use std::{ffi::OsStr, path::Path, process::Output}; -use action::{Action, ActionError}; - pub use error::NixInstallerError; pub use plan::InstallPlan; use planner::BuiltinPlanner; @@ -91,16 +89,18 @@ use planner::BuiltinPlanner; use reqwest::Certificate; use tokio::process::Command; +use crate::action::{Action, ActionErrorKind}; + #[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))] -async fn execute_command(command: &mut Command) -> Result { +async fn execute_command(command: &mut Command) -> Result { tracing::trace!("Executing"); let output = command .output() .await - .map_err(|e| ActionError::command(command, e))?; + .map_err(|e| ActionErrorKind::command(command, e))?; match output.status.success() { true => Ok(output), - false => Err(ActionError::command_output(command, output)), + false => Err(ActionErrorKind::command_output(command, output)), } } diff --git a/src/plan.rs b/src/plan.rs index 8c8a24d..5ea06f7 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -175,12 +175,11 @@ impl InstallPlan { } tracing::info!("Step: {}", action.tracing_synopsis()); - let typetag_name = action.inner_typetag_name(); if let Err(err) = action.try_execute().await { if let Err(err) = write_receipt(self.clone()).await { tracing::error!("Error saving receipt: {:?}", err); } - let err = NixInstallerError::Action(typetag_name.into(), err); + let err = NixInstallerError::Action(err); #[cfg(feature = "diagnostics")] if let Some(diagnostic_data) = &self.diagnostic_data { diagnostic_data @@ -296,6 +295,7 @@ impl InstallPlan { ) -> Result<(), NixInstallerError> { let Self { actions, .. } = self; let mut cancel_channel = cancel_channel.into(); + let mut errors = vec![]; // This is **deliberately sequential**. // Actions which are parallelizable are represented by "group actions" like CreateUsers @@ -324,39 +324,40 @@ impl InstallPlan { } tracing::info!("Revert: {}", action.tracing_synopsis()); - let typetag_name = action.inner_typetag_name(); - if let Err(err) = action.try_revert().await { - if let Err(err) = write_receipt(self.clone()).await { - tracing::error!("Error saving receipt: {:?}", err); - } - let err = NixInstallerError::Action(typetag_name.into(), err); - #[cfg(feature = "diagnostics")] - if let Some(diagnostic_data) = &self.diagnostic_data { - diagnostic_data - .clone() - .failure(&err) - .send( - crate::diagnostics::DiagnosticAction::Uninstall, - crate::diagnostics::DiagnosticStatus::Failure, - ) - .await?; - } - return Err(err); + if let Err(errs) = action.try_revert().await { + errors.push(errs); } } - #[cfg(feature = "diagnostics")] - if let Some(diagnostic_data) = &self.diagnostic_data { - diagnostic_data - .clone() - .send( - crate::diagnostics::DiagnosticAction::Uninstall, - crate::diagnostics::DiagnosticStatus::Success, - ) - .await?; - } + if errors.is_empty() { + #[cfg(feature = "diagnostics")] + if let Some(diagnostic_data) = &self.diagnostic_data { + diagnostic_data + .clone() + .send( + crate::diagnostics::DiagnosticAction::Uninstall, + crate::diagnostics::DiagnosticStatus::Success, + ) + .await?; + } - Ok(()) + Ok(()) + } else { + let error = NixInstallerError::ActionRevert(errors); + #[cfg(feature = "diagnostics")] + if let Some(diagnostic_data) = &self.diagnostic_data { + diagnostic_data + .clone() + .failure(&error) + .send( + crate::diagnostics::DiagnosticAction::Uninstall, + crate::diagnostics::DiagnosticStatus::Failure, + ) + .await?; + } + + return Err(error); + } } } diff --git a/src/settings.rs b/src/settings.rs index 8fb1e5e..1d2e9b1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -232,6 +232,7 @@ impl CommonSettings { let url; let nix_build_user_prefix; let nix_build_user_id_base; + let ssl_cert_file; use target_lexicon::{Architecture, OperatingSystem}; match (Architecture::host(), OperatingSystem::host()) { @@ -240,18 +241,21 @@ impl CommonSettings { url = NIX_X64_64_LINUX_URL; nix_build_user_prefix = "nixbld"; nix_build_user_id_base = 30000; + ssl_cert_file = None; }, #[cfg(target_os = "linux")] (Architecture::X86_32(_), OperatingSystem::Linux) => { url = NIX_I686_LINUX_URL; nix_build_user_prefix = "nixbld"; nix_build_user_id_base = 30000; + ssl_cert_file = None; }, #[cfg(target_os = "linux")] (Architecture::Aarch64(_), OperatingSystem::Linux) => { url = NIX_AARCH64_LINUX_URL; nix_build_user_prefix = "nixbld"; nix_build_user_id_base = 30000; + ssl_cert_file = None; }, #[cfg(target_os = "macos")] (Architecture::X86_64, OperatingSystem::MacOSX { .. }) @@ -259,6 +263,7 @@ impl CommonSettings { url = NIX_X64_64_DARWIN_URL; nix_build_user_prefix = "_nixbld"; nix_build_user_id_base = 300; + ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into()); }, #[cfg(target_os = "macos")] (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) @@ -266,6 +271,7 @@ impl CommonSettings { url = NIX_AARCH64_DARWIN_URL; nix_build_user_prefix = "_nixbld"; nix_build_user_id_base = 300; + ssl_cert_file = Some("/etc/ssl/certs/ca-certificates.crt".into()); }, _ => { return Err(InstallSettingsError::UnsupportedArchitecture( @@ -285,7 +291,7 @@ impl CommonSettings { proxy: Default::default(), extra_conf: Default::default(), force: false, - ssl_cert_file: Default::default(), + ssl_cert_file, #[cfg(feature = "diagnostics")] diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()), })