57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
573 lines
21 KiB
Bash
Executable File
573 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# deploy.sh – Main deployment orchestrator for Claude Code Agent Monitor
|
||
#
|
||
# Usage:
|
||
# ./deploy.sh --env dev|staging|production --method helm|kustomize|terraform
|
||
# ./deploy.sh --env production --method helm --strategy blue-green|canary|rolling
|
||
# ./deploy.sh --env staging --method helm --dry-run
|
||
# ./deploy.sh --help
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
set -euo pipefail
|
||
|
||
# ── Constants ───────────────────────────────────────────────────────────────
|
||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
readonly PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||
readonly DEPLOY_DIR="${PROJECT_ROOT}/deployments"
|
||
readonly APP_NAME="agent-monitor"
|
||
readonly APP_PORT=4820
|
||
readonly DEFAULT_REGISTRY="ghcr.io"
|
||
readonly DEFAULT_IMAGE_NAME="claude-code-agent-monitor"
|
||
|
||
# ── Colors & logging ───────────────────────────────────────────────────────
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
log() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; }
|
||
info() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; }
|
||
ok() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; }
|
||
warn() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; }
|
||
err() { echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; }
|
||
fatal() { err "$@"; exit 1; }
|
||
|
||
banner() {
|
||
echo ""
|
||
echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – Deploy ║${NC}"
|
||
echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
}
|
||
|
||
# ── Default parameter values ────────────────────────────────────────────────
|
||
ENVIRONMENT=""
|
||
METHOD=""
|
||
STRATEGY="rolling"
|
||
DRY_RUN=false
|
||
IMAGE_TAG=""
|
||
REGISTRY="${DOCKER_REGISTRY:-$DEFAULT_REGISTRY}"
|
||
IMAGE_NAME="${DOCKER_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}"
|
||
NAMESPACE=""
|
||
HELM_RELEASE="${APP_NAME}"
|
||
HELM_CHART_DIR="${DEPLOY_DIR}/helm/agent-monitor"
|
||
KUBE_CONTEXT=""
|
||
SKIP_BUILD=false
|
||
SKIP_HEALTH_CHECK=false
|
||
HEALTH_CHECK_RETRIES=30
|
||
HEALTH_CHECK_INTERVAL=5
|
||
VALUES_FILE=""
|
||
|
||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||
usage() {
|
||
cat <<EOF
|
||
${BOLD}Usage:${NC}
|
||
$(basename "$0") --env <environment> --method <method> [options]
|
||
|
||
${BOLD}Required:${NC}
|
||
--env, -e Environment: dev, staging, production
|
||
--method, -m Deployment method: helm, kustomize, terraform
|
||
|
||
${BOLD}Options:${NC}
|
||
--strategy, -s Deployment strategy: rolling (default), blue-green, canary
|
||
--tag, -t Docker image tag (default: git SHA)
|
||
--registry Container registry (default: ${DEFAULT_REGISTRY})
|
||
--image Image name (default: ${DEFAULT_IMAGE_NAME})
|
||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||
--release Helm release name (default: ${APP_NAME})
|
||
--context Kubernetes context to use
|
||
--values Additional Helm values file
|
||
--skip-build Skip container image build/push
|
||
--skip-health Skip post-deploy health check
|
||
--dry-run Preview changes without applying
|
||
--help, -h Show this help message
|
||
|
||
${BOLD}Examples:${NC}
|
||
$(basename "$0") --env dev --method helm
|
||
$(basename "$0") --env production --method helm --strategy blue-green --tag v1.2.3
|
||
$(basename "$0") --env staging --method kustomize --dry-run
|
||
$(basename "$0") --env production --method terraform
|
||
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||
parse_args() {
|
||
[[ $# -eq 0 ]] && usage
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--env|-e) ENVIRONMENT="$2"; shift 2 ;;
|
||
--method|-m) METHOD="$2"; shift 2 ;;
|
||
--strategy|-s) STRATEGY="$2"; shift 2 ;;
|
||
--tag|-t) IMAGE_TAG="$2"; shift 2 ;;
|
||
--registry) REGISTRY="$2"; shift 2 ;;
|
||
--image) IMAGE_NAME="$2"; shift 2 ;;
|
||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||
--release) HELM_RELEASE="$2"; shift 2 ;;
|
||
--context) KUBE_CONTEXT="$2"; shift 2 ;;
|
||
--values) VALUES_FILE="$2"; shift 2 ;;
|
||
--skip-build) SKIP_BUILD=true; shift ;;
|
||
--skip-health) SKIP_HEALTH_CHECK=true; shift ;;
|
||
--dry-run) DRY_RUN=true; shift ;;
|
||
--help|-h) usage ;;
|
||
*) fatal "Unknown option: $1. Use --help for usage." ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ── Validation ──────────────────────────────────────────────────────────────
|
||
validate_args() {
|
||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||
[[ -z "$METHOD" ]] && fatal "Missing required argument: --method"
|
||
|
||
case "$ENVIRONMENT" in
|
||
dev|staging|production) ;;
|
||
*) fatal "Invalid environment: $ENVIRONMENT. Must be dev, staging, or production." ;;
|
||
esac
|
||
|
||
case "$METHOD" in
|
||
helm|kustomize|terraform) ;;
|
||
*) fatal "Invalid method: $METHOD. Must be helm, kustomize, or terraform." ;;
|
||
esac
|
||
|
||
case "$STRATEGY" in
|
||
rolling|blue-green|canary) ;;
|
||
*) fatal "Invalid strategy: $STRATEGY. Must be rolling, blue-green, or canary." ;;
|
||
esac
|
||
|
||
# Default namespace
|
||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||
|
||
# Default image tag from git
|
||
if [[ -z "$IMAGE_TAG" ]]; then
|
||
IMAGE_TAG="$(git -C "$PROJECT_ROOT" rev-parse --short HEAD 2>/dev/null || echo 'latest')"
|
||
fi
|
||
|
||
readonly FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||
readonly MCP_IMAGE="${REGISTRY}/${IMAGE_NAME}-mcp:${IMAGE_TAG}"
|
||
}
|
||
|
||
# ── Prerequisite checks ────────────────────────────────────────────────────
|
||
check_prerequisites() {
|
||
info "Checking prerequisites..."
|
||
|
||
local missing=()
|
||
|
||
# Always need docker for building
|
||
if [[ "$SKIP_BUILD" == false ]]; then
|
||
command -v docker &>/dev/null || missing+=("docker")
|
||
fi
|
||
|
||
case "$METHOD" in
|
||
helm)
|
||
command -v kubectl &>/dev/null || missing+=("kubectl")
|
||
command -v helm &>/dev/null || missing+=("helm")
|
||
;;
|
||
kustomize)
|
||
command -v kubectl &>/dev/null || missing+=("kubectl")
|
||
command -v kustomize &>/dev/null || missing+=("kustomize")
|
||
;;
|
||
terraform)
|
||
command -v terraform &>/dev/null || missing+=("terraform")
|
||
;;
|
||
esac
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
fatal "Missing required tools: ${missing[*]}. Please install them and retry."
|
||
fi
|
||
|
||
# Validate kube context if specified
|
||
if [[ -n "$KUBE_CONTEXT" ]] && [[ "$METHOD" != "terraform" ]]; then
|
||
if ! kubectl config get-contexts "$KUBE_CONTEXT" &>/dev/null; then
|
||
fatal "Kubernetes context '$KUBE_CONTEXT' not found."
|
||
fi
|
||
kubectl config use-context "$KUBE_CONTEXT"
|
||
fi
|
||
|
||
# Validate Helm chart exists
|
||
if [[ "$METHOD" == "helm" ]] && [[ ! -f "${HELM_CHART_DIR}/Chart.yaml" ]]; then
|
||
fatal "Helm chart not found at ${HELM_CHART_DIR}"
|
||
fi
|
||
|
||
ok "All prerequisites satisfied"
|
||
}
|
||
|
||
# ── Build & push container images ───────────────────────────────────────────
|
||
build_and_push() {
|
||
if [[ "$SKIP_BUILD" == true ]]; then
|
||
info "Skipping image build (--skip-build)"
|
||
return
|
||
fi
|
||
|
||
info "Building container images..."
|
||
|
||
local docker_cmd="docker build"
|
||
local push_cmd="docker push"
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
info "[DRY-RUN] Would build: ${FULL_IMAGE}"
|
||
info "[DRY-RUN] Would build: ${MCP_IMAGE}"
|
||
return
|
||
fi
|
||
|
||
# Build main application image
|
||
log "Building main app image: ${FULL_IMAGE}"
|
||
docker build \
|
||
--file "${PROJECT_ROOT}/Dockerfile" \
|
||
--tag "${FULL_IMAGE}" \
|
||
--label "org.opencontainers.image.revision=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null || echo 'unknown')" \
|
||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||
"${PROJECT_ROOT}"
|
||
ok "Main app image built: ${FULL_IMAGE}"
|
||
|
||
# Build MCP sidecar image
|
||
log "Building MCP sidecar image: ${MCP_IMAGE}"
|
||
docker build \
|
||
--file "${PROJECT_ROOT}/mcp/Dockerfile" \
|
||
--tag "${MCP_IMAGE}" \
|
||
"${PROJECT_ROOT}"
|
||
ok "MCP sidecar image built: ${MCP_IMAGE}"
|
||
|
||
# Push images
|
||
log "Pushing images to registry..."
|
||
docker push "${FULL_IMAGE}"
|
||
docker push "${MCP_IMAGE}"
|
||
ok "Images pushed to ${REGISTRY}"
|
||
}
|
||
|
||
# ── Helm deployment ─────────────────────────────────────────────────────────
|
||
deploy_helm() {
|
||
info "Deploying via Helm (strategy: ${STRATEGY})..."
|
||
|
||
local helm_args=(
|
||
upgrade --install "${HELM_RELEASE}" "${HELM_CHART_DIR}"
|
||
--namespace "${NAMESPACE}"
|
||
--create-namespace
|
||
--set "image.repository=${REGISTRY}/${IMAGE_NAME}"
|
||
--set "image.tag=${IMAGE_TAG}"
|
||
--set "environment=${ENVIRONMENT}"
|
||
--set "mcp.image.repository=${REGISTRY}/${IMAGE_NAME}-mcp"
|
||
--set "mcp.image.tag=${IMAGE_TAG}"
|
||
--timeout 600s
|
||
--wait
|
||
--atomic
|
||
)
|
||
|
||
# Environment-specific values
|
||
local env_values="${HELM_CHART_DIR}/values-${ENVIRONMENT}.yaml"
|
||
if [[ -f "$env_values" ]]; then
|
||
helm_args+=(--values "$env_values")
|
||
fi
|
||
|
||
# User-provided values file
|
||
if [[ -n "$VALUES_FILE" ]] && [[ -f "$VALUES_FILE" ]]; then
|
||
helm_args+=(--values "$VALUES_FILE")
|
||
fi
|
||
|
||
# Strategy-specific settings
|
||
case "$STRATEGY" in
|
||
blue-green)
|
||
helm_args+=(--set "strategy.type=blue-green")
|
||
;;
|
||
canary)
|
||
helm_args+=(--set "strategy.type=canary")
|
||
helm_args+=(--set "strategy.canary.weight=10")
|
||
;;
|
||
rolling)
|
||
helm_args+=(--set "strategy.type=rolling")
|
||
helm_args+=(--set "strategy.rolling.maxUnavailable=25%")
|
||
helm_args+=(--set "strategy.rolling.maxSurge=25%")
|
||
;;
|
||
esac
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
helm_args+=(--dry-run --debug)
|
||
info "[DRY-RUN] Helm command:"
|
||
echo " helm ${helm_args[*]}"
|
||
helm "${helm_args[@]}" 2>&1 | head -100
|
||
return
|
||
fi
|
||
|
||
# Store current revision for rollback
|
||
local current_revision
|
||
current_revision=$(helm history "${HELM_RELEASE}" -n "${NAMESPACE}" --max 1 -o json 2>/dev/null \
|
||
| grep -o '"revision":[0-9]*' | head -1 | cut -d: -f2 || echo "0")
|
||
info "Current Helm revision: ${current_revision}"
|
||
|
||
# Execute deployment
|
||
if ! helm "${helm_args[@]}"; then
|
||
err "Helm deployment failed!"
|
||
if [[ "$current_revision" != "0" ]]; then
|
||
warn "Attempting auto-rollback to revision ${current_revision}..."
|
||
helm rollback "${HELM_RELEASE}" "${current_revision}" -n "${NAMESPACE}" --wait --timeout 300s \
|
||
&& ok "Auto-rollback to revision ${current_revision} succeeded" \
|
||
|| fatal "Auto-rollback also failed! Manual intervention required."
|
||
fi
|
||
exit 1
|
||
fi
|
||
|
||
ok "Helm deployment succeeded"
|
||
}
|
||
|
||
# ── Kustomize deployment ───────────────────────────────────────────────────
|
||
deploy_kustomize() {
|
||
info "Deploying via Kustomize (overlay: ${ENVIRONMENT})..."
|
||
|
||
local overlay_dir="${DEPLOY_DIR}/kubernetes/overlays/${ENVIRONMENT}"
|
||
if [[ ! -d "$overlay_dir" ]]; then
|
||
fatal "Kustomize overlay not found at ${overlay_dir}"
|
||
fi
|
||
|
||
# Set image in kustomization
|
||
local kustomize_cmd="kubectl apply -k ${overlay_dir}"
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
info "[DRY-RUN] Kustomize output:"
|
||
kubectl kustomize "${overlay_dir}" | head -100
|
||
return
|
||
fi
|
||
|
||
# Update image reference using kustomize edit
|
||
pushd "${overlay_dir}" > /dev/null
|
||
kustomize edit set image "${APP_NAME}=${FULL_IMAGE}" 2>/dev/null || true
|
||
popd > /dev/null
|
||
|
||
# Apply with server-side apply for safety
|
||
if ! kubectl apply -k "${overlay_dir}" --server-side --force-conflicts; then
|
||
err "Kustomize deployment failed!"
|
||
warn "Run: kubectl rollout undo deployment/${APP_NAME} -n ${NAMESPACE}"
|
||
exit 1
|
||
fi
|
||
|
||
# Wait for rollout
|
||
info "Waiting for rollout to complete..."
|
||
if ! kubectl rollout status "deployment/${APP_NAME}" -n "${NAMESPACE}" --timeout=600s; then
|
||
err "Rollout did not complete in time!"
|
||
warn "Attempting auto-rollback..."
|
||
kubectl rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}" \
|
||
&& ok "Auto-rollback succeeded" \
|
||
|| fatal "Auto-rollback failed! Manual intervention required."
|
||
exit 1
|
||
fi
|
||
|
||
ok "Kustomize deployment succeeded"
|
||
}
|
||
|
||
# ── Terraform deployment ───────────────────────────────────────────────────
|
||
deploy_terraform() {
|
||
info "Deploying via Terraform (environment: ${ENVIRONMENT})..."
|
||
|
||
local tf_dir="${DEPLOY_DIR}/terraform"
|
||
local env_vars_file="${tf_dir}/environments/${ENVIRONMENT}/terraform.tfvars"
|
||
|
||
if [[ ! -d "$tf_dir" ]]; then
|
||
fatal "Terraform directory not found at ${tf_dir}"
|
||
fi
|
||
|
||
pushd "${tf_dir}" > /dev/null
|
||
|
||
# Initialize
|
||
info "Running terraform init..."
|
||
terraform init -input=false
|
||
|
||
# Plan
|
||
local plan_args=(-input=false -out=tfplan)
|
||
if [[ -f "$env_vars_file" ]]; then
|
||
plan_args+=(-var-file="$env_vars_file")
|
||
fi
|
||
plan_args+=(-var "app_container_image=${FULL_IMAGE}")
|
||
plan_args+=(-var "environment=${ENVIRONMENT}")
|
||
|
||
info "Running terraform plan..."
|
||
terraform plan "${plan_args[@]}"
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
info "[DRY-RUN] Terraform plan complete. Skipping apply."
|
||
rm -f tfplan
|
||
popd > /dev/null
|
||
return
|
||
fi
|
||
|
||
# Apply
|
||
info "Applying terraform plan..."
|
||
if ! terraform apply -input=false tfplan; then
|
||
err "Terraform apply failed!"
|
||
fatal "Review state and run 'terraform plan' to diagnose."
|
||
fi
|
||
|
||
rm -f tfplan
|
||
popd > /dev/null
|
||
|
||
ok "Terraform deployment succeeded"
|
||
}
|
||
|
||
# ── Post-deployment health check ───────────────────────────────────────────
|
||
run_health_check() {
|
||
if [[ "$SKIP_HEALTH_CHECK" == true ]] || [[ "$DRY_RUN" == true ]]; then
|
||
info "Skipping health check"
|
||
return
|
||
fi
|
||
|
||
info "Running post-deployment health check..."
|
||
|
||
# Determine health check URL
|
||
local health_url=""
|
||
|
||
if [[ "$METHOD" == "terraform" ]]; then
|
||
info "For Terraform deployments, verify health via the load balancer URL in terraform output."
|
||
return
|
||
fi
|
||
|
||
# Try to get service URL from cluster
|
||
local svc_type
|
||
svc_type=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" -o jsonpath='{.spec.type}' 2>/dev/null || echo "")
|
||
|
||
case "$svc_type" in
|
||
LoadBalancer)
|
||
local lb_host
|
||
lb_host=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \
|
||
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "")
|
||
[[ -z "$lb_host" ]] && lb_host=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \
|
||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
|
||
[[ -n "$lb_host" ]] && health_url="http://${lb_host}:${APP_PORT}/api/health"
|
||
;;
|
||
NodePort)
|
||
local node_port
|
||
node_port=$(kubectl get svc "${APP_NAME}" -n "${NAMESPACE}" \
|
||
-o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "")
|
||
[[ -n "$node_port" ]] && health_url="http://localhost:${node_port}/api/health"
|
||
;;
|
||
*)
|
||
# Use port-forward for ClusterIP
|
||
info "Service type is ClusterIP – using kubectl port-forward for health check"
|
||
local local_port=14820
|
||
kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" &
|
||
local pf_pid=$!
|
||
sleep 3
|
||
health_url="http://localhost:${local_port}/api/health"
|
||
;;
|
||
esac
|
||
|
||
if [[ -z "$health_url" ]]; then
|
||
warn "Could not determine health check URL. Checking pod readiness instead."
|
||
if kubectl wait --for=condition=ready pod -l "app.kubernetes.io/name=${APP_NAME}" \
|
||
-n "${NAMESPACE}" --timeout=120s; then
|
||
ok "Pods are ready"
|
||
else
|
||
err "Pods did not become ready"
|
||
trigger_auto_rollback
|
||
fi
|
||
return
|
||
fi
|
||
|
||
# Run health check script
|
||
if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then
|
||
if ! "${SCRIPT_DIR}/health-check.sh" \
|
||
--url "${health_url}" \
|
||
--retries "${HEALTH_CHECK_RETRIES}" \
|
||
--interval "${HEALTH_CHECK_INTERVAL}"; then
|
||
err "Health check failed after deployment!"
|
||
# Kill port-forward if running
|
||
[[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true
|
||
trigger_auto_rollback
|
||
fi
|
||
else
|
||
# Inline health check
|
||
local attempt=0
|
||
while [[ $attempt -lt $HEALTH_CHECK_RETRIES ]]; do
|
||
if curl -sf --max-time 5 "${health_url}" | grep -q '"status":"ok"'; then
|
||
ok "Health check passed"
|
||
[[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true
|
||
return
|
||
fi
|
||
attempt=$((attempt + 1))
|
||
info "Health check attempt ${attempt}/${HEALTH_CHECK_RETRIES}..."
|
||
sleep "${HEALTH_CHECK_INTERVAL}"
|
||
done
|
||
err "Health check failed after ${HEALTH_CHECK_RETRIES} attempts!"
|
||
[[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true
|
||
trigger_auto_rollback
|
||
fi
|
||
|
||
# Cleanup port-forward
|
||
[[ -n "${pf_pid:-}" ]] && kill "$pf_pid" 2>/dev/null || true
|
||
}
|
||
|
||
trigger_auto_rollback() {
|
||
warn "Triggering auto-rollback..."
|
||
case "$METHOD" in
|
||
helm)
|
||
helm rollback "${HELM_RELEASE}" -n "${NAMESPACE}" --wait --timeout 300s \
|
||
&& ok "Auto-rollback succeeded" \
|
||
|| fatal "Auto-rollback failed! Manual intervention required."
|
||
;;
|
||
kustomize)
|
||
kubectl rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}" \
|
||
&& ok "Auto-rollback succeeded" \
|
||
|| fatal "Auto-rollback failed! Manual intervention required."
|
||
;;
|
||
terraform)
|
||
warn "Terraform auto-rollback not supported. Review state manually."
|
||
;;
|
||
esac
|
||
exit 1
|
||
}
|
||
|
||
# ── Production safety gate ──────────────────────────────────────────────────
|
||
confirm_production() {
|
||
if [[ "$ENVIRONMENT" == "production" ]] && [[ "$DRY_RUN" == false ]]; then
|
||
echo ""
|
||
warn "You are about to deploy to ${BOLD}PRODUCTION${NC}"
|
||
echo -e " ${BOLD}Method:${NC} ${METHOD}"
|
||
echo -e " ${BOLD}Strategy:${NC} ${STRATEGY}"
|
||
echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}"
|
||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||
echo ""
|
||
read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm
|
||
[[ "$confirm" == "yes" ]] || fatal "Deployment cancelled."
|
||
fi
|
||
}
|
||
|
||
# ── Main ────────────────────────────────────────────────────────────────────
|
||
main() {
|
||
banner
|
||
parse_args "$@"
|
||
validate_args
|
||
|
||
info "Deployment configuration:"
|
||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||
echo -e " ${BOLD}Method:${NC} ${METHOD}"
|
||
echo -e " ${BOLD}Strategy:${NC} ${STRATEGY}"
|
||
echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}"
|
||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||
echo -e " ${BOLD}Dry run:${NC} ${DRY_RUN}"
|
||
echo ""
|
||
|
||
check_prerequisites
|
||
confirm_production
|
||
build_and_push
|
||
|
||
case "$METHOD" in
|
||
helm) deploy_helm ;;
|
||
kustomize) deploy_kustomize ;;
|
||
terraform) deploy_terraform ;;
|
||
esac
|
||
|
||
run_health_check
|
||
|
||
echo ""
|
||
ok "${BOLD}Deployment complete!${NC}"
|
||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||
echo -e " ${BOLD}Image:${NC} ${FULL_IMAGE}"
|
||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
echo ""
|
||
}
|
||
|
||
main "$@"
|