aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/populate/populate.sh
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/populate/populate.sh')
-rwxr-xr-xpkgs/populate/populate.sh276
1 files changed, 276 insertions, 0 deletions
diff --git a/pkgs/populate/populate.sh b/pkgs/populate/populate.sh
new file mode 100755
index 0000000..670d25b
--- /dev/null
+++ b/pkgs/populate/populate.sh
@@ -0,0 +1,276 @@
+#! /bin/sh
+set -efu
+
+main() {(
+ self=$(readlink -f "$0")
+ basename=${0##*/}
+
+ debug=false
+ force=false
+ origin_host=${HOSTNAME-cat /proc/sys/kernel/hostname}
+ origin_user=$LOGNAME
+ target_spec=
+
+
+ abort=false
+
+ error() {
+ echo "$basename: error: $1" >&2
+ abort=true
+ }
+
+ for arg; do
+ case $arg in
+ --force)
+ force=true
+ ;;
+ -*)
+ error "bad argument: $arg"
+ ;;
+ *)
+ if test -n "$target_spec"; then
+ error "bad argument: $arg"
+ else
+ target_spec=$arg
+ fi
+ ;;
+ esac
+ done
+
+ if test -z "$target_spec"; then
+ error 'no target specified'
+ fi
+
+ if test "$abort" = true; then
+ exit 11
+ fi
+
+ target=$(
+ export origin_host
+ export origin_user
+ echo "$target_spec" | jq -R '
+ def default(value; f): if . == null then value else f end;
+ def default(value): default(value; .);
+
+ match("^(?:([^@]+)@)?(?:([^:/]+))?(?::([^/]+))?(/.*)?")
+ | {
+ user: .captures[0].string | default(env.origin_user),
+ host: .captures[1].string | default(env.origin_host),
+ port: .captures[2].string | default(22;
+ if test("^[0-9]+$") then fromjson else
+ error(@json "bad target port: \(.)")
+ end),
+ path: .captures[3].string | default("/var/src"),
+ }
+ '
+ )
+
+ echo $target | jq . >&2
+
+ target_host=$(echo $target | jq -r .host)
+ target_path=$(echo $target | jq -r .path)
+ target_port=$(echo $target | jq -r .port)
+ target_user=$(echo $target | jq -r .user)
+
+ if test "$force" = true; then
+ force_target
+ else
+ check_target
+ fi
+
+ jq -c 'to_entries | group_by(.value.type) | flatten[]' |
+ while read -r source; do
+ key=$(echo "$source" | jq -r .key)
+ type=$(echo "$source" | jq -r .value.type)
+ conf=$(echo "$source" | jq -r .value.${type})
+
+ printf '\e[1;33m%s\e[m\n' "populate_$type $key $conf" >&2
+
+ populate_"$type" "$key" "$conf"
+ done
+)}
+
+# Safeguard to prevent clobbering of misspelled targets.
+# This function has to be called first.
+check_target() {
+ {
+ echo target_host=$(quote "$target_host")
+ echo target_path=$(quote "$target_path")
+ echo 'sentinel_file=$target_path/.populate'
+ echo 'if ! test -f "$sentinel_file"; then'
+ echo ' echo "error: missing sentinel file: $target_host:$sentinel_file" >&2'
+ echo ' exit 1'
+ echo 'fi'
+ } \
+ |
+ target_shell
+}
+
+force_target() {
+ {
+ echo target_path=$(quote "$target_path")
+ echo 'sentinel_file=$target_path/.populate'
+ echo 'mkdir -vp "$target_path"'
+ echo 'touch "$sentinel_file"'
+ } \
+ |
+ target_shell
+}
+
+is_local_target() {
+ test "$target_host" = "$origin_host" &&
+ test "$target_user" = "$origin_user"
+}
+
+populate_file() {(
+ file_name=$1
+ file_path=$(echo "$2" | jq -r .path)
+
+ if is_local_target; then
+ file_target=$target_path/$file_name
+ else
+ file_target=$target_user@$target_host:$target_path/$file_name
+ fi
+
+ rsync \
+ -vFrlptD \
+ --delete-excluded \
+ "$file_path"/ \
+ -e "ssh -o ControlPersist=no -p $target_port" \
+ "$file_target"
+)}
+
+populate_git() {(
+ git_name=$1
+ git_url=$(echo "$2" | jq -r .url)
+ git_ref=$(echo "$2" | jq -r .ref)
+
+ git_work_tree=$target_path/$git_name
+
+ {
+ echo set -efu
+
+ echo git_url=$(quote "$git_url")
+ echo git_ref=$(quote "$git_ref")
+
+ echo git_work_tree=$(quote "$git_work_tree")
+
+ echo 'if ! test -e "$git_work_tree"; then'
+ echo ' git clone "$git_url" "$git_work_tree"'
+ echo 'fi'
+
+ echo 'cd $git_work_tree'
+
+ echo 'if ! url=$(git config remote.origin.url); then'
+ echo ' git remote add origin "$git_url"'
+ echo 'elif test "$url" != "$git_url"; then'
+ echo ' git remote set-url origin "$git_url"'
+ echo 'fi'
+
+ # TODO resolve git_ref to commit hash
+ echo 'hash=$git_ref'
+
+ echo 'if ! test "$(git log --format=%H -1)" = "$hash"; then'
+ echo ' if ! git log -1 "$hash" >/dev/null 2>&1; then'
+ echo ' git fetch origin'
+ echo ' fi'
+ echo ' git checkout "$hash" -- "$git_work_tree"'
+ echo ' git -c advice.detachedHead=false checkout -f "$hash"'
+ echo 'fi'
+
+ echo 'git clean -dfx'
+
+ } \
+ |
+ target_shell
+)}
+
+populate_pass() {(
+ pass_target_name=$1
+ pass_dir=$(echo "$2" | jq -r .dir)
+ pass_name_root=$(echo "$2" | jq -r .name)
+
+ if is_local_target; then
+ pass_target=$target_path/$pass_target_name
+ else
+ pass_target=$target_user@$target_host:$target_path/$pass_target_name
+ fi
+
+ umask 0077
+
+ tmp_dir=$(mktemp -dt populate-pass.XXXXXXXX)
+ trap cleanup EXIT
+ cleanup() {
+ rm -fR "$tmp_dir"
+ }
+
+ pass_prefix=$pass_dir/$pass_name_root/
+
+ find "$pass_prefix" -type f |
+ while read -r pass_gpg_file_path; do
+
+ rel_name=${pass_gpg_file_path:${#pass_prefix}}
+ rel_name=${rel_name%.gpg}
+
+ pass_name=$pass_name_root/$rel_name
+ tmp_path=$tmp_dir/$rel_name
+
+ mkdir -p "$(dirname "$tmp_path")"
+ PASSWORD_STORE_DIR=$pass_dir pass show "$pass_name" > "$tmp_path"
+ done
+
+ rsync \
+ --checksum \
+ -vFrlptD \
+ --delete-excluded \
+ "$tmp_dir"/ \
+ -e "ssh -o ControlPersist=no -p $target_port" \
+ "$pass_target"
+)}
+
+populate_pipe() {(
+ pipe_target_name=$1
+ pipe_command=$(echo "$2" | jq -r .command)
+
+ result_path=$target_path/$pipe_target_name
+
+ "$pipe_command" | target_shell -c "cat > $(quote "$result_path")"
+)}
+
+populate_symlink() {(
+ symlink_name=$1
+ symlink_target=$(echo "$2" | jq -r .target)
+ link_name=$target_path/$symlink_name
+
+ {
+ # TODO rm -fR instead of ln -f?
+ echo ln -fns $(quote "$symlink_target" "$link_name")
+ } \
+ |
+ target_shell
+)}
+
+quote() {
+ printf %s "$1" | sed 's/./\\&/g'
+ while test $# -gt 1; do
+ printf ' '
+ shift
+ printf %s "$1" | sed 's/./\\&/g'
+ done
+ echo
+}
+
+target_shell() {
+ if is_local_target; then
+ /bin/sh "$@"
+ else
+ ssh "$target_host" \
+ -l "$target_user" \
+ -o ControlPersist=no \
+ -p "$target_port" \
+ -T \
+ /bin/sh "$@"
+ fi
+}
+
+main "$@"