195 lines
6.6 KiB
Bash
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"
|