├── README.md ├── erl-shell.sh ├── help.sh ├── helpers.sh ├── iex-observer.sh ├── iex-remsh.sh ├── iex-shell.sh ├── ktl.sh ├── pg-copy.sh ├── pg-diagnose.sh ├── pg-dump.sh ├── pg-kill.sh ├── pg-open.sh ├── pg-outliers.sh ├── pg-proxy.sh ├── pg-ps.sh ├── pg-psql.sh ├── pg-restore.sh ├── promote.sh ├── shell.sh └── status.sh /README.md: -------------------------------------------------------------------------------- 1 | # k8s-utils 2 | 3 | Kubernetes utils for debugging our development or production environments. 4 | 5 | For more details run `ktl.sh help`. 6 | 7 | ## Installation 8 | 9 | On macOS, clone this repo and create a symlink: 10 | 11 | `ln -s {path_to_this repo}/ktl.sh /usr/local/bin/ktl` 12 | -------------------------------------------------------------------------------- /erl-shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script provides easy way to debug remote Erlang nodes that is running in a Kubernetes cluster. 3 | # 4 | # Application on remote node should include `:runtime_tools` in it's applications dependencies, otherwise 5 | # you will receive `rpc:handle_call` error. 6 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 7 | source ${K8S_UTILS_DIR}/helpers.sh 8 | 9 | function show_help { 10 | echo " 11 | ktl erl:shell -lSELECTOR or -pPOD_NAME [-nNAMESPACE -h] 12 | 13 | Connect to a shell of running Erlang/OTP node. Shell is executed wihin the pod. 14 | 15 | If there are multuple pods that match the selector - random one is choosen. 16 | 17 | Examples: 18 | ktl erl:shell -lapp=hammer-web Connect to one of the pods of hammer-web application in default namespace. 19 | ktl erl:shell -lapp=hammer-web -nweb Connect to one of the pods of hammer-web application in web namespace. 20 | ktl erl:shell -phammer-web-kfjsu-3827 -nweb Connect to hammer-web pod in web namespace. 21 | " 22 | } 23 | 24 | K8S_NAMESPACE="" 25 | POD_NAME= 26 | K8S_SELECTOR= 27 | 28 | # Read configuration from CLI 29 | while getopts "n:l:p:h" opt; do 30 | case "$opt" in 31 | n) K8S_NAMESPACE=${OPTARG} 32 | ;; 33 | l) K8S_SELECTOR=${OPTARG} 34 | ;; 35 | p) POD_NAME=${OPTARG} 36 | ;; 37 | h) show_help 38 | exit 0 39 | ;; 40 | esac 41 | done 42 | 43 | # Required part of config 44 | if [[ ! $K8S_SELECTOR && ! $POD_NAME ]]; then 45 | error "You need to specify Kubernetes selector with '-l' option or pod name via '-p' option." 46 | fi 47 | 48 | if [ ! $POD_NAME ]; then 49 | POD_NAME=$(fetch_pod_name "${K8S_NAMESPACE}" "${K8S_SELECTOR}") 50 | fi 51 | 52 | if [ ! $K8S_NAMESPACE ]; then 53 | K8S_NAMESPACE=$(get_pod_namespace "${POD_NAME}") 54 | fi 55 | 56 | POD_DNS=$(get_pod_dns_record "${K8S_NAMESPACE}" "${POD_NAME}") 57 | 58 | log_step "Entering shell on remote Erlang/OTP node." 59 | kubectl exec ${POD_NAME} --namespace=${K8S_NAMESPACE} \ 60 | -it \ 61 | -- /bin/sh -c 'erl -name debug_cli_'$(whoami)'@'${POD_DNS}' -setcookie ${ERLANG_COOKIE} -hidden -remsh $(epmd -names | tail -n 1 | awk '"'"'{print $2}'"'"')@'${POD_DNS} 62 | -------------------------------------------------------------------------------- /help.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | KUBECTL_HELP=$(kubectl help) 5 | KTL_HELP="\ 6 | Basic Commands (ktl): 7 | promote Promote staging image versions to production by updating values files 8 | shell Connects to the shell of a random pod selected by label and namespace 9 | pg:ps View active queries with execution time 10 | pg:kill Kill a query. 11 | pg:outliers Show queries that have longest execution time in aggregate 12 | pg:diagnose Shows diagnostics report 13 | pg:psql Run psql with a cluster database 14 | pg:open Open local app binded to postgres:// protocol with a cluster database 15 | pg:proxy Port-forward cluster database to connect on localhost 16 | pg:dump Dumps PostgreSQL database to local directory in binary format 17 | pg:resotre Restore PostgreSQL database from dump 18 | pg:copy Copy query result from a remote PostgreSQL database and to a local one 19 | erl:shell Connect to a Erlang shell of running Erlang/OTP node (executes wihin the pod) 20 | iex:shell Connect to a IEx shell of running Erlang/OTP node (executes wihin the pod) 21 | iex:remsh Remote shell into a running Erlang/OTP node (via port-foward and iex --remsh), run with sudo 22 | iex:observer Connect to a running Erlang/OTP node an locally run observer, run with sudo 23 | status Show version information for deployed containers 24 | 25 | " 26 | 27 | echo "${KUBECTL_HELP/Basic Commands (Beginner):/${KTL_HELP}Basic Commands (Beginner):}" 28 | -------------------------------------------------------------------------------- /helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -meuo pipefail 3 | 4 | PROJECT_ROOT_DIR=$(git rev-parse --show-toplevel) 5 | OS=`uname` 6 | 7 | function prepend() { 8 | while read line; do echo "${1}${line}"; done; 9 | } 10 | 11 | function log_step_append() { 12 | echo "$1" | prepend " " >&2 13 | } 14 | 15 | function log_step() { 16 | echo "- $1" >&2 17 | } 18 | 19 | function log_step_with_progress() { 20 | echo -n "- $1" >&2 21 | } 22 | 23 | function log_progess_step() { 24 | echo -n "." >&2 25 | } 26 | 27 | function banner() { 28 | echo "" 29 | echo " $1" 30 | echo "" 31 | } 32 | 33 | function error() { 34 | echo "[E] $1" >&2 35 | exit 1 36 | } 37 | 38 | function warning() { 39 | echo "[W] $1" >&2 40 | } 41 | 42 | # A basic wrapper for `sed` that works with both macOS and GNU versions 43 | function delete_pattern_in_file { 44 | if [ "${OS}" = "Darwin" ]; then 45 | sudo sed -i '' "$1" "$2" 46 | else 47 | sudo sed -i "$1" "$2" 48 | fi 49 | } 50 | 51 | function replace_pattern_in_file { 52 | if [ "${OS}" = "Darwin" ]; then 53 | sed -E -i '' "$1" "$2" 54 | else 55 | sed -E -i "$1" "$2" 56 | fi 57 | } 58 | 59 | function fetch_pod_name() { 60 | NAMESPACE=$1 61 | SELECTOR=$2 62 | 63 | if [[ "${NAMESPACE}" = "" ]]; then 64 | NAMESPACE_OPT="--all-namespaces=true" 65 | else 66 | NAMESPACE_OPT="--namespace=${NAMESPACE}" 67 | fi 68 | 69 | log_step "Selecting pod with '${NAMESPACE_OPT} --selector=${SELECTOR}' options." 70 | 71 | POD_NAME=$( 72 | kubectl get pods ${NAMESPACE_OPT} \ 73 | --selector="${SELECTOR}" \ 74 | --field-selector='status.phase=Running' \ 75 | --output="json" \ 76 | | jq -r '.items[] | select((.status.conditions[] | select (.status == "True" and .type == "Ready"))) | .metadata.name' \ 77 | | head -n 1 78 | ) 79 | 80 | if [[ "${POD_NAME}" == "" || "${POD_NAME}" == "null" ]]; then 81 | error "Pod not found. Use -h for list of available selector options." 82 | else 83 | echo "${POD_NAME}" 84 | fi 85 | } 86 | 87 | # Pods 88 | 89 | function get_pod_namespace() { 90 | POD_NAME=$1 91 | 92 | log_step "Getting pod ${POD_NAME} namespace" 93 | 94 | kubectl get pods --all-namespaces=true --output="json" \ 95 | | jq -r ".items[] | select(.metadata.name == \"${POD_NAME}\") | .metadata.namespace" 96 | } 97 | 98 | function get_pod_ip_address() { 99 | NAMESPACE=$1 100 | POD_NAME=$2 101 | 102 | log_step "Getting pod ${POD_NAME} cluster IP address" 103 | 104 | kubectl get pod ${POD_NAME} --namespace="${NAMESPACE}" --output="json" \ 105 | | jq -r '.status.podIP' 106 | } 107 | 108 | function get_pod_dns_record() { 109 | NAMESPACE=$1 110 | POD_NAME=$2 111 | 112 | POD_IP=$(get_pod_ip_address "${NAMESPACE}" "${POD_NAME}") 113 | echo $(echo "${POD_IP}" | sed 's/\./-/g')."${NAMESPACE}.pod.cluster.local" 114 | } 115 | 116 | # Networking 117 | 118 | function get_free_random_port() { 119 | PORT=$(awk 'BEGIN{srand();print int(rand()*(63000-2000))+2000 }') 120 | if nc -z localhost ${PORT} < /dev/null; then 121 | echo $(get_random_port) 122 | else 123 | echo ${PORT} 124 | fi 125 | } 126 | 127 | function ensure_port_is_free() { 128 | PORT=$1 129 | 130 | if nc -z localhost ${PORT} < /dev/null; then 131 | error "Port ${PORT} is busy, try to specify different port number. Use -h for list of available selector options." 132 | else 133 | return 0 134 | fi 135 | } 136 | 137 | function is_port_free() { 138 | PORT=$1 139 | 140 | if nc -z localhost ${PORT} < /dev/null; then 141 | echo "false" 142 | else 143 | echo "true" 144 | fi 145 | } 146 | 147 | function wait_for_ports_to_become_busy() { 148 | PORT=$1 149 | 150 | log_step "Waiting for for ports ${PORT} to start responding" 151 | 152 | for i in `seq 1 30`; do 153 | [[ "${i}" == "30" ]] && error "Failed waiting for ports ${PORT} to start responding" 154 | nc -z localhost ${PORT} && break 155 | echo -n . 156 | sleep 1 157 | done 158 | } 159 | 160 | # Google Cloud SQL specific 161 | 162 | function get_postgres_connection_url() { 163 | USER=$1 164 | PASSWORD=$2 165 | PORT=$3 166 | DB=$4 167 | 168 | echo "postgres://${USER}:${PASSWORD}@localhost:${PORT}/${DB}" 169 | } 170 | 171 | function get_postgres_user_password() { 172 | SQL_INSTANCE_NAME=$1 173 | POSTGRES_USER=$2 174 | 175 | log_step "Resolving PostgreSQL ${POSTGRES_USER} user password for Cloud SQL instance ${SQL_INSTANCE_NAME}" 176 | 177 | POSTGRES_USER_BASE64=$(printf "%s" "${POSTGRES_USER}" | base64) 178 | 179 | POSTGRES_PASSWORD=$( 180 | kubectl get secrets --all-namespaces=true \ 181 | -l "service=google_cloud_sql,instance_name=${SQL_INSTANCE_NAME}" \ 182 | -o json \ 183 | | jq -r '.items[] | select(.data.username == "'${POSTGRES_USER_BASE64}'") | .data.password' \ 184 | | base64 -d 185 | ) 186 | 187 | if [[ "${POSTGRES_PASSWORD}" == "" ]]; then 188 | error "Can not find secret with connection params for user ${POSTGRES_USER} at Cloud SQL instance ${SQL_INSTANCE_NAME}" 189 | else 190 | echo "${POSTGRES_PASSWORD}" 191 | fi 192 | } 193 | 194 | function list_sql_proxy_pods() { 195 | COMMAND=$1 196 | PADDING=${2:-""} 197 | 198 | ktl get pods -n kube-system -l proxy_to=google_cloud_sql --all-namespaces=true -o json \ 199 | | jq -r ".items[] | \"\(.metadata.namespace)\t\(.metadata.name)\t\(.metadata.labels.instance_name)\t${COMMAND}\"" \ 200 | | awk -v PADDING="$PADDING" -v FS="," 'BEGIN{print PADDING"Namespace\tPod Name\tCloud SQL Instance_Name\tktl command";}{printf PADDING"%s\t%s\t%s\t%s%s",$1,$2,$3,$4,ORS}' \ 201 | | column -ts $'\t' 202 | } 203 | 204 | function list_sql_proxy_users() { 205 | COMMAND=$1 206 | PADDING=${2:-""} 207 | 208 | kubectl get secrets --all-namespaces=true \ 209 | -l "service=google_cloud_sql" \ 210 | -o json \ 211 | | jq -r ".items[] | \"\(.metadata.namespace)\t\(.metadata.labels.instance_name)\t\(.data.username | @base64d)\t${COMMAND}\"" \ 212 | | awk -v PADDING="$PADDING" -v FS="," 'BEGIN{print PADDING"Namespace\tCloud SQL Instance_Name\tUsername\tktl command";}{printf PADDING"%s\t%s\t%s\t%s%s",$1,$2,$3,$4,ORS}' \ 213 | | column -ts $'\t' 214 | } 215 | 216 | function tunnel_postgres_connections() { 217 | PROXY_POD_NAMESPACE=$1 218 | PROXY_POD_NAME=$2 219 | PORT=$3 220 | 221 | # Trap exit so we can try to kill proxies that has stuck in background 222 | function cleanup { 223 | log_step "Stopping port forwarding." 224 | kill $! &> /dev/null 225 | } 226 | trap cleanup EXIT 227 | 228 | log_step "Port forwarding remote PostgreSQL to localhost port ${PORT}." 229 | kubectl --namespace="${PROXY_POD_NAMESPACE}" port-forward ${PROXY_POD_NAME} ${PORT}:5432 &> /dev/null & 230 | 231 | wait_for_ports_to_become_busy ${PORT} 232 | 233 | return 0 234 | } 235 | 236 | # Elixir/Erlang-specific 237 | 238 | function get_erlang_cookie() { 239 | NAMESPACE=$1 240 | POD_NAME=$2 241 | 242 | log_step "Resolving Erlang cookie from secret linked to pod ${POD_NAME} variables." 243 | 244 | ERLANG_COOKIE_SECRET_NAME=$( 245 | kubectl get pod ${POD_NAME} \ 246 | --namespace=${NAMESPACE} \ 247 | -o jsonpath='{$.spec.containers[0].env[?(@.name=="ERLANG_COOKIE")].valueFrom.secretKeyRef.name}' 248 | ) 249 | 250 | ERLANG_COOKIE_SECRET_KEY_NAME=$( 251 | kubectl get pod ${POD_NAME} \ 252 | --namespace=${NAMESPACE} \ 253 | -o jsonpath='{$.spec.containers[0].env[?(@.name=="ERLANG_COOKIE")].valueFrom.secretKeyRef.key}' 254 | ) 255 | 256 | kubectl get secret ${ERLANG_COOKIE_SECRET_NAME} \ 257 | --namespace=${NAMESPACE} \ 258 | -o jsonpath='{$.data.'${ERLANG_COOKIE_SECRET_KEY_NAME}'}' | base64 -d 259 | } 260 | 261 | function get_epmd_names() { 262 | NAMESPACE=$1 263 | POD_NAME=$2 264 | 265 | kubectl exec ${POD_NAME} --namespace="${NAMESPACE}" -i -t -- epmd -names 266 | } 267 | 268 | function ger_erlang_release_name_from_epmd_names() { 269 | log_step "Resolving Erlang release name" 270 | echo "$1" | tail -n 1 | awk '{print $2;}' 271 | } 272 | 273 | function get_erlang_distribution_ports_from_epmd_names() { 274 | log_step "Resolving ports used by Erlang distribution" 275 | 276 | while read -r DIST_PORT; do 277 | DIST_PORT=$(echo "${DIST_PORT}" | sed 's/.*port //g; s/[^0-9]*//g') 278 | log_step " Found port: ${DIST_PORT}" 279 | DIST_PORTS+=(${DIST_PORT}) 280 | done <<< "$1" 281 | 282 | echo "${DIST_PORTS[@]}" 283 | } 284 | 285 | function tunnel_erlang_distribution_connections() { 286 | POD_NAMESPACE=$1 287 | POD_NAME=$2 288 | POD_DNS=$3 289 | LOCAL_DIST_PORT=$4 290 | ERLANG_DISTRIBUTION_PORTS=$5 291 | 292 | HOST_RECORD="127.0.0.1 ${POD_DNS}" 293 | 294 | # Trap exit so we can try to kill proxies that has stuck in background 295 | function cleanup { 296 | set +x 297 | delete_pattern_in_file "/${HOST_RECORD}/d" /etc/hosts 298 | log_step "Stopping kubectl proxy." 299 | kill $! &> /dev/null 300 | } 301 | trap cleanup EXIT 302 | 303 | log_step "Stopping local EPMD" 304 | killall epmd &> /dev/null || true 305 | 306 | log_step "Adding new record to /etc/hosts." 307 | echo "${HOST_RECORD}" >> /etc/hosts 308 | 309 | log_step "Port forwarding remote Erlang Distribution ports ${ERLANG_DISTRIBUTION_PORTS} ${LOCAL_DIST_PORT} to localhost." 310 | kubectl port-forward --namespace=${POD_NAMESPACE} \ 311 | ${POD_NAME} \ 312 | ${ERLANG_DISTRIBUTION_PORTS} \ 313 | ${LOCAL_DIST_PORT} \ 314 | &> /dev/null & 315 | 316 | wait_for_ports_to_become_busy ${LOCAL_DIST_PORT} 317 | 318 | return 0 319 | } 320 | 321 | # Tests: 322 | # POD_NAME=$(fetch_pod_name talkinto app.kubernetes.io/name=talkinto-web) 323 | # POD_IP=$(get_pod_ip_address talkinto $POD_NAME) 324 | # POD_DNS=$(get_pod_dns_record talkinto $POD_NAME) 325 | # ERLANG_COOKIE=$(get_erlang_cookie talkinto $POD_NAME) 326 | # ensure_port_is_free 5433 327 | # wait_for_ports_to_become_busy 5432 328 | # EPMD_NAMES=$(get_epmd_names talkinto $POD_NAME) 329 | # RELEASE_NAME=$(ger_erlang_release_name_from_epmd_names "${EPMD_NAMES}") 330 | # ERLANG_DISTRIBUTION_PORTS=$(get_erlang_distribution_ports_from_epmd_names "${EPMD_NAMES}") 331 | # POSTGRES_PASSWORD=$(get_postgres_user_password talkinto-staging talkinto) 332 | # get_postgres_connection_url talkinto "${POSTGRES_PASSWORD}" 5432 talkinto 333 | 334 | 335 | # COMMAND="ktl pg:proxy -n \(.metadata.namespace) -l instance_name=\(.metadata.labels.instance_name) -p5433" 336 | # PADDING=" " 337 | # list_sql_proxy_pods "${COMMAND}" " " 338 | # list_sql_proxy_users "foo" " " 339 | 340 | # echo "foo" 341 | -------------------------------------------------------------------------------- /iex-observer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script provides easy way to debug remote Erlang nodes that is running in a Kubernetes cluster. 3 | # 4 | # Application on remote node should include `:runtime_tools` in it's applications dependencies, otherwise 5 | # you will receive `rpc:handle_call` error. 6 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 7 | source ${K8S_UTILS_DIR}/helpers.sh 8 | 9 | function show_help { 10 | echo " 11 | ktl iex:observer -lSELECTOR or -pPOD_NAME [-nNAMESPACE -h] 12 | 13 | Connect local IEx session to a remote running Erlang/OTP node and start an Observer application. 14 | 15 | If there are multuple pods that match the selector - random one is choosen. 16 | 17 | Examples: 18 | ktl iex:observer -lapp=hammer-web Connect to one of the pods of hammer-web application in default namespace. 19 | ktl iex:observer -lapp=hammer-web -nweb Connect to one of the pods of hammer-web application in web namespace. 20 | ktl iex:observer -phammer-web-kfjsu-3827 -nweb Connect to hammer-web pod in web namespace. 21 | " 22 | } 23 | 24 | POD_NAMESPACE="" 25 | POD_NAME= 26 | K8S_SELECTOR= 27 | 28 | # Read configuration from CLI 29 | while getopts "n:l:p:h" opt; do 30 | case "$opt" in 31 | n) POD_NAMESPACE=${OPTARG} 32 | ;; 33 | l) K8S_SELECTOR=${OPTARG} 34 | ;; 35 | p) POD_NAME=${OPTARG} 36 | ;; 37 | h) show_help 38 | exit 0 39 | ;; 40 | esac 41 | done 42 | 43 | if [[ $UID != 0 ]]; then 44 | error "Please run this script with sudo: sudo ktl iex:observer $*" 45 | fi 46 | 47 | if [[ ! $K8S_SELECTOR && ! $POD_NAME ]]; then 48 | error "You need to specify Kubernetes selector with '-l' option or pod name via '-p' option." 49 | fi 50 | 51 | if [ ! $POD_NAME ]; then 52 | POD_NAME=$(fetch_pod_name "${POD_NAMESPACE}" "${K8S_SELECTOR}") 53 | fi 54 | 55 | if [ ! $POD_NAMESPACE ]; then 56 | POD_NAMESPACE=$(get_pod_namespace "${POD_NAME}") 57 | fi 58 | 59 | LOCAL_DIST_PORT=$(get_free_random_port) 60 | ERLANG_COOKIE=$(get_erlang_cookie "${POD_NAMESPACE}" "${POD_NAME}") 61 | POD_DNS=$(get_pod_dns_record "${POD_NAMESPACE}" "${POD_NAME}") 62 | EPMD_NAMES=$(get_epmd_names "${POD_NAMESPACE}" "${POD_NAME}") 63 | RELEASE_NAME=$(ger_erlang_release_name_from_epmd_names "${EPMD_NAMES}") 64 | ERLANG_DISTRIBUTION_PORTS=$(get_erlang_distribution_ports_from_epmd_names "${EPMD_NAMES}") 65 | 66 | tunnel_erlang_distribution_connections "${POD_NAMESPACE}" "${POD_NAME}" "${POD_DNS}" "${LOCAL_DIST_PORT}" "${ERLANG_DISTRIBUTION_PORTS}" 67 | 68 | log_step "You can use following node name to manually connect to it in Observer: " 69 | banner "${RELEASE_NAME}@${POD_DNS}" 70 | 71 | log_step "Connecting to ${RELEASE_NAME} on ports ${ERLANG_DISTRIBUTION_PORTS} with cookie '${ERLANG_COOKIE}'." 72 | 73 | WHOAMI=$(whoami) 74 | # Run observer in hidden mode to avoid hurting cluster's health 75 | iex \ 76 | --name "debug-remsh-${WHOAMI}@${POD_DNS}" \ 77 | --cookie "${ERLANG_COOKIE}" \ 78 | --erl "-start_epmd false" \ 79 | --hidden \ 80 | -e ":observer.start()" 81 | -------------------------------------------------------------------------------- /iex-remsh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script provides easy way to debug remote Erlang nodes that is running in a Kubernetes cluster. 3 | # 4 | # Application on remote node should include `:runtime_tools` in it's applications dependencies, otherwise 5 | # you will receive `rpc:handle_call` error. 6 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 7 | source ${K8S_UTILS_DIR}/helpers.sh 8 | 9 | function show_help { 10 | echo " 11 | ktl iex:remsh -lSELECTOR or -pPOD_NAME [-nNAMESPACE -h] 12 | 13 | Connect local IEx session to a remote running Erlang/OTP node. 14 | 15 | If there are multuple pods that match the selector - random one is choosen. 16 | 17 | Examples: 18 | ktl iex:remsh -lapp=hammer-web Connect to one of the pods of hammer-web application in default namespace. 19 | ktl iex:remsh -lapp=hammer-web -nweb Connect to one of the pods of hammer-web application in web namespace. 20 | ktl iex:remsh -phammer-web-kfjsu-3827 -nweb Connect to hammer-web pod in web namespace. 21 | " 22 | } 23 | 24 | POD_NAMESPACE="" 25 | POD_NAME= 26 | K8S_SELECTOR= 27 | 28 | # Read configuration from CLI 29 | while getopts "n:l:p:h" opt; do 30 | case "$opt" in 31 | n) POD_NAMESPACE=${OPTARG} 32 | ;; 33 | l) K8S_SELECTOR=${OPTARG} 34 | ;; 35 | p) POD_NAME=${OPTARG} 36 | ;; 37 | h) show_help 38 | exit 0 39 | ;; 40 | esac 41 | done 42 | 43 | if [[ $UID != 0 ]]; then 44 | error "Please run this script with sudo: sudo ktl iex:remsh $*" 45 | fi 46 | 47 | if [[ ! $K8S_SELECTOR && ! $POD_NAME ]]; then 48 | error "You need to specify Kubernetes selector with '-l' option or pod name via '-p' option." 49 | fi 50 | 51 | if [ ! $POD_NAME ]; then 52 | POD_NAME=$(fetch_pod_name "${POD_NAMESPACE}" "${K8S_SELECTOR}") 53 | fi 54 | 55 | if [ ! $POD_NAMESPACE ]; then 56 | POD_NAMESPACE=$(get_pod_namespace "${POD_NAME}") 57 | fi 58 | 59 | LOCAL_DIST_PORT=$(get_free_random_port) 60 | ERLANG_COOKIE=$(get_erlang_cookie "${POD_NAMESPACE}" "${POD_NAME}") 61 | POD_DNS=$(get_pod_dns_record "${POD_NAMESPACE}" "${POD_NAME}") 62 | EPMD_NAMES=$(get_epmd_names "${POD_NAMESPACE}" "${POD_NAME}") 63 | RELEASE_NAME=$(ger_erlang_release_name_from_epmd_names "${EPMD_NAMES}") 64 | ERLANG_DISTRIBUTION_PORTS=$(get_erlang_distribution_ports_from_epmd_names "${EPMD_NAMES}") 65 | 66 | tunnel_erlang_distribution_connections "${POD_NAMESPACE}" "${POD_NAME}" "${POD_DNS}" "${LOCAL_DIST_PORT}" "${ERLANG_DISTRIBUTION_PORTS}" 67 | 68 | log_step "Connecting to ${RELEASE_NAME} on ports ${ERLANG_DISTRIBUTION_PORTS} with cookie '${ERLANG_COOKIE}'." 69 | 70 | WHOAMI=$(whoami) 71 | # Run observer in hidden mode to avoid hurting cluster's health 72 | iex \ 73 | --name "debug-remsh-${WHOAMI}@${POD_DNS}" \ 74 | --cookie "${ERLANG_COOKIE}" \ 75 | --erl "-start_epmd false" \ 76 | --hidden \ 77 | --remsh ${RELEASE_NAME}@${POD_DNS} 78 | -------------------------------------------------------------------------------- /iex-shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script provides easy way to debug remote Erlang nodes that is running in a Kubernetes cluster. 3 | # 4 | # Application on remote node should include `:runtime_tools` in it's applications dependencies, otherwise 5 | # you will receive `rpc:handle_call` error. 6 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 7 | source ${K8S_UTILS_DIR}/helpers.sh 8 | 9 | function show_help { 10 | echo " 11 | ktl iex:shell -lSELECTOR or -pPOD_NAME [-nNAMESPACE -h] 12 | 13 | Connect to a IEx shell of running Erlang/OTP node. Shell is executed wihin the pod. 14 | 15 | If there are multuple pods that match the selector - random one is choosen. 16 | 17 | Examples: 18 | ktl iex:shell -lapp=hammer-web Connect to one of the pods of hammer-web application in default namespace. 19 | ktl iex:shell -lapp=hammer-web -nweb Connect to one of the pods of hammer-web application in web namespace. 20 | ktl iex:shell -phammer-web-kfjsu-3827 -nweb Connect to hammer-web pod in web namespace. 21 | " 22 | } 23 | 24 | K8S_NAMESPACE="" 25 | POD_NAME= 26 | K8S_SELECTOR= 27 | 28 | # Read configuration from CLI 29 | while getopts "n:l:p:h" opt; do 30 | case "$opt" in 31 | n) K8S_NAMESPACE=${OPTARG} 32 | ;; 33 | l) K8S_SELECTOR=${OPTARG} 34 | ;; 35 | p) POD_NAME=${OPTARG} 36 | ;; 37 | h) show_help 38 | exit 0 39 | ;; 40 | esac 41 | done 42 | 43 | # Required part of config 44 | if [[ ! $K8S_SELECTOR && ! $POD_NAME ]]; then 45 | error "You need to specify Kubernetes selector with '-l' option or pod name via '-p' option." 46 | fi 47 | 48 | if [ ! $POD_NAME ]; then 49 | POD_NAME=$(fetch_pod_name "${K8S_NAMESPACE}" "${K8S_SELECTOR}") 50 | fi 51 | 52 | if [ ! $K8S_NAMESPACE ]; then 53 | K8S_NAMESPACE=$(get_pod_namespace "${POD_NAME}") 54 | fi 55 | 56 | log_step "Entering shell on remote Erlang/OTP node." 57 | log_step "Pod ${POD_NAME} in namespace ${K8S_NAMESPACE}" 58 | kubectl exec ${POD_NAME} --namespace=${K8S_NAMESPACE} \ 59 | -it \ 60 | -- /bin/sh -c 'bin/${APPLICATION_NAME} remote' 61 | -------------------------------------------------------------------------------- /ktl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Check dependencies 5 | command -v jq >/dev/null 2>&1 || { echo >&2 "jq is not installed. Aborting."; exit 1; } 6 | command -v kubectl >/dev/null 2>&1 || { echo >&2 "kubectl is not installed. Aborting."; exit 1; } 7 | command -v gke-gcloud-auth-plugin --version >/dev/null 2>&1 || { echo >&2 "gke-gcloud-auth-plugin is not installed. Use 'gcloud components install gke-gcloud-auth-plugin'. Aborting."; exit 1; } 8 | 9 | CURRENT_DIR="$( cd "$( dirname $( readlink "${BASH_SOURCE[0]}") )" && pwd )" 10 | 11 | if [[ "$1" == "shell" ]]; then 12 | OPTIND=2 13 | source ${CURRENT_DIR}/shell.sh 14 | elif [[ "$1" == "erl:shell" ]]; then 15 | OPTIND=2 16 | source ${CURRENT_DIR}/erl-shell.sh 17 | elif [[ "$1" == "iex:shell" ]]; then 18 | OPTIND=2 19 | source ${CURRENT_DIR}/iex-shell.sh 20 | elif [[ "$1" == "iex:observer" ]]; then 21 | OPTIND=2 22 | source ${CURRENT_DIR}/iex-observer.sh 23 | elif [[ "$1" == "iex:remsh" ]]; then 24 | OPTIND=2 25 | source ${CURRENT_DIR}/iex-remsh.sh 26 | elif [[ "$1" == "pg:psql" ]]; then 27 | OPTIND=2 28 | source ${CURRENT_DIR}/pg-psql.sh 29 | elif [[ "$1" == "pg:open" ]]; then 30 | OPTIND=2 31 | source ${CURRENT_DIR}/pg-open.sh 32 | elif [[ "$1" == "pg:proxy" ]]; then 33 | OPTIND=2 34 | source ${CURRENT_DIR}/pg-proxy.sh 35 | elif [[ "$1" == "pg:ps" ]]; then 36 | OPTIND=2 37 | source ${CURRENT_DIR}/pg-ps.sh 38 | elif [[ "$1" == "pg:kill" ]]; then 39 | OPTIND=2 40 | source ${CURRENT_DIR}/pg-kill.sh 41 | elif [[ "$1" == "pg:outliers" ]]; then 42 | OPTIND=2 43 | source ${CURRENT_DIR}/pg-outliers.sh 44 | elif [[ "$1" == "pg:diagnose" ]]; then 45 | OPTIND=2 46 | source ${CURRENT_DIR}/pg-diagnose.sh 47 | elif [[ "$1" == "pg:dump" ]]; then 48 | OPTIND=2 49 | source ${CURRENT_DIR}/pg-dump.sh 50 | elif [[ "$1" == "pg:restore" ]]; then 51 | OPTIND=2 52 | source ${CURRENT_DIR}/pg-restore.sh 53 | elif [[ "$1" == "pg:copy" ]]; then 54 | OPTIND=2 55 | source ${CURRENT_DIR}/pg-copy.sh 56 | elif [[ "$1" == "status" ]]; then 57 | OPTIND=2 58 | source ${CURRENT_DIR}/status.sh 59 | elif [[ "$1" == "promote" ]]; then 60 | OPTIND=2 61 | source ${CURRENT_DIR}/promote.sh 62 | elif [[ "$1" == "help" ]]; then 63 | OPTIND=2 64 | source ${CURRENT_DIR}/help.sh 65 | elif [[ "$1" == "apply" ]]; then 66 | # We override default behaviour to store update history 67 | kubectl $@ --record=true 68 | else 69 | kubectl $@ 70 | fi; 71 | -------------------------------------------------------------------------------- /pg-copy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:copy -istaging -utalkinto -dtalkinto [-q \"SELECT * FROM accounts;\" -nkube-system -h] 8 | 9 | Copies data from remote PostgreSQL database to a local one. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -dpostgres Database name. Required. 16 | -h Show help and exit. 17 | -t Name of a table that needs to be copied. 18 | -q Custom query which would be used to select copied data. 19 | -c Psql connection url for an instance to which the data is copied. 20 | 21 | 22 | Examples: 23 | ktl pg:copy -italkinto -utalkinto -dtalkinto -taccounts -c postgres://postgres:@localhost/talkinto_dev 24 | ktl pg:copy -italkinto -utalkinto -dtalkinto -taccounts -q \"SELECT * FROM accounts WHERE id = 1\" -c postgres://postgres:@localhost/talkinto_dev 25 | 26 | Available database instances: 27 | " 28 | list_sql_proxy_users "ktl pg:copy -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME" " " 29 | } 30 | 31 | POSTGRES_USER= 32 | INSTANCE_NAME= 33 | PORT=$(get_free_random_port) 34 | PROXY_POD_NAMESPACE="kube-system" 35 | POSTGRES_CONNECTION_STRING="" 36 | TABLE_NAME="" 37 | COPY_QUERY="" 38 | 39 | # Read configuration from CLI 40 | while getopts "hn:i:u:d:t:q:c:" opt; do 41 | case "$opt" in 42 | n) PROXY_POD_NAMESPACE="${OPTARG}" 43 | ;; 44 | i) INSTANCE_NAME="${OPTARG}" 45 | ;; 46 | u) POSTGRES_USER="${OPTARG}" 47 | ;; 48 | d) POSTGRES_DB="${OPTARG}" 49 | ;; 50 | h) show_help 51 | exit 0 52 | ;; 53 | t) TABLE_NAME="${OPTARG}" 54 | ;; 55 | q) COPY_QUERY="${OPTARG}" 56 | ;; 57 | c) DESTINATION_POSTGRES_CONNECTION_STRING="${OPTARG}" 58 | ;; 59 | esac 60 | done 61 | 62 | if [[ "${INSTANCE_NAME}" == "" ]]; then 63 | error "Instance name is not set, use -i option to set it or -h for list of available values" 64 | fi 65 | 66 | if [[ "${POSTGRES_USER}" == "" ]]; then 67 | error "User name is not set, use -u option to set it or -h for list of available values" 68 | fi 69 | 70 | if [[ "${POSTGRES_DB}" == "" ]]; then 71 | error "Posgres database is not set, use -d option." 72 | fi 73 | 74 | if [[ "${TABLE_NAME}" == "" ]]; then 75 | error "Table name is not set, use -t option." 76 | fi 77 | 78 | if [[ "${COPY_QUERY}" == "" ]]; then 79 | COPY_QUERY="SELECT * FROM ${TABLE_NAME}" 80 | fi 81 | 82 | if [[ "${DESTINATION_POSTGRES_CONNECTION_STRING}" == "" ]]; then 83 | error "Destination connection URL is not set, use -c option." 84 | fi 85 | 86 | COPY_PATH="./ktl_pg_copy_tmp" 87 | COPY_TMP_FILE_PATH="${COPY_PATH}/${TABLE_NAME}.csv" 88 | 89 | if [[ -e "${COPY_PATH}" ]]; then 90 | error "${COPY_PATH} already exists, delete it with rm -rf ${COPY_PATH}" 91 | fi 92 | 93 | log_step "Selecting Cloud SQL proxy pod" 94 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 95 | 96 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 97 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 98 | 99 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 100 | 101 | log_step "Temporary files will be stored in ${COPY_PATH}" 102 | mkdir -p "${COPY_PATH}/" 103 | 104 | log_step "Copying data using query '${COPY_QUERY}'" 105 | set -x 106 | 107 | psql "${POSTGRES_CONNECTION_STRING}" --echo-queries --command "\copy (${COPY_QUERY}) TO ${COPY_TMP_FILE_PATH} CSV HEADER;" 108 | 109 | psql "${DESTINATION_POSTGRES_CONNECTION_STRING}" --echo-queries --command "\copy ${TABLE_NAME} FROM ${COPY_TMP_FILE_PATH} WITH CSV HEADER;" 110 | 111 | set +x 112 | 113 | log_step "Removing temporary files from ${COPY_PATH}" 114 | rm -rf ${COPY_PATH} 115 | -------------------------------------------------------------------------------- /pg-diagnose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:diagnose -istaging -utalkinto [-nkube-system -h] 8 | 9 | Shows diagnostics report for PostgreSQL database. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -h Show help and exit. 16 | 17 | Examples: 18 | ktl pg:diagnose -istaging -utalkinto 19 | ktl pg:diagnose -istaging -utalkinto -nkube-system 20 | 21 | Available databases: 22 | " 23 | 24 | list_sql_proxy_users "ktl pg:diagnose -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d)" " " 25 | } 26 | 27 | # TODO: add more stats from https://github.com/heroku/heroku-pg-extras 28 | 29 | PORT=$(get_free_random_port) 30 | POSTGRES_DB="postgres" 31 | PROXY_POD_NAMESPACE="kube-system" 32 | 33 | # Read configuration from CLI 34 | while getopts "hn:i:u:" opt; do 35 | case "$opt" in 36 | n) PROXY_POD_NAMESPACE="--namespace=${OPTARG}" 37 | ;; 38 | i) INSTANCE_NAME="${OPTARG}" 39 | ;; 40 | u) POSTGRES_USER="${OPTARG}" 41 | ;; 42 | h) show_help 43 | exit 0 44 | ;; 45 | esac 46 | done 47 | 48 | if [[ "${INSTANCE_NAME}" == "" ]]; then 49 | error "Instance name is not set, use -i option to set it or -h for list of available values" 50 | fi 51 | 52 | if [[ "${POSTGRES_USER}" == "" ]]; then 53 | error "User name is not set, use -u option to set it or -h for list of available values" 54 | fi 55 | 56 | log_step "Selecting Cloud SQL proxy pod" 57 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 58 | 59 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 60 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 61 | 62 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 63 | 64 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command " 65 | WITH table_scans as ( 66 | SELECT relid, 67 | tables.idx_scan + tables.seq_scan as all_scans, 68 | ( tables.n_tup_ins + tables.n_tup_upd + tables.n_tup_del ) as writes, 69 | pg_relation_size(relid) as table_size 70 | FROM pg_stat_user_tables as tables 71 | ), 72 | all_writes as ( 73 | SELECT sum(writes) as total_writes 74 | FROM table_scans 75 | ), 76 | indexes as ( 77 | SELECT idx_stat.relid, idx_stat.indexrelid, 78 | idx_stat.schemaname, idx_stat.relname as tablename, 79 | idx_stat.indexrelname as indexname, 80 | idx_stat.idx_scan, 81 | pg_relation_size(idx_stat.indexrelid) as index_bytes, 82 | indexdef ~* 'USING btree' AS idx_is_btree 83 | FROM pg_stat_user_indexes as idx_stat 84 | JOIN pg_index 85 | USING (indexrelid) 86 | JOIN pg_indexes as indexes 87 | ON idx_stat.schemaname = indexes.schemaname 88 | AND idx_stat.relname = indexes.tablename 89 | AND idx_stat.indexrelname = indexes.indexname 90 | WHERE pg_index.indisunique = FALSE 91 | ), 92 | index_ratios AS ( 93 | SELECT schemaname, tablename, indexname, 94 | idx_scan, all_scans, 95 | round(( CASE WHEN all_scans = 0 THEN 0.0::NUMERIC 96 | ELSE idx_scan::NUMERIC/all_scans * 100 END),2) as index_scan_pct, 97 | writes, 98 | round((CASE WHEN writes = 0 THEN idx_scan::NUMERIC ELSE idx_scan::NUMERIC/writes END),2) 99 | as scans_per_write, 100 | pg_size_pretty(index_bytes) as index_size, 101 | pg_size_pretty(table_size) as table_size, 102 | idx_is_btree, index_bytes 103 | FROM indexes 104 | JOIN table_scans 105 | USING (relid) 106 | ), 107 | index_groups AS ( 108 | SELECT 'Never Used Indexes' as reason, *, 1 as grp 109 | FROM index_ratios 110 | WHERE 111 | idx_scan = 0 112 | and idx_is_btree 113 | UNION ALL 114 | SELECT 'Low Scans, High Writes' as reason, *, 2 as grp 115 | FROM index_ratios 116 | WHERE 117 | scans_per_write <= 1 118 | and index_scan_pct < 10 119 | and idx_scan > 0 120 | and writes > 100 121 | and idx_is_btree 122 | UNION ALL 123 | SELECT 'Seldom Used Large Indexes' as reason, *, 3 as grp 124 | FROM index_ratios 125 | WHERE 126 | index_scan_pct < 5 127 | and scans_per_write > 1 128 | and idx_scan > 0 129 | and idx_is_btree 130 | and index_bytes > 100000000 131 | UNION ALL 132 | SELECT 'High-Write Large Non-Btree' as reason, index_ratios.*, 4 as grp 133 | FROM index_ratios, all_writes 134 | WHERE 135 | ( writes::NUMERIC / ( total_writes + 1 ) ) > 0.02 136 | AND NOT idx_is_btree 137 | AND index_bytes > 100000000 138 | ORDER BY grp, index_bytes DESC ) 139 | SELECT reason, schemaname, tablename, indexname, 140 | index_scan_pct, scans_per_write, index_size, table_size 141 | FROM index_groups; 142 | " 143 | -------------------------------------------------------------------------------- /pg-dump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:dump -istaging -utalkinto -dtalkinto [-nkube-system -h -t -f -e] 8 | 9 | Dumps PostgreSQL database to local directory in binary format. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -dpostgres Database name. Required. 16 | -t List of tables to export. By default all tables are exported. Comma delimited. 17 | -e List of tables to exclude from export. Comma delimited. By default no tables are ignored. 18 | -f Path to directory where dump would be stored. By default: ./dumps 19 | -h Show help and exit. 20 | 21 | Examples: 22 | ktl pg:dump -istaging -utalkinto -dtalkinto 23 | ktl pg:dump -istaging -utalkinto -dtalkinto -eschema_migrations 24 | ktl pg:dump -istaging -utalkinto -dtalkinto -tapis,plugins,requests 25 | 26 | Available databases: 27 | " 28 | list_sql_proxy_users "ktl pg:dump -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME" " " 29 | } 30 | 31 | PORT=$(get_free_random_port) 32 | PROXY_POD_NAMESPACE="kube-system" 33 | 34 | DUMP_PATH="./dumps" 35 | TABLES="" 36 | EXCLUDE_TABLES="" 37 | 38 | # Read configuration from CLI 39 | while getopts "hn:i:u:d:t:e:f:" opt; do 40 | case "$opt" in 41 | n) PROXY_POD_NAMESPACE="${OPTARG}" 42 | ;; 43 | i) INSTANCE_NAME="${OPTARG}" 44 | ;; 45 | u) POSTGRES_USER="${OPTARG}" 46 | ;; 47 | d) POSTGRES_DB="${OPTARG}" 48 | ;; 49 | h) show_help 50 | exit 0 51 | ;; 52 | t) TABLES="${OPTARG}" 53 | TABLES="--table=${TABLES//,/ --table=}" 54 | ;; 55 | f) DUMP_PATH="${OPTARG}" 56 | ;; 57 | e) EXCLUDE_TABLES="${OPTARG}" 58 | EXCLUDE_TABLES="--exclude-table=${EXCLUDE_TABLES//,/ --exclude-table=}" 59 | ;; 60 | esac 61 | done 62 | 63 | if [[ "${INSTANCE_NAME}" == "" ]]; then 64 | error "Instance name is not set, use -i option to set it or -h for list of available values" 65 | fi 66 | 67 | if [[ "${POSTGRES_USER}" == "" ]]; then 68 | error "User name is not set, use -u option to set it or -h for list of available values" 69 | fi 70 | 71 | if [[ "${POSTGRES_DB}" == "" ]]; then 72 | error "Posgres database is not set, use -d option." 73 | fi 74 | 75 | if [[ -e "${DUMP_PATH}/${POSTGRES_DB}" ]]; then 76 | error "${DUMP_PATH}/${POSTGRES_DB} already exists, delete it or specify another path with -f option" 77 | fi 78 | 79 | log_step "Selecting Cloud SQL proxy pod" 80 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 81 | 82 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 83 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 84 | 85 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 86 | 87 | log_step "Dump will be stored in ${DUMP_PATH}/${POSTGRES_DB}" 88 | mkdir -p "${DUMP_PATH}/" 89 | 90 | log_step "Dumping ${POSTGRES_DB} DB to ${DUMP_PATH}/${POSTGRES_DB}" 91 | 92 | set -x 93 | PGPASSWORD="$POSTGRES_PASSWORD" \ 94 | pg_dump ${POSTGRES_DB} \ 95 | -h localhost \ 96 | -p ${PORT} \ 97 | -U ${POSTGRES_USER} \ 98 | --format c \ 99 | --compress 0 \ 100 | --file ${DUMP_PATH}/${POSTGRES_DB} ${TABLES} ${EXCLUDE_TABLES} \ 101 | --verbose 102 | 103 | set +x 104 | -------------------------------------------------------------------------------- /pg-kill.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:kill -istaging -utalkinto -p3443 -dtalkinto [-nkube-system -h -f] 8 | 9 | Kill a query by pid. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -dpostgres Database name to use. Default: postgres. 16 | -pPID Query PID. 17 | -f Force kill. 18 | -h Show help and exit. 19 | 20 | Examples: 21 | ktl pg:kill -istaging -utalkinto -dtalkinto -p3453 22 | 23 | Available databases: 24 | " 25 | 26 | list_sql_proxy_users "ktl pg:kill -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME -p\$PID" " " 27 | } 28 | 29 | PORT=$(get_free_random_port) 30 | POSTGRES_DB="postgres" 31 | PROXY_POD_NAMESPACE="kube-system" 32 | COMMAND="pg_cancel_backend" 33 | 34 | # Read configuration from CLI 35 | while getopts "hn:i:u:d:fp:" opt; do 36 | case "$opt" in 37 | n) PROXY_POD_NAMESPACE="${OPTARG}" 38 | ;; 39 | i) INSTANCE_NAME="${OPTARG}" 40 | ;; 41 | u) POSTGRES_USER="${OPTARG}" 42 | ;; 43 | p) PID="${OPTARG}" 44 | ;; 45 | d) POSTGRES_DB="${OPTARG}" 46 | ;; 47 | h) show_help 48 | exit 0 49 | ;; 50 | f) COMMAND="pg_terminate_backend" 51 | ;; 52 | esac 53 | done 54 | 55 | if [[ "${INSTANCE_NAME}" == "" ]]; then 56 | error "Instance name is not set, use -i option to set it or -h for list of available values" 57 | fi 58 | 59 | if [[ "${POSTGRES_USER}" == "" ]]; then 60 | error "User name is not set, use -u option to set it or -h for list of available values" 61 | fi 62 | 63 | if [[ "${POSTGRES_DB}" == "" ]]; then 64 | error "Posgres database is not set, use -d option." 65 | fi 66 | 67 | if [[ "${PID}" == "" ]]; then 68 | error "PID to kill is not set, use -p option." 69 | fi 70 | 71 | log_step "Selecting Cloud SQL proxy pod" 72 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 73 | 74 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 75 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 76 | 77 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 78 | 79 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command "SELECT ${COMMAND}(${PID});" 80 | -------------------------------------------------------------------------------- /pg-open.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:open -istaging -utalkinto [-h -p5432 -dpostgres -nkube-system] 8 | 9 | Run psql on localhost and connect it to a remote PostgreSQL instance. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -pPORT Local port for forwarding. Default: random port. 16 | -dpostgres Database name to use. Default: postgres. 17 | -h Show help and exit. 18 | 19 | Examples: 20 | ktl pg:open -istaging -utalkinto -dtalkinto 21 | ktl pg:open -istaging -utalkinto -p5433 22 | 23 | Available databases: 24 | " 25 | 26 | list_sql_proxy_users "ktl pg:open -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d)" " " 27 | } 28 | 29 | PORT="" 30 | POSTGRES_DB="postgres" 31 | PROXY_POD_NAMESPACE="kube-system" 32 | 33 | INSTANCE_NAME=${KTL_PG_DEFAULT_INSTANCE_NAME} 34 | POSTGRES_USER=${KTL_PG_DEFAULT_USERNAME} 35 | POSTGRES_DB=${KTL_PG_DEFAULT_DATABASE} 36 | 37 | # Read configuration from CLI 38 | while getopts "hn:i:u:p:d:" opt; do 39 | case "$opt" in 40 | n) PROXY_POD_NAMESPACE="${OPTARG}" 41 | ;; 42 | i) INSTANCE_NAME="${OPTARG}" 43 | ;; 44 | u) POSTGRES_USER="${OPTARG}" 45 | ;; 46 | p) PORT="${OPTARG}" 47 | ;; 48 | d) POSTGRES_DB="${OPTARG}" 49 | ;; 50 | h) show_help 51 | exit 0 52 | ;; 53 | esac 54 | done 55 | 56 | if [[ "${INSTANCE_NAME}" == "" ]]; then 57 | error "Instance name is not set, use -i option to set it or -h for list of available values" 58 | fi 59 | 60 | if [[ "${POSTGRES_USER}" == "" ]]; then 61 | error "User name is not set, use -u option to set it or -h for list of available values" 62 | fi 63 | 64 | if [[ "${PORT}" == "" && $(is_port_free "5433") == "true" ]]; then 65 | PORT="5433" 66 | elif [[ "${PORT}" == "" ]]; then 67 | PORT=$(get_free_random_port) 68 | else 69 | ensure_port_is_free ${PORT} 70 | fi 71 | 72 | log_step "Selecting Cloud SQL proxy pod" 73 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 74 | 75 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 76 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 77 | 78 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 79 | 80 | echo " - Running: open postgres://${POSTGRES_USER}:***@localhost:${PORT}/${POSTGRES_DB}?create_favorite=true&connect_favorite=true&nickname=ktl/${INSTANCE_NAME}/${POSTGRES_DB}/${POSTGRES_USER}" 81 | open "${POSTGRES_CONNECTION_STRING}?create_favorite=true&connect_favorite=true&nickname=ktl/${INSTANCE_NAME}/${POSTGRES_DB}/${POSTGRES_USER}" 82 | fg 83 | -------------------------------------------------------------------------------- /pg-outliers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:outliers -istaging -utalkinto -dtalkinto [-nkube-system -h -r -t -n] 8 | 9 | Show queries that have longest execution time in aggregate. Requires pg_stat_statements. 10 | 11 | If you get ERROR: 42P01: relation \"pg_stat_statements\" does not exist, then pg_stat_statements 12 | extension is not enabled. To enable it run execute \"CREATE EXTENSION pg_stat_statements\". 13 | 14 | Options: 15 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 16 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 17 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 18 | -dpostgres Database name to use. Required. 19 | -h Show help and exit. 20 | -t Do not truncate queries to 40 characters. 21 | -r Resets statistics gathered by pg_stat_statements. 22 | -c10 Number of queries to display. Default: 10. 23 | 24 | Examples: 25 | ktl pg:outliers -istaging -utalkinto -dtalkinto 26 | ktl pg:outliers -istaging -utalkinto -dtalkinto -r -c10 -t 27 | 28 | Available databases: 29 | " 30 | 31 | list_sql_proxy_users "ktl pg:outliers -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME" " " 32 | } 33 | 34 | PORT=$(get_free_random_port) 35 | POSTGRES_DB="postgres" 36 | PROXY_POD_NAMESPACE="kube-system" 37 | RESET="" 38 | NUMBER=10 39 | TRUNCATE="CASE WHEN length(query) <= 40 THEN query ELSE substr(query, 0, 39) || '…' END" 40 | 41 | # Read configuration from CLI 42 | while getopts "hn:i:u:p:rn:td:" opt; do 43 | case "$opt" in 44 | n) PROXY_POD_NAMESPACE="${OPTARG}" 45 | ;; 46 | i) INSTANCE_NAME="${OPTARG}" 47 | ;; 48 | u) POSTGRES_USER="${OPTARG}" 49 | ;; 50 | p) PORT="${OPTARG}" 51 | ;; 52 | d) POSTGRES_DB="${OPTARG}" 53 | ;; 54 | h) show_help 55 | exit 0 56 | ;; 57 | r) RESET="true" 58 | ;; 59 | n) NUMBER="${OPTARG}" 60 | ;; 61 | t) TRUNCATE="query" 62 | ;; 63 | esac 64 | done 65 | 66 | if [[ "${INSTANCE_NAME}" == "" ]]; then 67 | error "Instance name is not set, use -i option to set it or -h for list of available values" 68 | fi 69 | 70 | if [[ "${POSTGRES_USER}" == "" ]]; then 71 | error "User name is not set, use -u option to set it or -h for list of available values" 72 | fi 73 | 74 | if [[ "${POSTGRES_DB}" == "" ]]; then 75 | error "Posgres database is not set, use -d option." 76 | fi 77 | 78 | log_step "Selecting Cloud SQL proxy pod" 79 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 80 | 81 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 82 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 83 | 84 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 85 | 86 | if [[ "${RESET}" == "true" ]]; then 87 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command "SELECT pg_stat_statements_reset();" 88 | fi 89 | 90 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command " 91 | SELECT 92 | rolname AS rolname, 93 | interval '1 millisecond' * total_time AS total_exec_time, 94 | to_char((total_time/sum(total_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time, 95 | mean_time, 96 | max_time, 97 | stddev_time, 98 | interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time, 99 | rows, 100 | to_char(calls, 'FM999G999G999G990') AS ncalls, 101 | regexp_replace(${TRUNCATE}, '[ \t\n]+', ' ', 'g') AS query 102 | FROM pg_stat_statements 103 | JOIN pg_roles r ON r.oid = userid 104 | WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1) 105 | ORDER BY total_time DESC 106 | LIMIT ${NUMBER} 107 | " 108 | -------------------------------------------------------------------------------- /pg-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:proxy -istaging -utalkinto [-h -p5432 -dpostgres -nkube-system] 8 | 9 | Proxy PostgresSQL port to localhost. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -pPORT Local port for forwarding. Default: random port. 16 | -dpostgres Database name to use to build connection URL. Default: postgres. 17 | -h Show help and exit. 18 | 19 | Examples: 20 | ktl pg:proxy -istaging -utalkinto -dtalkinto 21 | ktl pg:proxy -istaging -utalkinto -p5433 22 | 23 | Available databases: 24 | " 25 | 26 | list_sql_proxy_users "ktl pg:proxy -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d)" " " 27 | } 28 | 29 | PORT="" 30 | POSTGRES_DB="postgres" 31 | PROXY_POD_NAMESPACE="kube-system" 32 | 33 | INSTANCE_NAME=${KTL_PG_DEFAULT_INSTANCE_NAME} 34 | POSTGRES_USER=${KTL_PG_DEFAULT_USERNAME} 35 | POSTGRES_DB=${KTL_PG_DEFAULT_DATABASE} 36 | 37 | # Read configuration from CLI 38 | while getopts "hn:i:u:p:d:" opt; do 39 | case "$opt" in 40 | n) PROXY_POD_NAMESPACE="${OPTARG}" 41 | ;; 42 | i) INSTANCE_NAME="${OPTARG}" 43 | ;; 44 | u) POSTGRES_USER="${OPTARG}" 45 | ;; 46 | p) PORT="${OPTARG}" 47 | ;; 48 | d) POSTGRES_DB="${OPTARG}" 49 | ;; 50 | h) show_help 51 | exit 0 52 | ;; 53 | esac 54 | done 55 | 56 | if [[ "${INSTANCE_NAME}" == "" ]]; then 57 | error "Instance name is not set, use -i option to set it or -h for list of available values" 58 | fi 59 | 60 | if [[ "${POSTGRES_USER}" == "" ]]; then 61 | error "User name is not set, use -u option to set it or -h for list of available values" 62 | fi 63 | 64 | if [[ "${PORT}" == "" && $(is_port_free "5433") == "true" ]]; then 65 | PORT="5433" 66 | elif [[ "${PORT}" == "" ]]; then 67 | PORT=$(get_free_random_port) 68 | else 69 | ensure_port_is_free ${PORT} 70 | fi 71 | 72 | log_step "Selecting Cloud SQL proxy pod" 73 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 74 | 75 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 76 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 77 | 78 | banner "You can use 'psql ${POSTGRES_CONNECTION_STRING}' command to connect to the database" 79 | 80 | kubectl --namespace="${PROXY_POD_NAMESPACE}" port-forward ${PROXY_POD_NAME} ${PORT}:5432 81 | -------------------------------------------------------------------------------- /pg-ps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:ps -istaging -utalkinto -dtalkinto [-nkube-system -h -v] 8 | 9 | View active queries with execution time. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -dpostgres Database name to use. Required. 16 | -h Show help and exit. 17 | -v Verbose output, includes idle transactions. 18 | 19 | Examples: 20 | ktl pg:ps -istaging -utalkinto -dtalkinto 21 | 22 | Available databases: 23 | " 24 | 25 | list_sql_proxy_users "ktl pg:ps -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME" " " 26 | } 27 | 28 | PORT=$(get_free_random_port) 29 | POSTGRES_DB="postgres" 30 | PROXY_POD_NAMESPACE="kube-system" 31 | VERBOSE="AND state <> 'idle'" 32 | 33 | # Read configuration from CLI 34 | while getopts "hn:i:u:d:v" opt; do 35 | case "$opt" in 36 | n) PROXY_POD_NAMESPACE="--namespace=${OPTARG}" 37 | ;; 38 | i) INSTANCE_NAME="${OPTARG}" 39 | ;; 40 | u) POSTGRES_USER="${OPTARG}" 41 | ;; 42 | d) POSTGRES_DB="${OPTARG}" 43 | ;; 44 | h) show_help 45 | exit 0 46 | ;; 47 | v) VERBOSE="" 48 | ;; 49 | esac 50 | done 51 | 52 | if [[ "${INSTANCE_NAME}" == "" ]]; then 53 | error "Instance name is not set, use -i option to set it or -h for list of available values" 54 | fi 55 | 56 | if [[ "${POSTGRES_USER}" == "" ]]; then 57 | error "User name is not set, use -u option to set it or -h for list of available values" 58 | fi 59 | 60 | if [[ "${POSTGRES_DB}" == "" ]]; then 61 | error "Posgres database is not set, use -d option." 62 | fi 63 | 64 | log_step "Selecting Cloud SQL proxy pod" 65 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 66 | 67 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 68 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 69 | 70 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 71 | 72 | WAIT_RAND=$(awk 'BEGIN{srand();print int(rand()*(63000-2000))+2000 }') 73 | WAIT_RETURN=$( 74 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command "SELECT '${WAIT_RAND}' || '${WAIT_RAND}' WHERE EXISTS ( 75 | SELECT 1 FROM information_schema.columns WHERE table_schema = 'pg_catalog' 76 | AND table_name = 'pg_stat_activity' 77 | AND column_name = 'waiting' 78 | ) 79 | " 80 | ) 81 | 82 | if [[ "${WAIT_RETURN}" = *"${WAIT_RAND}${WAIT_RAND}"* ]]; then 83 | WAITING="waiting" 84 | else 85 | WAITING="wait_event IS NOT NULL AS waiting" 86 | fi 87 | 88 | echo "Active queries: " 89 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command " 90 | SELECT 91 | pid, 92 | state, 93 | application_name AS source, 94 | usename AS username, 95 | age(now(),xact_start) AS running_for, 96 | xact_start AS transaction_start, 97 | ${WAITING}, 98 | query 99 | FROM pg_stat_activity 100 | WHERE query <> '' 101 | ${VERBOSE} 102 | AND pid <> pg_backend_pid() 103 | ORDER BY query_start DESC 104 | " 105 | 106 | echo "Queries with active locks: " 107 | psql "${POSTGRES_CONNECTION_STRING}" --no-psqlrc --command " 108 | SELECT 109 | pg_stat_activity.pid, 110 | pg_class.relname, 111 | pg_locks.transactionid, 112 | pg_locks.granted, 113 | CASE WHEN length(pg_stat_activity.query) <= 40 THEN pg_stat_activity.query ELSE substr(pg_stat_activity.query, 0, 39) || '…' END AS query_snippet, 114 | age(now(),pg_stat_activity.query_start) AS lock_age 115 | FROM pg_stat_activity,pg_locks left 116 | OUTER JOIN pg_class 117 | ON (pg_locks.relation = pg_class.oid) 118 | WHERE pg_stat_activity.query <> '' 119 | AND pg_locks.pid = pg_stat_activity.pid 120 | AND pg_locks.mode = 'ExclusiveLock' 121 | AND pg_stat_activity.pid <> pg_backend_pid() order by query_start; 122 | " 123 | -------------------------------------------------------------------------------- /pg-psql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:psql -istaging -utalkinto [-h -p5432 -dpostgres -nkube-system -ffoo.sql] [SELECT true;] 8 | 9 | Run psql on localhost and connect it to a remote PostgreSQL instance. 10 | 11 | Options: 12 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 13 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 14 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 15 | -pPORT Local port for forwarding. Default: random port. 16 | -dpostgres Database name to use. Default: postgres. 17 | -sSECRET_NAMESPACE Namespace to search for the secret that holds DB credentials. Default: all. 18 | -fFILE Executes SQL commands from specficied file. 19 | -vASSIGNMENT Define a psql variable. 20 | -h Show help and exit. 21 | 22 | Examples: 23 | ktl pg:psql -istaging -utalkinto -dtalkinto 24 | ktl pg:psql -istaging -utalkinto -p5433 25 | 26 | Available databases: 27 | " 28 | 29 | list_sql_proxy_users "ktl pg:psql -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d)" " " 30 | } 31 | 32 | PORT="" 33 | POSTGRES_DB="postgres" 34 | PROXY_POD_NAMESPACE="kube-system" 35 | FILE="" 36 | COMMAND="" 37 | VARIABLES=() 38 | 39 | INSTANCE_NAME=${KTL_PG_DEFAULT_INSTANCE_NAME:-} 40 | POSTGRES_USER=${KTL_PG_DEFAULT_USERNAME:-} 41 | POSTGRES_DB=${KTL_PG_DEFAULT_DATABASE:-} 42 | 43 | # Read configuration from CLI 44 | while getopts "hn:i:u:p:d:f:v:" opt; do 45 | case "$opt" in 46 | n) PROXY_POD_NAMESPACE="${OPTARG}" 47 | ;; 48 | i) INSTANCE_NAME="${OPTARG}" 49 | ;; 50 | u) POSTGRES_USER="${OPTARG}" 51 | ;; 52 | p) PORT="${OPTARG}" 53 | ;; 54 | d) POSTGRES_DB="${OPTARG}" 55 | ;; 56 | s) SECRET_NAMESPACE="--namespace=${OPTARG}" 57 | ;; 58 | f) FILE="${OPTARG}" 59 | ;; 60 | v) VARIABLES+=("-v ${OPTARG}") 61 | ;; 62 | h) show_help 63 | exit 0 64 | ;; 65 | esac 66 | done 67 | 68 | shift $(expr $OPTIND - 1) 69 | COMMAND=$@ 70 | 71 | if [[ "${INSTANCE_NAME}" == "" ]]; then 72 | error "Instance name is not set, use -i option to set it or -h for list of available values" 73 | fi 74 | 75 | if [[ "${POSTGRES_USER}" == "" ]]; then 76 | error "User name is not set, use -u option to set it or -h for list of available values" 77 | fi 78 | 79 | if [[ "${PORT}" == "" && $(is_port_free "5433") == "true" ]]; then 80 | PORT="5433" 81 | elif [[ "${PORT}" == "" ]]; then 82 | PORT=$(get_free_random_port) 83 | else 84 | ensure_port_is_free ${PORT} 85 | fi 86 | 87 | log_step "Selecting Cloud SQL proxy pod" 88 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 89 | 90 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 91 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 92 | 93 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 94 | 95 | if [ ${#VARIABLES[@]} -eq 0 ]; then 96 | PSQL_VARIABLES='' 97 | else 98 | log_step "Assigning provided variables" 99 | PSQL_VARIABLES=${VARIABLES[@]} 100 | fi 101 | 102 | if [[ "${COMMAND}" != "" ]]; then 103 | log_step "Executing SQL query '${COMMAND}' on postgres://${POSTGRES_USER}:***@localhost:${PORT}/${POSTGRES_DB}" 104 | psql "${POSTGRES_CONNECTION_STRING}" --echo-queries --command "${COMMAND};" ${PSQL_VARIABLES} 105 | elif [[ "${FILE}" != "" ]]; then 106 | log_step "Executing SQL queries from file '${FILE}' on postgres://${POSTGRES_USER}:***@localhost:${PORT}/${POSTGRES_DB}" 107 | psql "${POSTGRES_CONNECTION_STRING}" --echo-queries --file=${FILE} ${PSQL_VARIABLES} 108 | else 109 | log_step "Running: psql postgres://${POSTGRES_USER}:***@localhost:${PORT}/${POSTGRES_DB}" 110 | psql "${POSTGRES_CONNECTION_STRING}" ${PSQL_VARIABLES} 111 | fi 112 | -------------------------------------------------------------------------------- /pg-restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl pg:restore -istaging -utalkinto -dtalkinto [-nkube-system -h -eschema_migrations -tapis,plugins -f dumps/ -dpostgres] 8 | 9 | Restores PostgreSQL database from a local directory (in binary format). 10 | 11 | By default, it would restore entire database. This can be overriden by -o option that would only restore 12 | data in existing tables. You can also drop existing data by using -c option. 13 | 14 | Options: 15 | -iINSTANCE_NAME Cloud SQL Instance name to which connection is established. Required. 16 | -uUSERNAME PostgreSQL user name which would be used to log in. Required. 17 | -nNAMESPACE Namespace for a pod that exposes PostgreSQL instance. Default: kube-system. 18 | -dpostgres Database name. Required. 19 | -t List of tables to export. By default all tables are exported. Comma delimited. 20 | -e List of tables to exclude from export. Comma delimited. By default no tables are ignored. 21 | -f Path to directory where dumps are stored. By default: ./dumps 22 | -h Show help and exit. 23 | -o Only insert data within single transaction and do not clean database before insert. 24 | -c Clean database before inserting data. 25 | 26 | Examples: 27 | ktl pg:restore -dtalkinto -istaging -utalkinto 28 | ktl pg:restore -dtalkinto -istaging -utalkinto -eschema_migrations 29 | ktl pg:restore -dtalkinto -istaging -utalkinto -tapis,plugins,requests 30 | 31 | Available databases: 32 | " 33 | list_sql_proxy_users "ktl pg:restore -i\(.metadata.labels.instance_name) -u\(.data.username | @base64d) -d\$DATABASE_NAME" " " 34 | } 35 | 36 | PORT=$(get_free_random_port) 37 | PROXY_POD_NAMESPACE="kube-system" 38 | 39 | DUMP_PATH="./dumps" 40 | TABLES="" 41 | EXCLUDE_TABLES="" 42 | DATA_COMMAND="" 43 | CLEAN_COMMAND="" 44 | 45 | # Read configuration from CLI 46 | while getopts "hn:i:u:d:t:e:f:oc" opt; do 47 | case "$opt" in 48 | n) PROXY_POD_NAMESPACE="${OPTARG}" 49 | ;; 50 | i) INSTANCE_NAME="${OPTARG}" 51 | ;; 52 | u) POSTGRES_USER="${OPTARG}" 53 | ;; 54 | d) POSTGRES_DB="${OPTARG}" 55 | ;; 56 | h) show_help 57 | exit 0 58 | ;; 59 | t) TABLES="${OPTARG}" 60 | TABLES="--table=${TABLES//,/ --table=}" 61 | ;; 62 | f) DUMP_PATH="${OPTARG}" 63 | ;; 64 | e) EXCLUDE_TABLES="${OPTARG}" 65 | EXCLUDE_TABLES="--exclude-table=${EXCLUDE_TABLES//,/ --exclude-table=}" 66 | ;; 67 | o) DATA_COMMAND="--data-only --single-transaction" 68 | ;; 69 | c) CLEAN_COMMAND="--clean" 70 | ;; 71 | esac 72 | done 73 | 74 | if [[ "${INSTANCE_NAME}" == "" ]]; then 75 | error "Instance name is not set, use -i option to set it or -h for list of available values" 76 | fi 77 | 78 | if [[ "${POSTGRES_USER}" == "" ]]; then 79 | error "User name is not set, use -u option to set it or -h for list of available values" 80 | fi 81 | 82 | if [[ "${POSTGRES_DB}" == "" ]]; then 83 | error "Posgres database is not set, use -d option." 84 | fi 85 | 86 | if [[ ! -e "${DUMP_PATH}/${POSTGRES_DB}" ]]; then 87 | error "${DUMP_PATH}/${POSTGRES_DB} does not exist, specify backup path with -f option" 88 | fi 89 | 90 | log_step "Selecting Cloud SQL proxy pod" 91 | PROXY_POD_NAME=$(fetch_pod_name "${PROXY_POD_NAMESPACE}" "instance_name=${INSTANCE_NAME}") 92 | 93 | POSTGRES_PASSWORD=$(get_postgres_user_password "${INSTANCE_NAME}" "${POSTGRES_USER}") 94 | POSTGRES_CONNECTION_STRING=$(get_postgres_connection_url "${POSTGRES_USER}" "${POSTGRES_PASSWORD}" ${PORT} "${POSTGRES_DB}") 95 | 96 | tunnel_postgres_connections "${PROXY_POD_NAMESPACE}" "${PROXY_POD_NAME}" ${PORT} 97 | 98 | log_step "Restoring remote ${POSTGRES_DB} DB from ./dumps/${POSTGRES_DB}" 99 | 100 | 101 | set -x 102 | PGPASSWORD="$POSTGRES_PASSWORD" \ 103 | pg_restore dumps/${POSTGRES_DB} \ 104 | -h localhost \ 105 | -p ${PORT} \ 106 | -U ${POSTGRES_USER} \ 107 | -d ${POSTGRES_DB} \ 108 | --format c \ 109 | ${DATA_COMMAND} ${CLEAN_COMMAND} --no-acl --no-owner \ 110 | --exit-on-error \ 111 | --verbose ${TABLES} ${EXCLUDE_TABLES} 112 | set +x 113 | -------------------------------------------------------------------------------- /promote.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl promote [-f=staging -t=production -a=talkinto-domain] 8 | 9 | Promotes image tag in terraform's versions.auto.tfvars value files taking it from other environment file. 10 | 11 | Options: 12 | -fFROM Environment name from which the version would be taken. Default: staging. 13 | -tTO Environment name in which the version would be updated. Default: production. 14 | -aAPPLICATION Application name which should be promoted. By default all applications are promoted. 15 | " 16 | } 17 | 18 | FROM="staging" 19 | TO="production" 20 | APPLICATION="" 21 | 22 | while getopts "hf:t:a:" opt; do 23 | case "$opt" in 24 | f) FROM="${OPTARG}" 25 | ;; 26 | t) TO="${OPTARG}" 27 | ;; 28 | a) APPLICATION="${OPTARG}" 29 | ;; 30 | h) show_help 31 | exit 0 32 | ;; 33 | esac 34 | done 35 | 36 | TERRAFORM_DIR="${PROJECT_ROOT_DIR}/rel/deployment/terraform/environments" 37 | 38 | function versions_path() { 39 | echo "${TERRAFORM_DIR}/$1/versions.auto.tfvars" 40 | } 41 | 42 | function get_version() { 43 | VERSIONS_PATH=$(versions_path $2) 44 | [[ -e "${VERSIONS_PATH}" ]] && cat "${VERSIONS_PATH}" | grep "$1_image_tag" | awk '{print $NF;}' | sed 's/"//g' || echo "" 45 | } 46 | 47 | function commit_changes() { 48 | local APPLICATION=$1 49 | local FROM=$2 50 | local FROM_VERSION=$3 51 | local TO=$4 52 | local TO_VERSION=$5 53 | local VERSIONS_PATH=$6 54 | 55 | git add ${VERSIONS_PATH} 56 | git commit \ 57 | -m "Promote ${TO}/${APPLICATION} from ${FROM_VERSION} to ${TO_VERSION} [ci skip]" \ 58 | -m "Promoted from ${FROM} to ${TO} environment." \ 59 | &> /dev/null 60 | } 61 | 62 | function log_changelog() { 63 | APPLICATION=$1 64 | FROM_VERSION=$2 65 | 66 | set +eo pipefail 67 | MIX_CHANGELOG=$(cd ${PROJECT_ROOT_DIR} && mix rel.changelog --from-version ${FROM_VERSION} --application ${APPLICATION} 2>&1 | sed '/^\s*$/d') 68 | set -eo pipefail 69 | 70 | log_step_append "" 71 | log_step_append "${MIX_CHANGELOG}" 72 | log_step_append "" 73 | } 74 | 75 | function promote() { 76 | local APPLICATION=$1 77 | local FROM=$2 78 | local TO=$3 79 | local DRY=$4 80 | local FROM_VERSION=$(get_version $APPLICATION $TO) 81 | local TO_VERSION=$(get_version $APPLICATION $FROM) 82 | local VERSIONS_PATH=$(versions_path $TO) 83 | 84 | if [[ "${FROM_VERSION}" != "" && "${TO_VERSION}" != "" ]]; then 85 | if [[ "${FROM_VERSION}" != "${TO_VERSION}" ]]; then 86 | GIT_CHANGES=$(git status --porcelain ${VERSIONS_PATH}) 87 | if [[ ${GIT_CHANGES} ]]; then 88 | error "${VERSIONS_PATH} has changes in the git working tree, commit or stash all the changes before promoting" 89 | elif [[ "${DRY}" == "dry" ]]; then 90 | log_step "Going to promote ${APPLICATION} ${FROM_VERSION} -> ${TO_VERSION}" 91 | log_changelog ${APPLICATION} ${FROM_VERSION} 92 | else 93 | replace_pattern_in_file 's#('${APPLICATION}'_image_tag[ ]*=[ ]*"[^"]*"[ ]*)#'${APPLICATION}'_image_tag = "'${TO_VERSION}'"#' "${VERSIONS_PATH}" 94 | commit_changes ${APPLICATION} ${FROM} ${FROM_VERSION} ${TO} ${TO_VERSION} ${VERSIONS_PATH} 95 | log_step "Promoted ${APPLICATION} ${FROM_VERSION} -> ${TO_VERSION}" 96 | fi 97 | else 98 | log_step "Skipping ${APPLICATION} because there are no version changes compared to ${TO} environment" 99 | fi 100 | elif [[ "${FROM_VERSION}" == "" ]]; then 101 | warning "Skipping ${APPLICATION} app because it have no configuration for ${FROM} environment" 102 | elif [[ "${TO_VERSION}" == "" ]]; then 103 | warning "Skipping ${APPLICATION} app because it have no configuration for ${TO} environment" 104 | fi; 105 | } 106 | 107 | function list_all_applications() { 108 | local VERSIONS_PATH=$(versions_path $1) 109 | [[ -e "${VERSIONS_PATH}" ]] && cat "${VERSIONS_PATH}" | grep "image_tag" | awk '{print $1;}' | sed 's/_image_tag//g' 110 | } 111 | 112 | function promote_all() { 113 | local TERRAFORM_DIR=$1 114 | local APPLICATION=$2 115 | local FROM=$3 116 | local TO=$4 117 | local DRY=${5:-"clean"} 118 | 119 | APPLICATION=${APPLICATION//-/_} 120 | 121 | if [[ "${APPLICATION}" == "" ]]; then 122 | for a in $(list_all_applications $FROM) ; do 123 | APPLICATION=$(basename "$a") 124 | promote $APPLICATION $FROM $TO $DRY 125 | done 126 | else 127 | if [[ $(get_version $APPLICATION $FROM) == "" ]]; then 128 | error "Application ${APPLICATION} does not exist" 129 | fi; 130 | 131 | promote $APPLICATION $FROM $TO $DRY 132 | fi; 133 | } 134 | 135 | if [[ $(git diff --name-only --cached | wc -l) -gt 0 ]]; then 136 | error "You have staged changes, please commit or stash them first" 137 | fi 138 | 139 | git pull origin $(git branch --show-current) &> /dev/null 140 | git fetch --tags --force &> /dev/null 141 | 142 | promote_all "${TERRAFORM_DIR}" "${APPLICATION}" "${FROM}" "${TO}" "dry" 143 | 144 | read -p "[?] Promote and commit all changes? (Yy/Nn)" -n 1 -r 145 | echo # (optional) move to a new line 146 | if [[ $REPLY =~ ^[Yy]$ ]] 147 | then 148 | banner "Promoting and commiting changes" 149 | promote_all "${TERRAFORM_DIR}" "${APPLICATION}" "${FROM}" "${TO}" 150 | else 151 | log_step "Cancelled, got ${REPLY}" 152 | fi 153 | -------------------------------------------------------------------------------- /shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 3 | source ${K8S_UTILS_DIR}/helpers.sh 4 | 5 | function show_help { 6 | echo " 7 | ktl shell -lSELECTOR or -pPOD_NAME [-nNAMESPACE -h] [/bin/sh] 8 | 9 | Connects to the shell of a random pod selected by label and namespace. 10 | 11 | By default it runs /bin/sh. 12 | 13 | Examples: 14 | ktl shell -lapp=hammer-web Connect to one of the pods of hammer-web application in default namespace. 15 | ktl shell -lapp=hammer-web -nweb Connect to one of the pods of hammer-web application in web namespace. 16 | ktl shell -lapp=hammer-web /bin/bash Connect to one of the pods of hammer-web application and run /bin/bash. 17 | " 18 | } 19 | 20 | POD_NAMESPACE="" 21 | POD_SELECTOR= 22 | COMMAND="/bin/sh" 23 | POD_NAME= 24 | 25 | # Read configuration from CLI 26 | while getopts "n:l:p:h" opt; do 27 | case "$opt" in 28 | n) POD_NAMESPACE=${OPTARG} 29 | ;; 30 | l) POD_SELECTOR=${OPTARG} 31 | ;; 32 | c) COMMAND=${OPTARG} 33 | ;; 34 | p) POD_NAME=${OPTARG} 35 | ;; 36 | h) show_help 37 | exit 0 38 | ;; 39 | esac 40 | done 41 | shift $(expr $OPTIND - 1) 42 | REST_COMMAND=$@ 43 | 44 | if [[ "${REST_COMMAND}" != "" ]]; then 45 | COMMAND="${REST_COMMAND}" 46 | fi 47 | 48 | if [[ ! $POD_SELECTOR && ! $POD_NAME ]]; then 49 | error "You need to specify Kubernetes selector with '-l' option or pod name via '-p' option." 50 | fi 51 | 52 | if [ ! $POD_NAME ]; then 53 | POD_NAME=$(fetch_pod_name "${POD_NAMESPACE}" "${POD_SELECTOR}") 54 | fi 55 | 56 | if [ ! $POD_NAMESPACE ]; then 57 | POD_NAMESPACE=$(get_pod_namespace "${POD_NAME}") 58 | fi 59 | 60 | log_step "Running ${COMMAND} on pod ${POD_NAME} in namespace ${POD_NAMESPACE}." 61 | kubectl exec --namespace=${POD_NAMESPACE} ${POD_NAME} -it -- ${COMMAND} 62 | -------------------------------------------------------------------------------- /status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script backups all critical data, allowing to move it from one environment to another 3 | K8S_UTILS_DIR="${BASH_SOURCE%/*}" 4 | source ${K8S_UTILS_DIR}/helpers.sh 5 | 6 | if [[ "${DOCKER_PASSWORD:-}" == "" ]]; then 7 | log_step "Creating temporarely Docker Hub token to pull list of container tags." 8 | 9 | DOCKER_USERNAME_AND_PASSWORD=$(jq -r '.auths."https://index.docker.io/v1/".auth' ~/.docker/config.json) 10 | DOCKER_CREDENTIAL_HELPER=$(jq -r .credsStore ~/.docker/config.json) 11 | 12 | if [[ "${DOCKER_USERNAME_AND_PASSWORD}" != "null" ]]; then 13 | log_step_append "Resolved Docker Hub login and password from ~/.docker/config.json file" 14 | DOCKER_USERNAME_AND_PASSWORD=$(echo "${DOCKER_USERNAME_AND_PASSWORD}" | base64 --decode) 15 | DOCKER_USERNAME_AND_PASSWORD_ARRAY=(${DOCKER_USERNAME_AND_PASSWORD/:/ }) 16 | DOCKER_USERNAME=${DOCKER_USERNAME_AND_PASSWORD_ARRAY[0]} 17 | DOCKER_PASSWORD=${DOCKER_USERNAME_AND_PASSWORD_ARRAY[1]} 18 | elif [[ "${DOCKER_CREDENTIAL_HELPER}" != "null" ]]; then 19 | log_step_append "Fetching Docker Hub password from credentials helper. Only OSX Keychain is currently supported." 20 | 21 | DOCKER_CREDENTIALS=$(docker-credential-${DOCKER_CREDENTIAL_HELPER} list | \ 22 | jq -r 'to_entries[].key' | \ 23 | while read; do 24 | docker-credential-${DOCKER_CREDENTIAL_HELPER} get <<<"$REPLY"; 25 | done) 26 | DOCKER_USERNAME=$(echo "${DOCKER_CREDENTIALS}" | jq -r .Username) 27 | DOCKER_PASSWORD=$(echo "${DOCKER_CREDENTIALS}" | jq -r .Secret) 28 | else 29 | error "Can not automatically resolve Docker Hub password, you set explicitly DOCKER_USERNAME and DOCKER_PASSWORD." 30 | fi 31 | fi 32 | 33 | PROJECT_ROOT_DIR=$(git rev-parse --show-toplevel) 34 | KUBECTL_CONTEXT=$(kubectl config current-context) 35 | 36 | function get_cluser_versions() { 37 | kubectl get deployments \ 38 | --all-namespaces=true \ 39 | --output=json | \ 40 | jq --raw-output '[ 41 | .items[] 42 | | select( 43 | .metadata.namespace != "kube-system" 44 | and .metadata.namespace != "kube-monitoring" 45 | and .metadata.namespace != "kube-ingress" 46 | and .metadata.namespace != "kube-ops" 47 | ) 48 | | { 49 | app: .metadata.labels."app.kubernetes.io/name", 50 | version: .spec.template.spec.containers[0].image | split(":")[1], 51 | container: .spec.template.spec.containers[0].image | split(":")[0], 52 | ns: .metadata.namespace, 53 | replicas: .status.replicas, 54 | available_replicas: .status.availableReplicas 55 | } 56 | ]' 57 | } 58 | 59 | function get_docker_hub_tags() { 60 | DOCKER_HUB_TOKEN=$(curl -u${DOCKER_USERNAME}:${DOCKER_PASSWORD} --silent "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$1:pull" | jq -r .token) 61 | curl -Ss -H "Content-Type: application/json" -H "Authorization: Bearer ${DOCKER_HUB_TOKEN}" "https://registry.hub.docker.com/v2/$1/tags/list?n=100" 62 | } 63 | 64 | function get_manifest_version() { 65 | CHART_NAME=$1 66 | VALUES_FILE_EXT="${2:-""}.yaml" 67 | 68 | VALUES_PATH="${PROJECT_ROOT_DIR}/rel/deployment/charts/applications/${CHART_NAME}/values${VALUES_FILE_EXT}" 69 | [[ -e "${VALUES_PATH}" ]] && cat "${VALUES_PATH}" | grep "imageTag" | awk '{print $NF;}' | sed 's/"//g' || echo "Unknown" 70 | } 71 | 72 | function prepend_newline() { 73 | while read line; do echo $line; done; 74 | echo "" >&2 75 | echo "" >&2 76 | } 77 | 78 | log_step_with_progress "Loading cluster status (this may take a while)" 79 | 80 | get_cluser_versions | \ 81 | jq -r '.[] | "\(.ns)|\(.app)|\(.container)|\(.version)|\(.replicas)|\(.available_replicas)"' | \ 82 | while read key 83 | do 84 | log_progess_step 85 | 86 | REPO_SLUG=$(echo ${key} | awk -F "|" '{print $3}') 87 | CURRENT_VERSION=$(echo ${key} | awk -F "|" '{print $4}') 88 | LATEST_VERSION=$(get_docker_hub_tags "${REPO_SLUG}" "${CURRENT_VERSION}" | jq --raw-output '([.tags[] | scan("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}")] | max_by(split(".") | (.[0] | tonumber) * 1000000 + (.[1] | tonumber) * 10000 + (.[2] | tonumber)))') 89 | STAGING_VALUES_VERSION=$(get_manifest_version $(echo ${key} | awk -F "|" '{print $2}') ".staging") 90 | PRODUCTION_VALUES_VERSION=$(get_manifest_version $(echo ${key} | awk -F "|" '{print $2}') "") 91 | 92 | echo -e ${key}'|'${LATEST_VERSION}'|'${STAGING_VALUES_VERSION}'|'${PRODUCTION_VALUES_VERSION} 93 | done | \ 94 | prepend_newline | \ 95 | awk \ 96 | -F "|" \ 97 | -v red="$(tput setaf 1)" \ 98 | -v white="$(tput setaf 7)" \ 99 | -v reset="$(tput sgr0)" \ 100 | -v context=${KUBECTL_CONTEXT} \ 101 | 'BEGIN {printf "Namespace|App|Image|Latest|Deployed to %s|.staging.yaml|.yaml\n", context} {printf "%s|%s|%s|%s|%s (%s/%s)|%s|%s\n", $1, $2, $3, $7, $4, $6, $5, $8, $9;}' | \ 102 | column -t -s'|' 103 | --------------------------------------------------------------------------------