krops/pkgs/populate/populate.sh

281 lines
5.9 KiB
Bash
Executable File

#! /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
if test -d "$file_path"; then
file_path=$file_path/
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 "$@"