#!/bin/bash # ============================================================================= # ccp-deregister.sh — Remove this host's Instance from the CCP # # Pairs with scripts/pangolin-teardown.sh. Use when wiping a test instance # that was phone-home-registered with a CCP. Without this, the CCP retains # a stale Instance row after the underlying stack is gone and will block # re-registration of the same slug (CCP's slug-collision behaviour). # # Credentials are read from .env (CCP_URL, CCP_AGENT_URL) and a CCP admin # token, unless overridden by flags. # # Usage: # ./scripts/ccp-deregister.sh --token CCP_ADMIN_TOKEN # ./scripts/ccp-deregister.sh --token T --yes # execute (default: dry-run) # ./scripts/ccp-deregister.sh --ccp-url URL --agent-url URL --token T --yes # # Matching: by default, matches Instances whose agentUrl equals this host's # CCP_AGENT_URL. Pass --slug SLUG or --instance-id ID for explicit targeting. # # Exit codes: 0 success, 1 error, 2 nothing matched # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" ENV_FILE="${PROJECT_DIR}/.env" RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' CCP_URL=""; CCP_AGENT_URL=""; TOKEN=""; SLUG=""; INSTANCE_ID="" CONFIRM=false usage() { sed -n '2,22p' "$0" | sed 's/^# \?//' exit 0 } while [[ $# -gt 0 ]]; do case "$1" in --yes|-y) CONFIRM=true; shift ;; --ccp-url) CCP_URL="$2"; shift 2 ;; --agent-url) CCP_AGENT_URL="$2"; shift 2 ;; --token) TOKEN="$2"; shift 2 ;; --slug) SLUG="$2"; shift 2 ;; --instance-id) INSTANCE_ID="$2"; shift 2 ;; -h|--help) usage ;; *) echo "Unknown flag: $1"; exit 1 ;; esac done # Load missing fields from .env if [[ -z "$CCP_URL" || -z "$CCP_AGENT_URL" ]]; then if [[ -f "$ENV_FILE" ]]; then CCP_URL="${CCP_URL:-$(grep -E '^CCP_URL=' "$ENV_FILE" | head -1 | cut -d= -f2-)}" CCP_AGENT_URL="${CCP_AGENT_URL:-$(grep -E '^CCP_AGENT_URL=' "$ENV_FILE" | head -1 | cut -d= -f2-)}" fi fi # Token may also come from env var TOKEN="${TOKEN:-${CCP_ADMIN_TOKEN:-}}" if [[ -z "$CCP_URL" ]]; then echo -e "${RED}ERROR:${NC} CCP_URL not set. Pass --ccp-url or set in .env." exit 1 fi if [[ -z "$TOKEN" ]]; then echo -e "${RED}ERROR:${NC} No CCP admin token. Pass --token or set CCP_ADMIN_TOKEN env var." echo " Obtain via: curl -X POST \$CCP_URL/api/auth/login -d @login.json" exit 1 fi if ! command -v python3 >/dev/null 2>&1; then echo -e "${RED}ERROR:${NC} python3 is required for JSON parsing" exit 1 fi echo -e "${BOLD}CCP deregister${NC}" echo " CCP: $CCP_URL" [[ -n "$CCP_AGENT_URL" ]] && echo " Agent URL: $CCP_AGENT_URL (match target)" [[ -n "$SLUG" ]] && echo " Slug: $SLUG (match target)" [[ -n "$INSTANCE_ID" ]] && echo " Instance ID: $INSTANCE_ID (explicit)" [[ "$CONFIRM" == "false" ]] && echo -e " ${YELLOW}Mode: DRY RUN${NC} (pass --yes to execute)" echo "" # Fetch instances RAW=$(curl -sf -H "Authorization: Bearer $TOKEN" "$CCP_URL/api/instances" 2>&1 || true) if [[ -z "$RAW" ]]; then echo -e "${RED}ERROR:${NC} Could not fetch /api/instances (check token + URL)" exit 1 fi # Build match list — the API may wrap instances in {data: [...]} or return raw []. # Also: the /api/instances list omits pangolinEndpoint for brevity, so match fields are scoped to slug/agentUrl/id. MATCHES=$(echo "$RAW" | python3 -c " import sys, json d = json.load(sys.stdin) items = d.get('data', d) if isinstance(d, dict) else d target_slug = '${SLUG}' or None target_id = '${INSTANCE_ID}' or None target_agent = '${CCP_AGENT_URL}' or None for inst in items: if target_id: if inst.get('id') == target_id: print(f\"{inst['id']}\t{inst.get('slug','?')}\t{inst.get('agentUrl','?')}\") elif target_slug: if inst.get('slug') == target_slug: print(f\"{inst['id']}\t{inst.get('slug','?')}\t{inst.get('agentUrl','?')}\") elif target_agent: if inst.get('agentUrl') == target_agent: print(f\"{inst['id']}\t{inst.get('slug','?')}\t{inst.get('agentUrl','?')}\") ") MATCH_COUNT=$(echo -n "$MATCHES" | grep -c . || true) echo -e "${CYAN}Matches: $MATCH_COUNT${NC}" if [[ "$MATCH_COUNT" -eq 0 ]]; then echo " (no instances matched the criteria)" exit 2 fi echo "$MATCHES" | awk -F'\t' '{printf " - [%s] slug=%s agentUrl=%s\n", $1, $2, $3}' if [[ "$CONFIRM" == "false" ]]; then echo "" echo -e "${YELLOW}Dry run complete.${NC} Re-run with --yes to actually delete." exit 0 fi echo "" echo -e "${BOLD}Deleting...${NC}" FAILURES=0 while IFS=$'\t' read -r iid slug aurl; do [[ -z "$iid" ]] && continue code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ "$CCP_URL/api/instances/$iid" \ -H "Authorization: Bearer $TOKEN") if [[ "$code" == "200" || "$code" == "204" ]]; then echo -e " ${GREEN}OK${NC} instance $iid ($slug) deleted" else echo -e " ${RED}FAIL${NC} instance $iid ($slug) HTTP $code" FAILURES=$((FAILURES + 1)) fi done <<< "$MATCHES" if [[ $FAILURES -gt 0 ]]; then echo -e "${YELLOW}Completed with $FAILURES failure(s).${NC}" exit 2 fi echo -e "${GREEN}Deregister complete.${NC}"