240 lines
6.8 KiB
Bash
Executable File
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
|