krops/pkgs/populate/default.nix

225 lines
6.7 KiB
Nix

with import ../../lib;
with shell;
{ coreutils, dash, findutils, git, jq, openssh, pass, rsync, writers }:
let
check = { force, target }: let
sentinelFile = "${target.path}/.populate";
in runShell target /* sh */ ''
${optionalString force /* sh */ ''
mkdir -vp ${quote (dirOf sentinelFile)} >&2
touch ${quote sentinelFile}
''}
if ! test -e ${quote sentinelFile}; then
>&2 printf 'error: missing sentinel file: %s\n' ${quote (
optionalString (!isLocalTarget target) "${target.host}:" +
sentinelFile
)}
exit 1
fi
'';
do-backup = { target }: let
sentinelFile = "${target.path}/.populate";
in
runShell target /* sh */ ''
if ! test -d ${quote sentinelFile}; then
>&2 printf 'error" sentinel file is not a directory: %s\n' ${quote (
optionalString (!isLocalTarget target) "${target.host}:" +
sentinelFile
)}
exit 1
fi
rsync >&2 \
-aAXF \
--delete \
--exclude /.populate \
--link-dest=${quote target.path} \
${target.path}/ \
${target.path}/.populate/backup/
'';
pop.derivation = target: source: runShell target /* sh */ ''
nix-build -E ${quote source.text} -o ${quote target.path} >&2
'';
pop.file = target: source: let
configAttrs = ["useChecksum" "exclude" "filters" "deleteExcluded"];
config = filterAttrs (name: _: elem name configAttrs) source;
in
rsync' target config (quote source.path);
pop.git = target: source: runShell target /* sh */ ''
set -efu
if ! test -e ${quote target.path}; then
${if source.shallow then /* sh */ ''
git init ${quote target.path}
'' else /* sh */ ''
git clone --recurse-submodules ${quote source.url} ${quote target.path}
''}
fi
cd ${quote target.path}
if ! url=$(git config remote.origin.url); then
git remote add origin ${quote source.url}
elif test "$url" != ${quote source.url}; then
git remote set-url origin ${quote source.url}
fi
# TODO resolve git_ref to commit hash
hash=${quote source.ref}
if ! test "$(git log --format=%H -1)" = "$hash"; then
${if source.fetchAlways then /* sh */ ''
${if source.shallow then /* sh */ ''
git fetch --depth=1 origin "$hash"
'' else /* sh */ ''
git fetch origin
''}
'' else /* sh */ ''
if ! git log -1 "$hash" >/dev/null 2>&1; then
${if source.shallow then /* sh */ ''
git fetch --depth=1 origin "$hash"
'' else /* sh */ ''
git fetch origin
''}
fi
''}
git reset --hard "$hash" >&2
git submodule update --init --recursive
fi
git clean -dfx \
${concatMapStringsSep " "
(pattern: /* sh */ "-e ${quote pattern}")
source.clean.exclude }
'';
pop.pass = target: source: let
passPrefix = "${source.dir}/${source.name}";
in /* sh */ ''
set -efu
umask 0077
if test -e ${quote source.dir}/.git; then
local_pass_info=${quote source.name}\ $(${git}/bin/git -C ${quote source.dir} log -1 --format=%H ${quote source.name})
remote_pass_info=$(${runShell target /* sh */ ''
cat ${quote target.path}/.pass_info || :
''})
if test "$local_pass_info" = "$remote_pass_info"; then
exit 0
fi
fi
tmp_dir=$(${coreutils}/bin/mktemp -dt populate-pass.XXXXXXXX)
trap cleanup EXIT
cleanup() {
rm -fR "$tmp_dir"
}
${findutils}/bin/find ${quote passPrefix} -type f -follow ! -name .gpg-id |
while read -r gpg_path; do
rel_name=''${gpg_path#${quote passPrefix}}
rel_name=''${rel_name%.gpg}
pass_date=$(
if test -e ${quote source.dir}/.git; then
${git}/bin/git -C ${quote source.dir} log -1 --format=%aI "$gpg_path"
fi
)
pass_name=${quote source.name}/$rel_name
tmp_path=$tmp_dir/$rel_name
${coreutils}/bin/mkdir -p "$(${coreutils}/bin/dirname "$tmp_path")"
PASSWORD_STORE_DIR=${quote source.dir} ${pass}/bin/pass show "$pass_name" > "$tmp_path"
if [ -n "$pass_date" ]; then
${coreutils}/bin/touch -d "$pass_date" "$tmp_path"
fi
done
if test -n "''${local_pass_info-}"; then
echo "$local_pass_info" > "$tmp_dir"/.pass_info
fi
${rsync' target {} /* sh */ "$tmp_dir"}
'';
pop.pipe = target: source: /* sh */ ''
${quote source.command} | {
${runShell target /* sh */ "cat > ${quote target.path}"}
}
'';
# TODO rm -fR instead of ln -f?
pop.symlink = target: source: runShell target /* sh */ ''
ln -fnsT ${quote source.target} ${quote target.path}
'';
populate = target: name: source: let
source' = source.${source.type};
target' = target // { path = "${target.path}/${name}"; };
in writers.writeDash "populate.${target'.host}.${name}" ''
set -efu
${pop.${source.type} target' source'}
'';
rsync' = target: config: sourcePath: /* sh */ ''
source_path=${sourcePath}
if test -d "$source_path"; then
source_path=$source_path/
fi
${rsync}/bin/rsync \
${optionalString (config.useChecksum or false) /* sh */ "--checksum"} \
${optionalString target.sudo /* sh */ "--rsync-path=\"sudo rsync\""} \
${concatMapStringsSep " "
(pattern: /* sh */ "--exclude ${quote pattern}")
(config.exclude or [])} \
${concatMapStringsSep " "
(filter: /* sh */ "--${filter.type} ${quote filter.pattern}")
(config.filters or [])} \
-e ${quote (ssh' target)} \
-vFrlptD \
${optionalString (config.deleteExcluded or true) /* sh */ "--delete-excluded"} \
"$source_path" \
${quote (
optionalString (!isLocalTarget target) (
(optionalString (target.user != "") "${target.user}@") +
"${target.host}:"
) +
target.path
)} \
>&2
'';
runShell = target: command:
if isLocalTarget target
then command
else
if target.sudo then /* sh */ ''
${ssh' target} ${quote target.host} ${quote "sudo bash -c ${quote command}"}
'' else ''
${ssh' target} ${quote target.host} ${quote command}
'';
ssh' = target: concatMapStringsSep " " quote (flatten [
"${openssh}/bin/ssh"
(optionals (target.user != "") ["-l" target.user])
"-p" target.port
"-T"
target.extraOptions
]);
in
{ backup ? false, force ? false, source, target }:
writers.writeDash "populate.${target.host}" ''
set -efu
${check { inherit force target; }}
set -x
${optionalString backup (do-backup { inherit target; })}
${concatStringsSep "\n" (mapAttrsToList (populate target) source)}
''