default.nix 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. with import ../../lib;
  2. with shell;
  3. { coreutils, dash, findutils, git, jq, openssh, pass, rsync, writers }:
  4. let
  5. check = { force, target }: let
  6. sentinelFile = "${target.path}/.populate";
  7. in runShell target /* sh */ ''
  8. ${optionalString force /* sh */ ''
  9. mkdir -vp ${quote (dirOf sentinelFile)} >&2
  10. touch ${quote sentinelFile}
  11. ''}
  12. if ! test -e ${quote sentinelFile}; then
  13. >&2 printf 'error: missing sentinel file: %s\n' ${quote (
  14. optionalString (!isLocalTarget target) "${target.host}:" +
  15. sentinelFile
  16. )}
  17. exit 1
  18. fi
  19. '';
  20. do-backup = { target }: let
  21. sentinelFile = "${target.path}/.populate";
  22. in
  23. runShell target /* sh */ ''
  24. if ! test -d ${quote sentinelFile}; then
  25. >&2 printf 'error" sentinel file is not a directory: %s\n' ${quote (
  26. optionalString (!isLocalTarget target) "${target.host}:" +
  27. sentinelFile
  28. )}
  29. exit 1
  30. fi
  31. rsync >&2 \
  32. -aAXF \
  33. --delete \
  34. --exclude /.populate \
  35. --link-dest=${quote target.path} \
  36. ${target.path}/ \
  37. ${target.path}/.populate/backup/
  38. '';
  39. pop.derivation = target: source: runShell target /* sh */ ''
  40. nix-build -E ${quote source.text} -o ${quote target.path} >&2
  41. '';
  42. pop.file = target: source: let
  43. configAttrs = ["useChecksum" "exclude" "filters" "deleteExcluded"];
  44. config = filterAttrs (name: _: elem name configAttrs) source;
  45. in
  46. rsync' target config (quote source.path);
  47. pop.git = target: source: runShell target /* sh */ ''
  48. set -efu
  49. if ! test -e ${quote target.path}; then
  50. ${if source.shallow then /* sh */ ''
  51. git init ${quote target.path}
  52. '' else /* sh */ ''
  53. git clone --recurse-submodules ${quote source.url} ${quote target.path}
  54. ''}
  55. fi
  56. cd ${quote target.path}
  57. if ! url=$(git config remote.origin.url); then
  58. git remote add origin ${quote source.url}
  59. elif test "$url" != ${quote source.url}; then
  60. git remote set-url origin ${quote source.url}
  61. fi
  62. # TODO resolve git_ref to commit hash
  63. hash=${quote source.ref}
  64. if ! test "$(git log --format=%H -1)" = "$hash"; then
  65. ${if source.fetchAlways then /* sh */ ''
  66. ${if source.shallow then /* sh */ ''
  67. git fetch --depth=1 origin "$hash"
  68. '' else /* sh */ ''
  69. git fetch origin
  70. ''}
  71. '' else /* sh */ ''
  72. if ! git log -1 "$hash" >/dev/null 2>&1; then
  73. ${if source.shallow then /* sh */ ''
  74. git fetch --depth=1 origin "$hash"
  75. '' else /* sh */ ''
  76. git fetch origin
  77. ''}
  78. fi
  79. ''}
  80. git reset --hard "$hash" >&2
  81. git submodule update --init --recursive
  82. fi
  83. git clean -dfx \
  84. ${concatMapStringsSep " "
  85. (pattern: /* sh */ "-e ${quote pattern}")
  86. source.clean.exclude }
  87. '';
  88. pop.pass = target: source: let
  89. passPrefix = "${source.dir}/${source.name}";
  90. in /* sh */ ''
  91. set -efu
  92. umask 0077
  93. if test -e ${quote source.dir}/.git; then
  94. local_pass_info=${quote source.name}\ $(${git}/bin/git -C ${quote source.dir} log -1 --format=%H ${quote source.name})
  95. remote_pass_info=$(${runShell target /* sh */ ''
  96. cat ${quote target.path}/.pass_info || :
  97. ''})
  98. if test "$local_pass_info" = "$remote_pass_info"; then
  99. exit 0
  100. fi
  101. fi
  102. tmp_dir=$(${coreutils}/bin/mktemp -dt populate-pass.XXXXXXXX)
  103. trap cleanup EXIT
  104. cleanup() {
  105. rm -fR "$tmp_dir"
  106. }
  107. ${findutils}/bin/find ${quote passPrefix} -type f |
  108. while read -r gpg_path; do
  109. rel_name=''${gpg_path#${quote passPrefix}}
  110. rel_name=''${rel_name%.gpg}
  111. pass_date=$(
  112. ${git}/bin/git -C ${quote source.dir} log -1 --format=%aI "$gpg_path"
  113. )
  114. pass_name=${quote source.name}/$rel_name
  115. tmp_path=$tmp_dir/$rel_name
  116. ${coreutils}/bin/mkdir -p "$(${coreutils}/bin/dirname "$tmp_path")"
  117. PASSWORD_STORE_DIR=${quote source.dir} ${pass}/bin/pass show "$pass_name" > "$tmp_path"
  118. ${coreutils}/bin/touch -d "$pass_date" "$tmp_path"
  119. done
  120. if test -n "''${local_pass_info-}"; then
  121. echo "$local_pass_info" > "$tmp_dir"/.pass_info
  122. fi
  123. ${rsync' target {} /* sh */ "$tmp_dir"}
  124. '';
  125. pop.pipe = target: source: /* sh */ ''
  126. ${quote source.command} | {
  127. ${runShell target /* sh */ "cat > ${quote target.path}"}
  128. }
  129. '';
  130. # TODO rm -fR instead of ln -f?
  131. pop.symlink = target: source: runShell target /* sh */ ''
  132. ln -fnsT ${quote source.target} ${quote target.path}
  133. '';
  134. populate = target: name: source: let
  135. source' = source.${source.type};
  136. target' = target // { path = "${target.path}/${name}"; };
  137. in writers.writeDash "populate.${target'.host}.${name}" ''
  138. set -efu
  139. ${pop.${source.type} target' source'}
  140. '';
  141. rsync' = target: config: sourcePath: /* sh */ ''
  142. source_path=${sourcePath}
  143. if test -d "$source_path"; then
  144. source_path=$source_path/
  145. fi
  146. ${rsync}/bin/rsync \
  147. ${optionalString (config.useChecksum or false) /* sh */ "--checksum"} \
  148. ${optionalString target.sudo /* sh */ "--rsync-path=\"sudo rsync\""} \
  149. ${concatMapStringsSep " "
  150. (pattern: /* sh */ "--exclude ${quote pattern}")
  151. (config.exclude or [])} \
  152. ${concatMapStringsSep " "
  153. (filter: /* sh */ "--${filter.type} ${quote filter.pattern}")
  154. (config.filters or [])} \
  155. -e ${quote (ssh' target)} \
  156. -vFrlptD \
  157. ${optionalString (config.deleteExcluded or true) /* sh */ "--delete-excluded"} \
  158. "$source_path" \
  159. ${quote (
  160. optionalString (!isLocalTarget target) (
  161. (optionalString (target.user != "") "${target.user}@") +
  162. "${target.host}:"
  163. ) +
  164. target.path
  165. )} \
  166. >&2
  167. '';
  168. runShell = target: command:
  169. if isLocalTarget target
  170. then command
  171. else
  172. if target.sudo then /* sh */ ''
  173. ${ssh' target} ${quote target.host} ${quote "sudo bash -c ${quote command}"}
  174. '' else ''
  175. ${ssh' target} ${quote target.host} ${quote command}
  176. '';
  177. ssh' = target: concatMapStringsSep " " quote (flatten [
  178. "${openssh}/bin/ssh"
  179. (optionals (target.user != "") ["-l" target.user])
  180. "-p" target.port
  181. "-T"
  182. target.extraOptions
  183. ]);
  184. in
  185. { backup ? false, force ? false, source, target }:
  186. writers.writeDash "populate.${target.host}" ''
  187. set -efu
  188. ${check { inherit force target; }}
  189. set -x
  190. ${optionalString backup (do-backup { inherit target; })}
  191. ${concatStringsSep "\n" (mapAttrsToList (populate target) source)}
  192. ''