#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSTALL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${INSTALL_DIR}/.." && pwd)" # shellcheck source=install/lib/common.sh source "${INSTALL_DIR}/lib/common.sh" # shellcheck source=install/lib/logging.sh source "${INSTALL_DIR}/lib/logging.sh" atlas_set_script_name "20-sql-server.sh" atlas_install_error_trap COMPOSE_FILE="${REPO_ROOT}/config/sql-server/compose.yml" COMPOSE_PROJECT="atlas-sql" CONTAINER_NAME="atlas-sql-server" SQL_IMAGE="mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04" SQL_IMAGE_DIGEST="sha256:698682bab57c02c42bc0aa274b158aeb242d8e9104149a7489628d5535805816" SQL_ENV_DIR="/etc/atlas" SQL_ENV_FILE="${SQL_ENV_DIR}/sql-server.env" SQL_ROOT="/sql/mssql" SQL_DIRS=(data log secrets backup) usage() { cat <<'USAGE' Usage: install/phases/20-sql-server.sh [options] Deploy SQL Server 2025 Developer using Microsoft's official container image. Requires Docker Engine and Docker Compose. Options: --help Show this help text. --version Show script/framework version. --dry-run Show intended actions without making changes. --verbose Print additional diagnostic output. USAGE } while [[ "$#" -gt 0 ]]; do case "$1" in --help) usage exit "${ATLAS_EXIT_OK}" ;; --version) atlas_print_version exit "${ATLAS_EXIT_OK}" ;; --dry-run) atlas_enable_dry_run ;; --verbose) atlas_enable_verbose ;; *) atlas_usage_error "Unknown option: $1" exit "${ATLAS_EXIT_USAGE}" ;; esac shift done docker_cmd() { if docker info >/dev/null 2>&1; then docker "$@" else sudo -n docker "$@" fi } compose_cmd() { if docker info >/dev/null 2>&1; then docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" "$@" else sudo -n docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" "$@" fi } require_prerequisites() { atlas_require_sudo_noninteractive || { atlas_log_error "Passwordless non-interactive sudo is required" exit "${ATLAS_EXIT_PREREQ}" } atlas_require_command docker docker compose version >/dev/null 2>&1 || { atlas_log_error "Docker Compose plugin is required" exit "${ATLAS_EXIT_PREREQ}" } systemctl is-active --quiet docker || { atlas_log_error "Docker service is not active" exit "${ATLAS_EXIT_PREREQ}" } findmnt -rn --target /sql >/dev/null 2>&1 || { atlas_log_error "/sql must be mounted before SQL Server deployment" exit "${ATLAS_EXIT_PREREQ}" } } pull_image() { if docker_cmd image inspect "$SQL_IMAGE" >/dev/null 2>&1; then atlas_log_ok "SQL Server image already present: ${SQL_IMAGE}" elif [[ "${ATLAS_DRY_RUN}" == "1" ]]; then atlas_log_info "Dry-run mode: would pull ${SQL_IMAGE}" else docker_cmd pull "$SQL_IMAGE" fi } inspect_sql_uid_gid() { if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then printf '10001:10001\n' return 0 fi docker_cmd run --rm --entrypoint /bin/bash "$SQL_IMAGE" -lc 'printf "%s:%s\n" "$(id -u mssql)" "$(id -g mssql)"' } ensure_storage() { local uid_gid="$1" atlas_log_info "Ensuring SQL Server persistent storage under ${SQL_ROOT}" if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then atlas_log_info "Dry-run mode: would create ${SQL_ROOT}/{data,log,secrets,backup}" atlas_log_info "Dry-run mode: would chown directories to ${uid_gid}" return 0 fi sudo -n install -d -m 0770 "$SQL_ROOT" local dir for dir in "${SQL_DIRS[@]}"; do sudo -n install -d -m 0770 "${SQL_ROOT}/${dir}" done sudo -n chown "$uid_gid" "$SQL_ROOT" "${SQL_ROOT}/data" "${SQL_ROOT}/log" "${SQL_ROOT}/secrets" "${SQL_ROOT}/backup" sudo -n chmod 0770 "$SQL_ROOT" "${SQL_ROOT}/data" "${SQL_ROOT}/log" "${SQL_ROOT}/secrets" "${SQL_ROOT}/backup" } generate_password() { python3 - <<'PY' import secrets alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789" print("A1a_" + "".join(secrets.choice(alphabet) for _ in range(28))) PY } ensure_env_file() { if sudo -n test -f "$SQL_ENV_FILE"; then atlas_log_ok "SQL Server environment file already exists" return 0 fi if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then atlas_log_info "Dry-run mode: would create protected ${SQL_ENV_FILE}" return 0 fi local password tmp_file password="$(generate_password)" tmp_file="$(mktemp)" cat >"$tmp_file" </dev/null 2>&1; then local state image state="$(docker_cmd inspect -f '{{.State.Status}}' "$CONTAINER_NAME")" image="$(docker_cmd inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")" if [[ "$image" == "$SQL_IMAGE" ]]; then if [[ "$state" == "running" ]]; then atlas_log_ok "SQL Server container is already running" else atlas_log_info "Starting existing SQL Server container" docker_cmd start "$CONTAINER_NAME" >/dev/null fi return 0 fi atlas_log_error "Existing ${CONTAINER_NAME} uses unexpected image: ${image}" exit "${ATLAS_EXIT_UNSAFE}" fi compose_cmd up -d } wait_for_health() { if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then atlas_log_info "Dry-run mode: would wait for SQL Server health check" return 0 fi local status for _ in {1..60}; do status="$(docker_cmd inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$CONTAINER_NAME" 2>/dev/null || true)" case "$status" in healthy) atlas_log_ok "SQL Server container is healthy" return 0 ;; unhealthy) atlas_log_error "SQL Server container health check is unhealthy" docker_cmd logs --tail 80 "$CONTAINER_NAME" >&2 || true exit "${ATLAS_EXIT_VALIDATION}" ;; esac sleep 5 done atlas_log_error "Timed out waiting for SQL Server container health" docker_cmd logs --tail 80 "$CONTAINER_NAME" >&2 || true exit "${ATLAS_EXIT_VALIDATION}" } verify_sqlcmd() { if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then atlas_log_info "Dry-run mode: would verify sqlcmd connection" return 0 fi "${REPO_ROOT}/scripts/sql/sqlcmd.sh" -Q "SELECT @@VERSION;" >/dev/null atlas_log_ok "SQL Server local sqlcmd verification passed" } main() { require_prerequisites pull_image uid_gid="$(inspect_sql_uid_gid)" ensure_storage "$uid_gid" ensure_env_file start_sql_server wait_for_health verify_sqlcmd } main