Skip to content

Commit

Permalink
Create new script to apply Gatheouse DB Schema migrations.
Browse files Browse the repository at this point in the history
  • Loading branch information
AshCorr committed Jan 23, 2025
1 parent 33b5aea commit a46af57
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,16 @@ file.
TODO: Instructions on how to generate an Okta access token locally.
We use the Okta Code org locally because this makes it easier to develop and test with users that have already been
registered in [Code gateway](https://profile.code.dev-theguardian.com/).

## Database schema migrations

This repo contains the schema files for the Gatehouse database, these can be found in the [`./db`](./db/) folder.

### Creating a new database schema migration

1. Create a new file in [`./db/migrations`](./db/migrations/) with the naming scheme `V(YYYYMMDD)__Migration_Description.sql`. The naming scheme is important and the migration will be skipped if it does not adhere to it, take note of the double underscore between the version number and migration description.
2. Run `./db/test.sh` to test and verify your new migration against the local test database.
3. Create a PR and merge your new migration into `main`
4. Run `./db/migrate.sh CODE` to apply the migrations to CODE.
- You will be prompted to rotate the admin user credentials, we suggest you always do this but you may want to delay the rotation if you are worried about your migration causing any outages.
5. Run `./db/migrate.sh PROD` to apply the migrations to PROD.
201 changes: 201 additions & 0 deletions db/migrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#!/bin/bash

export AWS_REGION=eu-west-1
export AWS_PROFILE=identity

# flyway/flyway:11-alpine
FLYWAY_CONTAINER="flyway/flyway@sha256:1850b2e6b257f774cdd6ad7554dc53cc278351ff0202f7b9b696ceafccbea493"

Grey=$"\033[30m"
Red='\033[31m'
GreenBold='\033[1;32m'
Yellow='\033[33m'
White=$"\033[37m"
WhiteBold=$"\033[1;37m"
Reset='\033[0m'

STAGE=$1
APPLY_MIGRATIONS=$2

# Applying migrations from a non-mainline branch will mess up the database schema
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "${BRANCH}" != "main" && "${STAGE}" != "DEV" ]]; then
echo './migrate.sh can only be ran on main branch.'
exit 1
fi

case $STAGE in
DEV)
;;
CODE)
;;
PROD)
echo -e "${Red}--------------------------------------\n"
echo -e "WARNING: You are about to run this script on the PROD environment. Are you sure? ${Yellow}[y/N]${Red}\n"
echo -e "${Red}--------------------------------------${Reset}"
read -r input
if [[ "${input}" != [Yy] ]]; then
echo "Aborting."
exit 1
fi
;;
*)
echo "Usage: ${0} {CODE|PROD}"
exit 1
;;
esac

echo -e "${White}Starting schema migrations on ${Yellow}${STAGE}${Reset}"

if [[ "$STAGE" != "DEV" ]]; then
DB_CLUSTER_IDENTIFIER=$(
aws ssm get-parameter \
--name "/$STAGE/identity/gatehouse/db-identifier" \
--query "Parameter.Value" \
--output text
)
if [[ -z "${DB_CLUSTER_IDENTIFIER}" ]]; then
echo "Failed to retrieve database secret ARN from SSM."
exit 1
fi
else
DB_CLUSTER_IDENTIFIER="DEV"
fi

echo -e "${White}Resolved DB Cluster as: ${Yellow}${DB_CLUSTER_IDENTIFIER}${Reset}"

if [[ "${STAGE}" != "DEV" ]]; then
DB_WRITER_ENDPOINT=$(
aws rds describe-db-clusters \
--db-cluster-identifier "${DB_CLUSTER_IDENTIFIER}" \
--query "DBClusters[0].Endpoint" \
--output text
)
if [[ -z "${DB_WRITER_ENDPOINT}" ]]; then
echo "Failed to retrieve writer endpoint for the database cluster."
exit 1
fi
else
DB_WRITER_ENDPOINT="127.0.0.1"
fi

echo -e "${White}Resolved Writer endpoint as: ${Yellow}${DB_WRITER_ENDPOINT}${Reset}"

if [[ "$STAGE" != "DEV" ]]; then
DB_SECRET_ARN=$(
aws rds describe-db-clusters \
--db-cluster-identifier "${DB_CLUSTER_IDENTIFIER}" \
--query "DBClusters[0].MasterUserSecret.SecretArn" \
--output text
)
if [[ -z "${DB_WRITER_ENDPOINT}" ]]; then
echo "Failed to retrieve writer endpoint for the database cluster."
exit 1
fi

echo -e "${White}Resolved Master user secret ARN as: ${Yellow}${DB_SECRET_ARN}${Reset}"

DB_CREDENTIALS=$(
aws secretsmanager get-secret-value \
--secret-id "${DB_SECRET_ARN}" \
--query "SecretString" \
--output text
)
if [[ -z "${DB_CREDENTIALS}" ]]; then
echo "Failed to retrieve database credentials from Secrets Manager."
exit 1
fi

DB_USERNAME=$(
echo "${DB_CREDENTIALS}" | jq -r ".username"
)
DB_PASSWORD=$(
echo "${DB_CREDENTIALS}" | jq -r ".password"
)

echo -e "${White}Starting SSH tunnel to writer endpoint...${Clear}"

# Start SSH session in foreground and fork to background after connection is established
SSH_TUNNEL_COMMAND="$(ssm ssh --raw -t identity-psql-client,${STAGE},identity 2>/dev/null) \
-o ExitOnForwardFailure=yes -fN -L 6543:${DB_WRITER_ENDPOINT}:5432"

