sillyspace/deploy/hetzner/provision.sh

203 lines
7.8 KiB
Bash
Raw Normal View History

2026-06-04 02:59:03 +00:00
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=deploy/hetzner/lib.sh
. "$DEPLOY_DIR/lib.sh"
load_optional_production_env
render_cloud_init() {
sed \
-e "s|__BLIISH_DOMAIN__|$(escape_sed_replacement "$BLIISH_DOMAIN")|g" \
-e "s|__BLIISH_BASE_URL__|$(escape_sed_replacement "$BLIISH_BASE_URL")|g" \
-e "s|__BLIISH_REPOSITORY_URL__|$(escape_sed_replacement "$BLIISH_REPOSITORY_URL")|g" \
-e "s|__BLIISH_REPOSITORY_REF__|$(escape_sed_replacement "$BLIISH_REPOSITORY_REF")|g" \
-e "s|__BLIISH_ADMIN_USER_ID__|$(escape_sed_replacement "$BLIISH_ADMIN_USER_ID")|g" \
-e "s|__BLIISH_MEDIA_CONCURRENCY__|$(escape_sed_replacement "$BLIISH_MEDIA_CONCURRENCY")|g" \
-e "s|__BLIISH_NODE_MAJOR__|$(escape_sed_replacement "$BLIISH_NODE_MAJOR")|g" \
"$DEPLOY_DIR/cloud-init.yml" > "$USER_DATA_FILE"
}
# Check local tooling before touching Hetzner resources.
require_command hcloud
require_command sed
require_command mktemp
# Operator-overridable defaults. The deployment guide documents each one.
BLIISH_SERVER_NAME="${BLIISH_SERVER_NAME:-bliishspace-1}"
BLIISH_HCLOUD_SERVER_TYPE="${BLIISH_HCLOUD_SERVER_TYPE:-cx23}"
BLIISH_HCLOUD_IMAGE="${BLIISH_HCLOUD_IMAGE:-ubuntu-24.04}"
BLIISH_HCLOUD_LOCATION="${BLIISH_HCLOUD_LOCATION:-nbg1}"
BLIISH_HCLOUD_ENABLE_IPV4="${BLIISH_HCLOUD_ENABLE_IPV4:-true}"
BLIISH_DEPLOY_SSH_KEY="${BLIISH_DEPLOY_SSH_KEY:-$HOME/.ssh/bliishspace-deploy}"
BLIISH_HCLOUD_SSH_KEY_NAME="${BLIISH_HCLOUD_SSH_KEY_NAME:-bliishspace-admin}"
BLIISH_HCLOUD_SSH_PUBLIC_KEY="${BLIISH_HCLOUD_SSH_PUBLIC_KEY:-$BLIISH_DEPLOY_SSH_KEY.pub}"
BLIISH_HCLOUD_FIREWALL_NAME="${BLIISH_HCLOUD_FIREWALL_NAME:-bliishspace-web}"
BLIISH_HCLOUD_FIREWALL_RULES="${BLIISH_HCLOUD_FIREWALL_RULES:-$DEPLOY_DIR/firewall-rules.json}"
BLIISH_REPOSITORY_URL="${BLIISH_REPOSITORY_URL:-https://github.com/bliish-com/bliishspace.git}"
BLIISH_REPOSITORY_REF="${BLIISH_REPOSITORY_REF:-main}"
BLIISH_ADMIN_USER_ID="${BLIISH_ADMIN_USER_ID:-1}"
BLIISH_MEDIA_CONCURRENCY="${BLIISH_MEDIA_CONCURRENCY:-1}"
BLIISH_NODE_MAJOR="${BLIISH_NODE_MAJOR:-24}"
BLIISH_HCLOUD_ENABLE_BACKUPS="${BLIISH_HCLOUD_ENABLE_BACKUPS:-false}"
BLIISH_HCLOUD_ENABLE_DELETE_PROTECTION="${BLIISH_HCLOUD_ENABLE_DELETE_PROTECTION:-true}"
# Accept either BLIISH_DOMAIN or a full BLIISH_BASE_URL, then normalize them for
# cloud-init and Caddy.
if [ -z "${BLIISH_DOMAIN:-}" ] && [ -n "${BLIISH_BASE_URL:-}" ]; then
BLIISH_DOMAIN="$(normalize_domain "$BLIISH_BASE_URL")"
fi
if [ -z "${BLIISH_DOMAIN:-}" ]; then
echo "Set BLIISH_DOMAIN, for example: BLIISH_DOMAIN=bliish.space deploy/hetzner/provision.sh" >&2
exit 1
fi
BLIISH_DOMAIN="$(normalize_domain "$BLIISH_DOMAIN")"
BLIISH_BASE_URL="${BLIISH_BASE_URL:-https://$BLIISH_DOMAIN}"
USER_DATA_FILE="${BLIISH_HCLOUD_USER_DATA_FILE:-$(mktemp "${TMPDIR:-/tmp}/bliishspace-cloud-init.XXXXXX")}"
EXPECTED_BLIISH_BASE_URL="https://$BLIISH_DOMAIN"
# These values are interpolated into cloud-init, so reject control characters
# and shell-sensitive tokens before rendering the template.
require_no_control_chars BLIISH_DOMAIN "$BLIISH_DOMAIN"
require_no_control_chars BLIISH_BASE_URL "$BLIISH_BASE_URL"
require_no_control_chars BLIISH_HCLOUD_LOCATION "$BLIISH_HCLOUD_LOCATION"
require_no_control_chars BLIISH_HCLOUD_SERVER_TYPE "$BLIISH_HCLOUD_SERVER_TYPE"
require_no_control_chars BLIISH_HCLOUD_IMAGE "$BLIISH_HCLOUD_IMAGE"
require_no_control_chars BLIISH_REPOSITORY_URL "$BLIISH_REPOSITORY_URL"
require_no_control_chars BLIISH_REPOSITORY_REF "$BLIISH_REPOSITORY_REF"
require_no_control_chars BLIISH_ADMIN_USER_ID "$BLIISH_ADMIN_USER_ID"
require_no_control_chars BLIISH_MEDIA_CONCURRENCY "$BLIISH_MEDIA_CONCURRENCY"
require_no_control_chars BLIISH_NODE_MAJOR "$BLIISH_NODE_MAJOR"
require_simple_token BLIISH_DOMAIN "$BLIISH_DOMAIN" '^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$'
require_simple_token BLIISH_HCLOUD_LOCATION "$BLIISH_HCLOUD_LOCATION" '^[a-z0-9][a-z0-9-]*$'
require_simple_token BLIISH_HCLOUD_SERVER_TYPE "$BLIISH_HCLOUD_SERVER_TYPE" '^[A-Za-z0-9][A-Za-z0-9._-]*$'
require_simple_token BLIISH_HCLOUD_IMAGE "$BLIISH_HCLOUD_IMAGE" '^[A-Za-z0-9][A-Za-z0-9._-]*$'
require_simple_token BLIISH_ADMIN_USER_ID "$BLIISH_ADMIN_USER_ID" '^[0-9]+$'
require_simple_token BLIISH_MEDIA_CONCURRENCY "$BLIISH_MEDIA_CONCURRENCY" '^[1-8]$'
require_simple_token BLIISH_NODE_MAJOR "$BLIISH_NODE_MAJOR" '^[0-9]+$'
case "$BLIISH_BASE_URL" in
https://*) ;;
*)
echo "BLIISH_BASE_URL must use https:// for this production deployment path" >&2
exit 1
;;
esac
if [ "$BLIISH_BASE_URL" != "$EXPECTED_BLIISH_BASE_URL" ]; then
echo "BLIISH_BASE_URL must exactly match $EXPECTED_BLIISH_BASE_URL" >&2
exit 1
fi
case "$BLIISH_REPOSITORY_URL" in
-*)
echo "BLIISH_REPOSITORY_URL must not start with '-'" >&2
exit 1
;;
*[[:space:]\\]*)
echo "BLIISH_REPOSITORY_URL must not contain whitespace or backslashes" >&2
exit 1
;;
esac
require_simple_token BLIISH_REPOSITORY_URL "$BLIISH_REPOSITORY_URL" '^[A-Za-z0-9][A-Za-z0-9.+:/_@~=-]*$'
require_repository_ref BLIISH_REPOSITORY_REF "$BLIISH_REPOSITORY_REF"
# Reuse the shared SSH key and firewall, but never provision over an existing
# server.
if [ ! -r "$BLIISH_DEPLOY_SSH_KEY" ] && [ ! -r "$BLIISH_HCLOUD_SSH_PUBLIC_KEY" ]; then
require_command ssh-keygen
install -d -m 0700 "$(dirname "$BLIISH_DEPLOY_SSH_KEY")"
ssh-keygen -t ed25519 -f "$BLIISH_DEPLOY_SSH_KEY" -N "" -C "bliishspace-deploy"
fi
if [ ! -r "$BLIISH_HCLOUD_SSH_PUBLIC_KEY" ] && [ -r "$BLIISH_DEPLOY_SSH_KEY" ]; then
require_command ssh-keygen
ssh-keygen -y -f "$BLIISH_DEPLOY_SSH_KEY" > "$BLIISH_HCLOUD_SSH_PUBLIC_KEY"
fi
if [ ! -r "$BLIISH_HCLOUD_SSH_PUBLIC_KEY" ]; then
echo "SSH public key not found: $BLIISH_HCLOUD_SSH_PUBLIC_KEY" >&2
exit 1
fi
if [ ! -r "$BLIISH_HCLOUD_FIREWALL_RULES" ]; then
echo "Firewall rules file not found: $BLIISH_HCLOUD_FIREWALL_RULES" >&2
exit 1
fi
if hcloud server describe "$BLIISH_SERVER_NAME" >/dev/null 2>&1; then
echo "Server already exists: $BLIISH_SERVER_NAME" >&2
echo "Use deploy/hetzner/update.sh for application updates." >&2
exit 1
fi
if ! hcloud ssh-key describe "$BLIISH_HCLOUD_SSH_KEY_NAME" >/dev/null 2>&1; then
hcloud ssh-key create \
--name "$BLIISH_HCLOUD_SSH_KEY_NAME" \
--label "app=bliishspace" \
--public-key-from-file "$BLIISH_HCLOUD_SSH_PUBLIC_KEY"
fi
if ! hcloud firewall describe "$BLIISH_HCLOUD_FIREWALL_NAME" >/dev/null 2>&1; then
hcloud firewall create \
--name "$BLIISH_HCLOUD_FIREWALL_NAME" \
--label "app=bliishspace" \
--rules-file "$BLIISH_HCLOUD_FIREWALL_RULES"
fi
render_cloud_init
# Keep optional hcloud flags in an array to avoid word splitting.
create_args=(
server create
--name "$BLIISH_SERVER_NAME"
--type "$BLIISH_HCLOUD_SERVER_TYPE"
--image "$BLIISH_HCLOUD_IMAGE"
--location "$BLIISH_HCLOUD_LOCATION"
--ssh-key "$BLIISH_HCLOUD_SSH_KEY_NAME"
--firewall "$BLIISH_HCLOUD_FIREWALL_NAME"
--label "app=bliishspace"
--user-data-from-file "$USER_DATA_FILE"
)
case "$BLIISH_HCLOUD_ENABLE_IPV4" in
0|false|FALSE|no|NO)
create_args+=(--without-ipv4)
;;
esac
case "$BLIISH_HCLOUD_ENABLE_BACKUPS" in
1|true|TRUE|yes|YES)
create_args+=(--enable-backup)
;;
esac
case "$BLIISH_HCLOUD_ENABLE_DELETE_PROTECTION" in
1|true|TRUE|yes|YES)
create_args+=(--enable-protection delete --enable-protection rebuild)
;;
esac
hcloud "${create_args[@]}"
server_ip="$(hcloud server ip "$BLIISH_SERVER_NAME")"
# Print the DNS and first-boot checks operators usually need next.
cat <<EOF
Created $BLIISH_SERVER_NAME at $server_ip.
Next:
1. Point DNS A/AAAA records for $BLIISH_DOMAIN at the server IP.
2. Wait for first boot setup:
ssh root@$server_ip 'cloud-init status --wait'
3. Check the app:
ssh root@$server_ip 'systemctl status bliishspace --no-pager'
ssh root@$server_ip 'journalctl -u bliishspace -n 100 --no-pager'
Rendered cloud-init: $USER_DATA_FILE
EOF