From 4a356409ca4ad300de3f844cd36dd2b70de0d150 Mon Sep 17 00:00:00 2001 From: matthewcroughan Date: Mon, 23 May 2022 18:57:50 +0100 Subject: [PATCH] Add support for hermetic remote builds A hermetic remote rebuild is when Nixinate sends a specific Nix binary to the remote first, then uses it when activating a system closure, rather than using the Nix that is already on the remote. This is defaulted to false for the time being, as the bandwidth usage can be high. This allows users to avoid the need to bootstrap the remote first by enabling flakes on the remote Nix binary. --- README.md | 28 ++++++++++++++++++++++++++++ flake.nix | 29 +++++++++++++++++++++++------ tests/vmTest/default.nix | 16 +++++++++++----- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 292d576..ea780e7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Below is a minimal example: host = "itchy.scratchy.com"; sshUser = "matthew"; buildOn = "remote"; # valid args are "local" or "remote" + hermetic = false; }; } # ... other configuration ... @@ -76,3 +77,30 @@ reloading user units for matthew... setting up tmpfiles Connection to itchy.scratchy.com closed. ``` + +# Available arguments via `_module.args.nixinate` + +- `host` *`string`* + + A string representing the hostname or IP address of a machine to connect to + via ssh. + +- `sshUser` *`string`* + + A string representing the username a machine to connect to via ssh. + +- `buildOn` *`"remote"`* or *`"local"`* + + - `"remote"` + + Push the flake to the remote, build and activate entirely remotely, + returning logs via SSH. + + - `"local"` + + Build the system closure locally, copy to the remote and activate. + +- `hermetic` *`bool`* + + Whether to copy Nix to the remote for usage when building and activating, + instead of using the Nix which is already installed on the remote. diff --git a/flake.nix b/flake.nix index 7d3215d..f174abf 100644 --- a/flake.nix +++ b/flake.nix @@ -16,32 +16,49 @@ { herculesCI.ciSystems = [ "x86_64-linux" ]; overlay = final: prev: { + nixinate = { + nix = prev.pkgs.writeShellScriptBin "nix" + ''${final.nixVersions.unstable}/bin/nix --experimental-features "nix-command flakes" "$@"''; + nixos-rebuild = prev.nixos-rebuild.override { inherit (final) nix; }; + }; generateApps = flake: let machines = builtins.attrNames flake.nixosConfigurations; validMachines = final.lib.remove "" (final.lib.forEach machines (x: final.lib.optionalString (flake.nixosConfigurations."${x}"._module.args ? nixinate) "${x}" )); mkDeployScript = { machine, dryRun }: let inherit (builtins) abort; + inherit (final.lib) getExe; + nix = "${getExe final.nix}"; + nixos-rebuild = "${getExe final.nixos-rebuild}"; + openssh = "${getExe final.openssh}"; n = flake.nixosConfigurations.${machine}._module.args.nixinate; + hermetic = n.hermetic or false; user = n.sshUser or "root"; host = n.host; where = n.buildOn or "remote"; remote = if where == "remote" then true else if where == "local" then false else abort "_module.args.nixinate.buildOn is not set to a valid value of 'local' or 'remote'"; switch = if dryRun then "dry-activate" else "switch"; - script = '' + script = + '' set -e echo "🚀 Deploying nixosConfigurations.${machine} from ${flake}" echo "👤 SSH User: ${user}" echo "🌐 SSH Host: ${host}" '' + (if remote then '' echo "🚀 Sending flake to ${machine} via nix copy:" - ( set -x; ${final.nix}/bin/nix copy ${flake} --to ssh://${user}@${host} ) - echo "🤞 Activating configuration on ${machine} via ssh:" - ( set -x; ${final.openssh}/bin/ssh -t ${user}@${host} 'sudo nixos-rebuild ${switch} --flake ${flake}#${machine}' ) + ( set -x; ${nix} copy ${flake} --to ssh://${user}@${host} ) + '' + (if hermetic then '' + echo "🤞 Activating configuration hermetically on ${machine} via ssh:" + ( set -x; ${nix} build ${nixos-rebuild} --no-link --store ssh://${user}@${host} ) + ( set -x; ${openssh} -t ${user}@${host} 'sudo ${nixos-rebuild} ${switch} --flake ${flake}#${machine}' ) '' else '' + echo "🤞 Activating configuration non-hermetically on ${machine} via ssh:" + ( set -x; ${openssh} -t ${user}@${host} 'sudo nixos-rebuild ${switch} --flake ${flake}#${machine}' ) + '') + else '' echo "🔨 Building system closure locally, copying it to remote store and activating it:" - ( set -x; NIX_SSHOPTS="-t" ${final.nixos-rebuild}/bin/nixos-rebuild ${switch} --flake ${flake}#${machine} --target-host ${user}@${host} --use-remote-sudo ) + ( set -x; NIX_SSHOPTS="-t" ${nixos-rebuild} ${switch} --flake ${flake}#${machine} --target-host ${user}@${host} --use-remote-sudo ) ''); in final.writeScript "deploy-${machine}.sh" script; in @@ -78,7 +95,7 @@ let vmTests = import ./tests { makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest; - inherit pkgs inputs; + inherit inputs; pkgs = nixpkgsFor.${system}; }; in pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux. diff --git a/tests/vmTest/default.nix b/tests/vmTest/default.nix index ef16371..097d4fa 100644 --- a/tests/vmTest/default.nix +++ b/tests/vmTest/default.nix @@ -1,5 +1,6 @@ { pkgs, makeTest, inputs }: let + inherit (pkgs) lib; # Return a store path with a closure containing everything including # derivations and all build dependency outputs, all the way down. allDrvOutputs = pkg: @@ -22,7 +23,7 @@ let ((import (path + "/flake.nix")).outputs (inputs // {self = r;})); in r; - mkNixinateTest = buildOn: + mkNixinateTest = { buildOn, hermetic ? false, ... }: let exampleFlake = pkgs.writeTextFile { name = "nixinate-example-flake"; @@ -41,6 +42,7 @@ let host = "nixinatee"; sshUser = "nixinator"; buildOn = "${buildOn}"; # valid args are "local" or "remote" + hermetic = ${lib.boolToString hermetic}; # valid args are true or false }; } ]; @@ -66,7 +68,9 @@ let ]; virtualisation = { writableStore = true; - additionalPaths = [] ++ pkgs.lib.optional (buildOn == "remote") (allDrvOutputs exampleSystem); + additionalPaths = [] + ++ lib.optional (buildOn == "remote") (allDrvOutputs exampleSystem) + ++ lib.optional (hermetic == true) (pkgs.nixinate.nixos-rebuild); }; }; nixinator = { ... }: { @@ -77,7 +81,7 @@ let additionalPaths = [ (allDrvOutputs exampleSystem) ] - ++ pkgs.lib.optional (buildOn == "remote") exampleFlake; + ++ lib.optional (buildOn == "remote") exampleFlake; }; }; }; @@ -103,6 +107,8 @@ let }; in { - local = (mkNixinateTest "local"); - remote = (mkNixinateTest "remote"); + local = (mkNixinateTest { buildOn = "local"; }); + remote = (mkNixinateTest { buildOn = "remote"; }); + localHermetic = (mkNixinateTest { buildOn = "local"; hermetic = true; }); + remoteHermetic = (mkNixinateTest { buildOn = "remote"; hermetic = true; }); }