Files
baiyue-logistics-chat/deploy/publish-gray.sh
T

77 lines
3.7 KiB
Bash

#!/usr/bin/env bash
set +x
set -Eeuo pipefail
umask 077
: "${TARGET_SSH_KEY:?}" "${TARGET_SSH_USER:?}" "${TARGET_SSH_HOST:?}" "${TARGET_SSH_KNOWN_HOSTS:?}"
: "${PREVIEW_USERNAME:?}" "${PREVIEW_PASSWORD:?}" "${CHAT_IMAGE:?}" "${CHAT_IMAGE_ID:?}" "${CERTBOT_IMAGE:?}"
[[ "$PREVIEW_USERNAME" =~ ^[A-Za-z0-9_.@-]+$ ]] || exit 2
[[ "$TARGET_SSH_USER" =~ ^[A-Za-z0-9_.-]+$ ]] || exit 2
[[ "$TARGET_SSH_HOST" =~ ^[A-Za-z0-9.-]+$ ]] || exit 2
temporary=$(mktemp -d)
cleanup() { rm -rf -- "$temporary"; }
trap cleanup EXIT
python3 -m deploy.gray_config "$temporary/release"
release=$(awk -F= '$1 == "CHAT_RELEASE" {print $2}' "$temporary/release/release.env")
root=$(awk -F= '$1 == "CHAT_ROOT" {print $2}' "$temporary/release/release.env")
python3 - <<'TARGET'
import os
import socket
from urllib.parse import urlsplit
def addresses(host):
return {item[4][0] for item in socket.getaddrinfo(host, 443, socket.AF_INET, socket.SOCK_STREAM)}
if not addresses(urlsplit(os.environ['PUBLIC_ORIGIN']).hostname) & addresses(os.environ['TARGET_SSH_HOST']):
raise SystemExit('The public domain does not resolve to the managed gray target.')
TARGET
printf '%s:' "$PREVIEW_USERNAME" > "$temporary/release/site.htpasswd"
printf '%s\n' "$PREVIEW_PASSWORD" | openssl passwd -apr1 -stdin >> "$temporary/release/site.htpasswd"
ssh_options=(-i "$TARGET_SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes
-o "UserKnownHostsFile=$TARGET_SSH_KNOWN_HOSTS" -o ConnectTimeout=12)
target="$TARGET_SSH_USER@$TARGET_SSH_HOST"
run_ssh() { ssh "${ssh_options[@]}" "$target" "$@"; }
# Only an owned, dedicated directory may receive releases or runtime data.
run_ssh bash -s -- "$root" "$release" <<'PREPARE'
set -Eeuo pipefail
root=$1
release=$2
[[ "$root" == /*/gray-logistics-chat && "$root" != *..* ]] || exit 2
[[ "$release" == "$root"/releases/* ]] || exit 2
[[ "$(id -u)" == 0 ]] || { echo 'The managed gray deployment account must have the required host permissions.' >&2; exit 2; }
for program in docker nginx openssl curl ss systemctl; do command -v "$program" >/dev/null; done
if [[ -e "$root" && ! -f "$root/.project" ]] && [[ -n "$(find "$root" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
echo 'Refusing to reuse an unowned deployment directory.' >&2
exit 2
fi
if [[ -f "$root/.project" ]]; then
[[ "$(cat "$root/.project")" == baiyue-logistics-chat-gray ]] || exit 2
fi
install -d -m 700 "$root" "$root/releases" "$root/data" "$root/logs"
printf 'baiyue-logistics-chat-gray\n' > "$root/.project"
chown 10001:10001 "$root/data" "$root/logs"
[[ ! -e "$release" ]] || { echo 'Release directory already exists; inspect its state before retrying.' >&2; exit 2; }
install -d -m 700 "$release"
PREPARE
# Transport only tested images built without runtime credentials.
docker save "$CHAT_IMAGE" "$CERTBOT_IMAGE" | gzip | run_ssh "bash -o pipefail -c 'gzip -dc | docker load'"
scp "${ssh_options[@]}" "$temporary/release/"* "$target:$release/"
run_ssh bash "$release/prepare-tls.sh" "$root" "$release"
run_ssh bash "$release/activate.sh" "$root" "$release"
# Verify the public TLS endpoint and the actual acceptance password without putting it in arguments.
python3 - <<'VERIFY'
import json
import os
from urllib.request import HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm, build_opener
origin = os.environ['PUBLIC_ORIGIN'].rstrip('/')
manager = HTTPPasswordMgrWithDefaultRealm()
manager.add_password(None, origin, os.environ['PREVIEW_USERNAME'], os.environ['PREVIEW_PASSWORD'])
opener = build_opener(HTTPBasicAuthHandler(manager))
with opener.open(origin + '/healthz', timeout=30) as response:
assert response.status == 200 and json.load(response) == {'status': 'ok'}
print('Authenticated gray HTTPS health check passed.')
VERIFY