240 lines
6.8 KiB
Bash
Executable File

#!/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" <<EOF
ACCEPT_EULA=Y
MSSQL_PID=Developer
MSSQL_TCP_PORT=1433
MSSQL_SA_PASSWORD=${password}
EOF
sudo -n install -d -m 0750 "$SQL_ENV_DIR"
sudo -n install -m 0640 -o root -g docker "$tmp_file" "$SQL_ENV_FILE"
rm -f "$tmp_file"
atlas_log_warn "Created ${SQL_ENV_FILE}; the generated SQL administrator password was not printed"
}
start_sql_server() {
if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then
atlas_log_info "Dry-run mode: would run docker compose up -d"
return 0
fi
if docker_cmd inspect "$CONTAINER_NAME" >/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