feat: new-service and delete-service scaffolder
One command from nothing to a running service: generate repo from template, Actions secrets, namespace + unleash token, gitops render, kick CI, register with ArgoCD once the first image exists, optional --wait health check. delete-service reverses all of it. Idempotent, --dry-run supported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
253b6033c3
7 changed files with 809 additions and 0 deletions
152
bin/delete-service
Executable file
152
bin/delete-service
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
#!/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/*.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 <<EOF
|
||||
This permanently deletes, for service '$NAME':
|
||||
- gitops apps/$NAME.yaml and manifests/$NAME/ (ArgoCD prunes the workloads)
|
||||
- $([[ $OWN_NS == 1 ]] && echo "namespace $NAME, including its Postgres data (PVCs)" || echo "nothing else in shared namespace $NS (left in place)")
|
||||
- Forgejo repo $FORGEJO_OWNER/$NAME and its container package versions
|
||||
- local clone $APP_DIR
|
||||
EOF
|
||||
if ((!YES)) && ! is_dry; then
|
||||
read -r -p "Type the service name to confirm: " answer
|
||||
[[ $answer == "$NAME" ]] || die "aborted"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------ 1. ArgoCD app + gitops
|
||||
# The resources finalizer makes ArgoCD delete the app's workloads when the
|
||||
# root app prunes the Application (older apps were created without it).
|
||||
if kubectl get application -n argocd "$NAME" >/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/$NAME.yaml ]] && paths+=("apps/$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
|
||||
|
||||
# ------------------------------------------------------------ 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue