install: add scripts/ccp-deregister.sh + ship in tarball

Pairs with pangolin-teardown.sh. For instances that were phone-home
registered with a CCP, this script removes the CCP-side Instance row
during teardown so the slug is freed for re-registration.

Without it, tearing down a CCP-registered instance leaves a stale
Instance row in CCP's DB. The next phone-home-registration with the
same slug hits the unique-constraint violation we saw in marcelle
testing.

Matching: by agentUrl (default from .env CCP_AGENT_URL), --slug, or
--instance-id. Dry-run by default; --yes to execute. CCP admin token
via --token or CCP_ADMIN_TOKEN env var.

Added to build-release.sh whitelist so release tarballs include it
alongside pangolin-teardown.sh and validate-env.sh.

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-16 13:07:12 -06:00
parent c2f12aa2bf
commit ce8c5aaf1f
2 changed files with 151 additions and 1 deletions

View File

@ -122,7 +122,7 @@ for script in nocodb-init.sh gitea-init.sh mkdocs-entrypoint.sh \
backup.sh restore.sh \
upgrade.sh upgrade-check.sh upgrade-watcher.sh \
uninstall.sh test-deployment.sh \
validate-env.sh pangolin-teardown.sh; do
validate-env.sh pangolin-teardown.sh ccp-deregister.sh; do
if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then
cp "$PROJECT_DIR/scripts/$script" "$STAGE_DIR/scripts/"
fi

150
scripts/ccp-deregister.sh Executable file
View File

@ -0,0 +1,150 @@
#!/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}"