#!/usr/bin/env bash # delete-service: reverse new-service. gitops entry (ArgoCD prunes), namespace # (and with it CNPG PVCs), Forgejo repo, container package versions, local clone. # delete-service orders-web --yes && delete-service orders --yes set -euo pipefail # shellcheck source=../lib/common.sh source "$(dirname "$(readlink -f "$0")")/../lib/common.sh" usage() { cat <<'EOF' usage: delete-service NAME [--yes] [--dry-run] [--timeout SECONDS] --yes don't ask for confirmation --dry-run print every API call, git push and kubectl mutation instead --timeout per-wait timeout (default 600) EOF } NAME='' YES=0 TIMEOUT=600 while (($#)); do case $1 in --yes | -y) YES=1; shift ;; --dry-run) DRY_RUN=1; shift ;; --timeout) TIMEOUT=${2:?--timeout needs a value}; shift 2 ;; -h | --help) usage; exit 0 ;; -*) usage >&2; die "unknown option: $1" ;; *) [[ -z $NAME ]] || die "unexpected argument: $1"; NAME=$1; shift ;; esac done [[ -n $NAME ]] || { usage >&2; die "NAME is required"; } validate_name "$NAME" is_dry && log "DRY RUN: nothing below is executed except read-only checks" load_config gitops_sync # ------------------------------------------------------------ what exists NS=$(app_namespace "$NAME") if [[ -z $NS ]]; then NS=$(kubectl get application -n argocd "$NAME" -o jsonpath='{.spec.destination.namespace}' 2>/dev/null || true) fi NS=${NS:-$NAME} OWN_NS=0 [[ $NS == "$NAME" ]] && OWN_NS=1 # Refuse to pull a namespace out from under a frontend co-located with us. dependents=() for f in "$GITOPS_DIR/$APPS_DIR"/*.yaml; do other=$(basename "$f" .yaml) [[ $other != "$NAME" && $(app_namespace "$other") == "$NAME" ]] && dependents+=("$other") done ((${#dependents[@]} == 0)) || die "${dependents[*]} deploy(s) into namespace $NAME; delete-service them first" APP_DIR=$CODE_DIR/$NAME cat >&2 </dev/null 2>&1; then run kubectl patch application -n argocd "$NAME" --type merge \ -p '{"metadata":{"finalizers":["resources-finalizer.argocd.argoproj.io"]}}' >/dev/null fi paths=() [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml ]] && paths+=("$APPS_DIR/$NAME.yaml") [[ -d $GITOPS_DIR/manifests/$NAME ]] && paths+=("manifests/$NAME") if ((${#paths[@]})); then if is_dry; then dry "git -C $GITOPS_DIR rm -r ${paths[*]}" else git -C "$GITOPS_DIR" rm -rq -- "${paths[@]}" fi gitops_commit_push "chore($NAME): delete service" "${paths[@]}" argocd_refresh root else ok "gitops has no entry for $NAME" fi if ! is_dry; then app_gone() { ! kubectl get application -n argocd "$NAME" >/dev/null 2>&1; } if ! app_gone; then # If root never managed it (e.g. scaffold stopped before registering), delete directly. sleep 15 app_gone || kubectl get application -n argocd root -o json | jq -e --arg n "$NAME" '.status.resources[]? | select(.kind == "Application" and .name == $n)' >/dev/null || kubectl delete application -n argocd "$NAME" --wait=false >/dev/null wait_until "$TIMEOUT" 5 "ArgoCD to prune application $NAME and its resources" app_gone else ok "no ArgoCD application $NAME" fi fi # ------------------------------------------------------------ 1b. public DNS cf_cname_delete "$NAME" # ------------------------------------------------------------ 2. namespace if ((OWN_NS)); then if kubectl get namespace "$NAME" >/dev/null 2>&1; then run kubectl delete namespace "$NAME" --wait=false >/dev/null if ! is_dry; then ns_gone() { ! kubectl get namespace "$NAME" >/dev/null 2>&1; } wait_until "$TIMEOUT" 5 "namespace $NAME to terminate" ns_gone fi else ok "namespace $NAME absent" fi fi # ------------------------------------------------------------ 3. container package # The admin token has no package scope; the registry push token (same user) does. deleted=0 while :; do api GET "/packages/$FORGEJO_OWNER?type=container&q=$NAME&limit=50" '' registry [[ $API_CODE == 200 ]] || die "listing packages failed ($API_CODE): $API_BODY" mapfile -t versions < <(jq -r --arg n "$NAME" '.[] | select(.name == $n) | .version' <<<"$API_BODY") ((${#versions[@]})) || break for v in "${versions[@]}"; do api_write DELETE "/packages/$FORGEJO_OWNER/container/$NAME/$(jq -rn --arg v "$v" '$v | @uri')" '' registry [[ $API_CODE =~ ^20[04]$ || $API_CODE == 404 ]] || die "delete package $NAME/$v failed ($API_CODE): $API_BODY" deleted=$((deleted + 1)) done is_dry && break done ok "container package $NAME: $deleted version(s) deleted" # ------------------------------------------------------------ 4. repo if repo_exists "$NAME"; then [[ $(jq -r .template <<<"$API_BODY") != true ]] || die "refusing to delete template repo $NAME" api_write DELETE "/repos/$FORGEJO_OWNER/$NAME" [[ $API_CODE == 204 || $API_CODE == 200 ]] || die "delete repo failed ($API_CODE): $API_BODY" ok "repo $FORGEJO_OWNER/$NAME deleted" else ok "repo $FORGEJO_OWNER/$NAME absent" fi # ------------------------------------------------------------ 5. local clone if [[ -d $APP_DIR ]]; then origin=$(git -C "$APP_DIR" remote get-url origin 2>/dev/null || true) [[ $origin == "$FORGEJO_URL/$FORGEJO_OWNER/$NAME.git" ]] || die "$APP_DIR origin is '$origin', not this service's repo; not deleting it" run rm -rf -- "$APP_DIR" ok "removed $APP_DIR" fi ok "$NAME deleted in $(elapsed)s"