#!/bin/bash set -euo pipefail CONFIG_DIR="/etc/hardal" LOG_FILE="/var/log/hardal-install.log" HARDAL_TOKEN="" DESTINATION_TOKEN="" REPO_URL="" SIGNAL_BRANCH="master" SGTM_SCALE=5 APP_SCALE=2 AZURE_MODE=false VAULT_NAME="clickhouse-vault" SECRET_NAME="clickhouse-password" ITEMS_CRON_ENABLED=false ITEMS_CRON_SCHEDULE="50 * * * *" LEADS_CRON_ENABLED=false LEADS_CRON_SCHEDULE="50 * * * *" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' usage() { echo "Usage: $0 [OPTIONS]" echo "Required Options:" echo " -n, --signal-name Signal name" echo " -c, --container-config Container configuration" echo " -g, --gtag Google Tag" echo " -a, --api-key Signal API key" echo " -u, --ssh-user SSH user" echo " -p, --db-password Database password" echo " -t, --hardal-token Hardal token" echo " -r, --destination-token Destination token" echo "Optional Options:" echo " -d, --db-user ClickHouse readonly user password" echo " -s, --sgtm-scale Number of SGTM tagging instances (default: 5)" echo " -x, --app-scale Number of app instances (default: 2)" echo " --items-cron Enable items-backfill.sh cron job" echo " --items-cron-schedule Custom cron schedule for items-backfill.sh" echo " --leads-cron Enable leads-backfill.sh cron job" echo " --leads-cron-schedule Custom cron schedule for leads-backfill.sh" echo " -h, --help Display this help message" echo " -i, --interactive Force interactive mode" echo " -z, --azure Enable Azure Key Vault secret rotation" } log() { local level=$1 local message=$2 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') case $level in "INFO") echo -e "${GREEN}[INFO]${NC} ${timestamp} - ${message}" | tee -a "$LOG_FILE" ;; "WARN") echo -e "${YELLOW}[WARN]${NC} ${timestamp} - ${message}" | tee -a "$LOG_FILE" ;; "ERROR") echo -e "${RED}[ERROR]${NC} ${timestamp} - ${message}" | tee -a "$LOG_FILE" ;; esac } setup_azure_secret_rotation() { log "INFO" "Setting up Azure Key Vault secret rotation..." local VAULT_NAME="${VAULT_NAME}" local SECRET_NAME="${SECRET_NAME}" local FETCH_SCRIPT="/usr/local/bin/fetch-clickhouse-secrets.sh" # Write the fetch script sudo tee "$FETCH_SCRIPT" > /dev/null <<'FETCHEOF' #!/bin/bash set -euo pipefail VAULT_NAME="__VAULT_NAME__" SECRET_NAME="__SECRET_NAME__" echo "Fetching token from Managed Identity..." TOKEN=$(curl -sf "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net" -H "Metadata: true" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") echo "Fetching secret from Key Vault..." PASSWORD=$(curl -sf "https://${VAULT_NAME}.vault.azure.net/secrets/${SECRET_NAME}?api-version=7.3" -H "Authorization: Bearer ${TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin)['value'])") echo "Writing ClickHouse secrets.env..." mkdir -p /etc/clickhouse-server install -m 600 -o root -g root /dev/null /etc/clickhouse-server/secrets.env echo "CLICKHOUSE_USER_PASSWORD=${PASSWORD}" > /etc/clickhouse-server/secrets.env echo "Locating hardal-signal .env file..." ENV_FILE=$(find /home -maxdepth 5 -name ".env" -path "*/hardal-signal/*" 2>/dev/null | head -1) if [[ -z "${ENV_FILE}" ]]; then echo "WARNING: hardal-signal .env not found — skipping .env update (will be set during application setup)." else echo "Updating ${ENV_FILE}..." if grep -q "^CLICKHOUSE_PASSWORD=" "${ENV_FILE}"; then sed -i "s|^CLICKHOUSE_PASSWORD=.*|CLICKHOUSE_PASSWORD="${PASSWORD}"|" "${ENV_FILE}" else echo "CLICKHOUSE_PASSWORD="${PASSWORD}"" >> "${ENV_FILE}" fi echo "Done — ${ENV_FILE} updated." fi echo "Done — secrets written to /etc/clickhouse-server/secrets.env and ${ENV_FILE}." FETCHEOF # Substitute vault name and secret name into the script sudo sed -i "s|__VAULT_NAME__|${VAULT_NAME}|g" "$FETCH_SCRIPT" sudo sed -i "s|__SECRET_NAME__|${SECRET_NAME}|g" "$FETCH_SCRIPT" sudo chmod 700 "$FETCH_SCRIPT" sudo chown root:root "$FETCH_SCRIPT" log "INFO" "Fetch script written to ${FETCH_SCRIPT}" # Write clickhouse-secrets-fetch systemd service sudo tee /etc/systemd/system/clickhouse-secrets-fetch.service > /dev/null < /dev/null </dev/null | grep -v "fetch-clickhouse-secrets" | grep -v "0 * * * *.*clickhouse-server" | grep -v "0 1 * * *.*hardal-compose" || true echo "${cron_line}" ) | crontab - log "INFO" "Azure secret rotation cron registered" log "INFO" "Azure setup complete — secrets will rotate hourly from Key Vault: ${VAULT_NAME}" } parse_arguments() { local OPTS local interactive=false OPTS=$(getopt -o n:c:g:a:u:p:t:d:r:s:x:hiz --long signal-name:,container-config:,gtag:,api-key:,ssh-user:,db-password:,hardal-token:,db-user:,destination-token:,sgtm-scale:,app-scale:,items-cron,items-cron-schedule:,leads-cron,leads-cron-schedule:,help,interactive,azure -n "$0" -- "$@") if [ $? != 0 ]; then echo "Failed parsing options." >&2 exit 1 fi eval set -- "$OPTS" while true; do case "$1" in -n|--signal-name) signalName="$2" shift 2 ;; -z|--azure) AZURE_MODE=true shift ;; -c|--container-config) containerConfig="$2" shift 2 ;; -g|--gtag) gTag="$2" shift 2 ;; -a|--api-key) signalApiKey="$2" shift 2 ;; -u|--ssh-user) sshUser="$2" shift 2 ;; -p|--db-password) dbPassword="$2" shift 2 ;; -t|--hardal-token) HARDAL_TOKEN="$2" shift 2 ;; -d|--db-user) dbUserPassword="$2" shift 2 ;; -r|--destination-token) DESTINATION_TOKEN="$2" shift 2 ;; -s|--sgtm-scale) SGTM_SCALE="$2" shift 2 ;; -x|--app-scale) APP_SCALE="$2" shift 2 ;; --items-cron) ITEMS_CRON_ENABLED=true shift ;; --items-cron-schedule) ITEMS_CRON_SCHEDULE="$2" shift 2 ;; --leads-cron) LEADS_CRON_ENABLED=true shift ;; --leads-cron-schedule) LEADS_CRON_SCHEDULE="$2" shift 2 ;; -i|--interactive) interactive=true shift ;; -h|--help) usage exit 0 ;; --) shift break ;; *) echo "Internal error!" exit 1 ;; esac done local missing_args=() [[ -z "${signalName:-}" ]] && missing_args+=("Signal Name") [[ -z "${containerConfig:-}" ]] && missing_args+=("Container Config") [[ -z "${gTag:-}" ]] && missing_args+=("Google Tag") [[ -z "${signalApiKey:-}" ]] && missing_args+=("Signal API Key") [[ -z "${sshUser:-}" ]] && missing_args+=("SSH User") [[ -z "${dbPassword:-}" ]] && missing_args+=("Database Password") [[ -z "${HARDAL_TOKEN}" ]] && missing_args+=("Hardal token") [[ -z "${DESTINATION_TOKEN}" ]] && missing_args+=("Destination Token") if [[ "$interactive" == "true" ]]; then gather_input elif [[ ${#missing_args[@]} -gt 0 ]]; then echo "${RED}Error: The following required arguments are missing:${NC}" printf '%s\n' "${missing_args[@]}" usage exit 1 fi REPO_URL="https://${HARDAL_TOKEN}@github.com/usehardal/signal.git" } install_dependencies() { log "INFO" "Installing required dependencies..." sudo apt-get update sudo apt-get install -y \ gnupg2 \ apt-transport-https \ ca-certificates \ curl \ software-properties-common } handle_error() { local line_number=$1 local error_code=$2 log "ERROR" "Error occurred in script at line ${line_number} (Error code: ${error_code})" exit 1 } trap 'handle_error ${LINENO} $?' ERR gather_input() { sudo mkdir -p "$CONFIG_DIR" while [[ -z "${HARDAL_TOKEN}" ]]; do read -sp "Enter Hardal token: " HARDAL_TOKEN echo done while [[ -z "${DESTINATION_TOKEN}" ]]; do read -sp "Enter Destination token: " DESTINATION_TOKEN echo done REPO_URL="https://${HARDAL_TOKEN}@github.com/usehardal/signal.git" while [[ -z "${dbPassword:-}" ]]; do read -sp "Enter DB password: " dbPassword echo if [[ ${#dbPassword} -lt 8 ]]; then log "WARN" "Password must be at least 8 characters long" unset dbPassword fi done read -sp "Enter DB readonly user password (optional): " dbUserPassword echo while [[ -z "${signalName:-}" ]]; do read -p "Enter signal name: " signalName done while [[ -z "${containerConfig:-}" ]]; do read -p "Enter container config: " containerConfig done while [[ -z "${gTag:-}" ]]; do read -p "Enter Google Tag: " gTag done while [[ -z "${signalApiKey:-}" ]]; do read -p "Enter Signal API key: " signalApiKey done while [[ -z "${sshUser:-}" ]]; do read -p "Enter SSH User (run whoami): " sshUser done read -p "Enter number of SGTM tagging instances (default: 5): " sgtmScale SGTM_SCALE=${sgtmScale:-5} read -p "Enter number of app instances (default: 2): " appScale APP_SCALE=${appScale:-2} export dbPassword signalName containerConfig gTag signalApiKey sshUser HARDAL_TOKEN DESTINATION_TOKEN dbUserPassword SGTM_SCALE APP_SCALE ITEMS_CRON_ENABLED ITEMS_CRON_SCHEDULE LEADS_CRON_ENABLED LEADS_CRON_SCHEDULE cat << EOF | sudo tee "$CONFIG_DIR/install.conf" > /dev/null SIGNAL_NAME=$signalName CONTAINER_CONFIG=$containerConfig GTAG=$gTag SIGNAL_API_KEY=$signalApiKey SSH_USER=$sshUser HARDAL_TOKEN=$HARDAL_TOKEN DESTINATION_TOKEN=$DESTINATION_TOKEN SGTM_SCALE=$SGTM_SCALE APP_SCALE=$APP_SCALE ITEMS_CRON_ENABLED=$ITEMS_CRON_ENABLED ITEMS_CRON_SCHEDULE=$ITEMS_CRON_SCHEDULE LEADS_CRON_ENABLED=$LEADS_CRON_ENABLED LEADS_CRON_SCHEDULE=$LEADS_CRON_SCHEDULE EOF if [[ -n "${dbUserPassword:-}" ]]; then echo "DB_USER_PASSWORD=$dbUserPassword" | sudo tee -a "$CONFIG_DIR/install.conf" > /dev/null fi sudo chmod 600 "$CONFIG_DIR/install.conf" } install_clickhouse() { log "INFO" "Installing ClickHouse..." install_dependencies GNUPGHOME=$(mktemp -d) sudo GNUPGHOME="$GNUPGHOME" gpg --no-default-keyring --keyring /usr/share/keyrings/clickhouse-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 8919F6BD2B48D754 sudo rm -rf "$GNUPGHOME" sudo chmod +r /usr/share/keyrings/clickhouse-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg] https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y clickhouse-server clickhouse-client if [[ "${AZURE_MODE}" == "true" ]]; then # Write password config using from_env — actual value comes from secrets.env sudo tee /etc/clickhouse-server/users.d/default-password.xml > /dev/null <<'EOF' EOF else sudo sed -i "s||$dbPassword|g" /etc/clickhouse-server/users.xml fi sudo sed -i 's||::1|g' /etc/clickhouse-server/config.xml sudo sed -i 's||0.0.0.0|g' /etc/clickhouse-server/config.xml sudo sed -i 's|trace|none|g' /etc/clickhouse-server/config.xml sudo sed -i 's|||g' /etc/clickhouse-server/config.xml sudo systemctl restart clickhouse-server sudo systemctl enable clickhouse-server } setup_clickhouse_log_config() { local LOG_CONFIG_PATH="/etc/clickhouse-server/config.d/disable-logs.xml" log "INFO" "Configuring ClickHouse log retention at ${LOG_CONFIG_PATH}..." sudo mkdir -p "$(dirname "$LOG_CONFIG_PATH")" sudo tee "$LOG_CONFIG_PATH" > /dev/null << 'EOF' event_date + INTERVAL 3 DAY event_date + INTERVAL 3 DAY event_date + INTERVAL 7 DAY EOF sudo pkill -f clickhouse-server || true sudo systemctl restart clickhouse-server log "INFO" "ClickHouse restarted to apply log config" } install_docker() { log "INFO" "Installing Docker..." for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove "$pkg" -y || true done sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl start docker sudo systemctl enable docker if ! sudo docker run hello-world; then log "ERROR" "Docker installation verification failed" exit 1 fi } setup_application() { log "INFO" "Setting up application..." sudo useradd -m -s /bin/bash "$sshUser" || true sudo usermod -aG docker "$sshUser" || true sudo apt-get install -y git git config --global credential.helper store echo "https://mertusehardal:${HARDAL_TOKEN}@github.com" > ~/.git-credentials chmod 600 ~/.git-credentials cd "/home/$sshUser" git config --global http.postBuffer 524288000 git config --global http.lowSpeedLimit 0 git config --global http.lowSpeedTime 300 if [[ -d "hardal-signal" ]]; then sudo rm -rf "hardal-signal" fi git clone --branch ${SIGNAL_BRANCH} --depth 1 "$REPO_URL" "hardal-signal" for file in "Caddyfile" "docker-compose.yaml" "src/lib/container-configs.ts"; do if [[ -f "/home/$sshUser/hardal-signal/$file" ]]; then sudo sed -i "s||$signalName|g" "/home/$sshUser/hardal-signal/$file" else log "WARN" "File $file not found" fi done if [[ -f "/home/$sshUser/hardal-signal/docker-compose.yaml" ]]; then sudo sed -i "s||$containerConfig|g" "/home/$sshUser/hardal-signal/docker-compose.yaml" sudo sed -i "s||$signalName|g" "/home/$sshUser/hardal-signal/docker-compose.yaml" fi if [[ -f "/home/$sshUser/hardal-signal/src/lib/container-configs.ts" ]]; then sudo sed -i "s||$signalName|g" "/home/$sshUser/hardal-signal/src/lib/container-configs.ts" sudo sed -i "s||$gTag|g" "/home/$sshUser/hardal-signal/src/lib/container-configs.ts" sudo sed -i "s||$signalApiKey|g" "/home/$sshUser/hardal-signal/src/lib/container-configs.ts" fi if [[ -f "/home/$sshUser/hardal-signal/src/lib/clickhouse.ts" ]]; then sudo sed -i "s||$dbPassword|g" "/home/$sshUser/hardal-signal/src/lib/clickhouse.ts" fi sudo chown -R "$sshUser:$sshUser" "hardal-signal" sudo chmod -R 755 "hardal-signal" ENV_PATH="/home/$sshUser/hardal-signal/.env" sudo tee "$ENV_PATH" > /dev/null < /dev/null [Unit] Description=Hardal Docker Compose Application Requires=docker.service After=docker.service [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/home/$sshUser/hardal-signal ExecStart=/usr/bin/docker compose -f docker-compose.yaml up --build -d --remove-orphans --scale sgtm_tagging=$SGTM_SCALE --scale app=$APP_SCALE ExecStop=/usr/bin/docker compose -f docker-compose.yaml down Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target EOF sudo chmod 644 /etc/systemd/system/hardal-compose.service sudo systemctl daemon-reload sudo systemctl enable hardal-compose sudo systemctl start hardal-compose } setup_cronjob() { log "INFO" "Setting up cron jobs..." local current_user=$(logname 2>/dev/null || whoami) local new_entries="" if [[ "${AZURE_MODE}" != "true" ]]; then new_entries+="0 1 * * * /bin/systemctl restart hardal-compose.service\n" fi if [[ "$ITEMS_CRON_ENABLED" == "true" ]]; then local items_script="/home/$sshUser/hardal-signal/items-backfill.sh" if [[ -f "$items_script" ]]; then new_entries+="${ITEMS_CRON_SCHEDULE} /bin/bash $items_script >> /var/log/hardal-items-backfill.log 2>&1\n" fi fi if [[ "$LEADS_CRON_ENABLED" == "true" ]]; then local leads_script="/home/$sshUser/hardal-signal/leads-backfill.sh" if [[ -f "$leads_script" ]]; then new_entries+="${LEADS_CRON_SCHEDULE} /bin/bash $leads_script >> /var/log/hardal-leads-backfill.log 2>&1\n" fi fi ( crontab -l 2>/dev/null || true printf "%b" "$new_entries" ) | crontab - log "INFO" "Cron jobs registered for $current_user" } post_install_runs() { for script in cron-campaign.sh items-backfill.sh leads-backfill.sh sessions-backfill.sh; do local path="/home/$sshUser/hardal-signal/$script" if [[ -f "$path" ]]; then sudo chmod +x "$path" fi done } main() { if [[ $EUID -ne 0 ]]; then log "ERROR" "This script must be run as root" exit 1 fi parse_arguments "$@" log "INFO" "Starting installation..." touch "$LOG_FILE" chmod 644 "$LOG_FILE" install_dependencies # if [[ "$AZURE_MODE" == "true" ]]; then # setup_azure_secret_rotation # fi install_clickhouse setup_clickhouse_log_config install_docker setup_application setup_service setup_cronjob post_install_runs log "INFO" "Installation completed successfully!" } main "$@"