Add deployment
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backs up the ASP.NET Data Protection keyring. Without this keyring, PHI
|
||||
# encrypted by Phase 32 cannot be decrypted — a database backup alone is not
|
||||
# sufficient to restore the system. See docs/ops/phi-encryption-runbook.md.
|
||||
#
|
||||
# Volume name matches docker-compose.prod.yml `name: vigilcare` → vigilcare_dp_keys.
|
||||
# Do not derive the name from the host directory; /opt/vigilcare would otherwise
|
||||
# produce a different volume than a checkout named VigilCareClinical.
|
||||
#
|
||||
# Usage (on the production host):
|
||||
# ./scripts/backup-dp-keys.sh
|
||||
# BACKUP_DIR=/mnt/offsite/vigilcare/dp-keys ./scripts/backup-dp-keys.sh
|
||||
#
|
||||
# Cron example (daily 02:15 UTC):
|
||||
# 15 2 * * * /opt/vigilcare/scripts/backup-dp-keys.sh >> /var/log/vigilcare-dp-backup.log 2>&1
|
||||
set -euo pipefail
|
||||
|
||||
VOLUME_NAME="${VOLUME_NAME:-vigilcare_dp_keys}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-/var/backups/vigilcare/dp-keys}"
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
ARCHIVE="${BACKUP_DIR}/dp-keys-${STAMP}.tar.gz"
|
||||
|
||||
if ! docker volume inspect "${VOLUME_NAME}" >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker volume '${VOLUME_NAME}' not found." >&2
|
||||
echo "Confirm docker-compose.prod.yml uses 'name: vigilcare' and the API has started once." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
chmod 700 "${BACKUP_DIR}"
|
||||
|
||||
echo "Backing up volume ${VOLUME_NAME} → ${ARCHIVE}"
|
||||
docker run --rm \
|
||||
-v "${VOLUME_NAME}:/keys:ro" \
|
||||
-v "${BACKUP_DIR}:/backup" \
|
||||
alpine tar czf "/backup/dp-keys-${STAMP}.tar.gz" -C /keys .
|
||||
|
||||
chmod 600 "${ARCHIVE}"
|
||||
|
||||
# Refuse empty archives (volume had no key XML yet).
|
||||
if ! tar tzf "${ARCHIVE}" | grep -q '\.xml$'; then
|
||||
echo "ERROR: archive contains no *.xml key files — refusing to keep empty backup." >&2
|
||||
rm -f "${ARCHIVE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Keyring backed up to ${ARCHIVE}"
|
||||
|
||||
# Retain 30 days.
|
||||
find "${BACKUP_DIR}" -name 'dp-keys-*.tar.gz' -mtime +30 -delete
|
||||
echo "Retention: removed archives older than 30 days under ${BACKUP_DIR}"
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds a self-contained EF Core migration bundle for VigilCareClinicalAPI.
|
||||
# CD (Phase 36 Step 12) runs the same command; this script is for local dry-runs
|
||||
# and for operators who apply schema changes before rolling the API image.
|
||||
#
|
||||
# Usage (from repo root or anywhere):
|
||||
# ./scripts/build-api-migration-bundle.sh
|
||||
# RID=win-x64 ./scripts/build-api-migration-bundle.sh # local Windows test
|
||||
#
|
||||
# Apply against a database (DDL-privileged connection string):
|
||||
# ./artifacts/migrate-api --connection "$PG_CONNECTION_DDL"
|
||||
#
|
||||
# Zero-downtime discipline: every migration must be backwards-compatible with
|
||||
# the previously deployed API image (expand-then-contract). The bundle runs
|
||||
# while old containers are still serving traffic.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/artifacts}"
|
||||
RID="${RID:-linux-x64}"
|
||||
OUTPUT="${OUT_DIR}/migrate-api"
|
||||
|
||||
# Git Bash on Windows often lacks the host PATH; prefer known install locations.
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
for candidate in \
|
||||
"/c/Program Files/dotnet/dotnet" \
|
||||
"/mnt/c/Program Files/dotnet/dotnet" \
|
||||
"${HOME}/.dotnet/dotnet"
|
||||
do
|
||||
if [[ -x "${candidate}" ]]; then
|
||||
PATH="$(dirname "${candidate}"):${PATH}"
|
||||
export PATH
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
echo "dotnet SDK is required (not found on PATH)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${OUT_DIR}"
|
||||
|
||||
# Prefer a globally installed dotnet-ef; install 8.0.4 when nothing is present.
|
||||
# A newer global tool is fine — do not downgrade an existing install.
|
||||
if ! dotnet ef --version >/dev/null 2>&1; then
|
||||
echo "Installing dotnet-ef 8.0.4..."
|
||||
dotnet tool install --global dotnet-ef --version 8.0.4
|
||||
export PATH="${PATH}:${HOME}/.dotnet/tools:${USERPROFILE:-}/.dotnet/tools"
|
||||
fi
|
||||
|
||||
echo "Building API migration bundle → ${OUTPUT} (RID=${RID})"
|
||||
dotnet ef migrations bundle \
|
||||
--project "${ROOT_DIR}/VigilCareClinicalAPI" \
|
||||
--startup-project "${ROOT_DIR}/VigilCareClinicalAPI" \
|
||||
--configuration Release \
|
||||
--self-contained -r "${RID}" \
|
||||
--output "${OUTPUT}" \
|
||||
--force
|
||||
|
||||
echo "Bundle ready: ${OUTPUT}"
|
||||
echo "Apply with: ${OUTPUT} --connection \"\$PG_CONNECTION_DDL\""
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Restores the ASP.NET Data Protection keyring into the production Docker volume.
|
||||
# REQUIRED after volume loss or host migration — without the matching keyring,
|
||||
# every encrypted patient PHI column is permanently unreadable.
|
||||
#
|
||||
# See docs/ops/phi-encryption-runbook.md (Keyring restore).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/restore-dp-keys.sh /var/backups/vigilcare/dp-keys/dp-keys-20260804T021500Z.tar.gz
|
||||
#
|
||||
# Stops the API container, replaces volume contents, then leaves the operator
|
||||
# to bring the stack back up (so schema/image tags stay under compose control).
|
||||
set -euo pipefail
|
||||
|
||||
ARCHIVE="${1:-}"
|
||||
VOLUME_NAME="${VOLUME_NAME:-vigilcare_dp_keys}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-/opt/vigilcare/docker-compose.prod.yml}"
|
||||
COMPOSE_DIR="$(dirname "${COMPOSE_FILE}")"
|
||||
API_SERVICE="${API_SERVICE:-api}"
|
||||
|
||||
if [[ -z "${ARCHIVE}" || ! -f "${ARCHIVE}" ]]; then
|
||||
echo "Usage: $0 <path-to-dp-keys-YYYYMMDDThhmmssZ.tar.gz>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! tar tzf "${ARCHIVE}" | grep -q '\.xml$'; then
|
||||
echo "ERROR: ${ARCHIVE} does not contain Data Protection *.xml key files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stopping API service (${API_SERVICE}) so the volume is unused..."
|
||||
if [[ -f "${COMPOSE_FILE}" ]]; then
|
||||
docker compose -f "${COMPOSE_FILE}" --env-file "${COMPOSE_DIR}/.env" stop "${API_SERVICE}" || true
|
||||
else
|
||||
echo "WARN: ${COMPOSE_FILE} not found — ensure no container has ${VOLUME_NAME} mounted." >&2
|
||||
fi
|
||||
|
||||
if ! docker volume inspect "${VOLUME_NAME}" >/dev/null 2>&1; then
|
||||
echo "Creating volume ${VOLUME_NAME}..."
|
||||
docker volume create "${VOLUME_NAME}" >/dev/null
|
||||
fi
|
||||
|
||||
echo "Restoring ${ARCHIVE} → volume ${VOLUME_NAME}"
|
||||
# Clear existing keys then extract. Use a throwaway alpine container.
|
||||
docker run --rm \
|
||||
-v "${VOLUME_NAME}:/keys" \
|
||||
-v "$(cd "$(dirname "${ARCHIVE}")" && pwd):/backup:ro" \
|
||||
alpine sh -c "rm -rf /keys/* /keys/.[!.]* 2>/dev/null; tar xzf /backup/$(basename "${ARCHIVE}") -C /keys"
|
||||
|
||||
echo "Restored key files:"
|
||||
docker run --rm -v "${VOLUME_NAME}:/keys:ro" alpine ls -la /keys
|
||||
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo " 1. docker compose -f ${COMPOSE_FILE} --env-file ${COMPOSE_DIR}/.env up -d ${API_SERVICE}"
|
||||
echo " 2. curl -fsS http://localhost:5270/health/ready"
|
||||
echo " 3. Fetch a known patient and confirm firstName/lastName decrypt"
|
||||
echo " 4. Record this restore in the ops log (date, archive name, operator)"
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Reference in New Issue
Block a user