Last active
February 21, 2023 13:15
-
-
Save keymon/8abbc70cdafa17f4c159b30b9b0bf9d3 to your computer and use it in GitHub Desktop.
Warpper script to get dynamic IAM RDS credentials and call a command (psql, terraform, whatever)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -o pipefail -e -u | |
SCRIPT_NAME="$0" | |
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | |
PROJECT_ROOT_DIR="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)" | |
# Role to assume before trying to assume the master_user role | |
RDS_CREDS_ROLE_ARN="${RDS_CREDS_ROLE_ARN:-}" | |
assume_role() { | |
local role_arn="$1" | |
local session_name="$2" | |
# Be sure we unset the AWS credential variables, so there are no conflicts | |
# with the env vars used in different AWS tool versions | |
# Related: https://aws.amazon.com/blogs/security/a-new-and-standardized-way-to-manage-credentials-in-the-aws-sdks/ | |
echo "unset AWS_SESSION_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SECURITY_TOKEN AWS_ROLE_ARN" | |
if which aws > /dev/null 2>&1; then | |
( | |
[ -n "${LOCAL_AWS_PROFILE:-}" ] && export AWS_PROFILE="${LOCAL_AWS_PROFILE}" | |
aws sts assume-role --role-arn "${role_arn}" --role-session-name "${session_name}" | \ | |
jq -r '"export -n AWS_PROFILE; export AWS_ACCESS_KEY_ID=\(.Credentials.AccessKeyId) AWS_SECRET_ACCESS_KEY=\(.Credentials.SecretAccessKey) AWS_SESSION_TOKEN=\"\(.Credentials.SessionToken)\""' | |
) | |
# Support assume-role-arn, for instance on atlantis | |
elif which assume-role-arn > /dev/null 2>&1; then | |
( | |
[ -n "${LOCAL_AWS_PROFILE:-}" ] && export AWS_PROFILE="${LOCAL_AWS_PROFILE}" | |
assume-role-arn --role "${role_arn}" --name "${session_name}" | |
) | |
else | |
echo "Unable to find aws cli or assume-role-arn" 1>&2 | |
return 1 | |
fi | |
} | |
read_db_info() { | |
local rds_name="$1" | |
local rw_or_ro="$2" | |
local rds_config_file="${PROJECT_ROOT_DIR}/assets/rds-config.${rds_name}.sh" | |
if [ ! -f "${rds_config_file}" ]; then | |
echo "ERROR: Cannot find ${rds_config_file} with the static config of RDS" 1>&2 | |
return 1 | |
fi | |
if [ "${rw_or_ro}" == "ro" ]; then | |
cat "${rds_config_file}" | sed 's/PGHOST_RO/PGHOST/' | |
else | |
cat "${rds_config_file}" | sed 's/PGHOST_RW/PGHOST/' | |
fi | |
} | |
# Assumes AWS creds and DB env vars are set | |
aws_generate_db_auth_token() { | |
if which aws > /dev/null 2>&1; then | |
aws rds generate-db-auth-token \ | |
--hostname "${PGHOST}" \ | |
--port "${PGPORT}" \ | |
--region "${REGION}" \ | |
--username "${PGUSER}" | |
# Support rds-auth-token, for instance on atlantis | |
elif which rds-auth-token > /dev/null 2>&1; then | |
rds-auth-token \ | |
--hostname "${PGHOST}" \ | |
--port "${PGPORT}" \ | |
--region "${REGION}" \ | |
--username "${PGUSER}" | |
else | |
echo "Unable to find aws cli or rds-auth-token" 1>&2 | |
return 1 | |
fi | |
} | |
get_rds_creds() { | |
local rds_name="$1" | |
local rw_or_ro="$2" | |
# store injected PGHOST and PGPORT | |
OVERRIDE_PGHOST="${PGHOST:-}" | |
OVERRIDE_PGPORT="${PGPORT:-}" | |
# Load DB config | |
db_info="$(read_db_info "${rds_name}" "${rw_or_ro}")" | |
# echo "${db_info}" 1>&2 | |
eval "${db_info}" | |
# Get the new PGPASSWORD token | |
if [ -n "${PGPASSWORD:-}" ]; then | |
echo "# WARNING: Using already defined \$PGPASSWORD. Not generating a dynamic | IAM one." 1>&2 | |
else | |
if [ -n "${LOCAL_AWS_PROFILE:-}" ] && [ \ | |
-n "${AWS_SESSION_TOKEN:-}" -o \ | |
-n "${AWS_ACCESS_KEY_ID:-}" -o \ | |
-n "${AWS_SECRET_ACCESS_KEY:-}" -o \ | |
-n "${AWS_SECURITY_TOKEN:-}" \ | |
]; then | |
echo "WARNING: Meant to be using a AWS_PROFILE=${LOCAL_AWS_PROFILE} but also some AWS environment creds are set. Might cause a conflict and generate an invalid PGPASSWORD. You might want to run: 'unset AWS_SESSION_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SECURITY_TOKEN AWS_ROLE_ARN'" 1>&2 | |
fi | |
if [ -z "${PGUSER_ROLE_ARN:-}" ]; then | |
echo "ERROR: neither PGPASSWORD nor PGUSER_ROLE_ARN are set. cannot get credentials" 1>&2 | |
exit 1 | |
fi | |
PGPASSWORD="$( | |
# Assume firts role for the account if defined | |
if [ -n "${RDS_CREDS_ROLE_ARN}" ]; then | |
assume_role="$(assume_role "${RDS_CREDS_ROLE_ARN}" "rds_iam_master_user")" | |
[ -n "${assume_role}" ] || exit 1 | |
eval "${assume_role}" | |
fi | |
# assume AWS role to connect to the DB as master user | |
assume_master_user_role="$(assume_role "${PGUSER_ROLE_ARN}" "rds_iam_master_user")" | |
[ -n "${assume_master_user_role}" ] || exit 1 | |
eval "${assume_master_user_role}" | |
aws_generate_db_auth_token | |
)" | |
[ -n "${PGPASSWORD:-}" ] || return 1 | |
fi | |
export PGSSLMODE="${PGSSLMODE}" | |
export PGHOST_REAL="${PGHOST}" | |
export PGPORT_REAL="${PGPORT}" | |
export PGHOST="${OVERRIDE_PGHOST:-${PGHOST}}" | |
export PGPORT="${OVERRIDE_PGPORT:-${PGPORT}}" | |
export PGDATABASE="${PGDATABASE}" | |
export PGUSER="${PGUSER}" | |
export REGION="${REGION}" | |
export PGPASSWORD="${PGPASSWORD}" | |
} | |
print_connection_vars() { | |
local pgpassword_redacted | |
local pgpassword_encoded | |
if [[ "${PGPASSWORD}" == *X-Amz-Credential* ]]; then | |
pgpassword_redacted="${PGPASSWORD}" | |
pgpassword_encoded="$(echo "${pgpassword_redacted}" | jq -Rr '. | @uri')" | |
else | |
pgpassword_redacted="<redacted>" | |
pgpassword_encoded="<redacted>" | |
echo "WARNING: Redacting non RDS IAM PGPASSWORD" 1>&2 | |
fi | |
cat <<EOF | |
# DB connection parameters. Note: RDS IAM password lasts 15m | |
export PGSSLMODE="$PGSSLMODE" | |
export PGHOST="$PGHOST" | |
export PGPORT="$PGPORT" | |
export PGDATABASE="$PGDATABASE" | |
export PGUSER="$PGUSER" | |
export PGPASSWORD="$pgpassword_redacted" | |
export REGION="$REGION" | |
# Original remote db endpoints | |
# export PGHOST="$PGHOST_REAL" | |
# export PGPORT="$PGPORT_REAL" | |
# DB connection postgres:// url | |
export DATABASE_URL="postgres://${PGUSER}:${pgpassword_encoded}@${PGHOST}:${PGPORT}/${PGDATABASE}?${PGSSLMODE:+sslmode=${PGSSLMODE}}" | |
EOF | |
} | |
list_configs() { | |
ls "${PROJECT_ROOT_DIR}"/assets/*.sh | sed -n "s|.*/\?rds-config.\([^.]*\).sh| $SCRIPT_NAME \1 rw|p" | |
} | |
usage() { | |
cat 1>&2 <<EOF | |
Generates config and variables for RDS in form of shell vars. | |
Reads the RDS config from the files in: | |
\$GITROOT/assets/rds-config.<dbname>.sh | |
and it generates a IAM RDS password using the role from \$PGUSER_ROLE_ARN. | |
The script will assume that role. | |
Usage: | |
$SCRIPT_NAME [options] <dbname> [rw|ro] [cmd] | |
Options: | |
--port Override the port to export. Equivalent to export PGPORT | |
--host Override the host to export. Equivalent to export PGHOST | |
Examples: | |
$SCRIPT_NAME my-cool-db-prod rw | |
$SCRIPT_NAME my-cool-db-prod rw <cmd> | |
Env vars: | |
PGPASSWORD if set, uses that value instead of a IAM generated one. | |
This is good to use static credentials. See below | |
PGPORT PGHOST if set, will export/print these instead of the config ones. | |
The configuration PGHOST and PGPORT are still used to generate | |
the RDS IAM creds. | |
RDS_CREDS_ROLE_ARN if set, assume this role before assuming PGUSER_ROLE_ARN | |
LOOP Print the creds in a loop | |
Example using static creds: | |
terraform output | |
read -s -p "Introduce static pass from terraform: " PGPASSWORD | |
export PGPASSWORD | |
${SCRIPT_NAME} my-db devel rw | |
Known configs: | |
$(list_configs) | |
EOF | |
exit 1 | |
} | |
############################################################## | |
# Parse arguments | |
while true; do | |
case "$1" in | |
--help|-h) | |
usage | |
exit 0 | |
;; | |
-p|--port) | |
PGPORT="${2}" | |
shift | |
shift | |
;; | |
-h|--host) | |
PGHOST="${2}" | |
shift | |
shift | |
;; | |
-*) | |
echo "ERROR: Unknown option: $1" 1>&2 | |
usage | |
exit 1 | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
if [ $# -lt 2 ]; then | |
usage | |
fi | |
dbname="${1}"; shift | |
mode="${1:-rw}"; shift || true | |
# Store the passed password in a different var | |
PGPASSWORD_ORIG="${PGPASSWORD:-}" | |
if [ $# -lt 1 ]; then | |
# Loop generating RDS creds | |
while true; do | |
if get_rds_creds "${dbname}" "${mode}"; then | |
print_connection_vars | |
else | |
echo "ERROR: Failed generating RDS IAM creds. Maybe you need to run gimme-aws-creds again?"1>&2 | |
[ -n "${LOOP:-}" ] || exit 1 | |
fi | |
[ -n "${LOOP:-}" ] || break | |
# Restore the originally passed if any, so next loops generate it again | |
PGPASSWORD="${PGPASSWORD_ORIG}" | |
read -p "Press enter to generate new creds. Ctrl+C to continue." | |
done | |
else | |
get_rds_creds "${dbname}" "${mode}" | |
"$@" | |
fi | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Where to start the tunnel | |
export TUNNEL_CONTEXT="some-k8s-cluster-used-for-tunneling" | |
export PGSSLMODE=require | |
export PGHOST_RW=my-cool-db.asdfghjkl.us-west-2.rds.amazonaws.com | |
# export PGHOST_RO=n/a | |
export PGPORT=5432 | |
export PGDATABASE=my-cool-db | |
export PGUSER=${PGUSER:-theboss} | |
export REGION=us-west-2 | |
export PGUSER_ROLE_ARN="arn:aws:iam::1234567890:role/my-cool-db_admin_master_user" | |
# AWS profile to use when running locally | |
export LOCAL_AWS_PROFILE=my-production-account-profile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# shellcheck shell=bash | |
# shellcheck disable=SC2088 | |
set -e -o pipefail -u | |
SCRIPT_NAME="$0" | |
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | |
PROJECT_ROOT_DIR="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)" | |
usage() { | |
cat <<EOF | |
Combines retrieval of the DB creds (ie RDS IAM) with setting up a tunnel | |
to the RDS instance. | |
See the scripts for more details: | |
./${SCRIPT_DIR}/rds-tunnel-pod.sh | |
./${SCRIPT_DIR}/rds_creds.sh | |
for more details | |
Loads the DB config from ./assets/* | |
Usage: | |
${SCRIPT_NAME} <dbname> <rw|ro> [cmd] | |
If no cmd is passed, it will just sleep and print the credentials | |
Existing RDS databases: | |
EOF | |
# Print the existing configs | |
ls ${PROJECT_ROOT_DIR}/assets/rds-config.* | sed "s|.*/rds-config.\(.*\)\.sh| ${SCRIPT_NAME} \1 rw psql|" | |
echo | |
exit 0 | |
} | |
if [ $# -lt 2 ]; then | |
usage | |
fi | |
dbname="${1}"; shift | |
mode="${1:-rw}"; shift || true | |
# Run rds_creds to fail quick if AWS creds are not working | |
"${SCRIPT_DIR}"/rds_creds.sh "${dbname}" "${mode}" > /dev/null | |
if [ $# -lt 1 ]; then | |
LOOP=1 \ | |
"${SCRIPT_DIR}"/rds-tunnel-pod.sh run "${dbname}" "${mode}" \ | |
"${SCRIPT_DIR}"/rds_creds.sh "${dbname}" "${mode}" | |
else | |
"${SCRIPT_DIR}"/rds-tunnel-pod.sh run "${dbname}" "${mode}" \ | |
"${SCRIPT_DIR}"/rds_creds.sh "${dbname}" "${mode}" "$@" | |
fi | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -o pipefail -e -u | |
SCRIPT_NAME="$0" | |
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | |
PROJECT_ROOT_DIR="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)" | |
usage() { | |
cat 1>&2 <<EOF | |
Helper script that re-exports all the postgres RDS connection vars into | |
variables with some prefix. | |
This is very useful to pass credentials to terraform as TF_VAR_<foo>. | |
It will unset the postgres variables. It can call a sub command or | |
just print to stdout. | |
This is meant to be used in combination with other scripts. | |
Usage: | |
$SCRIPT_NAME <prefix> [cmd] | |
Example: | |
./scripts/rds-connect-wrapper.sh my-cool-db rw \ | |
$SCRIPT_NAME TF_VAR_my-cool-db-rw \ | |
terraform plan | |
EOF | |
} | |
print_connection_vars() { | |
local prefix="$1" | |
cat <<EOF | |
# DB connection parameters. Note: RDS IAM password lasts 15m | |
export ${prefix}_pgsslmode="$PGSSLMODE" | |
export ${prefix}_pghost="$PGHOST" | |
export ${prefix}_pgport="$PGPORT" | |
export ${prefix}_pgdatabase="$PGDATABASE" | |
export ${prefix}_pguser="$PGUSER" | |
export ${prefix}_pgpassword="$PGPASSWORD" | |
export ${prefix}_region="$REGION" | |
# DB connection postgres:// url | |
export ${prefix}_database_url="postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/${PGDATABASE}?${PGSSLMODE:+sslmode=${PGSSLMODE}}" | |
EOF | |
} | |
unset_vars() { | |
unset PGSSLMODE PGHOST PGPORT PGDATABASE PGUSER PGPASSWORD REGION | |
} | |
if [ $# -lt 1 ]; then | |
usage | |
fi | |
prefix="${1}"; shift | |
if [ $# -lt 1 ]; then | |
print_connection_vars "${prefix}" | |
else | |
eval "$(print_connection_vars "${prefix}")" | |
# Unset the PG* before running the command | |
unset PGSSLMODE PGHOST PGPORT PGDATABASE PGUSER PGPASSWORD | |
"$@" | |
fi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# shellcheck shell=bash | |
# shellcheck disable=SC2088 | |
set -e -o pipefail -u | |
SCRIPT_NAME="$0" | |
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | |
PROJECT_ROOT_DIR="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)" | |
#---------------------------------------------------------- | |
TUNNEL_NAMESPACE=rds-tunnel | |
TUNNEL_LABEL=type=rds-tunnel | |
TUNNEL_TTL=3600 | |
TUNNEL_WAIT_TTL=360 | |
# Override for instance when running on docker to host.docker.internal | |
TUNNEL_HOST="${TUNNEL_HOST:-localhost}" | |
# Override if running on docker with, for instance | |
# localhost,172.17.0.1 | |
# or the output of | |
# docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}' | |
TUNNEL_LISTEN_ADDR="${TUNNEL_LISTEN_ADDR:-localhost}" | |
#---------------------------------------------------------- | |
check_aws_creds() { | |
# TODO: add more smart checks, like check that the account id matches | |
if ! aws sts get-caller-identity > /dev/null; then | |
echo "Unable to use AWS creds. Did you load the creds for the corresponding environment (ie. gimme-aws-creds)?" 1>&2 | |
if [ -n "${AWS_ACCESS_KEY_ID}" ]; then | |
echo "AWS_ACCESS_KEY_ID is set. Maybe credentials are expired?" 1>&2 | |
fi | |
if [ -z "${AWS_PROFILE}" ]; then | |
echo "AWS_PROFILE is not set, maybe set it? For instance: export AWS_PROFILE=my-prod-env-db-administrator" 1>&2 | |
else | |
echo "AWS_PROFILE is set. Maybe credentials are expired?" 1>&2 | |
fi | |
exit 1 | |
fi | |
} | |
wait_kubectl_tunnel() { | |
local output_file="$1" | |
local kubectl_tunnel_pid="$2" | |
echo "Waiting for tunnel to start. Press Ctrl+C to cancel..." 1>&2 | |
if [ -z "${CCTUNNEL_SLEEP_WAIT:-}" ]; then | |
# Wait for at least one "Forwarding" line | |
while ! grep -q "Forwarding" "${output_file}"; do sleep 1; done | |
echo "Tunnel started in $!" 1>&2 | |
else | |
# Simple sleep wait | |
sleep 10 | |
fi | |
if ! ps -ea | grep -q "${kubectl_tunnel_pid}"; then | |
echo "WARNING: kubectl PID ${kubectl_tunnel_pid} not running, tunnel might not be up!" 1>&2 | |
fi | |
} | |
# Tries to find a free port randomly from a given range | |
random_free_port() { | |
local max_attempts=5 | |
local low_range=8432 | |
local range=200 | |
for i in $(seq "${max_attempts}"); do | |
# Pick a random port between 8432-8632 | |
local port="$((${low_range} + ${RANDOM} % ${range}))" | |
# Check if the port is not open | |
if ! nc -z -w 0 localhost 8433; then | |
echo "Found free port ${port}" 1>&2 | |
echo "${port}" | |
return | |
fi | |
done | |
echo "Unable to find a free port after 5 attempts" 1>&2 | |
return 1 | |
} | |
start_socat_pod() { | |
local k8s_context="$1" | |
local endpoint="$2" | |
local port="$3" | |
local pod_name="$4" | |
if ! kubectl --context "${k8s_context}" get ns "${TUNNEL_NAMESPACE}" > /dev/null 2>&1; then | |
kubectl --context "${k8s_context}" create ns "${TUNNEL_NAMESPACE}" | |
fi | |
if kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" get pod "${pod_name}" > /dev/null 2>&1; then | |
echo "Pod ${TUNNEL_NAMESPACE}/${pod_name} already exists. Tunnel pod already running?" 1>&2 | |
return 0 | |
fi | |
kubectl --context "${k8s_context}" run "${pod_name}" \ | |
--namespace "${TUNNEL_NAMESPACE}" \ | |
--image=alpine/socat \ | |
-it --tty \ | |
--labels=type=rds-tunnel \ | |
--expose=false \ | |
--attach=false \ | |
--port=${port} \ | |
--restart=Never \ | |
--pod-running-timeout=5m \ | |
--command -- sh -c \ | |
"socat -d -d -d tcp-listen:${port},fork,reuseaddr tcp-connect:${endpoint}:${port} 2>&1 | | |
( | |
while read -t ${TUNNEL_TTL} l; do true; done; | |
echo 'No activity detected. Exiting'; killall socat; | |
)" > /dev/null | |
echo "Waiting for pod to start..." 1>&2 | |
START_EPOCH="$(date +%s)" | |
while true; do | |
if kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" wait --for=condition=Ready "pod/${pod_name}" --timeout=20s; then | |
kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" get pod "${pod_name}" | |
break | |
fi | |
if [ "$((START_EPOCH + TUNNEL_WAIT_TTL))" -lt "$(date +%s)" ]; then | |
echo "Timeout waiting for pod" 1>&2 | |
return 1 | |
fi | |
kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" get pod "${pod_name}" --no-headers=true | |
done | |
} | |
tunnel_started_tip() { | |
local k8s_context="$1" | |
local endpoint="$2" | |
local port="$3" | |
local pod_name="$4" | |
cat 1>&2 <<EOF | |
Pod ${pod_name} started. You can start a tunnel now with: | |
kubectl port-forward \\ | |
--context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" \\ | |
--address ${TUNNEL_LISTEN_ADDR} \\ | |
pod/${pod_name} ${port}:${port} | |
You can then connect using: | |
export PGHOST=localhost | |
export PGPORT="${port}" | |
read PGUSER | |
read PGPASSWORD | |
psql | |
EOF | |
} | |
run_kubectl_tunnel() { | |
local k8s_context="$1" | |
local remote_port="$2" | |
local local_port="$3" | |
local pod_name="$4" | |
echo "starting local kubectl tunnel on ${local_port}" | |
kubectl --context "${k8s_context}" -n "${TUNNEL_NAMESPACE}" \ | |
port-forward \ | |
--address "${TUNNEL_LISTEN_ADDR}" \ | |
"pod/${pod_name}" "${local_port}:${remote_port}" \ | |
> "${TMPDIR}/kubectl-tunnel.stdout" & | |
KUBECTL_TUNNEL_PID="$!" | |
wait_kubectl_tunnel "${TMPDIR}/kubectl-tunnel.stdout" "${KUBECTL_TUNNEL_PID}" | |
export PGHOST="${TUNNEL_HOST}" | |
export PGPORT="${local_port}" | |
export TF_VAR_override_db_host="${TUNNEL_HOST}" | |
export TF_VAR_override_db_port="${local_port}" | |
cat <<EOF | |
Kubectl tunnel started. Setting environment variables to use with clients/terraform | |
export PGHOST="${PGHOST}" | |
export PGPORT="${PGPORT}" | |
export TF_VAR_override_db_host="${TF_VAR_override_db_host}" | |
export TF_VAR_override_db_port="${TF_VAR_override_db_port}" | |
EOF | |
} | |
delete_tunnel_pods() { | |
local k8s_context="$1" | |
local pod_name="$2" | |
echo "Deleting tunnel pod ${pod_name}..." 1>&2 | |
kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" \ | |
delete pods --wait=0 --ignore-not-found=true "${pod_name}" | |
} | |
clean_terminated_pods() { | |
local k8s_context="$1" | |
pods_to_delete="$( | |
kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" get pods -l "${TUNNEL_LABEL}" -o json | \ | |
jq -r '.items[]|select(.status.containerStatuses[].state.terminated)|.metadata.name' | |
)" | |
if [ "${pods_to_delete}" ]; then | |
echo "Found ${pods_to_delete//$'\n'/,} orphaned pods. Deleting them" 1>&2 | |
for p in ${pods_to_delete}; do | |
kubectl --context "${k8s_context}" --namespace "${TUNNEL_NAMESPACE}" delete pod --wait=false "$p" | |
done | |
fi | |
} | |
load_config() { | |
local rds_name="$1" | |
local rw_or_ro="$2" | |
# Load the config | |
local rds_config_file="${PROJECT_ROOT_DIR}/assets/rds-config.${rds_name}.sh" | |
if [ ! -f "${rds_config_file}" ]; then | |
echo "ERROR: Cannot find ${rds_config_file} with the static config of RDS" 1>&2 | |
return 1 | |
fi | |
eval "$( | |
cat ${rds_config_file} | \ | |
grep \ | |
-e TUNNEL_CONTEXT= \ | |
-e PGHOST_RW= \ | |
-e PGHOST_RO= \ | |
-e PGPORT= | |
)" | |
if [ "${rw_or_ro}" == "ro" ]; then | |
export ENDPOINT="${PGHOST_RO}" | |
else | |
export ENDPOINT="${PGHOST_RW}" | |
fi | |
export PORT="${PGPORT}" | |
} | |
usage() { | |
cat <<EOF | |
Starts a pod to used as TCP tunnel to the remote RDS instance in the | |
corresponding k8s cluster. | |
Loads the DB config from ./assets/* | |
Usage: | |
${SCRIPT_NAME} [options] [start|stop] <dbname> <rw|ro> | |
${SCRIPT_NAME} [options] run <dbname> <rw|ro> <cmd> | |
Actions | |
- start: start the remote pod in the cluster | |
- stop: deletes the remote pod | |
- run: starts the tunnel, runs the given command | |
Options: | |
- --port <n> Use given port locally rather than a random one | |
Examples: | |
${SCRIPT_NAME} start my-cool-db rw | |
${SCRIPT_NAME} stop my-cool-db rw | |
${SCRIPT_NAME} run my-cool-db rw terraform plan | |
Env Vars: | |
PRINT_RDS_CREDS Print RDS connection info | |
TUNNEL_PORT Use given port locally rather than a random one | |
Existing RDS databases: | |
EOF | |
# Print the existing configs | |
ls ${PROJECT_ROOT_DIR}/assets/rds-config.* | sed "s|.*/rds-config.\(.*\)\.sh| ${SCRIPT_NAME} run \1 rw|" | |
echo | |
exit 0 | |
} | |
trap_handler() { | |
if [ -d "${TMPDIR:-}" ]; then | |
rm -rf "${TMPDIR:-/tmp/foo}" | |
fi | |
# kill kubectl tunnel if running | |
if [ "${KUBECTL_TUNNEL_PID:-}" ]; then | |
kill "${KUBECTL_TUNNEL_PID}" | |
fi | |
} | |
############################################################## | |
# Parse arguments | |
while true; do | |
case "$1" in | |
--help|-h) | |
usage | |
exit 0 | |
;; | |
-p|--port) | |
TUNNEL_PORT="${2}" | |
shift | |
shift | |
;; | |
-*) | |
echo "ERROR: Unknown option: $1" 1>&2 | |
usage | |
exit 1 | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
if [ $# -lt 3 ]; then | |
usage | |
fi | |
action="${1}"; shift | |
dbname="${1}"; shift | |
mode="${1}"; shift | |
# Unique pod name | |
TUNNEL_POD_NAME="rds-tunnel-${dbname}-${mode}-${USER}" | |
TUNNEL_POD_NAME="${TUNNEL_POD_NAME//_/-}" | |
# Initial checks | |
# [ -z "${SKIP_CHECK_AWS_CREDS:-}" ] && check_aws_creds | |
export -n TMPDIR # avoid bugs caused by a exported TMPDIR | |
TMPDIR="$(mktemp -d)" | |
trap 'trap_handler' EXIT | |
load_config "${dbname}" "${mode}" | |
case "${action}" in | |
start) | |
clean_terminated_pods "${TUNNEL_CONTEXT}" | |
start_socat_pod "${TUNNEL_CONTEXT}" "${ENDPOINT}" "${PORT}" "${TUNNEL_POD_NAME}" | |
tunnel_started_tip "${TUNNEL_CONTEXT}" "${ENDPOINT}" "${PORT}" "${TUNNEL_POD_NAME}" | |
;; | |
stop) | |
clean_terminated_pods "${TUNNEL_CONTEXT}" | |
delete_tunnel_pods "${TUNNEL_CONTEXT}" "${TUNNEL_POD_NAME}" | |
;; | |
run) | |
# Get a random free port | |
TUNNEL_PORT="${TUNNEL_PORT:-$(random_free_port)}" | |
clean_terminated_pods "${TUNNEL_CONTEXT}" | |
start_socat_pod "${TUNNEL_CONTEXT}" "${ENDPOINT}" "${PORT}" "${TUNNEL_POD_NAME}" | |
run_kubectl_tunnel "${TUNNEL_CONTEXT}" "${PORT}" "${TUNNEL_PORT}" "${TUNNEL_POD_NAME}" | |
if [ $# -ge 1 ]; then | |
"$@" | |
else | |
echo "Tip: Run '$SCRIPT_DIR/rds_creds.sh ${dbname} ${mode}' to get new RDS IAM creds" | |
echo "Sleeping... Ctrl+C to exit" 1>&2 | |
sleep 14400 | |
fi | |
;; | |
esac | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment