diff options
| -rw-r--r-- | lib/default.nix | 16 | ||||
| -rw-r--r-- | pkgs/krops/default.nix | 20 | ||||
| -rw-r--r-- | pkgs/populate/default.nix | 148 | ||||
| -rwxr-xr-x | pkgs/populate/populate.sh | 280 | 
4 files changed, 153 insertions, 311 deletions
| diff --git a/lib/default.nix b/lib/default.nix index 7197fe9..3ebefdc 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -34,6 +34,12 @@ let {        if lib.length y != 1 then throw "malformed /etc/hostname" else        lib.elemAt y 0; +    isLocalTarget = let +      origin = lib.mkTarget ""; +    in target: +      target.user == origin.user && +      lib.elem target.host [origin.host "localhost"]; +      mkTarget = s: let        default = defVal: val: if val != null then val else defVal;        parse = lib.match "(([^@]+)@)?(([^:/]+))?(:([^/]+))?(/.*)?" s; @@ -45,6 +51,16 @@ let {        path = default "/var/src" /* no default? */ (elemAt' parse 6);      }; +    shell = let +      isSafeChar = lib.testString "[-+./0-9:=A-Z_a-z]"; +      quoteChar = c: +        if isSafeChar c then c +        else if c == "\n" then "'\n'" +        else "\\${c}"; +    in { +      quote = x: if x == "" then "''" else lib.stringAsChars quoteChar x; +    }; +      test = re: x: lib.isString x && lib.testString re x;      testString = re: x: lib.match re x != null; diff --git a/pkgs/krops/default.nix b/pkgs/krops/default.nix index fc52327..d2f9c8a 100644 --- a/pkgs/krops/default.nix +++ b/pkgs/krops/default.nix @@ -1,11 +1,5 @@  let -  lib = import ../../lib // { -    isLocalTarget = let -      origin = lib.mkTarget ""; -    in target: -      target.host == origin.host && -      target.user == origin.user; -  }; +  lib = import ../../lib;  in  { nix, openssh, populate, writeDash, writeJSON }: { @@ -15,11 +9,7 @@ in    in      writeDash name ''        set -efu - -      ${populate}/bin/populate \ -          ${target'.user}@${target'.host}:${target'.port}${target'.path} \ -        < ${writeJSON "${name}-source.json" source} - +      ${populate { inherit source; target = target'; }}        ${openssh}/bin/ssh \            ${target'.user}@${target'.host} -p ${target'.port} \            nixos-rebuild switch -I ${target'.path} @@ -31,11 +21,7 @@ in      assert lib.isLocalTarget target';      writeDash name ''        set -efu - -      ${populate}/bin/populate --force \ -          ${target'.path} \ -        < ${writeJSON "${name}-source.json" source} - +      ${populate { inherit source; target = target'; }}        ${nix}/bin/nix-build \            -A config.system.build.toplevel \            -I ${target'.path} \ diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index acb5a5f..f0eb7d1 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -1,20 +1,140 @@ -{ coreutils, findutils, git, gnused, jq, openssh, pass, rsync, runCommand, stdenv }: +with import ../../lib; +with shell; + +{ coreutils, dash, findutils, git, jq, openssh, rsync, writeDash }:  let -  PATH = stdenv.lib.makeBinPath [ -    coreutils -    findutils -    git -    gnused -    jq -    openssh -    pass -    rsync +  check = { force, target }: let +    sentinelFile = "${target.path}/.populate"; +  in shell' target /* sh */ '' +    ${optionalString force /* sh */ '' +      mkdir -vp ${quote (dirOf sentinelFile)} +      touch ${quote sentinelFile} +    ''} +    if ! test -f ${quote sentinelFile}; then +      >&2 printf 'error: missing sentinel file: %s\n' ${quote ( +        optionalString (!isLocalTarget target) "${target.host}:" + +        sentinelFile +      )} +      exit 1 +    fi +  ''; + +  pop.file = target: file: rsync' target (quote file.path); + +  pop.git = target: git: shell' target /* sh */ '' +    if ! test -e ${quote target.path}; then +      git clone --recurse-submodules ${quote git.url} ${quote target.path} +    fi +    cd ${quote target.path} +    if ! url=$(git config remote.origin.url); then +      git remote add origin ${quote git.url} +    elif test "$url" != ${quote git.url}; then +      git remote set-url origin ${quote git.url} +    fi + +    # TODO resolve git_ref to commit hash +    hash=${quote git.ref} + +    if ! test "$(git log --format=%H -1)" = "$hash"; then +      if ! git log -1 "$hash" >/dev/null 2>&1; then +        git fetch origin +      fi +      git checkout "$hash" -- ${quote target.path} +      git -c advice.detachedHead=false checkout -f "$hash" +      git submodule update --init --recursive +    fi + +    git clean -dfx +  ''; + +  pop.pass = target: pass: let +    passPrefix = "${pass.dir}/${pass.name}"; +  in /* sh */ '' +    umask 0077 + +    tmp_dir=$(${coreutils}/bin/mktemp -dt populate-pass.XXXXXXXX) +    trap cleanup EXIT +    cleanup() { +      rm -fR "$tmp_dir" +    } + +    ${findutils}/bin/find ${quote passPrefix} -type f | +    while read -r gpg_path; do + +      rel_name=''${gpg_path#${quote passPrefix}} +      rel_name=''${rel_name%.gpg} + +      pass_date=$( +        ${git}/bin/git -C ${quote pass.dir} log -1 --format=%aI "$gpg_path" +      ) +      pass_name=${quote pass.name}/$rel_name +      tmp_path=$tmp_dir/$rel_name + +      ${coreutils}/bin/mkdir -p "$(${coreutils}/bin/dirname "$tmp_path")" +      PASSWORD_STORE_DIR=${quote pass.dir} pass show "$pass_name" > "$tmp_path" +      ${coreutils}/bin/touch -d "$pass_date" "$tmp_path" +    done + +    ${rsync' target /* sh */ "$tmp_dir"} +  ''; + +  pop.pipe = target: pipe: /* sh */ '' +    ${quote pipe.command} | { +      ${shell' target /* sh */ "cat > ${quote target.path}"} +    } +  ''; + +  # TODO rm -fR instead of ln -f? +  pop.symlink = target: symlink: shell' target /* sh */ '' +    ln -fns ${quote symlink.target} ${quote target.path} +  ''; + +  populate = target: name: source: let +    source' = source.${source.type}; +    target' = target // { path = "${target.path}/${name}"; }; +  in writeDash "populate.${target'.host}.${name}" '' +    set -efu +    ${pop.${source.type} target' source'} +  ''; + +  rsync' = target: sourcePath: /* sh */ '' +    source_path=${sourcePath} +    if test -d "$source_path"; then +      source_path=$source_path/ +    fi +    ${rsync}/bin/rsync \ +        -e ${quote (ssh' target)} \ +        -vFrlptD \ +        --delete-excluded \ +        "$source_path" \ +        ${quote ( +          optionalString (!isLocalTarget target) +                         "${target.user}@${target.host}:" + +          target.path +        )} +  ''; + +  shell' = target: script: +    if isLocalTarget target +      then script +      else /* sh */ '' +        ${ssh' target} ${quote target.host} ${quote script} +      ''; + +  ssh' = target: concatMapStringsSep " " quote [ +    "${openssh}/bin/ssh" +    "-l" target.user +    "-o" "ControlPersist=no" +    "-p" target.port +    "-T"    ]; +  in -runCommand "populate-2.2.0" {} '' -  mkdir -p $out/bin -  cp ${./populate.sh} $out/bin/populate -  sed -i '1s,.*,&\nPATH=${PATH},' $out/bin/populate +{ force ? false, source, target }: writeDash "populate.${target.host}" '' +  set -efu +  ${check { inherit force target; }} +  set -x +  ${concatStringsSep "\n" (mapAttrsToList (populate target) source)}  '' diff --git a/pkgs/populate/populate.sh b/pkgs/populate/populate.sh deleted file mode 100755 index 9627fb7..0000000 --- a/pkgs/populate/populate.sh +++ /dev/null @@ -1,280 +0,0 @@ -#! /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 "$@" | 
