Skip to content

Instantly share code, notes, and snippets.

@wbyoung
Last active August 26, 2020 09:48
Show Gist options
  • Save wbyoung/81ff1c12818564d15f362a4781fe6f44 to your computer and use it in GitHub Desktop.
Save wbyoung/81ff1c12818564d15f362a4781fe6f44 to your computer and use it in GitHub Desktop.
#!/bin/bash
set -e
JQ="jq --raw-output --exit-status"
if [ -x "$(command -v kustomize)" ]; then
KUSTOMIZE="kustomize build"
else
KUSTOMIZE="kubectl kustomize"
fi
escape-json() {
python -c "import sys; import json; sys.stdout.write(json.dumps(sys.stdin.read().strip())[1:-1])"
}
escape-subst() {
# adapted to read from stdin from: https://stackoverflow.com/a/29613573/98069
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g')
printf %s "${REPLY%$'\n'}"
}
# @env project_id
bootstrap-gcloud() {
if [ ! -x "$(command -v gcloud)" ]; then
echo Installing gcloud command...
curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts > /dev/null
export PATH="$PATH:/root/google-cloud-sdk/bin"
fi
if [ -n "$GCP_ACCOUNT_KEY" ]; then
echo "$GCP_ACCOUNT_KEY" > /tmp/service-account-key.json
gcloud auth activate-service-account \
--key-file=/tmp/service-account-key.json
fi
gcloud auth configure-docker --quiet --project=$project_id
}
# @arg remote_image
# @env project_id
# @env commit_sha
# @returns via stderr
image-tag() {
local remote_image="$1"
echo "gcr.io/$project_id/${remote_image%%:*}:${commit_sha:0:9}" >&2
}
# @arg local_image
# @arg tag
# @env project_id
push-image() {
local local_image="$1"
local tag="$2"
echo 'Pushing container image...'
docker tag "$local_image" "$tag"
docker push "$tag"
}
# @arg manifest
# @env commit_sha
# @env config_keys
# @env config_value
# @returns via stderr
build-manifest() {
local manifest="$1"
local definition=$(
$KUSTOMIZE $manifest |
sed \
-e "s/{{COMMIT_SHA}}/${commit_sha:0:9}/g" \
)
for i in "${!config_keys[@]}"; do
local key="${config_keys[$i]}"
local value="${config_values[$i]}"
local escaped_value=$(echo "$value" | escape-subst)
definition=$(
echo "$definition" | sed -e "s/{{$key}}/$escaped_value/g"
)
done
echo "$definition" >&2
}
usage() {
if [ $# -gt 1 ]; then
echo $@
echo
fi
echo 'gke-deploy'
echo ' --project-id <value>'
echo ' --zone <value>'
echo ' --pre-deploy <value>'
echo ' --image <local> <remote>'
echo ' --manifest <value>'
echo ' [--config <key> <value>]'
echo
echo 'OPTIONS'
echo
echo ' --project-id (string)'
echo ' The GCP account ID'
echo
echo ' --zone (string)'
echo ' The GCP zone'
echo
echo ' --pre-deploy <name> <container> <manifest> (string, string)'
echo ' The pod name, container name & path to a Kustomize manifest with '
echo ' only that Kubernetes pod in it. The pod will be run before the '
echo ' main deployment.'
echo
echo ' --image <local> <remote> (string, string>)'
echo ' The name of an image to push (can be specified multiple times)'
echo
echo ' --manifest (string)'
echo ' The path to the Kustomize manifest with the Kubernetes '
echo ' configuration'
echo
echo ' [--config <key> <value>]'
echo ' Add a substitution value for the manifest, i.e.'
echo
echo ' gke-deploy --config MY_VAR true'
echo
echo ' Would replace occurrences of {{MY_VAR}} with true in the manifest. '
echo ' This option can be repeated for multiple config variables.'
echo
}
gke-deploy() {
local config_keys=()
local config_values=()
local local_images=()
local remote_images=()
while [ $# -ne 0 ]; do
case "$1" in
--project-id)
shift; local project_id="$1"
shift
;;
--zone)
shift; local zone="$1"
shift
;;
--pre-deploy)
shift; local pre_name="$1"
shift; local pre_container="$1"
shift; local pre_manifest="$1"
shift
;;
--image)
shift; local_images+=("$1")
shift; remote_images+=("$1")
shift
;;
-m|--manifest)
shift; local manifest="$1"
shift
;;
-C|--cluster)
shift; local cluster="$1"
shift
;;
-c|--config)
shift; config_keys+=("$1")
shift; config_values+=("$1")
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage unknown option "$1"
exit 1
;;
esac
done
[ -n "$project_id" ] || { usage missing --project-id; exit 1; }
[ -n "$zone" ] || { usage missing --zone; exit 1; }
[ -n "$local_images" ] || { usage missing --image; exit 1; }
[ -n "$remote_images" ] || { usage missing --image; exit 1; }
[ -n "$manifest" ] || { usage missing --manifest; exit 1; }
[ -n "$cluster" ] || { usage missing --cluster; exit 1; }
for local_image in "${local_images[@]}"; do
[ -n "$local_image" ] || { usage missing --image local value; exit 1; }
done
for remote_image in "${remote_images[@]}"; do
[ -n "$remote_image" ] || { usage missing --image remote value; exit 1; }
done
for key in "${config_keys[@]}"; do
[ -n "$key" ] || { usage missing --config key; exit 1; }
done
for value in "${config_values[@]}"; do
[ -n "$value" ] || { usage missing --config value; exit 1; }
done
bootstrap-gcloud
gcloud container clusters get-credentials $cluster \
--project=$project_id \
--zone=$zone
local commit_sha=$(git rev-parse HEAD)
for i in "${!local_images[@]}"; do
local local_image="${local_images[$i]}"
local remote_image="${remote_images[$i]}"
local tag=$(image-tag $remote_image 3>&2 2>&1 1>&3)
push-image "$local_image" "$tag"
done
local apply_manifest=$(build-manifest "$manifest" 3>&2 2>&1 1>&3)
if [ -n "$pre_name" ] && [ -n "$pre_container" ] && [ -n "$pre_manifest" ]; then
local pre_complete=false
local pre_success=false
local pre_apply_manifest=$(
build-manifest "$pre_manifest" 3>&2 2>&1 1>&3
)
echo "Running pre-deployment job $pre_name"
echo "$pre_apply_manifest"
echo "$pre_apply_manifest" | kubectl apply -f -
# wait for target container in pod to finish
while true; do
local pre_status=$(kubectl get pod migrate -ojson)
local pre_phase=$(echo "$pre_status" | $JQ .status.phase)
local state_query=".status.containerStatuses[] | \
select(.name == \"$pre_container\") | \
.state | keys[0]"
local code_query=".status.containerStatuses[] | \
select(.name == \"$pre_container\") | \
.state.terminated.exitCode"
local pre_container_state=$(echo "$pre_status" | $JQ "$state_query")
local pre_termination_code=$(echo "$pre_status" | $JQ "$code_query")
local message=""
message+="pod \"$pre_name\" $pre_phase, "
message+="container \"$pre_container\" $pre_container_state"
[ "$pre_phase" != "Pending" ] && \
[ "$pre_phase" != "Running" ] && \
pre_complete=true
[ "$pre_container_state" = "terminated" ] && \
pre_complete=true
[ "$pre_container_state" = "terminated" ] && \
[ "$pre_termination_code" = "0" ] && \
pre_success=true
[ "$pre_complete" == "true" ] && message+=", (exit $pre_termination_code)"
echo "$message"
[ "$pre_complete" == "true" ] && break
sleep 1
done
kubectl logs --timestamps -c $pre_container pods/$pre_name
if [ "$pre_success" != "true" ]; then
echo "Pre-deploy task failed; pod may still exist."
exit 1
fi
kubectl delete pod $pre_name
fi
echo "$apply_manifest"
echo "$apply_manifest" | kubectl apply -f -
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
gke-deploy "$@"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment