Files
AItrackwalker/deploy/scripts/deploy.sh
T

195 lines
6.6 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_BUNDLE_ROOT="$(cd "$DEPLOY_ROOT/.." && pwd)"
PROFILE=""
INVENTORY=""
CONFIG=""
SECRETS=""
BUNDLE_ROOT="$DEFAULT_BUNDLE_ROOT"
ALLOW_DEVELOPMENT="false"
ALLOW_PLAINTEXT_SECRETS="false"
TRUSTED_PUBLIC_KEY=""
PREFLIGHT_ONLY="false"
VERIFY_ONLY="false"
SKIP_APP="false"
RESUME_FROM="preflight"
usage() {
cat >&2 <<'USAGE'
Usage:
deploy.sh --profile single|ha --inventory <hosts.yml> --config <site.yml>
--secrets <secrets.sops.yaml> [options]
Options:
--bundle-root <path>
--trusted-public-key <path>
--allow-development-bundle
--allow-plaintext-secrets
--preflight-only
--verify-only
--skip-app
--resume-from preflight|os-baseline|airgap|rke2|addons|application|verify
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) PROFILE="${2:?missing profile}"; shift 2 ;;
--inventory) INVENTORY="${2:?missing inventory}"; shift 2 ;;
--config) CONFIG="${2:?missing config}"; shift 2 ;;
--secrets) SECRETS="${2:?missing secrets}"; shift 2 ;;
--bundle-root) BUNDLE_ROOT="${2:?missing bundle root}"; shift 2 ;;
--trusted-public-key) TRUSTED_PUBLIC_KEY="${2:?missing trusted public key}"; shift 2 ;;
--allow-development-bundle) ALLOW_DEVELOPMENT="true"; shift ;;
--allow-plaintext-secrets) ALLOW_PLAINTEXT_SECRETS="true"; shift ;;
--preflight-only) PREFLIGHT_ONLY="true"; shift ;;
--verify-only) VERIFY_ONLY="true"; shift ;;
--skip-app) SKIP_APP="true"; shift ;;
--resume-from) RESUME_FROM="${2:?missing phase}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage; exit 2 ;;
esac
done
[[ "$PROFILE" == "single" || "$PROFILE" == "ha" ]] || { usage; exit 2; }
[[ -f "$INVENTORY" && -f "$CONFIG" && -f "$SECRETS" ]] || {
echo "Inventory, config or secrets file is missing" >&2
exit 2
}
BUNDLE_ROOT="$(cd "$BUNDLE_ROOT" && pwd)"
export PATH="$BUNDLE_ROOT/tools/bin:$PATH"
STATE_DIR="${TRACKWALKER_STATE_DIR:-$DEPLOY_ROOT/.state}"
mkdir -p "$STATE_DIR"
LOCK_DIR="$STATE_DIR/deploy.lock"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "Another deployment appears to be running: $LOCK_DIR" >&2
exit 1
fi
umask 077
TEMP_DIR="$(mktemp -d "$STATE_DIR/run.XXXXXX")"
cleanup() {
rm -rf "$TEMP_DIR"
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
LOG_FILE="$STATE_DIR/deploy-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
VERIFY_ARGS=(--bundle-root "$BUNDLE_ROOT")
if [[ -n "$TRUSTED_PUBLIC_KEY" ]]; then
VERIFY_ARGS+=(--trusted-public-key "$TRUSTED_PUBLIC_KEY")
fi
RELEASE_VALIDATION=(--release)
if [[ "$ALLOW_DEVELOPMENT" == "true" ]]; then
VERIFY_ARGS+=(--allow-development-bundle)
RELEASE_VALIDATION=()
fi
"$SCRIPT_DIR/verify-bundle.sh" "${VERIFY_ARGS[@]}"
DECRYPTED_SECRETS="$TEMP_DIR/secrets.yaml"
if grep -q '^[[:space:]]*sops:' "$SECRETS"; then
command -v sops >/dev/null 2>&1 || { echo "sops is required" >&2; exit 1; }
sops --decrypt "$SECRETS" > "$DECRYPTED_SECRETS"
else
[[ "$ALLOW_PLAINTEXT_SECRETS" == "true" ]] || {
echo "Plaintext secrets require --allow-plaintext-secrets" >&2
exit 1
}
cp "$SECRETS" "$DECRYPTED_SECRETS"
fi
chmod 0600 "$DECRYPTED_SECRETS"
python3 "$SCRIPT_DIR/validate_config.py" \
--site "$CONFIG" \
--inventory "$INVENTORY" \
--secrets "$DECRYPTED_SECRETS" \
--versions "$DEPLOY_ROOT/versions.lock.yaml" \
"${RELEASE_VALIDATION[@]}"
CONFIG_PROFILE="$(python3 -c 'import sys,yaml; print(yaml.safe_load(open(sys.argv[1], encoding="utf-8"))["deployment"]["profile"])' "$CONFIG")"
[[ "$CONFIG_PROFILE" == "$PROFILE" ]] || {
echo "CLI profile $PROFILE does not match site profile $CONFIG_PROFILE" >&2
exit 1
}
export KUBECONFIG="${KUBECONFIG:-$STATE_DIR/kubeconfig}"
if [[ "$VERIFY_ONLY" == "true" ]]; then
"$SCRIPT_DIR/verify.sh" "$CONFIG"
exit 0
fi
case "$RESUME_FROM" in
preflight) ANSIBLE_TAGS="preflight,os-baseline,airgap,rke2,postcheck" ;;
os-baseline) ANSIBLE_TAGS="os-baseline,airgap,rke2,postcheck" ;;
airgap) ANSIBLE_TAGS="airgap,rke2,postcheck" ;;
rke2) ANSIBLE_TAGS="rke2,postcheck" ;;
addons|application|verify) ANSIBLE_TAGS="" ;;
*) echo "Unsupported resume phase: $RESUME_FROM" >&2; exit 2 ;;
esac
if [[ "$PREFLIGHT_ONLY" == "true" ]]; then
ANSIBLE_TAGS="preflight"
fi
if [[ -n "$ANSIBLE_TAGS" ]]; then
command -v ansible-playbook >/dev/null 2>&1 || { echo "ansible-playbook is required" >&2; exit 1; }
ANSIBLE_CONFIG="$DEPLOY_ROOT/ansible/ansible.cfg" \
ansible-playbook \
--inventory "$INVENTORY" \
"$DEPLOY_ROOT/ansible/site.yml" \
--extra-vars "@$CONFIG" \
--extra-vars "@$DECRYPTED_SECRETS" \
--extra-vars "offline_artifact_root=$BUNDLE_ROOT" \
--extra-vars "deployment_kubeconfig_path=$KUBECONFIG" \
--tags "$ANSIBLE_TAGS"
fi
if [[ "$PREFLIGHT_ONLY" == "true" || "$SKIP_APP" == "true" ]]; then
exit 0
fi
if [[ "$RESUME_FROM" != "application" && "$RESUME_FROM" != "verify" ]]; then
"$SCRIPT_DIR/install-addons.sh" "$CONFIG" "$BUNDLE_ROOT" "$DEPLOY_ROOT"
fi
if [[ "$RESUME_FROM" != "verify" ]]; then
command -v helm >/dev/null 2>&1 || { echo "helm is required" >&2; exit 1; }
command -v kubectl >/dev/null 2>&1 || { echo "kubectl is required" >&2; exit 1; }
NAMESPACE="$(python3 -c 'import sys,yaml; print(yaml.safe_load(open(sys.argv[1], encoding="utf-8"))["deployment"]["namespace"])' "$CONFIG")"
RELEASE_NAME="$(python3 -c 'import sys,yaml; print(yaml.safe_load(open(sys.argv[1], encoding="utf-8"))["deployment"]["releaseName"])' "$CONFIG")"
SECRET_NAME="$(python3 -c 'import sys,yaml; print(yaml.safe_load(open(sys.argv[1], encoding="utf-8"))["helmValues"]["global"]["existingSecret"])' "$CONFIG")"
SECRET_ENV="$TEMP_DIR/application-secrets.env"
SITE_VALUES="$TEMP_DIR/site-values.yaml"
python3 "$SCRIPT_DIR/render_secret_env.py" --secrets "$DECRYPTED_SECRETS" --output "$SECRET_ENV"
python3 "$SCRIPT_DIR/render_helm_values.py" \
--site "$CONFIG" \
--versions "$DEPLOY_ROOT/versions.lock.yaml" \
--output "$SITE_VALUES"
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
kubectl --namespace "$NAMESPACE" create secret generic "$SECRET_NAME" \
--from-env-file "$SECRET_ENV" \
--dry-run=client \
-o yaml | kubectl apply -f -
helm upgrade --install "$RELEASE_NAME" "$DEPLOY_ROOT/charts/trackwalker" \
--namespace "$NAMESPACE" \
--values "$DEPLOY_ROOT/charts/trackwalker/values.yaml" \
--values "$DEPLOY_ROOT/charts/trackwalker/values-$PROFILE.yaml" \
--values "$SITE_VALUES" \
--wait \
--timeout 20m \
--atomic
fi
"$SCRIPT_DIR/verify.sh" "$CONFIG"
echo "Deployment completed. Log: $LOG_FILE"