#!/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 sha256sum; 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 # Verify the exact exported archive before loading. Classic and containerd image # stores can report different IDs for the same archive, so retain target IDs too. docker save "$CHAT_IMAGE" "$CERTBOT_IMAGE" | gzip > "$temporary/images.tar.gz" archive_sha=$(sha256sum "$temporary/images.tar.gz" | awk '{print $1}') scp "${ssh_options[@]}" "$temporary/images.tar.gz" "$target:$release/images.tar.gz" run_ssh bash -s -- "$root" "$release" "$archive_sha" "$CHAT_IMAGE" "$CERTBOT_IMAGE" "$SOURCE_COMMIT" <<'LOAD' set -Eeuo pipefail umask 077 root=$1 release=$2 checksum=$3 app_image=$4 certbot_image=$5 source_commit=$6 [[ "$root" == /*/gray-logistics-chat && "$root" != *..* ]] || exit 2 [[ "$release" == "$root"/releases/* && "$release" != *..* ]] || exit 2 [[ "$(cat "$root/.project")" == baiyue-logistics-chat-gray ]] || exit 2 [[ "$checksum" =~ ^[a-f0-9]{64}$ && "$source_commit" =~ ^[a-f0-9]{40}$ ]] || exit 2 [[ "$app_image" =~ ^baiyue-logistics-chat-gray:[a-f0-9]{12}-b[0-9]+$ ]] || exit 2 [[ "$certbot_image" =~ ^baiyue-logistics-chat-certbot:[a-f0-9]{12}-b[0-9]+$ ]] || exit 2 printf '%s %s\n' "$checksum" "$release/images.tar.gz" | sha256sum --check --status gzip -dc "$release/images.tar.gz" | docker load for image in "$app_image" "$certbot_image"; do [[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$image")" == "$source_commit" ]] || exit 2 done app_id=$(docker image inspect --format '{{.Id}}' "$app_image") certbot_id=$(docker image inspect --format '{{.Id}}' "$certbot_image") [[ "$app_id" =~ ^sha256:[a-f0-9]{64}$ && "$certbot_id" =~ ^sha256:[a-f0-9]{64}$ ]] || exit 2 printf 'ARCHIVE_SHA256=%s\nCHAT_IMAGE_ID=%s\nCERTBOT_IMAGE_ID=%s\n' "$checksum" "$app_id" "$certbot_id" > "$release/loaded-image-ids.env" rm -f -- "$release/images.tar.gz" echo 'Image archive checksum and source labels verified; target image IDs recorded.' 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