aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md47
-rw-r--r--lib/types/populate.nix19
-rw-r--r--pkgs/populate/default.nix98
3 files changed, 163 insertions, 1 deletions
diff --git a/README.md b/README.md
index d8268e5..84b1671 100644
--- a/README.md
+++ b/README.md
@@ -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)} \