echo -e "${White}Executing SSH tunnel command:\n\n${Grey}${SSH_TUNNEL_COMMAND}${Clear}\n"

# Slightly hacky but couldn't get SSH ControlMaster to work with the AWS session-manager-plugin
# Terminate the SSH connection when the script exits as SSH doesn't seem to be able to clean up
# AWS's session-manager-plugin properly.
cleanup_ssh_tunnel() { kill $(pgrep -f session-manager-plugin); }
trap "cleanup_ssh_tunnel" EXIT

eval "$SSH_TUNNEL_COMMAND"
echo -e "${White}SSH tunnel open, Database available on ${Yellow}127.0.0.1:6543${White}.${Reset}"
else
DB_USERNAME=postgres
DB_PASSWORD=postgres
fi

echo -e "${White}Starting migration...${Grey}\n"

LOCALHOST='host.docker.internal'
if [[ ! -z "$CI" ]]; then
# When running in Github Actions docker doesn't have docker.host.internal as a valid hostname
# Likely as it doesn't need to run the container in a VM unlike local development on MacOS
LOCALHOST="172.17.0.1"
fi

# Check pending migrations
FLYWAY_OPTS="-url=jdbc:postgresql://${LOCALHOST}:6543/gatehouse -user=${DB_USERNAME} -password=${DB_PASSWORD} -locations=filesystem:./migrations"
docker run --net host --rm -v $(dirname "$0")/migrations:/flyway/migrations ${FLYWAY_CONTAINER} \
info ${FLYWAY_OPTS}

if [[ "$?" != "0" ]]; then
echo -e "${Red}Database migration failed.${Reset}"
exit 1
fi

echo -e "${WhiteBold}Apply pending migrations? ${Yellow}[y/N]${Reset}${Grey}"

if [[ "${APPLY_MIGRATIONS}" == "true" ]]; then
echo -e "${White}Applying migrations automatically.${Grey}"
else
read -r input
if [[ "${input}" != [Yy] ]]; then
echo "Aborting."
exit 1
fi
fi

echo ""

# Apply database migrations
docker run --net host --rm -v $(dirname "$0")/migrations:/flyway/migrations ${FLYWAY_CONTAINER} \
migrate ${FLYWAY_OPTS}

echo ""

if [[ "$?" != "0" ]]; then
echo -e "${Red}Database migration failed.${Reset}"
exit 1
else
echo -e "${GreenBold}Database migration for ${STAGE} completed successfully.${Reset}"
fi

# Rotate admin user credentials
if [[ "$STAGE" != "DEV" ]]; then
echo -e "${WhiteBold}Rotate Admin Credentials?${Reset}"
echo -e "${White}Rotating admin credentials will take a few minutes and break this script until it has completed.${Reset}\n"
echo -e "${White}You can also trigger the secret rotation manually using the following command:${Grey}\n\naws secretsmanager rotate-secret --secret-id ${DB_SECRET_ARN} --profile identity --region eu-west-1\n"
echo -e "${White}Rotate admin user credentials now? ${Yellow}[Y/n]${Reset}\n"

read -r input
if [[ "${input}" == [Nn] ]]; then
exit 1
fi

echo -e "${White}Rotating admin user credentials.${Reset}"

aws secretsmanager rotate-secret \
--secret-id "${DB_SECRET_ARN}"

echo -e "${White}Done, new credentials will take a few minutes to take effect.${Reset}"
fi
11 changes: 11 additions & 0 deletions db/migrations/V0__init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE USER identity_api;

DO
$do$
BEGIN
-- The rds_iam role is created by the RDS IAM extension, which is not available in DEV
IF EXISTS (select * from pg_roles where rolname='rds_iam') THEN
GRANT rds_iam TO identity_api;
END IF;
END
$do$;
35 changes: 35 additions & 0 deletions db/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash

# Launch a Postgres test container and run the migrations against it.

White=$"\033[37m"
Red='\033[31m'
GreenBold='\033[1;32m'
Yellow='\033[33m'
Reset='\033[0m'

# postgres:16.6
POSTGRES_CONTAINER="postgres@sha256:c965017e1d29eb03e18a11abc25f5e3cd78cb5ac799d495922264b8489d5a3a1"

# Exit on error
set -e

echo -e "${White}Starting test database container...${Reset}"

# Run postgres and clean it up when the script exits
CONTAINER_ID=$(docker run \
--rm -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gatehouse -p 6543:5432 ${POSTGRES_CONTAINER}
)
cleanup() {
echo -e "${White}Stopping test database container...${Reset}"
docker stop ${CONTAINER_ID}
}
trap "cleanup" EXIT

echo -e "${White}Postgres Container ID: ${Yellow}${CONTAINER_ID}${Reset}"
echo -e "${White}Waiting 10 seconds for database to warm up.${Reset}"
sleep 10

$(dirname "$0")/migrate.sh DEV true

echo -e "${GreenBold}Tests passed!${Reset}"
3 changes: 3 additions & 0 deletions scripts/ci
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ set -e
yarn synth
)

# Verify database migrations are correct
./db/test.sh

# Build a Debian package of app
sbt clean scalafmtCheckAll scalafmtSbtCheck compile Test/compile test Debian/packageBin

Expand Down

0 comments on commit a46af57

Please sign in to comment.