feat: Claude Code Monitor — lanes, pipelines and a merged workspace
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.
This commit is contained in:
Executable
+294
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# blue-green-switch.sh – Switch traffic between blue/green deployment slots
|
||||
#
|
||||
# Usage:
|
||||
# ./blue-green-switch.sh --env production --target green
|
||||
# ./blue-green-switch.sh --env production --target blue --skip-health
|
||||
# ./blue-green-switch.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly APP_NAME="agent-monitor"
|
||||
readonly APP_PORT=4820
|
||||
|
||||
# ── 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; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
ENVIRONMENT=""
|
||||
TARGET=""
|
||||
NAMESPACE=""
|
||||
SERVICE_NAME="${APP_NAME}"
|
||||
SKIP_HEALTH_CHECK=false
|
||||
DRY_RUN=false
|
||||
|
||||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${BOLD}Usage:${NC}
|
||||
$(basename "$0") --env <environment> --target <blue|green> [options]
|
||||
|
||||
${BOLD}Required:${NC}
|
||||
--env, -e Environment: dev, staging, production
|
||||
--target, -t Target slot: blue, green
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||||
--service Service name (default: ${APP_NAME})
|
||||
--skip-health Skip health check on target before switching
|
||||
--dry-run Show what would change without applying
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --env production --target green
|
||||
$(basename "$0") --env production --target blue # instant rollback
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
[[ $# -eq 0 ]] && usage
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env|-e) ENVIRONMENT="$2"; shift 2 ;;
|
||||
--target|-t) TARGET="$2"; shift 2 ;;
|
||||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||||
--service) SERVICE_NAME="$2"; shift 2 ;;
|
||||
--skip-health) SKIP_HEALTH_CHECK=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) fatal "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||||
[[ -z "$TARGET" ]] && fatal "Missing required argument: --target"
|
||||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||||
|
||||
case "$TARGET" in
|
||||
blue|green) ;;
|
||||
*) fatal "Invalid target: $TARGET. Must be 'blue' or 'green'." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Detect current active slot ──────────────────────────────────────────────
|
||||
detect_current_slot() {
|
||||
local current
|
||||
current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \
|
||||
-o jsonpath='{.spec.selector.slot}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$current" ]]; then
|
||||
# Try alternative label names
|
||||
current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \
|
||||
-o jsonpath='{.spec.selector.deployment-slot}' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$current" ]]; then
|
||||
current=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" \
|
||||
-o jsonpath='{.spec.selector.color}' 2>/dev/null || echo "unknown")
|
||||
fi
|
||||
|
||||
echo "$current"
|
||||
}
|
||||
|
||||
# ── Check target slot is healthy ────────────────────────────────────────────
|
||||
check_target_health() {
|
||||
if [[ "$SKIP_HEALTH_CHECK" == true ]]; then
|
||||
info "Skipping target health check (--skip-health)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Checking health of ${BOLD}${TARGET}${NC} slot..."
|
||||
|
||||
# Verify pods exist and are ready
|
||||
local ready_pods
|
||||
ready_pods=$(kubectl get pods -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME},slot=${TARGET}" \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$ready_pods" ]]; then
|
||||
# Try with 'color' label
|
||||
ready_pods=$(kubectl get pods -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME},color=${TARGET}" \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$ready_pods" ]]; then
|
||||
# Try with 'deployment-slot' label
|
||||
ready_pods=$(kubectl get pods -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME},deployment-slot=${TARGET}" \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$ready_pods" ]]; then
|
||||
fatal "No running pods found for ${TARGET} slot. Deploy first."
|
||||
fi
|
||||
|
||||
info "Found running pods in ${TARGET} slot: ${ready_pods}"
|
||||
|
||||
# Health check via port-forward to first pod
|
||||
local first_pod
|
||||
first_pod=$(echo "$ready_pods" | awk '{print $1}')
|
||||
local local_port=14821
|
||||
|
||||
kubectl port-forward "pod/${first_pod}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" &
|
||||
local pf_pid=$!
|
||||
sleep 3
|
||||
|
||||
local healthy=false
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if curl -sf --max-time 5 "http://localhost:${local_port}/api/health" | grep -q '"status":"ok"'; then
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
info "Health check attempt ${attempt}/5..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
kill "$pf_pid" 2>/dev/null || true
|
||||
|
||||
if [[ "$healthy" != true ]]; then
|
||||
fatal "${TARGET} slot is NOT healthy. Aborting traffic switch."
|
||||
fi
|
||||
|
||||
ok "${TARGET} slot is healthy"
|
||||
}
|
||||
|
||||
# ── Switch traffic ──────────────────────────────────────────────────────────
|
||||
switch_traffic() {
|
||||
local current_slot
|
||||
current_slot=$(detect_current_slot)
|
||||
|
||||
info "Current active slot: ${BOLD}${current_slot}${NC}"
|
||||
info "Switching to: ${BOLD}${TARGET}${NC}"
|
||||
|
||||
if [[ "$current_slot" == "$TARGET" ]]; then
|
||||
warn "Traffic is already pointing to ${TARGET}. Nothing to do."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]] && [[ "$DRY_RUN" == false ]]; then
|
||||
echo ""
|
||||
warn "Switching ${BOLD}PRODUCTION${NC} traffic from ${current_slot} → ${TARGET}"
|
||||
read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "yes" ]] || fatal "Switch cancelled."
|
||||
fi
|
||||
|
||||
# Build the patch – try common label conventions
|
||||
local label_key="slot"
|
||||
local current_labels
|
||||
current_labels=$(kubectl get svc "${SERVICE_NAME}" -n "${NAMESPACE}" -o json 2>/dev/null)
|
||||
|
||||
if echo "$current_labels" | grep -q '"color"'; then
|
||||
label_key="color"
|
||||
elif echo "$current_labels" | grep -q '"deployment-slot"'; then
|
||||
label_key="deployment-slot"
|
||||
fi
|
||||
|
||||
local patch="{\"spec\":{\"selector\":{\"${label_key}\":\"${TARGET}\"}}}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
info "[DRY-RUN] Would patch service '${SERVICE_NAME}' with:"
|
||||
echo " ${patch}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! kubectl patch svc "${SERVICE_NAME}" -n "${NAMESPACE}" -p "${patch}"; then
|
||||
fatal "Failed to patch service selector!"
|
||||
fi
|
||||
|
||||
ok "Service '${SERVICE_NAME}' now routing to ${TARGET} slot"
|
||||
|
||||
# Verify the switch
|
||||
local new_slot
|
||||
new_slot=$(detect_current_slot)
|
||||
if [[ "$new_slot" != "$TARGET" ]]; then
|
||||
err "Verification failed! Service selector shows: ${new_slot}"
|
||||
warn "Attempting to revert to ${current_slot}..."
|
||||
kubectl patch svc "${SERVICE_NAME}" -n "${NAMESPACE}" \
|
||||
-p "{\"spec\":{\"selector\":{\"${label_key}\":\"${current_slot}\"}}}" \
|
||||
&& ok "Reverted to ${current_slot}" \
|
||||
|| fatal "Revert failed! Manual intervention needed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "Verified: traffic now routes to ${TARGET}"
|
||||
}
|
||||
|
||||
# ── Post-switch health check ───────────────────────────────────────────────
|
||||
post_switch_health() {
|
||||
if [[ "$DRY_RUN" == true ]] || [[ "$SKIP_HEALTH_CHECK" == true ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
info "Running post-switch health check via service..."
|
||||
sleep 5 # Let connections drain
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then
|
||||
local local_port=14822
|
||||
kubectl port-forward "svc/${SERVICE_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" &
|
||||
local pf_pid=$!
|
||||
sleep 3
|
||||
|
||||
if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 5 --interval 3; then
|
||||
ok "Post-switch health check passed"
|
||||
else
|
||||
warn "Post-switch health check failed – consider switching back!"
|
||||
fi
|
||||
|
||||
kill "$pf_pid" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – Blue/Green Switch ║${NC}"
|
||||
echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
info "Configuration:"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Target slot:${NC} ${TARGET}"
|
||||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||||
echo -e " ${BOLD}Service:${NC} ${SERVICE_NAME}"
|
||||
echo ""
|
||||
|
||||
check_target_health
|
||||
switch_traffic
|
||||
post_switch_health
|
||||
|
||||
echo ""
|
||||
ok "${BOLD}Blue-green switch complete!${NC}"
|
||||
echo -e " ${BOLD}Active slot:${NC} ${TARGET}"
|
||||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo -e " ${BOLD}Rollback:${NC} $(basename "$0") --env ${ENVIRONMENT} --target $([ "$TARGET" = "blue" ] && echo "green" || echo "blue")"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+298
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# db-backup.sh – SQLite database backup for Claude Code Agent Monitor
|
||||
#
|
||||
# Usage:
|
||||
# ./db-backup.sh --env production --output ./backups/
|
||||
# ./db-backup.sh --env production --output ./backups/ --upload s3://bucket/path
|
||||
# ./db-backup.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly APP_NAME="agent-monitor"
|
||||
readonly DB_PATH_IN_CONTAINER="/app/data"
|
||||
|
||||
# ── 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; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
ENVIRONMENT=""
|
||||
OUTPUT_DIR=""
|
||||
NAMESPACE=""
|
||||
UPLOAD_DEST=""
|
||||
POD_NAME=""
|
||||
DB_FILENAME=""
|
||||
COMPRESS=true
|
||||
|
||||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${BOLD}Usage:${NC}
|
||||
$(basename "$0") --env <environment> --output <directory> [options]
|
||||
|
||||
${BOLD}Required:${NC}
|
||||
--env, -e Environment: dev, staging, production
|
||||
--output, -o Local directory for the backup file
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||||
--pod Specific pod name to copy from (auto-detected if omitted)
|
||||
--upload Upload backup to S3/GCS (e.g., s3://bucket/backups/)
|
||||
--no-compress Skip gzip compression
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --env production --output ./backups/
|
||||
$(basename "$0") --env staging --output /tmp/backups --upload s3://my-bucket/db-backups/
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
[[ $# -eq 0 ]] && usage
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env|-e) ENVIRONMENT="$2"; shift 2 ;;
|
||||
--output|-o) OUTPUT_DIR="$2"; shift 2 ;;
|
||||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||||
--pod) POD_NAME="$2"; shift 2 ;;
|
||||
--upload) UPLOAD_DEST="$2"; shift 2 ;;
|
||||
--no-compress) COMPRESS=false; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) fatal "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||||
[[ -z "$OUTPUT_DIR" ]] && fatal "Missing required argument: --output"
|
||||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||||
}
|
||||
|
||||
# ── Find target pod ────────────────────────────────────────────────────────
|
||||
find_pod() {
|
||||
if [[ -n "$POD_NAME" ]]; then
|
||||
info "Using specified pod: ${POD_NAME}"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Finding running pod in namespace '${NAMESPACE}'..."
|
||||
|
||||
POD_NAME=$(kubectl get pods -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$POD_NAME" ]]; then
|
||||
fatal "No running pods found for ${APP_NAME} in ${NAMESPACE}"
|
||||
fi
|
||||
|
||||
info "Selected pod: ${POD_NAME}"
|
||||
}
|
||||
|
||||
# ── Create backup ──────────────────────────────────────────────────────────
|
||||
create_backup() {
|
||||
local timestamp
|
||||
timestamp=$(date -u +%Y%m%d_%H%M%S)
|
||||
DB_FILENAME="${APP_NAME}_${ENVIRONMENT}_${timestamp}.db"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
info "Creating backup..."
|
||||
|
||||
# Use sqlite3 .backup inside the pod for a consistent snapshot
|
||||
# This avoids copying a potentially locked/in-flight database
|
||||
local remote_backup_path="/tmp/${DB_FILENAME}"
|
||||
|
||||
info "Running SQLite backup inside pod (consistent snapshot)..."
|
||||
if kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
|
||||
sh -c "
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 '${DB_PATH_IN_CONTAINER}/dashboard.db' '.backup ${remote_backup_path}'
|
||||
else
|
||||
cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}'
|
||||
fi
|
||||
" 2>/dev/null; then
|
||||
ok "In-pod backup created at ${remote_backup_path}"
|
||||
else
|
||||
# Fallback: also copy WAL files if present
|
||||
warn "sqlite3 not available in pod, falling back to file copy"
|
||||
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
|
||||
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db' '${remote_backup_path}'" \
|
||||
|| fatal "Failed to copy database file"
|
||||
|
||||
# Try to also get WAL and SHM files
|
||||
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
|
||||
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-wal' '${remote_backup_path}-wal' 2>/dev/null || true"
|
||||
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
|
||||
sh -c "cp '${DB_PATH_IN_CONTAINER}/dashboard.db-shm' '${remote_backup_path}-shm' 2>/dev/null || true"
|
||||
fi
|
||||
|
||||
# Copy backup from pod to local
|
||||
info "Copying backup to local filesystem..."
|
||||
kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}" "${OUTPUT_DIR}/${DB_FILENAME}" \
|
||||
|| fatal "Failed to copy backup from pod"
|
||||
|
||||
# Copy WAL if it exists
|
||||
kubectl cp "${NAMESPACE}/${POD_NAME}:${remote_backup_path}-wal" "${OUTPUT_DIR}/${DB_FILENAME}-wal" 2>/dev/null || true
|
||||
|
||||
# Cleanup remote temp file
|
||||
kubectl exec "${POD_NAME}" -n "${NAMESPACE}" -- \
|
||||
sh -c "rm -f '${remote_backup_path}' '${remote_backup_path}-wal' '${remote_backup_path}-shm'" 2>/dev/null || true
|
||||
|
||||
local file_size
|
||||
file_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}')
|
||||
ok "Backup saved: ${OUTPUT_DIR}/${DB_FILENAME} (${file_size})"
|
||||
}
|
||||
|
||||
# ── Validate backup integrity ──────────────────────────────────────────────
|
||||
validate_backup() {
|
||||
info "Validating backup integrity..."
|
||||
|
||||
local db_file="${OUTPUT_DIR}/${DB_FILENAME}"
|
||||
|
||||
if ! command -v sqlite3 &>/dev/null; then
|
||||
warn "sqlite3 not found locally – skipping integrity check"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check integrity
|
||||
local integrity
|
||||
integrity=$(sqlite3 "${db_file}" "PRAGMA integrity_check;" 2>/dev/null || echo "error")
|
||||
|
||||
if [[ "$integrity" == "ok" ]]; then
|
||||
ok "SQLite integrity check: OK"
|
||||
else
|
||||
err "SQLite integrity check failed: ${integrity}"
|
||||
warn "Backup may be corrupted – consider re-running the backup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Show basic stats
|
||||
local table_count
|
||||
table_count=$(sqlite3 "${db_file}" "SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "?")
|
||||
local page_count
|
||||
page_count=$(sqlite3 "${db_file}" "PRAGMA page_count;" 2>/dev/null || echo "?")
|
||||
local page_size
|
||||
page_size=$(sqlite3 "${db_file}" "PRAGMA page_size;" 2>/dev/null || echo "?")
|
||||
|
||||
info "Database stats: ${table_count} tables, ${page_count} pages × ${page_size} bytes"
|
||||
}
|
||||
|
||||
# ── Compress backup ────────────────────────────────────────────────────────
|
||||
compress_backup() {
|
||||
if [[ "$COMPRESS" == false ]]; then
|
||||
info "Compression skipped"
|
||||
return
|
||||
fi
|
||||
|
||||
local db_file="${OUTPUT_DIR}/${DB_FILENAME}"
|
||||
|
||||
if ! command -v gzip &>/dev/null; then
|
||||
warn "gzip not available – skipping compression"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Compressing backup..."
|
||||
gzip -k "${db_file}"
|
||||
DB_FILENAME="${DB_FILENAME}.gz"
|
||||
|
||||
local compressed_size
|
||||
compressed_size=$(du -sh "${OUTPUT_DIR}/${DB_FILENAME}" 2>/dev/null | awk '{print $1}')
|
||||
ok "Compressed: ${OUTPUT_DIR}/${DB_FILENAME} (${compressed_size})"
|
||||
}
|
||||
|
||||
# ── Upload to cloud storage ────────────────────────────────────────────────
|
||||
upload_backup() {
|
||||
if [[ -z "$UPLOAD_DEST" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local backup_file="${OUTPUT_DIR}/${DB_FILENAME}"
|
||||
|
||||
info "Uploading backup to ${UPLOAD_DEST}..."
|
||||
|
||||
if [[ "$UPLOAD_DEST" == s3://* ]]; then
|
||||
if ! command -v aws &>/dev/null; then
|
||||
fatal "AWS CLI not found. Install it to upload to S3."
|
||||
fi
|
||||
aws s3 cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \
|
||||
--storage-class STANDARD_IA \
|
||||
|| fatal "S3 upload failed"
|
||||
ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}"
|
||||
|
||||
elif [[ "$UPLOAD_DEST" == gs://* ]]; then
|
||||
if ! command -v gsutil &>/dev/null; then
|
||||
fatal "gsutil not found. Install Google Cloud SDK to upload to GCS."
|
||||
fi
|
||||
gsutil cp "${backup_file}" "${UPLOAD_DEST}${DB_FILENAME}" \
|
||||
|| fatal "GCS upload failed"
|
||||
ok "Uploaded to ${UPLOAD_DEST}${DB_FILENAME}"
|
||||
|
||||
elif [[ "$UPLOAD_DEST" == az://* ]] || [[ "$UPLOAD_DEST" == https://*.blob.core.windows.net/* ]]; then
|
||||
if ! command -v az &>/dev/null; then
|
||||
fatal "Azure CLI not found. Install it to upload to Azure Blob."
|
||||
fi
|
||||
local container_url="${UPLOAD_DEST}"
|
||||
az storage blob upload \
|
||||
--file "${backup_file}" \
|
||||
--name "${DB_FILENAME}" \
|
||||
--overwrite \
|
||||
|| fatal "Azure Blob upload failed"
|
||||
ok "Uploaded to Azure Blob Storage"
|
||||
|
||||
else
|
||||
warn "Unknown upload destination scheme: ${UPLOAD_DEST}"
|
||||
warn "Supported: s3://, gs://, az://"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${BLUE}║ Claude Code Agent Monitor – DB Backup ║${NC}"
|
||||
echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
info "Configuration:"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||||
echo -e " ${BOLD}Output:${NC} ${OUTPUT_DIR}"
|
||||
[[ -n "$UPLOAD_DEST" ]] && echo -e " ${BOLD}Upload:${NC} ${UPLOAD_DEST}"
|
||||
echo ""
|
||||
|
||||
find_pod
|
||||
create_backup
|
||||
validate_backup
|
||||
compress_backup
|
||||
upload_backup
|
||||
|
||||
echo ""
|
||||
ok "${BOLD}Backup complete!${NC}"
|
||||
echo -e " ${BOLD}File:${NC} ${OUTPUT_DIR}/${DB_FILENAME}"
|
||||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+396
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# db-restore.sh – SQLite database restore for Claude Code Agent Monitor
|
||||
#
|
||||
# Usage:
|
||||
# ./db-restore.sh --env production --input ./backups/agent-monitor_production_20240101_120000.db
|
||||
# ./db-restore.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly APP_NAME="agent-monitor"
|
||||
readonly APP_PORT=4820
|
||||
readonly DB_PATH_IN_CONTAINER="/app/data"
|
||||
|
||||
# ── 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; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
ENVIRONMENT=""
|
||||
INPUT_FILE=""
|
||||
NAMESPACE=""
|
||||
SKIP_HEALTH_CHECK=false
|
||||
FORCE=false
|
||||
BACKUP_BEFORE_RESTORE=true
|
||||
|
||||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${BOLD}Usage:${NC}
|
||||
$(basename "$0") --env <environment> --input <backup-file> [options]
|
||||
|
||||
${BOLD}Required:${NC}
|
||||
--env, -e Environment: dev, staging, production
|
||||
--input, -i Path to backup file (.db or .db.gz)
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||||
--no-backup Skip backing up current DB before restore
|
||||
--skip-health Skip post-restore health check
|
||||
--force Skip confirmation prompt
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --env production --input ./backups/agent-monitor_production_20240101_120000.db
|
||||
$(basename "$0") --env staging --input ./backups/backup.db.gz --force
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
[[ $# -eq 0 ]] && usage
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env|-e) ENVIRONMENT="$2"; shift 2 ;;
|
||||
--input|-i) INPUT_FILE="$2"; shift 2 ;;
|
||||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||||
--no-backup) BACKUP_BEFORE_RESTORE=false; shift ;;
|
||||
--skip-health) SKIP_HEALTH_CHECK=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) fatal "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||||
[[ -z "$INPUT_FILE" ]] && fatal "Missing required argument: --input"
|
||||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||||
|
||||
# Validate input file exists
|
||||
[[ -f "$INPUT_FILE" ]] || fatal "Input file not found: ${INPUT_FILE}"
|
||||
}
|
||||
|
||||
# ── Validate backup file ───────────────────────────────────────────────────
|
||||
validate_input() {
|
||||
info "Validating input file: ${INPUT_FILE}"
|
||||
|
||||
local restore_file="${INPUT_FILE}"
|
||||
|
||||
# Decompress if needed
|
||||
if [[ "$INPUT_FILE" == *.gz ]]; then
|
||||
info "Decompressing gzipped backup..."
|
||||
restore_file="${INPUT_FILE%.gz}"
|
||||
if [[ -f "$restore_file" ]]; then
|
||||
warn "Decompressed file already exists: ${restore_file}"
|
||||
else
|
||||
gzip -dk "${INPUT_FILE}" || fatal "Failed to decompress ${INPUT_FILE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
RESTORE_FILE="$restore_file"
|
||||
|
||||
# Validate with sqlite3 if available
|
||||
if command -v sqlite3 &>/dev/null; then
|
||||
local integrity
|
||||
integrity=$(sqlite3 "${RESTORE_FILE}" "PRAGMA integrity_check;" 2>/dev/null || echo "error")
|
||||
if [[ "$integrity" == "ok" ]]; then
|
||||
ok "SQLite integrity check passed"
|
||||
else
|
||||
fatal "Input file failed integrity check: ${integrity}"
|
||||
fi
|
||||
|
||||
local table_count
|
||||
table_count=$(sqlite3 "${RESTORE_FILE}" "SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo "?")
|
||||
info "Backup contains ${table_count} tables"
|
||||
else
|
||||
warn "sqlite3 not available – skipping integrity check"
|
||||
# Basic file header check
|
||||
local header
|
||||
header=$(head -c 16 "${RESTORE_FILE}" | strings 2>/dev/null || echo "")
|
||||
if echo "$header" | grep -q "SQLite format"; then
|
||||
ok "File appears to be a valid SQLite database"
|
||||
else
|
||||
fatal "File does not appear to be a SQLite database"
|
||||
fi
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(du -sh "${RESTORE_FILE}" 2>/dev/null | awk '{print $1}')
|
||||
info "Restore file size: ${file_size}"
|
||||
}
|
||||
|
||||
# ── Safety confirmation ─────────────────────────────────────────────────────
|
||||
confirm_restore() {
|
||||
if [[ "$FORCE" == true ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
warn "${BOLD}⚠ DATABASE RESTORE WARNING ⚠${NC}"
|
||||
echo ""
|
||||
echo -e " This will ${RED}${BOLD}REPLACE${NC} the current database in ${BOLD}${ENVIRONMENT}${NC}"
|
||||
echo -e " with the contents of: ${INPUT_FILE}"
|
||||
echo ""
|
||||
echo -e " The deployment will be ${BOLD}scaled down${NC} during restore."
|
||||
echo ""
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
echo -e " ${RED}${BOLD}THIS IS A PRODUCTION ENVIRONMENT!${NC}"
|
||||
echo ""
|
||||
read -r -p "$(echo -e "${YELLOW}Type the environment name to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "$ENVIRONMENT" ]] || fatal "Restore cancelled. You typed '${confirm}', expected '${ENVIRONMENT}'."
|
||||
else
|
||||
read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "yes" ]] || fatal "Restore cancelled."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Get deployment info ─────────────────────────────────────────────────────
|
||||
get_deployment_info() {
|
||||
info "Getting deployment info..."
|
||||
|
||||
DEPLOYMENT_NAME=$(kubectl get deployment -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$DEPLOYMENT_NAME" ]]; then
|
||||
fatal "No deployment found for ${APP_NAME} in ${NAMESPACE}"
|
||||
fi
|
||||
|
||||
ORIGINAL_REPLICAS=$(kubectl get deployment "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" \
|
||||
-o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1")
|
||||
|
||||
info "Deployment: ${DEPLOYMENT_NAME} (${ORIGINAL_REPLICAS} replicas)"
|
||||
}
|
||||
|
||||
# ── Backup current DB before restore ───────────────────────────────────────
|
||||
backup_current() {
|
||||
if [[ "$BACKUP_BEFORE_RESTORE" == false ]]; then
|
||||
info "Skipping pre-restore backup (--no-backup)"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Backing up current database before restore..."
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/db-backup.sh" ]]; then
|
||||
local backup_dir="${SCRIPT_DIR}/../../data/pre-restore-backups"
|
||||
"${SCRIPT_DIR}/db-backup.sh" \
|
||||
--env "${ENVIRONMENT}" \
|
||||
--output "${backup_dir}" \
|
||||
--namespace "${NAMESPACE}" \
|
||||
--no-compress \
|
||||
&& ok "Pre-restore backup created in ${backup_dir}" \
|
||||
|| warn "Pre-restore backup failed – proceeding anyway"
|
||||
else
|
||||
warn "db-backup.sh not found – skipping pre-restore backup"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Scale down deployment ───────────────────────────────────────────────────
|
||||
scale_down() {
|
||||
info "Scaling down deployment to 0 replicas..."
|
||||
|
||||
kubectl scale deployment "${DEPLOYMENT_NAME}" \
|
||||
--replicas=0 \
|
||||
-n "${NAMESPACE}" \
|
||||
|| fatal "Failed to scale down deployment"
|
||||
|
||||
# Wait for all pods to terminate
|
||||
info "Waiting for pods to terminate..."
|
||||
local wait_count=0
|
||||
while [[ $wait_count -lt 60 ]]; do
|
||||
local running
|
||||
running=$(kubectl get pods -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
--field-selector=status.phase=Running \
|
||||
--no-headers 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$running" == "0" ]]; then
|
||||
ok "All pods terminated"
|
||||
return
|
||||
fi
|
||||
|
||||
wait_count=$((wait_count + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
warn "Pods did not terminate within timeout"
|
||||
}
|
||||
|
||||
# ── Restore database ───────────────────────────────────────────────────────
|
||||
restore_database() {
|
||||
info "Restoring database..."
|
||||
|
||||
# We need a temporary pod to access the PVC
|
||||
# Create a helper pod that mounts the PVC
|
||||
local helper_pod="${APP_NAME}-db-restore-helper"
|
||||
|
||||
# Get PVC name
|
||||
local pvc_name
|
||||
pvc_name=$(kubectl get pvc -n "${NAMESPACE}" \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "agent-monitor-data")
|
||||
|
||||
info "Creating helper pod to access PVC: ${pvc_name}"
|
||||
|
||||
kubectl apply -n "${NAMESPACE}" -f - <<YAML
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: ${helper_pod}
|
||||
labels:
|
||||
app: db-restore-helper
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: helper
|
||||
image: alpine:3.19
|
||||
command: ["sleep", "3600"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: ${pvc_name}
|
||||
YAML
|
||||
|
||||
# Wait for helper pod to be ready
|
||||
info "Waiting for helper pod..."
|
||||
if ! kubectl wait --for=condition=ready "pod/${helper_pod}" -n "${NAMESPACE}" --timeout=120s; then
|
||||
kubectl delete pod "${helper_pod}" -n "${NAMESPACE}" --ignore-not-found=true
|
||||
fatal "Helper pod did not become ready"
|
||||
fi
|
||||
|
||||
# Backup existing DB in the PVC
|
||||
info "Moving existing database to .bak..."
|
||||
kubectl exec "${helper_pod}" -n "${NAMESPACE}" -- \
|
||||
sh -c "[ -f /data/dashboard.db ] && mv /data/dashboard.db /data/dashboard.db.bak || true"
|
||||
kubectl exec "${helper_pod}" -n "${NAMESPACE}" -- \
|
||||
sh -c "rm -f /data/dashboard.db-wal /data/dashboard.db-shm"
|
||||
|
||||
# Copy new database to pod, then to PVC path
|
||||
info "Uploading restore file..."
|
||||
kubectl cp "${RESTORE_FILE}" "${NAMESPACE}/${helper_pod}:/data/dashboard.db" \
|
||||
|| { kubectl delete pod "${helper_pod}" -n "${NAMESPACE}" --ignore-not-found=true; fatal "Failed to copy restore file"; }
|
||||
|
||||
# Verify copied file
|
||||
kubectl exec "${helper_pod}" -n "${NAMESPACE}" -- ls -la /data/dashboard.db
|
||||
|
||||
# Cleanup helper pod
|
||||
info "Removing helper pod..."
|
||||
kubectl delete pod "${helper_pod}" -n "${NAMESPACE}" --ignore-not-found=true --wait=false
|
||||
|
||||
ok "Database file restored"
|
||||
}
|
||||
|
||||
# ── Scale up deployment ─────────────────────────────────────────────────────
|
||||
scale_up() {
|
||||
info "Scaling deployment back to ${ORIGINAL_REPLICAS} replicas..."
|
||||
|
||||
kubectl scale deployment "${DEPLOYMENT_NAME}" \
|
||||
--replicas="${ORIGINAL_REPLICAS}" \
|
||||
-n "${NAMESPACE}" \
|
||||
|| fatal "Failed to scale up deployment"
|
||||
|
||||
# Wait for rollout
|
||||
info "Waiting for pods to start..."
|
||||
if ! kubectl rollout status "deployment/${DEPLOYMENT_NAME}" -n "${NAMESPACE}" --timeout=300s; then
|
||||
fatal "Deployment did not stabilize after restore!"
|
||||
fi
|
||||
|
||||
ok "Deployment scaled up successfully"
|
||||
}
|
||||
|
||||
# ── Post-restore health check ──────────────────────────────────────────────
|
||||
run_health_check() {
|
||||
if [[ "$SKIP_HEALTH_CHECK" == true ]]; then
|
||||
info "Skipping health check"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Running post-restore health check..."
|
||||
|
||||
# Wait for pods to be ready
|
||||
if ! kubectl wait --for=condition=ready pod \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
-n "${NAMESPACE}" --timeout=120s 2>/dev/null; then
|
||||
fatal "Pods did not become ready after restore!"
|
||||
fi
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then
|
||||
local local_port=14823
|
||||
kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" &
|
||||
local pf_pid=$!
|
||||
sleep 3
|
||||
|
||||
if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 10 --interval 3; then
|
||||
ok "Post-restore health check passed"
|
||||
else
|
||||
err "Post-restore health check failed!"
|
||||
warn "The application may need manual investigation"
|
||||
fi
|
||||
|
||||
kill "$pf_pid" 2>/dev/null || true
|
||||
else
|
||||
ok "Pods are ready"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${YELLOW}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${YELLOW}║ Claude Code Agent Monitor – DB Restore ║${NC}"
|
||||
echo -e "${BOLD}${YELLOW}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
validate_input
|
||||
confirm_restore
|
||||
get_deployment_info
|
||||
backup_current
|
||||
scale_down
|
||||
restore_database
|
||||
scale_up
|
||||
run_health_check
|
||||
|
||||
echo ""
|
||||
ok "${BOLD}Database restore complete!${NC}"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Source:${NC} ${INPUT_FILE}"
|
||||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+572
@@ -0,0 +1,572 @@
|
||||
#!/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 "$@"
|
||||
Executable
+342
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# health-check.sh – Comprehensive health check for Claude Code Agent Monitor
|
||||
#
|
||||
# Usage:
|
||||
# ./health-check.sh --url http://localhost:4820
|
||||
# ./health-check.sh --url http://host:port --retries 30 --interval 5
|
||||
# ./health-check.sh --url http://host:port --json
|
||||
# ./health-check.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
# ── 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() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; }
|
||||
info() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}ℹ${NC} $*"; }
|
||||
ok() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}✔${NC} $*"; }
|
||||
warn() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}⚠${NC} $*" >&2; }
|
||||
err() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}✖${NC} $*" >&2; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
BASE_URL=""
|
||||
RETRIES=30
|
||||
INTERVAL=5
|
||||
TIMEOUT=5
|
||||
RESPONSE_THRESHOLD=2000 # milliseconds
|
||||
JSON_OUTPUT=false
|
||||
CHECK_WEBSOCKET=true
|
||||
HEALTH_PATH="/api/health"
|
||||
|
||||
# ── Usage ───────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<HELP
|
||||
${BOLD}Usage:${NC}
|
||||
$(basename "$0") --url <base-url> [options]
|
||||
|
||||
${BOLD}Required:${NC}
|
||||
--url, -u Base URL (e.g., http://localhost:4820)
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--retries, -r Max retry attempts (default: 30)
|
||||
--interval, -i Seconds between retries (default: 5)
|
||||
--timeout HTTP request timeout in seconds (default: 5)
|
||||
--threshold Max response time in ms (default: 2000)
|
||||
--path Health endpoint path (default: /api/health)
|
||||
--no-websocket Skip WebSocket connectivity check
|
||||
--json Output results as JSON
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Exit codes:${NC}
|
||||
0 All checks passed
|
||||
1 One or more checks failed
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --url http://localhost:4820
|
||||
$(basename "$0") --url https://monitor.example.com --retries 10 --json
|
||||
$(basename "$0") --url http://10.0.1.5:4820 --threshold 500 --no-websocket
|
||||
|
||||
HELP
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Argument parsing ────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
[[ $# -eq 0 ]] && usage
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--url|-u) BASE_URL="$2"; shift 2 ;;
|
||||
--retries|-r) RETRIES="$2"; shift 2 ;;
|
||||
--interval|-i) INTERVAL="$2"; shift 2 ;;
|
||||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||||
--threshold) RESPONSE_THRESHOLD="$2"; shift 2 ;;
|
||||
--path) HEALTH_PATH="$2"; shift 2 ;;
|
||||
--no-websocket) CHECK_WEBSOCKET=false; shift ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$BASE_URL" ]] && { echo "Missing required argument: --url" >&2; exit 1; }
|
||||
|
||||
# Strip trailing slash
|
||||
BASE_URL="${BASE_URL%/}"
|
||||
}
|
||||
|
||||
# ── HTTP health check ──────────────────────────────────────────────────────
|
||||
check_http_health() {
|
||||
local url="${BASE_URL}${HEALTH_PATH}"
|
||||
local attempt=0
|
||||
local http_ok=false
|
||||
local status_code=""
|
||||
local response_body=""
|
||||
local response_time_ms=0
|
||||
|
||||
info "Checking HTTP health: ${url}"
|
||||
|
||||
while [[ $attempt -lt $RETRIES ]]; do
|
||||
attempt=$((attempt + 1))
|
||||
|
||||
# Measure response time and capture output
|
||||
local start_ns
|
||||
start_ns=$(date +%s%N 2>/dev/null || echo "0")
|
||||
|
||||
local http_response
|
||||
http_response=$(curl -sf \
|
||||
--max-time "${TIMEOUT}" \
|
||||
--write-out "\n%{http_code}\n%{time_total}" \
|
||||
"${url}" 2>/dev/null) || true
|
||||
|
||||
local end_ns
|
||||
end_ns=$(date +%s%N 2>/dev/null || echo "0")
|
||||
|
||||
if [[ -n "$http_response" ]]; then
|
||||
response_body=$(echo "$http_response" | head -n -2)
|
||||
status_code=$(echo "$http_response" | tail -2 | head -1)
|
||||
local time_total
|
||||
time_total=$(echo "$http_response" | tail -1)
|
||||
# Convert seconds to milliseconds
|
||||
response_time_ms=$(echo "$time_total" | awk '{printf "%.0f", $1 * 1000}' 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$status_code" == "200" ]] && echo "$response_body" | grep -q '"status":"ok"'; then
|
||||
http_ok=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $attempt -lt $RETRIES ]]; then
|
||||
info "Attempt ${attempt}/${RETRIES} – waiting ${INTERVAL}s..."
|
||||
sleep "${INTERVAL}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Results
|
||||
HTTP_OK="$http_ok"
|
||||
HTTP_STATUS="$status_code"
|
||||
HTTP_BODY="$response_body"
|
||||
HTTP_RESPONSE_TIME_MS="$response_time_ms"
|
||||
HTTP_ATTEMPTS="$attempt"
|
||||
|
||||
if [[ "$http_ok" == true ]]; then
|
||||
ok "HTTP health check passed (${response_time_ms}ms, ${attempt} attempt(s))"
|
||||
else
|
||||
err "HTTP health check failed after ${attempt} attempts"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Response time check ────────────────────────────────────────────────────
|
||||
check_response_time() {
|
||||
if [[ "$HTTP_OK" != true ]]; then
|
||||
LATENCY_OK=false
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$HTTP_RESPONSE_TIME_MS" -le "$RESPONSE_THRESHOLD" ]]; then
|
||||
LATENCY_OK=true
|
||||
ok "Response time ${HTTP_RESPONSE_TIME_MS}ms within threshold (${RESPONSE_THRESHOLD}ms)"
|
||||
else
|
||||
LATENCY_OK=false
|
||||
warn "Response time ${HTTP_RESPONSE_TIME_MS}ms exceeds threshold (${RESPONSE_THRESHOLD}ms)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── WebSocket connectivity check ───────────────────────────────────────────
|
||||
check_websocket() {
|
||||
WS_OK=false
|
||||
|
||||
if [[ "$CHECK_WEBSOCKET" == false ]]; then
|
||||
info "WebSocket check skipped"
|
||||
WS_OK=true # treat as pass when skipped
|
||||
return
|
||||
fi
|
||||
|
||||
# Construct WebSocket URL
|
||||
local ws_url="${BASE_URL}"
|
||||
ws_url="${ws_url/http:/ws:}"
|
||||
ws_url="${ws_url/https:/wss:}"
|
||||
ws_url="${ws_url}/ws"
|
||||
|
||||
info "Checking WebSocket: ${ws_url}"
|
||||
|
||||
# Check if we have a WebSocket testing tool
|
||||
if command -v websocat &>/dev/null; then
|
||||
if echo "" | websocat --one-message -t "${ws_url}" 2>/dev/null; then
|
||||
WS_OK=true
|
||||
ok "WebSocket connection succeeded (websocat)"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: use curl with upgrade headers to test the handshake
|
||||
local ws_status
|
||||
ws_status=$(curl -sf \
|
||||
--max-time "${TIMEOUT}" \
|
||||
-o /dev/null \
|
||||
-w "%{http_code}" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" \
|
||||
"${BASE_URL}/ws" 2>/dev/null) || ws_status="000"
|
||||
|
||||
# 101 = Switching Protocols (WebSocket upgrade success)
|
||||
# 400 = Bad Request (server recognized WS but rejected – still proves WS is available)
|
||||
case "$ws_status" in
|
||||
101)
|
||||
WS_OK=true
|
||||
ok "WebSocket handshake succeeded (HTTP 101)"
|
||||
;;
|
||||
400|426)
|
||||
WS_OK=true
|
||||
ok "WebSocket endpoint reachable (HTTP ${ws_status} – server recognized upgrade)"
|
||||
;;
|
||||
*)
|
||||
# Try Node.js one-liner as last resort
|
||||
if command -v node &>/dev/null; then
|
||||
local node_result
|
||||
node_result=$(node -e "
|
||||
const ws = new (require('ws'))('${ws_url}');
|
||||
const t = setTimeout(() => { process.stdout.write('timeout'); process.exit(1); }, 5000);
|
||||
ws.on('open', () => { clearTimeout(t); process.stdout.write('ok'); ws.close(); process.exit(0); });
|
||||
ws.on('error', (e) => { clearTimeout(t); process.stdout.write('error:' + e.message); process.exit(1); });
|
||||
" 2>/dev/null) || node_result="error"
|
||||
|
||||
if [[ "$node_result" == "ok" ]]; then
|
||||
WS_OK=true
|
||||
ok "WebSocket connection verified (node)"
|
||||
else
|
||||
WS_OK=false
|
||||
warn "WebSocket check failed: ${node_result}"
|
||||
fi
|
||||
else
|
||||
warn "WebSocket check inconclusive (no ws testing tool available, HTTP status: ${ws_status})"
|
||||
WS_OK=true # Don't fail the whole check for this
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Output results ──────────────────────────────────────────────────────────
|
||||
output_results() {
|
||||
local overall_healthy=true
|
||||
[[ "$HTTP_OK" != true ]] && overall_healthy=false
|
||||
[[ "$LATENCY_OK" != true ]] && overall_healthy=false
|
||||
[[ "$WS_OK" != true ]] && overall_healthy=false
|
||||
|
||||
if [[ "$JSON_OUTPUT" == true ]]; then
|
||||
cat <<JSON
|
||||
{
|
||||
"healthy": ${overall_healthy},
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"url": "${BASE_URL}",
|
||||
"checks": {
|
||||
"http": {
|
||||
"passed": ${HTTP_OK},
|
||||
"status_code": "${HTTP_STATUS:-null}",
|
||||
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
|
||||
"attempts": ${HTTP_ATTEMPTS:-0},
|
||||
"body": $(echo "${HTTP_BODY:-null}" | head -c 500 | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '"null"')
|
||||
},
|
||||
"latency": {
|
||||
"passed": ${LATENCY_OK},
|
||||
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
|
||||
"threshold_ms": ${RESPONSE_THRESHOLD}
|
||||
},
|
||||
"websocket": {
|
||||
"passed": ${WS_OK},
|
||||
"checked": ${CHECK_WEBSOCKET}
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
else
|
||||
echo ""
|
||||
echo -e "${BOLD}Health Check Summary${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local http_icon=$([[ "$HTTP_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}")
|
||||
local latency_icon=$([[ "$LATENCY_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${YELLOW}⚠${NC}")
|
||||
local ws_icon=$([[ "$WS_OK" == true ]] && echo "${GREEN}✔${NC}" || echo "${RED}✖${NC}")
|
||||
|
||||
echo -e " ${http_icon} HTTP /api/health (${HTTP_STATUS:-???}, ${HTTP_RESPONSE_TIME_MS:-?}ms, ${HTTP_ATTEMPTS:-?} attempts)"
|
||||
echo -e " ${latency_icon} Response time (${HTTP_RESPONSE_TIME_MS:-?}ms / ${RESPONSE_THRESHOLD}ms threshold)"
|
||||
echo -e " ${ws_icon} WebSocket (checked: ${CHECK_WEBSOCKET})"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ "$overall_healthy" == true ]]; then
|
||||
echo -e " ${GREEN}${BOLD}Overall: HEALTHY${NC}"
|
||||
else
|
||||
echo -e " ${RED}${BOLD}Overall: UNHEALTHY${NC}"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ "$overall_healthy" == true ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# Initialize result variables
|
||||
HTTP_OK=false
|
||||
HTTP_STATUS=""
|
||||
HTTP_BODY=""
|
||||
HTTP_RESPONSE_TIME_MS=0
|
||||
HTTP_ATTEMPTS=0
|
||||
LATENCY_OK=false
|
||||
WS_OK=false
|
||||
|
||||
[[ "$JSON_OUTPUT" != true ]] && {
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${GREEN}║ Claude Code Agent Monitor – Health Check ║${NC}"
|
||||
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
info "Target: ${BASE_URL}"
|
||||
info "Config: retries=${RETRIES}, interval=${INTERVAL}s, threshold=${RESPONSE_THRESHOLD}ms"
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_http_health
|
||||
check_response_time
|
||||
check_websocket
|
||||
|
||||
output_results
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# rollback.sh – Rollback deployments for Claude Code Agent Monitor
|
||||
#
|
||||
# Usage:
|
||||
# ./rollback.sh --env production --method helm --revision 5
|
||||
# ./rollback.sh --env staging --method kustomize
|
||||
# ./rollback.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly APP_NAME="agent-monitor"
|
||||
readonly APP_PORT=4820
|
||||
|
||||
# ── 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; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
ENVIRONMENT=""
|
||||
METHOD=""
|
||||
REVISION=""
|
||||
NAMESPACE=""
|
||||
HELM_RELEASE="${APP_NAME}"
|
||||
SKIP_HEALTH_CHECK=false
|
||||
|
||||
# ── 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 Method: helm, kustomize
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--revision, -r Helm revision or rollout history number to roll back to
|
||||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||||
--release Helm release name (default: ${APP_NAME})
|
||||
--skip-health Skip post-rollback health check
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --env production --method helm --revision 5
|
||||
$(basename "$0") --env staging --method kustomize
|
||||
$(basename "$0") --env production --method helm # rolls back to previous
|
||||
|
||||
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 ;;
|
||||
--revision|-r) REVISION="$2"; shift 2 ;;
|
||||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||||
--release) HELM_RELEASE="$2"; shift 2 ;;
|
||||
--skip-health) SKIP_HEALTH_CHECK=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) fatal "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||||
[[ -z "$METHOD" ]] && fatal "Missing required argument: --method"
|
||||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||||
}
|
||||
|
||||
# ── Show release history ───────────────────────────────────────────────────
|
||||
show_history() {
|
||||
info "Release history:"
|
||||
case "$METHOD" in
|
||||
helm)
|
||||
helm history "${HELM_RELEASE}" -n "${NAMESPACE}" --max 10 2>/dev/null || warn "No history found"
|
||||
;;
|
||||
kustomize)
|
||||
kubectl rollout history "deployment/${APP_NAME}" -n "${NAMESPACE}" 2>/dev/null || warn "No history found"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Confirm rollback ───────────────────────────────────────────────────────
|
||||
confirm_rollback() {
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
local target_msg="previous revision"
|
||||
[[ -n "$REVISION" ]] && target_msg="revision ${REVISION}"
|
||||
|
||||
warn "Rolling back ${BOLD}PRODUCTION${NC} to ${target_msg}"
|
||||
read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "yes" ]] || fatal "Rollback cancelled."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helm rollback ───────────────────────────────────────────────────────────
|
||||
rollback_helm() {
|
||||
info "Rolling back Helm release '${HELM_RELEASE}' in namespace '${NAMESPACE}'..."
|
||||
|
||||
local rollback_args=(rollback "${HELM_RELEASE}")
|
||||
[[ -n "$REVISION" ]] && rollback_args+=("${REVISION}")
|
||||
rollback_args+=(-n "${NAMESPACE}" --wait --timeout 300s)
|
||||
|
||||
if ! helm "${rollback_args[@]}"; then
|
||||
fatal "Helm rollback failed! Manual intervention required."
|
||||
fi
|
||||
|
||||
ok "Helm rollback completed"
|
||||
|
||||
# Show current status
|
||||
info "Current release status:"
|
||||
helm status "${HELM_RELEASE}" -n "${NAMESPACE}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Kustomize rollback ─────────────────────────────────────────────────────
|
||||
rollback_kustomize() {
|
||||
info "Rolling back deployment '${APP_NAME}' in namespace '${NAMESPACE}'..."
|
||||
|
||||
local undo_args=(rollout undo "deployment/${APP_NAME}" -n "${NAMESPACE}")
|
||||
if [[ -n "$REVISION" ]]; then
|
||||
undo_args+=(--to-revision="${REVISION}")
|
||||
fi
|
||||
|
||||
if ! kubectl "${undo_args[@]}"; then
|
||||
fatal "Kubectl rollback failed! Manual intervention required."
|
||||
fi
|
||||
|
||||
# Wait for rollout
|
||||
info "Waiting for rollout to complete..."
|
||||
if ! kubectl rollout status "deployment/${APP_NAME}" -n "${NAMESPACE}" --timeout=300s; then
|
||||
fatal "Rollout did not complete in time!"
|
||||
fi
|
||||
|
||||
ok "Kustomize rollback completed"
|
||||
}
|
||||
|
||||
# ── Post-rollback health check ─────────────────────────────────────────────
|
||||
run_health_check() {
|
||||
if [[ "$SKIP_HEALTH_CHECK" == true ]]; then
|
||||
info "Skipping health check"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Running post-rollback health check..."
|
||||
|
||||
# Wait for pods to be ready
|
||||
if ! kubectl wait --for=condition=ready pod \
|
||||
-l "app.kubernetes.io/name=${APP_NAME}" \
|
||||
-n "${NAMESPACE}" --timeout=120s 2>/dev/null; then
|
||||
fatal "Pods did not become ready after rollback!"
|
||||
fi
|
||||
|
||||
# Use health-check.sh if available
|
||||
if [[ -x "${SCRIPT_DIR}/health-check.sh" ]]; then
|
||||
# Port forward for check
|
||||
local local_port=14820
|
||||
kubectl port-forward "svc/${APP_NAME}" "${local_port}:${APP_PORT}" -n "${NAMESPACE}" &
|
||||
local pf_pid=$!
|
||||
sleep 3
|
||||
|
||||
if "${SCRIPT_DIR}/health-check.sh" --url "http://localhost:${local_port}" --retries 10 --interval 3; then
|
||||
ok "Health check passed after rollback"
|
||||
else
|
||||
err "Health check failed after rollback!"
|
||||
fi
|
||||
|
||||
kill "$pf_pid" 2>/dev/null || true
|
||||
else
|
||||
ok "Pods are ready"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${YELLOW}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${YELLOW}║ Claude Code Agent Monitor – Rollback ║${NC}"
|
||||
echo -e "${BOLD}${YELLOW}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
show_history
|
||||
confirm_rollback
|
||||
|
||||
case "$METHOD" in
|
||||
helm) rollback_helm ;;
|
||||
kustomize) rollback_kustomize ;;
|
||||
*) fatal "Rollback not supported for method: ${METHOD}" ;;
|
||||
esac
|
||||
|
||||
run_health_check
|
||||
|
||||
echo ""
|
||||
ok "${BOLD}Rollback complete!${NC}"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Method:${NC} ${METHOD}"
|
||||
echo -e " ${BOLD}Revision:${NC} ${REVISION:-previous}"
|
||||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+335
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# teardown.sh – Clean removal of Claude Code Agent Monitor infrastructure
|
||||
#
|
||||
# Usage:
|
||||
# ./teardown.sh --env dev --method helm
|
||||
# ./teardown.sh --env production --method terraform
|
||||
# ./teardown.sh --help
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
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"
|
||||
|
||||
# ── 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; }
|
||||
|
||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||
ENVIRONMENT=""
|
||||
METHOD=""
|
||||
NAMESPACE=""
|
||||
HELM_RELEASE="${APP_NAME}"
|
||||
SKIP_BACKUP=false
|
||||
FORCE=false
|
||||
DELETE_NAMESPACE=false
|
||||
DELETE_PVC=false
|
||||
|
||||
# ── 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 Method: helm, kustomize, terraform
|
||||
|
||||
${BOLD}Options:${NC}
|
||||
--namespace, -n Kubernetes namespace (default: agent-monitor-<env>)
|
||||
--release Helm release name (default: ${APP_NAME})
|
||||
--delete-namespace Also delete the Kubernetes namespace
|
||||
--delete-pvc Also delete PersistentVolumeClaims (data loss!)
|
||||
--skip-backup Skip data backup before teardown
|
||||
--force Skip all confirmation prompts
|
||||
--help, -h Show this help message
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
$(basename "$0") --env dev --method helm
|
||||
$(basename "$0") --env staging --method kustomize --delete-namespace
|
||||
$(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 ;;
|
||||
--namespace|-n) NAMESPACE="$2"; shift 2 ;;
|
||||
--release) HELM_RELEASE="$2"; shift 2 ;;
|
||||
--delete-namespace) DELETE_NAMESPACE=true; shift ;;
|
||||
--delete-pvc) DELETE_PVC=true; shift ;;
|
||||
--skip-backup) SKIP_BACKUP=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) fatal "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$ENVIRONMENT" ]] && fatal "Missing required argument: --env"
|
||||
[[ -z "$METHOD" ]] && fatal "Missing required argument: --method"
|
||||
[[ -z "$NAMESPACE" ]] && NAMESPACE="agent-monitor-${ENVIRONMENT}"
|
||||
}
|
||||
|
||||
# ── Confirm teardown ───────────────────────────────────────────────────────
|
||||
confirm_teardown() {
|
||||
if [[ "$FORCE" == true ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${RED}${BOLD}╔══════════════════════════════════════════╗${NC}"
|
||||
echo -e " ${RED}${BOLD}║ ⚠ TEARDOWN WARNING ⚠ ║${NC}"
|
||||
echo -e " ${RED}${BOLD}╚══════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " This will ${RED}${BOLD}DESTROY${NC} the following resources:"
|
||||
echo -e " • Environment: ${BOLD}${ENVIRONMENT}${NC}"
|
||||
echo -e " • Method: ${BOLD}${METHOD}${NC}"
|
||||
echo -e " • Namespace: ${BOLD}${NAMESPACE}${NC}"
|
||||
[[ "$DELETE_NAMESPACE" == true ]] && echo -e " • ${RED}Namespace will be deleted${NC}"
|
||||
[[ "$DELETE_PVC" == true ]] && echo -e " • ${RED}PVCs will be deleted (DATA LOSS!)${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
echo -e " ${RED}${BOLD}THIS IS A PRODUCTION ENVIRONMENT!${NC}"
|
||||
echo ""
|
||||
read -r -p "$(echo -e "${RED}Type 'destroy ${ENVIRONMENT}' to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "destroy ${ENVIRONMENT}" ]] || fatal "Teardown cancelled."
|
||||
|
||||
# Second confirmation for production
|
||||
echo ""
|
||||
read -r -p "$(echo -e "${RED}Are you absolutely sure? Type 'YES' in caps:${NC} ")" confirm2
|
||||
[[ "$confirm2" == "YES" ]] || fatal "Teardown cancelled."
|
||||
else
|
||||
read -r -p "$(echo -e "${YELLOW}Type 'yes' to confirm:${NC} ")" confirm
|
||||
[[ "$confirm" == "yes" ]] || fatal "Teardown cancelled."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Pre-teardown backup ────────────────────────────────────────────────────
|
||||
backup_data() {
|
||||
if [[ "$SKIP_BACKUP" == true ]]; then
|
||||
info "Skipping pre-teardown backup (--skip-backup)"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Creating backup before teardown..."
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/db-backup.sh" ]]; then
|
||||
local backup_dir="${PROJECT_ROOT}/data/pre-teardown-backups"
|
||||
"${SCRIPT_DIR}/db-backup.sh" \
|
||||
--env "${ENVIRONMENT}" \
|
||||
--output "${backup_dir}" \
|
||||
--namespace "${NAMESPACE}" \
|
||||
&& ok "Pre-teardown backup created" \
|
||||
|| warn "Backup failed – continuing with teardown"
|
||||
else
|
||||
warn "db-backup.sh not found – skipping backup"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Show current resources ──────────────────────────────────────────────────
|
||||
show_resources() {
|
||||
info "Current resources in namespace '${NAMESPACE}':"
|
||||
echo ""
|
||||
|
||||
kubectl get all -n "${NAMESPACE}" 2>/dev/null || warn "Could not list resources"
|
||||
|
||||
if [[ "$METHOD" != "terraform" ]]; then
|
||||
echo ""
|
||||
info "PersistentVolumeClaims:"
|
||||
kubectl get pvc -n "${NAMESPACE}" 2>/dev/null || warn "No PVCs found"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Helm teardown ──────────────────────────────────────────────────────────
|
||||
teardown_helm() {
|
||||
info "Uninstalling Helm release '${HELM_RELEASE}'..."
|
||||
|
||||
if helm status "${HELM_RELEASE}" -n "${NAMESPACE}" &>/dev/null; then
|
||||
helm uninstall "${HELM_RELEASE}" -n "${NAMESPACE}" --wait --timeout 300s \
|
||||
|| fatal "Helm uninstall failed"
|
||||
ok "Helm release '${HELM_RELEASE}' uninstalled"
|
||||
else
|
||||
warn "Helm release '${HELM_RELEASE}' not found in namespace '${NAMESPACE}'"
|
||||
fi
|
||||
|
||||
# Also try uninstalling blue/green releases
|
||||
for color in blue green; do
|
||||
if helm status "${HELM_RELEASE}-${color}" -n "${NAMESPACE}" &>/dev/null; then
|
||||
info "Uninstalling ${color} slot release..."
|
||||
helm uninstall "${HELM_RELEASE}-${color}" -n "${NAMESPACE}" --wait --timeout 300s \
|
||||
&& ok "Release '${HELM_RELEASE}-${color}' uninstalled" \
|
||||
|| warn "Failed to uninstall ${color} release"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── Kustomize teardown ─────────────────────────────────────────────────────
|
||||
teardown_kustomize() {
|
||||
local overlay_dir="${DEPLOY_DIR}/kubernetes/overlays/${ENVIRONMENT}"
|
||||
|
||||
if [[ -d "$overlay_dir" ]]; then
|
||||
info "Deleting Kustomize resources..."
|
||||
kubectl delete -k "${overlay_dir}" --ignore-not-found=true --wait=true --timeout=300s \
|
||||
&& ok "Kustomize resources deleted" \
|
||||
|| warn "Some resources may not have been deleted"
|
||||
else
|
||||
warn "Kustomize overlay not found at ${overlay_dir}"
|
||||
info "Deleting resources by label..."
|
||||
kubectl delete all -l "app.kubernetes.io/name=${APP_NAME}" -n "${NAMESPACE}" --wait=true \
|
||||
|| warn "Could not delete resources by label"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Terraform teardown ─────────────────────────────────────────────────────
|
||||
teardown_terraform() {
|
||||
local tf_dir="${DEPLOY_DIR}/terraform"
|
||||
local env_vars_file="${tf_dir}/environments/${ENVIRONMENT}/terraform.tfvars"
|
||||
|
||||
info "Destroying Terraform-managed infrastructure..."
|
||||
|
||||
pushd "${tf_dir}" > /dev/null
|
||||
|
||||
terraform init -input=false
|
||||
|
||||
local destroy_args=(-input=false -auto-approve)
|
||||
if [[ -f "$env_vars_file" ]]; then
|
||||
destroy_args+=(-var-file="$env_vars_file")
|
||||
fi
|
||||
# Need to provide required variables that may not have defaults
|
||||
destroy_args+=(-var "environment=${ENVIRONMENT}")
|
||||
|
||||
if ! terraform destroy "${destroy_args[@]}"; then
|
||||
popd > /dev/null
|
||||
fatal "Terraform destroy failed! Review state manually."
|
||||
fi
|
||||
|
||||
popd > /dev/null
|
||||
ok "Terraform infrastructure destroyed"
|
||||
}
|
||||
|
||||
# ── Cleanup PVCs ────────────────────────────────────────────────────────────
|
||||
cleanup_pvcs() {
|
||||
if [[ "$DELETE_PVC" != true ]]; then
|
||||
local pvc_count
|
||||
pvc_count=$(kubectl get pvc -n "${NAMESPACE}" --no-headers 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$pvc_count" -gt 0 ]]; then
|
||||
warn "PersistentVolumeClaims still exist. Use --delete-pvc to remove them."
|
||||
kubectl get pvc -n "${NAMESPACE}" 2>/dev/null
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
info "Deleting PersistentVolumeClaims..."
|
||||
kubectl delete pvc --all -n "${NAMESPACE}" --wait=true \
|
||||
&& ok "PVCs deleted" \
|
||||
|| warn "Some PVCs could not be deleted"
|
||||
}
|
||||
|
||||
# ── Cleanup namespace ──────────────────────────────────────────────────────
|
||||
cleanup_namespace() {
|
||||
if [[ "$DELETE_NAMESPACE" != true ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$ENVIRONMENT" == "production" ]]; then
|
||||
warn "Refusing to delete production namespace automatically."
|
||||
warn "Delete manually: kubectl delete namespace ${NAMESPACE}"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Deleting namespace '${NAMESPACE}'..."
|
||||
kubectl delete namespace "${NAMESPACE}" --ignore-not-found=true --wait=true --timeout=120s \
|
||||
&& ok "Namespace '${NAMESPACE}' deleted" \
|
||||
|| warn "Namespace deletion may be stuck. Check: kubectl get namespace ${NAMESPACE}"
|
||||
}
|
||||
|
||||
# ── Verify teardown ────────────────────────────────────────────────────────
|
||||
verify_teardown() {
|
||||
info "Verifying teardown..."
|
||||
|
||||
if [[ "$METHOD" == "terraform" ]]; then
|
||||
ok "Terraform state should reflect no resources"
|
||||
return
|
||||
fi
|
||||
|
||||
local remaining
|
||||
remaining=$(kubectl get all -n "${NAMESPACE}" --no-headers 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$remaining" -eq 0 ]]; then
|
||||
ok "No resources remaining in namespace '${NAMESPACE}'"
|
||||
else
|
||||
warn "${remaining} resources still exist in namespace '${NAMESPACE}':"
|
||||
kubectl get all -n "${NAMESPACE}" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${RED}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${RED}║ Claude Code Agent Monitor – Teardown ║${NC}"
|
||||
echo -e "${BOLD}${RED}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
info "Teardown configuration:"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Method:${NC} ${METHOD}"
|
||||
echo -e " ${BOLD}Namespace:${NC} ${NAMESPACE}"
|
||||
echo ""
|
||||
|
||||
if [[ "$METHOD" != "terraform" ]]; then
|
||||
show_resources
|
||||
fi
|
||||
|
||||
confirm_teardown
|
||||
backup_data
|
||||
|
||||
case "$METHOD" in
|
||||
helm) teardown_helm ;;
|
||||
kustomize) teardown_kustomize ;;
|
||||
terraform) teardown_terraform ;;
|
||||
*) fatal "Invalid method: ${METHOD}" ;;
|
||||
esac
|
||||
|
||||
if [[ "$METHOD" != "terraform" ]]; then
|
||||
cleanup_pvcs
|
||||
cleanup_namespace
|
||||
verify_teardown
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "${BOLD}Teardown complete!${NC}"
|
||||
echo -e " ${BOLD}Environment:${NC} ${ENVIRONMENT}"
|
||||
echo -e " ${BOLD}Method:${NC} ${METHOD}"
|
||||
echo -e " ${BOLD}Timestamp:${NC} $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user