diff options
| -rw-r--r-- | README.md | 47 | ||||
| -rw-r--r-- | lib/types/populate.nix | 19 | ||||
| -rw-r--r-- | pkgs/populate/default.nix | 98 |
3 files changed, 163 insertions, 1 deletions
@@ -265,6 +265,53 @@ Supported attributes: of rsync. Defaults to `true`. +### `flake` + +The flake source type delivers a flake, or one of its locked inputs, to the +target. The flake gets archived locally using `nix flake archive`, the +selected input and its own inputs get copied into the target's Nix store using +`nix copy`, and the target path becomes a symlink to the input's store path, +registered as garbage collector root. The input's own inputs get garbage +collector roots in `.populate/flake-inputs/<name>`, which turns the sentinel +file into a directory. + +Because the target path points at the store path named by the flake's lock +file, it can be used in `NIX_PATH` as well as by flakes evaluated on the +target, without network access. + +Supported attributes: + +* `flake` - + flake reference, e.g. the absolute path of a directory containing a + `flake.nix`. Its lock file is used as is; populating fails if the lock + file needs to be updated. + +* `input` (optional) - + name of the input to deliver. Inputs of inputs are separated by `/`, + e.g. `stockholm/nixpkgs`. If omitted, the flake itself gets delivered, + which includes only files tracked by Git. Use a `file` source for work + trees with untracked files. + +Instead of an attribute set, the flake reference can be given as a string. + +The flake commands run using krops' own Nix, so flakes don't need to be +enabled on the deploying machine. The target only needs `nix-store`. +Because the copied store paths aren't signed, the target user must be allowed +to import them, i.e. be `root` or a trusted user. Targets using `sudo` aren't +supported. + +Example: + +```nix +{ + nixpkgs.flake = { + flake = toString ./.; + input = "nixpkgs"; + }; +} +``` + + ### `git` Git sources that will be fetched on the target machine. diff --git a/lib/types/populate.nix b/lib/types/populate.nix index 6264f99..a194831 100644 --- a/lib/types/populate.nix +++ b/lib/types/populate.nix @@ -31,6 +31,14 @@ source-types.file ]); }; + flake = lib.mkOption { + apply = x: + if lib.types.str.check x + then { flake = x; } + else x; + default = null; + type = lib.types.nullOr (lib.types.either lib.types.str source-types.flake); + }; git = lib.mkOption { default = null; type = lib.types.nullOr source-types.git; @@ -137,6 +145,17 @@ }; }; }; + flake = lib.types.submodule { + options = { + flake = lib.mkOption { + type = lib.types.str; # TODO lib.types.flake-ref + }; + input = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + }; + }; + }; git = lib.types.submodule { options = { clean = { diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index 221ba6e..fc577cf 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -1,7 +1,7 @@ with import ../../lib; with shell; -{ coreutils, dash, findutils, git, jq, openssh, pass, passage, rsync, writers }: +{ coreutils, dash, findutils, git, jq, nix, openssh, pass, passage, rsync, writers }: let check = { force, target }: let @@ -61,6 +61,93 @@ let in rsync' target config sourcePath; + pop.flake = target: source: let + nix' = "${nix}/bin/nix --extra-experimental-features ${quote "nix-command flakes"}"; + input = source.input or null; + # jq filter selecting the flake or input in nix flake archive's output. + node = + if input == null then "." + else concatMapStrings (name: ".inputs[${toJSON name}]") (splitString "/" input); + sentinel = "${dirOf target.path}/.populate"; + # Directory of garbage collector roots for the input's own inputs. + inputsRoots = "${sentinel}/flake-inputs/${baseNameOf target.path}"; + # Reads store paths from stdin, links the first one at the target path, and + # registers it and the remaining ones as garbage collector roots. Roots + # get added before obsolete ones get removed, so the inputs stay rooted. + link = /* sh */ '' + set -efu + read -r path + if test -e ${quote target.path} && ! test -L ${quote target.path}; then + rm -fR ${quote target.path} + fi + nix-store --realise "$path" --add-root ${quote target.path} --indirect >/dev/null + + # The sentinel only needs to exist, so it can become a directory. + if test -f ${quote sentinel}; then + rm ${quote sentinel} + mkdir ${quote sentinel} + fi + mkdir -p ${quote inputsRoots} + roots= + while read -r path; do + root=$(basename "$path") + if test "$(readlink ${quote inputsRoots}/"$root")" != "$path"; then + nix-store --realise "$path" \ + --add-root ${quote inputsRoots}/"$root" --indirect \ + >/dev/null + fi + roots="$roots $root" + done + ls -A ${quote inputsRoots} | while read -r root; do + case " $roots " in + *" $root "*) ;; + *) rm ${quote inputsRoots}/"$root" ;; + esac + done + # An input without own inputs leaves the directory empty. + if test -z "$roots"; then + rmdir ${quote inputsRoots} + fi + ''; + in + if target.sudo then + throw "krops: flake source ${target.path}: targets with sudo are not supported" + else /* sh */ '' + set -efu + + # Archive each flake only once, even if several sources use it. + archive=$TMPDIR/flake-archive.$( + printf %s ${quote source.flake} | + ${coreutils}/bin/sha256sum | + ${coreutils}/bin/cut -c-32 + ).json + if ! test -e "$archive"; then + ${nix'} flake archive --json --no-update-lock-file ${quote source.flake} \ + > "$archive" + fi + + if ! ${jq}/bin/jq -e ${quote "${node} | .path"} "$archive" >/dev/null; then + >&2 printf 'error: flake %s has no input %s\n' \ + ${quote source.flake} ${quote (toString input)} + exit 1 + fi + + # The input followed by its own inputs, which flakes evaluated on the + # target need as well. + paths=$(${jq}/bin/jq -r ${quote "${node} | .. | .path? | strings"} "$archive") + + ${optionalString (!isLocalTarget target) /* sh */ '' + mkdir -p "$PWD"/bin + ln -fns ${sshNix' target} "$PWD"/bin/ssh + PATH=$PWD/bin:$PATH \ + ${nix'} copy --to ${quote "ssh://${target.host}"} $paths >&2 + ''} + + printf '%s\n' "$paths" | { + ${runShell target link} + } + ''; + pop.git = target: source: runShell target /* sh */ '' set -efu # Remove target path if it doesn't look like a git worktree. @@ -268,6 +355,15 @@ let ${ssh' target} ${quote target.host} ${quote command} ''; + # ssh for Nix's ssh:// store. Without the control socket, because Nix waits + # for its LocalCommand, which may not run over a multiplexed connection. + sshNix' = target: writers.writeDash "ssh-nix-wrapper" '' + exec ${openssh}/bin/ssh \ + ${concatMapStringsSep " " quote (mkUserPortSSHOpts target)} \ + ${concatMapStringsSep " " quote target.extraOptions} \ + "$@" + ''; + ssh' = target: writers.writeDash "ssh-wrapper" '' exec ${openssh}/bin/ssh \ ${concatMapStringsSep " " quote (mkUserPortSSHOpts target)} \ |
