Merge pull request #20 from MatthewCroughan/mc/hermetic-remote

Add support for hermetic remote builds
This commit is contained in:
MatthewCroughan 2022-05-30 19:53:20 +01:00 committed by GitHub
commit 949e4f41b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 11 deletions

View file

@ -32,6 +32,7 @@ Below is a minimal example:
host = "itchy.scratchy.com"; host = "itchy.scratchy.com";
sshUser = "matthew"; sshUser = "matthew";
buildOn = "remote"; # valid args are "local" or "remote" buildOn = "remote"; # valid args are "local" or "remote"
hermetic = false;
}; };
} }
# ... other configuration ... # ... other configuration ...
@ -76,3 +77,30 @@ reloading user units for matthew...
setting up tmpfiles setting up tmpfiles
Connection to itchy.scratchy.com closed. 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.

View file

@ -16,32 +16,49 @@
{ {
herculesCI.ciSystems = [ "x86_64-linux" ]; herculesCI.ciSystems = [ "x86_64-linux" ];
overlay = final: prev: { 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: generateApps = flake:
let let
machines = builtins.attrNames flake.nixosConfigurations; machines = builtins.attrNames flake.nixosConfigurations;
validMachines = final.lib.remove "" (final.lib.forEach machines (x: final.lib.optionalString (flake.nixosConfigurations."${x}"._module.args ? nixinate) "${x}" )); validMachines = final.lib.remove "" (final.lib.forEach machines (x: final.lib.optionalString (flake.nixosConfigurations."${x}"._module.args ? nixinate) "${x}" ));
mkDeployScript = { machine, dryRun }: let mkDeployScript = { machine, dryRun }: let
inherit (builtins) abort; 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; n = flake.nixosConfigurations.${machine}._module.args.nixinate;
hermetic = n.hermetic or false;
user = n.sshUser or "root"; user = n.sshUser or "root";
host = n.host; host = n.host;
where = n.buildOn or "remote"; 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'"; 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"; switch = if dryRun then "dry-activate" else "switch";
script = '' script =
''
set -e set -e
echo "🚀 Deploying nixosConfigurations.${machine} from ${flake}" echo "🚀 Deploying nixosConfigurations.${machine} from ${flake}"
echo "👤 SSH User: ${user}" echo "👤 SSH User: ${user}"
echo "🌐 SSH Host: ${host}" echo "🌐 SSH Host: ${host}"
'' + (if remote then '' '' + (if remote then ''
echo "🚀 Sending flake to ${machine} via nix copy:" echo "🚀 Sending flake to ${machine} via nix copy:"
( set -x; ${final.nix}/bin/nix copy ${flake} --to ssh://${user}@${host} ) ( set -x; ${nix} copy ${flake} --to ssh://${user}@${host} )
echo "🤞 Activating configuration on ${machine} via ssh:" '' + (if hermetic then ''
( set -x; ${final.openssh}/bin/ssh -t ${user}@${host} 'sudo nixos-rebuild ${switch} --flake ${flake}#${machine}' ) 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 '' '' 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:" 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 final.writeScript "deploy-${machine}.sh" script;
in in
@ -78,7 +95,7 @@
let let
vmTests = import ./tests { vmTests = import ./tests {
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest; makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit pkgs inputs; inherit inputs; pkgs = nixpkgsFor.${system};
}; };
in in
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux. pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.

View file

@ -1,5 +1,6 @@
{ pkgs, makeTest, inputs }: { pkgs, makeTest, inputs }:
let let
inherit (pkgs) lib;
# Return a store path with a closure containing everything including # Return a store path with a closure containing everything including
# derivations and all build dependency outputs, all the way down. # derivations and all build dependency outputs, all the way down.
allDrvOutputs = pkg: allDrvOutputs = pkg:
@ -22,7 +23,7 @@ let
((import (path + "/flake.nix")).outputs (inputs // {self = r;})); ((import (path + "/flake.nix")).outputs (inputs // {self = r;}));
in in
r; r;
mkNixinateTest = buildOn: mkNixinateTest = { buildOn, hermetic ? false, ... }:
let let
exampleFlake = pkgs.writeTextFile { exampleFlake = pkgs.writeTextFile {
name = "nixinate-example-flake"; name = "nixinate-example-flake";
@ -41,6 +42,7 @@ let
host = "nixinatee"; host = "nixinatee";
sshUser = "nixinator"; sshUser = "nixinator";
buildOn = "${buildOn}"; # valid args are "local" or "remote" buildOn = "${buildOn}"; # valid args are "local" or "remote"
hermetic = ${lib.boolToString hermetic}; # valid args are true or false
}; };
} }
]; ];
@ -66,7 +68,9 @@ let
]; ];
virtualisation = { virtualisation = {
writableStore = true; 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 = { ... }: { nixinator = { ... }: {
@ -77,7 +81,7 @@ let
additionalPaths = [ additionalPaths = [
(allDrvOutputs exampleSystem) (allDrvOutputs exampleSystem)
] ]
++ pkgs.lib.optional (buildOn == "remote") exampleFlake; ++ lib.optional (buildOn == "remote") exampleFlake;
}; };
}; };
}; };
@ -103,6 +107,8 @@ let
}; };
in in
{ {
local = (mkNixinateTest "local"); local = (mkNixinateTest { buildOn = "local"; });
remote = (mkNixinateTest "remote"); remote = (mkNixinateTest { buildOn = "remote"; });
localHermetic = (mkNixinateTest { buildOn = "local"; hermetic = true; });
remoteHermetic = (mkNixinateTest { buildOn = "remote"; hermetic = true; });
} }