Files
ljwx-k8s/governance-remediation.sh
2026-02-20 22:50:45 +08:00

304 lines
9.0 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 平台治理修复脚本
# 覆盖:ArgoCD 应用重叠检查、镜像策略巡检、infra ownerless Pod 清理、cert-manager 凭证/issuer 巡检
set -euo pipefail
KUBECTL_BIN="${KUBECTL_BIN:-kubectl}"
JQ_BIN="${JQ_BIN:-jq}"
MAX_ORPHAN_AGE_HOURS="${MAX_ORPHAN_AGE_HOURS:-72}"
ISSUE_COUNT=0
log_info() {
echo "[INFO] $1"
}
log_warn() {
echo "[WARN] $1"
}
log_ok() {
echo "[OK] $1"
}
inc_issue() {
ISSUE_COUNT=$((ISSUE_COUNT + 1))
}
usage() {
cat <<USAGE
用法: $0 <command>
commands:
audit 执行全部治理检查(只读)
fix-image-policy 修复 latest + imagePullPolicy=Never(改为 Always)
cleanup-orphan-pods 清理 infra 中超过 ${MAX_ORPHAN_AGE_HOURS}h 的无 owner 一次性 Pod
all 先 audit,再执行 fix-image-policy 与 cleanup-orphan-pods
USAGE
}
require_tools() {
command -v "$KUBECTL_BIN" >/dev/null 2>&1 || { echo "kubectl 不存在"; exit 1; }
command -v "$JQ_BIN" >/dev/null 2>&1 || { echo "jq 不存在"; exit 1; }
}
ensure_cluster_access() {
if ! "$KUBECTL_BIN" version --request-timeout='5s' >/dev/null 2>&1; then
echo "[WARN] 无法连接 Kubernetes API,请先确认 kubeconfig 和集群连通性"
exit 2
fi
}
decode_b64() {
if base64 --help 2>&1 | grep -q -- "--decode"; then
base64 --decode
else
base64 -D
fi
}
audit_argocd_overlap() {
log_info "检查 ArgoCD 应用重叠(SharedResourceWarning)..."
local apps_json
if ! apps_json="$($KUBECTL_BIN get applications.argoproj.io -A -o json 2>/dev/null)"; then
log_warn "集群未发现 ArgoCD Application CRD,跳过该检查"
return 0
fi
local overlaps
overlaps="$(echo "$apps_json" | "$JQ_BIN" -r '
.items[]?
| [ .status.conditions[]? | select(.type == "SharedResourceWarning") | .message ] as $msgs
| select(($msgs | length) > 0)
| "\(.metadata.namespace)/\(.metadata.name)\t\($msgs | join(" | "))"
')"
if [[ -z "$overlaps" ]]; then
log_ok "未发现 SharedResourceWarning"
else
inc_issue
log_warn "发现应用重叠,请按输出逐项收敛资源归属(一个资源只能由一个 Application 管理)"
echo "$overlaps"
fi
}
collect_bad_image_policy() {
"$KUBECTL_BIN" get deployment,statefulset,daemonset,job,cronjob -A -o json 2>/dev/null | "$JQ_BIN" -r '
.items[] as $w
| (if $w.kind == "CronJob" then $w.spec.jobTemplate.spec.template.spec else $w.spec.template.spec end) as $pod
| ($pod.containers // [])
| to_entries[]
| . as $c
| ($c.value.imagePullPolicy // "IfNotPresent") as $pull
| select(($c.value.image | test(":latest($|@)")) and $pull == "Never")
| "\($w.kind)\t\($w.metadata.namespace)\t\($w.metadata.name)\t\($c.key)\t\($c.value.name)\t\($c.value.image)\t\($pull)"
'
}
audit_image_policy() {
log_info "检查 latest + imagePullPolicy=Never 组合..."
local bad
bad="$(collect_bad_image_policy || true)"
if [[ -z "$bad" ]]; then
log_ok "未发现 latest + Never 组合"
else
inc_issue
log_warn "发现不合规镜像策略:"
echo -e "KIND\tNAMESPACE\tWORKLOAD\tIDX\tCONTAINER\tIMAGE\tPULL_POLICY"
echo "$bad"
fi
}
fix_image_policy() {
log_info "修复 latest + imagePullPolicy=Never -> Always..."
local bad
bad="$(collect_bad_image_policy || true)"
if [[ -z "$bad" ]]; then
log_ok "无需修复"
return 0
fi
while IFS=$'\t' read -r kind ns workload idx cname image pull; do
[[ -z "$kind" ]] && continue
local patch_path
if [[ "$kind" == "CronJob" ]]; then
patch_path="/spec/jobTemplate/spec/template/spec/containers/${idx}/imagePullPolicy"
"$KUBECTL_BIN" -n "$ns" patch cronjob "$workload" --type='json' \
-p="[{\"op\":\"replace\",\"path\":\"${patch_path}\",\"value\":\"Always\"}]" >/dev/null
elif [[ "$kind" == "Deployment" ]]; then
patch_path="/spec/template/spec/containers/${idx}/imagePullPolicy"
"$KUBECTL_BIN" -n "$ns" patch deployment "$workload" --type='json' \
-p="[{\"op\":\"replace\",\"path\":\"${patch_path}\",\"value\":\"Always\"}]" >/dev/null
elif [[ "$kind" == "StatefulSet" ]]; then
patch_path="/spec/template/spec/containers/${idx}/imagePullPolicy"
"$KUBECTL_BIN" -n "$ns" patch statefulset "$workload" --type='json' \
-p="[{\"op\":\"replace\",\"path\":\"${patch_path}\",\"value\":\"Always\"}]" >/dev/null
elif [[ "$kind" == "DaemonSet" ]]; then
patch_path="/spec/template/spec/containers/${idx}/imagePullPolicy"
"$KUBECTL_BIN" -n "$ns" patch daemonset "$workload" --type='json' \
-p="[{\"op\":\"replace\",\"path\":\"${patch_path}\",\"value\":\"Always\"}]" >/dev/null
elif [[ "$kind" == "Job" ]]; then
patch_path="/spec/template/spec/containers/${idx}/imagePullPolicy"
"$KUBECTL_BIN" -n "$ns" patch job "$workload" --type='json' \
-p="[{\"op\":\"replace\",\"path\":\"${patch_path}\",\"value\":\"Always\"}]" >/dev/null
else
log_warn "不支持的资源类型: $kind ($ns/$workload)"
continue
fi
log_ok "已修复: ${kind} ${ns}/${workload} container=${cname} (${image})"
done <<< "$bad"
}
collect_orphan_pods() {
"$KUBECTL_BIN" get pods -n infra -o json 2>/dev/null | "$JQ_BIN" -r --argjson max_age "$((MAX_ORPHAN_AGE_HOURS * 3600))" '
.items[]?
| select((.metadata.ownerReferences // []) | length == 0)
| select((now - (.metadata.creationTimestamp | fromdateiso8601)) > $max_age)
| "\(.metadata.name)\t\(.status.phase)\t\(.metadata.creationTimestamp)"
'
}
audit_orphan_pods() {
log_info "检查 infra 中长期无 owner Pod(>${MAX_ORPHAN_AGE_HOURS}h)..."
local pods
pods="$(collect_orphan_pods || true)"
if [[ -z "$pods" ]]; then
log_ok "未发现长期无 owner Pod"
else
inc_issue
log_warn "发现长期无 owner Pod:"
echo -e "NAME\tPHASE\tCREATED_AT"
echo "$pods"
fi
}
cleanup_orphan_pods() {
log_info "清理 infra 中长期无 owner 一次性 Pod(Succeeded/Failed)..."
local pods
pods="$(collect_orphan_pods || true)"
if [[ -z "$pods" ]]; then
log_ok "无需清理"
return 0
fi
while IFS=$'\t' read -r name phase created_at; do
[[ -z "$name" ]] && continue
if [[ "$phase" == "Succeeded" || "$phase" == "Failed" ]]; then
"$KUBECTL_BIN" delete pod -n infra "$name" --wait=false >/dev/null
log_ok "已删除: ${name} (${phase}, ${created_at})"
else
log_warn "保留(非一次性状态): ${name} (${phase}, ${created_at})"
fi
done <<< "$pods"
}
audit_cert_manager() {
log_info "检查 cert-manager 凭证格式与 issuer 路由..."
local dnspod_b64=""
local cloudflare_b64=""
dnspod_b64="$($KUBECTL_BIN get secret dnspod-api-token -n cert-manager -o jsonpath='{.data.api-token}' 2>/dev/null || true)"
if [[ -n "$dnspod_b64" ]]; then
local dnspod_token
dnspod_token="$(echo "$dnspod_b64" | decode_b64 2>/dev/null || true)"
if [[ "$dnspod_token" == *,* ]]; then
log_ok "DNSPod token 格式正确(ID,Token)"
else
inc_issue
log_warn "DNSPod token 格式异常,期望 ID,Token"
fi
else
log_warn "未找到 secret cert-manager/dnspod-api-token"
fi
cloudflare_b64="$($KUBECTL_BIN get secret cloudflare-api-token -n cert-manager -o jsonpath='{.data.api-token}' 2>/dev/null || true)"
if [[ -n "$cloudflare_b64" ]]; then
local cloudflare_token
cloudflare_token="$(echo "$cloudflare_b64" | decode_b64 2>/dev/null || true)"
if [[ "$cloudflare_token" == *,* ]]; then
inc_issue
log_warn "Cloudflare token 格式异常,不应使用 ID,Token"
else
log_ok "Cloudflare token 格式正确(单 token)"
fi
else
log_warn "未找到 secret cert-manager/cloudflare-api-token"
fi
local dnspod_group
dnspod_group="$($KUBECTL_BIN get clusterissuer dnspod-letsencrypt -o jsonpath='{.spec.acme.solvers[0].dns01.webhook.groupName}' 2>/dev/null || true)"
if [[ -n "$dnspod_group" ]]; then
if [[ "$dnspod_group" == "acme.omniverseai.net" ]]; then
log_ok "DNSPod issuer groupName 已统一: ${dnspod_group}"
else
inc_issue
log_warn "DNSPod issuer groupName 非预期: ${dnspod_group}(应为 acme.omniverseai.net)"
fi
else
log_warn "未找到 clusterissuer/dnspod-letsencrypt"
fi
for deprecated in letsencrypt-prod letsencrypt-staging; do
if $KUBECTL_BIN get clusterissuer "$deprecated" >/dev/null 2>&1; then
inc_issue
log_warn "发现已弃用 ClusterIssuer: ${deprecated}(建议迁移到 dnspod-/cloudflare-/http01-前缀)"
fi
done
}
run_audit() {
audit_argocd_overlap
audit_image_policy
audit_orphan_pods
audit_cert_manager
if [[ "$ISSUE_COUNT" -eq 0 ]]; then
log_ok "治理检查通过"
else
log_warn "治理检查完成,发现 ${ISSUE_COUNT} 个问题"
fi
}
main() {
require_tools
ensure_cluster_access
local cmd="${1:-audit}"
case "$cmd" in
audit)
run_audit
;;
fix-image-policy)
fix_image_policy
;;
cleanup-orphan-pods)
cleanup_orphan_pods
;;
all)
run_audit
fix_image_policy
cleanup_orphan_pods
;;
*)
usage
exit 1
;;
esac
}
main "$@"