| 1 | #!/usr/bin/env bash
|
| 2 | #
|
| 3 | # Pull the live deployment configs from the VPS and diff them against the repo
|
| 4 | # Read-only on the server. Exits non-zero if anything has drifted
|
| 5 | #
|
| 6 | # INTERNAL tool, run locally (or via `make deploy-diff`). It is deliberately
|
| 7 | # not wired into CI: it is more useful interactively than as a scheduled job
|
| 8 | #
|
| 9 | # ansible/scripts/pull-live-configs.sh
|
| 10 | #
|
| 11 | # Connection details are read from ansible/inventory/hosts.yml (git-ignored), or
|
| 12 | # from the VPS_HOST / VPS_USER / VPS_KEY environment variables
|
| 13 |
|
| 14 | set -euo pipefail
|
| 15 |
|
| 16 | repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
| 17 | ansible_dir="$repo_root/ansible"
|
| 18 | inventory="$ansible_dir/inventory/hosts.yml"
|
| 19 |
|
| 20 | # first value of a key in the (flat) inventory
|
| 21 | field() { awk -F': *' -v k="^[[:space:]]*$1" '$0 ~ k {print $2; exit}' "$inventory"; }
|
| 22 |
|
| 23 | HOST="${VPS_HOST:-}"
|
| 24 | USER="${VPS_USER:-}"
|
| 25 | KEY="${VPS_KEY:-}"
|
| 26 |
|
| 27 | if [ -f "$inventory" ]; then
|
| 28 | [ -n "$HOST" ] || HOST="$(field 'ansible_host:')"
|
| 29 | [ -n "$USER" ] || USER="$(field 'ansible_user:')"
|
| 30 | [ -n "$KEY" ] || KEY="$(field 'ansible_ssh_private_key_file:')"
|
| 31 | fi
|
| 32 |
|
| 33 | KEY="${KEY/#\~/$HOME}"
|
| 34 | [ -n "$HOST" ] || { echo "no host: set VPS_HOST or provide ansible/inventory/hosts.yml" >&2; exit 2; }
|
| 35 | [ -n "$USER" ] || { echo "no user: set VPS_USER or provide ansible/inventory/hosts.yml" >&2; exit 2; }
|
| 36 | KEY="${KEY:-$HOME/.ssh/id_ed25519}"
|
| 37 |
|
| 38 | tmp="$(mktemp -d)"
|
| 39 | trap 'rm -rf "$tmp"' EXIT
|
| 40 |
|
| 41 | # First vhost from vars/vhosts.yml, e.g. "usenexo.xyz"
|
| 42 | vhost="$(awk '/^vhosts:/{f=1;next} f&&/^[[:space:]]*- /{print $2; exit}' "$ansible_dir/vars/vhosts.yml")"
|
| 43 | [ -n "$vhost" ] || { echo "could not determine vhost from vars/vhosts.yml" >&2; exit 2; }
|
| 44 |
|
| 45 | ssh_opts=(-i "$KEY" -o StrictHostKeyChecking=accept-new)
|
| 46 |
|
| 47 | echo "Pulling live configs from ${USER}@${HOST} ..."
|
| 48 |
|
| 49 | pull() { # <remote path> <local name>
|
| 50 | if ssh "${ssh_opts[@]}" "${USER}@${HOST}" "cat '$1'" >"$tmp/$2" 2>/dev/null; then
|
| 51 | echo " pulled $1"
|
| 52 | else
|
| 53 | echo " MISSING on server: $1" >&2
|
| 54 | rm -f "$tmp/$2"
|
| 55 | fi
|
| 56 | }
|
| 57 |
|
| 58 | pull "/etc/nginx/http.d/${vhost}.conf" nginx.conf
|
| 59 | pull /srv/app/production/docker-compose.yml docker-compose.yml
|
| 60 | pull /srv/app/production/valkey.conf valkey.conf
|
| 61 |
|
| 62 | # The nginx config is templated; render it the same way the nginx role would
|
| 63 | render_nginx() {
|
| 64 | sed \
|
| 65 | -e "s/{{ vhosts\[0\] }}/${vhost}/g" \
|
| 66 | -e 's/{{ nginx_cert_dir }}/\/etc\/ssl\/uacme/g' \
|
| 67 | -e 's/{{ nginx_static_root }}/\/srv\/static/g' \
|
| 68 | -e 's/{{ nginx_proxy_pass }}/http:\/\/127.0.0.1:3000/g' \
|
| 69 | -e 's/{{ nginx_logs_proxy_pass }}/http:\/\/127.0.0.1:9428/g' \
|
| 70 | -e 's/{{ nginx_logs_htpasswd_file }}/\/etc\/nginx\/.htpasswd-logs/g' \
|
| 71 | "$ansible_dir/roles/nginx/templates/site.https.j2"
|
| 72 | }
|
| 73 |
|
| 74 | status=0
|
| 75 |
|
| 76 | report() { # <diff exit status> <label>
|
| 77 | if [ "$1" -eq 0 ]; then
|
| 78 | echo "ok $2"
|
| 79 | else
|
| 80 | echo "DRIFT $2"; status=1
|
| 81 | fi
|
| 82 | }
|
| 83 |
|
| 84 | show_diff() { # <expected file> <live file>
|
| 85 | diff -u "$1" "$2" | sed 's/^/ /' || true
|
| 86 | }
|
| 87 |
|
| 88 | echo
|
| 89 | if [ -f "$tmp/nginx.conf" ]; then
|
| 90 | render_nginx >"$tmp/nginx.expected"
|
| 91 | if diff -q "$tmp/nginx.expected" "$tmp/nginx.conf" >/dev/null; then
|
| 92 | report 0 "nginx server block"
|
| 93 | else
|
| 94 | report 1 "nginx server block"; show_diff "$tmp/nginx.expected" "$tmp/nginx.conf"
|
| 95 | fi
|
| 96 | else
|
| 97 | echo "?? nginx server block — no live copy"
|
| 98 | fi
|
| 99 |
|
| 100 | for pair in \
|
| 101 | "ansible/roles/app/files/docker-compose.production.yml:docker-compose.yml:production compose" \
|
| 102 | "ansible/roles/app/files/valkey.conf:valkey.conf:valkey config"; do
|
| 103 | IFS=: read -r repo_rel live name <<<"$pair"
|
| 104 | if [ ! -f "$tmp/$live" ]; then
|
| 105 | echo "?? $name — no live copy"
|
| 106 | elif diff -q "$repo_root/$repo_rel" "$tmp/$live" >/dev/null; then
|
| 107 | report 0 "$name"
|
| 108 | else
|
| 109 | report 1 "$name"; show_diff "$repo_root/$repo_rel" "$tmp/$live"
|
| 110 | fi
|
| 111 | done
|
| 112 |
|
| 113 | echo
|
| 114 | if [ "$status" -eq 0 ]; then
|
| 115 | echo "No drift."
|
| 116 | else
|
| 117 | echo "Drift detected: re-run the matching playbook (deploy.yml / app.yml)"
|
| 118 | fi
|
| 119 | exit "$status"
|