#!/usr/bin/env bash # 按「集群里实际存在的资源」遍历删除(全部由 kubectl 发现,不读仓库 YAML 目录) # 对应 docs/00-03 §2 步骤 2「重度清理」方向的实验室内清场(非 verify.sh 默认 teardown,亦非 k3s-uninstall)。 # 在任意目录执行均可;建议在仓库根:./scripts/k3s-delete-lab-stacks.sh [选项] # # 默认跳过系统命名空间:kube-system、kube-public、kube-node-lease # 每个命名空间内会跳过 Service/kubernetes(API 内置 Service) # 可选:PVC / ConfigMap / Secret(默认不删 Secret,避免误伤账号 token) # # 环境:kubectl 可用;export KUBECONFIG=... 按需设置 set -euo pipefail DRY_RUN=false YES=false PREVIEW_ONLY=false INCLUDE_KUBE_SYSTEM=false WITH_PVC=false WITH_CONFIGMAPS=false WITH_SECRETS=false # 空 = 自动枚举「非系统」命名空间;非空 = 仅处理列出的 NS(逗号分隔) NAMESPACES_ARG="" usage() { cat <<'EOF' 用法: k3s-delete-lab-stacks.sh [选项] 按 kubectl 当前集群中已部署的资源逐项删除(常见工作负载 + Ingress/IngressRoute 等)。 不依赖本仓库 ansible/files 目录。 选项: --preview 只列出将参与删除的命名空间及各资源(kubectl get),不执行删除 --dry-run 删除时使用 kubectl 的 --dry-run=client(不落库,部分环境仍会做校验) -y, --yes 跳过确认 --namespaces NS[,NS...] 只处理这些命名空间(仍受 --include-kube-system 与系统 NS 规则约束) --include-kube-system 也处理 kube-system(极危险,可能拆掉 Traefik/Coredns 等) --with-pvc 删除 PersistentVolumeClaim(数据卷,默认不删) --with-configmaps 删除 ConfigMap(会跳过 kube-root-ca.crt) --with-secrets 删除 Secret(会跳过 default-token-* 及 type=kubernetes.io/service-account-token) -h, --help 帮助 示例: export KUBECONFIG=/etc/rancher/k3s/k3s.yaml ./scripts/k3s-delete-lab-stacks.sh --preview ./scripts/k3s-delete-lab-stacks.sh --namespaces default -y ./scripts/k3s-delete-lab-stacks.sh --dry-run -y EOF exit 0 } ARGS=("$@") i=0 while [[ $i -lt ${#ARGS[@]} ]]; do case "${ARGS[$i]}" in --dry-run) DRY_RUN=true ;; -y|--yes) YES=true ;; --preview) PREVIEW_ONLY=true ;; --include-kube-system) INCLUDE_KUBE_SYSTEM=true ;; --with-pvc) WITH_PVC=true ;; --with-configmaps) WITH_CONFIGMAPS=true ;; --with-secrets) WITH_SECRETS=true ;; -h|--help) usage ;; --namespaces) i=$((i + 1)) if [[ $i -ge ${#ARGS[@]} ]]; then echo "[ERROR] --namespaces 需要参数" >&2; exit 1; fi NAMESPACES_ARG="${ARGS[$i]}" ;; *) echo "[ERROR] 未知参数: ${ARGS[$i]},使用 -h 查看帮助" >&2 exit 1 ;; esac i=$((i + 1)) done if ! command -v kubectl &>/dev/null; then echo "[ERROR] 未找到 kubectl" >&2 exit 1 fi KUBECTL_DELETE=(kubectl delete) if [[ "${DRY_RUN}" == true ]]; then KUBECTL_DELETE=(kubectl delete --dry-run=client) fi # 系统命名空间:默认不扫(除非 --include-kube-system 且用户未用 --namespaces 限制) SYSTEM_NS="kube-system kube-public kube-node-lease" is_system_ns() { local n="$1" for s in ${SYSTEM_NS}; do [[ "${n}" == "${s}" ]] && return 0 done return 1 } collect_namespaces() { if [[ -n "${NAMESPACES_ARG}" ]]; then IFS=',' read -r -a arr <<< "${NAMESPACES_ARG}" for raw in "${arr[@]}"; do n="${raw//[[:space:]]/}" [[ -z "${n}" ]] && continue if ! is_system_ns "${n}" || [[ "${INCLUDE_KUBE_SYSTEM}" == true ]]; then echo "${n}" else echo "[WARN] 已忽略系统命名空间(加 --include-kube-system 可处理): ${n}" >&2 fi done return fi local all all=$(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') while IFS= read -r n; do [[ -z "${n}" ]] && continue if is_system_ns "${n}" && [[ "${INCLUDE_KUBE_SYSTEM}" != true ]]; then continue fi echo "${n}" done <<< "${all}" } # 若 kind 在当前集群不存在,静默跳过 list_kind_in_ns() { local ns="$1" local kind="$2" kubectl get "${kind}" -n "${ns}" -o name 2>/dev/null || true } preview_kind_in_ns() { local ns="$1" local kind="$2" if kubectl get "${kind}" -n "${ns}" &>/dev/null; then echo "===== ${ns} / ${kind} =====" kubectl get "${kind}" -n "${ns}" -o wide 2>/dev/null || true fi } # 删除某 kind 在 ns 下 kubectl -o name 列出的所有资源 delete_kind_all_by_name() { local ns="$1" local kind="$2" local line name while IFS= read -r line; do [[ -z "${line}" ]] && continue # line 形如 deployment.apps/foo 或 pod/bar name="${line##*/}" echo "[DEL] ${line} -n ${ns}" "${KUBECTL_DELETE[@]}" "${line}" -n "${ns}" --ignore-not-found || true done < <(list_kind_in_ns "${ns}" "${kind}") } delete_services_safe() { local ns="$1" local names names=$(kubectl get svc -n "${ns}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) while IFS= read -r n; do [[ -z "${n}" ]] && continue [[ "${n}" == "kubernetes" ]] && continue echo "[DEL] service/${n} -n ${ns}" "${KUBECTL_DELETE[@]}" "svc" "${n}" -n "${ns}" --ignore-not-found || true done <<< "${names}" } delete_configmaps_safe() { local ns="$1" local cm while IFS= read -r cm; do [[ -z "${cm}" ]] && continue [[ "${cm}" == "kube-root-ca.crt" ]] && continue echo "[DEL] configmap/${cm} -n ${ns}" "${KUBECTL_DELETE[@]}" "configmap" "${cm}" -n "${ns}" --ignore-not-found || true done < <(kubectl get configmap -n "${ns}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) } delete_secrets_safe() { local ns="$1" if ! command -v jq &>/dev/null; then echo "[WARN] 未安装 jq,无法安全过滤 Secret,已跳过 ${ns}" >&2 return 0 fi local names names=$(kubectl get secret -n "${ns}" -o json 2>/dev/null | jq -r ' .items[]? | select(.type != "kubernetes.io/service-account-token") | select(.metadata.name | test("^default-token-") | not) | .metadata.name' 2>/dev/null || true) while IFS= read -r sn; do [[ -z "${sn}" ]] && continue echo "[DEL] secret/${sn} -n ${ns}" "${KUBECTL_DELETE[@]}" "secret" "${sn}" -n "${ns}" --ignore-not-found || true done <<< "${names}" } # Traefik / 扩展:不存在则 list 为空 TRAEFIK_KINDS=(ingressroute middleware tlsoption traefikservice serverstransport) process_namespace() { local ns="$1" if [[ "${PREVIEW_ONLY}" == true ]]; then echo "######## 命名空间: ${ns} ########" for k in cronjob job deployment statefulset daemonset ingress networkpolicy horizontalpodautoscaler hpa \ "${TRAEFIK_KINDS[@]}" service pvc configmap secret; do preview_kind_in_ns "${ns}" "${k}" || true done return fi echo "######## 删除: ${ns} ########" # 1) 定时与任务 delete_kind_all_by_name "${ns}" "cronjob" delete_kind_all_by_name "${ns}" "job" # 2) 路由(Traefik CRD 可能未装,list 为空) delete_kind_all_by_name "${ns}" "ingress" for tk in "${TRAEFIK_KINDS[@]}"; do delete_kind_all_by_name "${ns}" "${tk}" done # 3) 工作负载 delete_kind_all_by_name "${ns}" "deployment" delete_kind_all_by_name "${ns}" "statefulset" delete_kind_all_by_name "${ns}" "daemonset" delete_kind_all_by_name "${ns}" "replicaset" # 4) 其它常见附属(不主动删 Pod:由上层负载级联回收,避免误伤系统静态 Pod) delete_kind_all_by_name "${ns}" "networkpolicy" delete_kind_all_by_name "${ns}" "horizontalpodautoscaler" delete_kind_all_by_name "${ns}" "hpa" # 5) Service(保留 kubernetes) delete_services_safe "${ns}" # 6) PVC if [[ "${WITH_PVC}" == true ]]; then delete_kind_all_by_name "${ns}" "persistentvolumeclaim" fi if [[ "${WITH_CONFIGMAPS}" == true ]]; then delete_configmaps_safe "${ns}" fi if [[ "${WITH_SECRETS}" == true ]]; then if command -v jq &>/dev/null; then delete_secrets_safe "${ns}" else echo "[WARN] ${ns}: 未安装 jq,已跳过 --with-secrets" >&2 fi fi } NS_LIST=() while IFS= read -r _ns; do [[ -z "${_ns}" ]] && continue NS_LIST+=("${_ns}") done < <(collect_namespaces | sort -u) if [[ ${#NS_LIST[@]} -eq 0 ]]; then echo "[ERROR] 没有可处理的命名空间(检查 --namespaces / 集群连接)" >&2 exit 1 fi if [[ -n "${KUBECONFIG:-}" ]]; then echo "[INFO] KUBECONFIG=${KUBECONFIG}" else echo "[INFO] KUBECONFIG 未设置,使用 kubectl 默认配置" fi echo "[INFO] 命名空间 (${#NS_LIST[@]}): ${NS_LIST[*]}" echo "[INFO] include-kube-system=${INCLUDE_KUBE_SYSTEM} with-pvc=${WITH_PVC} with-configmaps=${WITH_CONFIGMAPS} with-secrets=${WITH_SECRETS} preview=${PREVIEW_ONLY} dry-run=${DRY_RUN}" if [[ "${PREVIEW_ONLY}" != true && "${YES}" != true && "${DRY_RUN}" != true ]]; then echo "[WARN] 将按上述命名空间删除工作负载与路由等资源(见脚本内顺序)。输入 yes 继续:" read -r confirm if [[ "${confirm}" != "yes" ]]; then echo "已取消" exit 1 fi fi for ns in "${NS_LIST[@]}"; do process_namespace "${ns}" done echo "[DONE] 完成"