varie
This commit is contained in:
438
exportManofest.sh
Normal file
438
exportManofest.sh
Normal file
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# export-k8s-manifests.sh
|
||||
#
|
||||
# Ricostruisce i manifest YAML dichiarativi delle risorse installate
|
||||
# su un cluster Kubernetes, per una lista di namespace data in input.
|
||||
#
|
||||
# Rimuove i campi "runtime" (status, managedFields, resourceVersion, uid,
|
||||
# creationTimestamp, ownerReferences generate da controller, ecc.) in modo
|
||||
# da produrre YAML riapplicabili con `kubectl apply -f`.
|
||||
#
|
||||
# Uso:
|
||||
# ./export-k8s-manifests.sh -n ns1,ns2,ns3 [-o output_dir] [-c kubeconfig] [-k context]
|
||||
# ./export-k8s-manifests.sh -f namespaces.txt [-o output_dir]
|
||||
#
|
||||
# Dipendenze:
|
||||
# - kubectl (obbligatorio)
|
||||
# - yq (v4+, https://github.com/mikefarah/yq) — consigliato per pulizia campi
|
||||
# Se assente, viene usato un fallback con python3.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Default
|
||||
# ------------------------------------------------------------------
|
||||
OUTPUT_DIR="./k8s-export-$(date +%Y%m%d-%H%M%S)"
|
||||
NAMESPACES=()
|
||||
NAMESPACE_FILE=""
|
||||
KUBECONFIG_OPT=()
|
||||
CONTEXT_OPT=()
|
||||
INCLUDE_SECRETS=false
|
||||
|
||||
# Tipi di risorsa namespaced da esportare.
|
||||
# Personalizza questa lista secondo le tue esigenze.
|
||||
RESOURCE_TYPES=(
|
||||
configmap
|
||||
secret
|
||||
service
|
||||
serviceaccount
|
||||
deployment
|
||||
statefulset
|
||||
daemonset
|
||||
replicaset
|
||||
job
|
||||
cronjob
|
||||
ingress
|
||||
networkpolicy
|
||||
poddisruptionbudget
|
||||
horizontalpodautoscaler
|
||||
persistentvolumeclaim
|
||||
role
|
||||
rolebinding
|
||||
|
||||
# --- API Gateway (namespaced) -------------------------------------
|
||||
# Gateway API standard (gateway.networking.k8s.io)
|
||||
gateway
|
||||
httproute
|
||||
grpcroute
|
||||
tcproute
|
||||
tlsroute
|
||||
referencegrant
|
||||
# Istio
|
||||
virtualservice
|
||||
destinationrule
|
||||
# Traefik
|
||||
ingressroute
|
||||
middleware
|
||||
# Kong
|
||||
kongingress
|
||||
kongplugin
|
||||
kongconsumer
|
||||
# APISIX
|
||||
apisixroute
|
||||
apisixpluginconfig
|
||||
# NGINX Gateway Fabric — CRD di estensione (gateway.nginx.org)
|
||||
# Le risorse Gateway API standard (gateway, httproute, gatewayclass, ecc.)
|
||||
# sono già coperte sopra; queste sono le estensioni specifiche NGF per
|
||||
# configurazioni non esprimibili nello standard Gateway API.
|
||||
nginxproxy
|
||||
clientsettingspolicy
|
||||
observabilitypolicy
|
||||
upstreamsettingspolicy
|
||||
snippetsfilter
|
||||
|
||||
# CRD comuni — decommenta/aggiungi secondo gli operator installati
|
||||
cluster.postgresql.cnpg.io # CloudNativePG Cluster
|
||||
scheduledbackup.postgresql.cnpg.io
|
||||
certificate.cert-manager.io
|
||||
)
|
||||
|
||||
# Tipi di risorsa CLUSTER-SCOPED da esportare.
|
||||
# Vengono filtrate per pertinenza ai namespace indicati dove possibile
|
||||
# (es. ClusterRoleBinding con subject in uno dei namespace, PV con claim
|
||||
# in uno dei namespace). Le risorse "globali" (StorageClass, GatewayClass,
|
||||
# IngressClass, PriorityClass) vengono esportate sempre, essendo poche
|
||||
# e prive di legame diretto con un namespace.
|
||||
CLUSTER_RESOURCE_TYPES_FILTERED=(
|
||||
clusterrolebinding
|
||||
persistentvolume
|
||||
)
|
||||
CLUSTER_RESOURCE_TYPES_GLOBAL=(
|
||||
storageclass
|
||||
priorityclass
|
||||
# API Gateway (cluster-scoped)
|
||||
gatewayclass
|
||||
ingressclass
|
||||
# Da valutare se esportare sempre (potenzialmente numerose/rumorose):
|
||||
# clusterrole
|
||||
# customresourcedefinition
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Parsing argomenti
|
||||
# ------------------------------------------------------------------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Uso: $0 -n ns1,ns2,ns3 [opzioni]
|
||||
$0 -f namespaces.txt [opzioni]
|
||||
|
||||
Opzioni:
|
||||
-n <ns1,ns2,...> Lista namespace separati da virgola
|
||||
-f <file> File con un namespace per riga (alternativo a -n)
|
||||
-o <dir> Directory di output (default: ${OUTPUT_DIR})
|
||||
-c <kubeconfig> Path al kubeconfig da usare
|
||||
-k <context> Nome del context kubectl da usare
|
||||
-s Includi anche i Secret (default: esclusi per sicurezza)
|
||||
-h Mostra questo help
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while getopts "n:f:o:c:k:sh" opt; do
|
||||
case "$opt" in
|
||||
n) IFS=',' read -r -a NAMESPACES <<< "$OPTARG" ;;
|
||||
f) NAMESPACE_FILE="$OPTARG" ;;
|
||||
o) OUTPUT_DIR="$OPTARG" ;;
|
||||
c) KUBECONFIG_OPT=(--kubeconfig "$OPTARG") ;;
|
||||
k) CONTEXT_OPT=(--context "$OPTARG") ;;
|
||||
s) INCLUDE_SECRETS=true ;;
|
||||
h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$NAMESPACE_FILE" ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" || "$line" =~ ^# ]] && continue
|
||||
NAMESPACES+=("$line")
|
||||
done < "$NAMESPACE_FILE"
|
||||
fi
|
||||
|
||||
if [[ ${#NAMESPACES[@]} -eq 0 ]]; then
|
||||
echo "Errore: nessun namespace specificato (usa -n o -f)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_SECRETS" == false ]]; then
|
||||
RESOURCE_TYPES=("${RESOURCE_TYPES[@]/secret}")
|
||||
fi
|
||||
|
||||
KCTL=(kubectl "${KUBECONFIG_OPT[@]}" "${CONTEXT_OPT[@]}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Verifica dipendenze
|
||||
# ------------------------------------------------------------------
|
||||
command -v kubectl >/dev/null 2>&1 || { echo "Errore: kubectl non trovato" >&2; exit 1; }
|
||||
|
||||
USE_YQ=false
|
||||
if command -v yq >/dev/null 2>&1; then
|
||||
USE_YQ=true
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
USE_YQ=false
|
||||
else
|
||||
echo "Errore: serve 'yq' oppure 'python3' per la pulizia dei manifest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verifica connessione al cluster..."
|
||||
"${KCTL[@]}" cluster-info >/dev/null 2>&1 || { echo "Errore: impossibile contattare il cluster" >&2; exit 1; }
|
||||
CLUSTER_NAME=$("${KCTL[@]}" config current-context 2>/dev/null || echo "unknown-cluster")
|
||||
echo "Cluster context: ${CLUSTER_NAME}"
|
||||
echo "Output dir: ${OUTPUT_DIR}"
|
||||
echo "Namespace da esportare: ${NAMESPACES[*]}"
|
||||
echo "Secret inclusi: ${INCLUDE_SECRETS}"
|
||||
echo
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Gli script Python vengono scritti su file temporanei perché in bash
|
||||
# `python3 - <<'HEREDOC'` sovrascrive lo stdin del processo con l'heredoc,
|
||||
# rendendo impossibile leggere i dati dalla pipeline (il fd 0 è già consumato
|
||||
# prima che il codice Python arrivi a sys.stdin.read()).
|
||||
_PY_CLEAN=$(mktemp)
|
||||
_PY_FILTER=$(mktemp)
|
||||
trap 'rm -f "$_PY_CLEAN" "$_PY_FILTER"' EXIT
|
||||
|
||||
if [[ "$USE_YQ" == false ]]; then
|
||||
cat > "$_PY_CLEAN" << 'PYCLEAN'
|
||||
import sys, yaml
|
||||
doc = yaml.safe_load(sys.stdin)
|
||||
if doc is None:
|
||||
sys.exit(0)
|
||||
doc.pop("status", None)
|
||||
md = doc.get("metadata", {})
|
||||
for f in ("uid", "resourceVersion", "generation", "creationTimestamp",
|
||||
"selfLink", "managedFields", "ownerReferences"):
|
||||
md.pop(f, None)
|
||||
ann = md.get("annotations")
|
||||
if ann:
|
||||
ann.pop("kubectl.kubernetes.io/last-applied-configuration", None)
|
||||
ann.pop("deployment.kubernetes.io/revision", None)
|
||||
if not ann:
|
||||
md.pop("annotations", None)
|
||||
spec = doc.get("spec", {})
|
||||
spec.pop("clusterIP", None)
|
||||
spec.pop("clusterIPs", None)
|
||||
tmpl = spec.get("template", {})
|
||||
if isinstance(tmpl, dict):
|
||||
tmpl.get("metadata", {}).pop("creationTimestamp", None)
|
||||
print(yaml.dump(doc, sort_keys=False, default_flow_style=False), end="")
|
||||
PYCLEAN
|
||||
|
||||
cat > "$_PY_FILTER" << 'PYFILTER'
|
||||
import json, os, sys
|
||||
mode = sys.argv[1]
|
||||
data = json.load(sys.stdin)
|
||||
ns_set = set(os.environ.get("NS_CSV", "").split())
|
||||
for item in data.get("items", []):
|
||||
if mode == "subjects":
|
||||
subs = item.get("subjects") or []
|
||||
match = any(s.get("namespace") in ns_set for s in subs)
|
||||
else:
|
||||
claim = (item.get("spec") or {}).get("claimRef") or {}
|
||||
match = claim.get("namespace") in ns_set
|
||||
if match:
|
||||
print(item["metadata"]["name"])
|
||||
PYFILTER
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Funzione di pulizia dei campi runtime
|
||||
# ------------------------------------------------------------------
|
||||
clean_manifest_yq() {
|
||||
# Rimuove campi generati dal cluster, mantenendo lo spec dichiarativo
|
||||
yq eval '
|
||||
del(.status) |
|
||||
del(.metadata.uid) |
|
||||
del(.metadata.resourceVersion) |
|
||||
del(.metadata.generation) |
|
||||
del(.metadata.creationTimestamp) |
|
||||
del(.metadata.selfLink) |
|
||||
del(.metadata.managedFields) |
|
||||
del(.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") |
|
||||
del(.metadata.annotations."deployment.kubernetes.io/revision") |
|
||||
del(.metadata.ownerReferences) |
|
||||
del(.spec.clusterIP) |
|
||||
del(.spec.clusterIPs) |
|
||||
del(.spec.template.metadata.creationTimestamp)
|
||||
' -
|
||||
}
|
||||
|
||||
clean_manifest_python() {
|
||||
# Usa il file temporaneo creato all'avvio; l'heredoc inline non funziona
|
||||
# in pipeline perché sovrascrive lo stdin del processo.
|
||||
python3 "$_PY_CLEAN"
|
||||
}
|
||||
|
||||
clean_manifest() {
|
||||
if [[ "$USE_YQ" == true ]]; then
|
||||
clean_manifest_yq
|
||||
else
|
||||
clean_manifest_python
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Export per namespace / kind / risorsa
|
||||
# ------------------------------------------------------------------
|
||||
SUMMARY_FILE="${OUTPUT_DIR}/EXPORT_SUMMARY.md"
|
||||
{
|
||||
echo "# Export manifest Kubernetes"
|
||||
echo
|
||||
echo "- Cluster context: \`${CLUSTER_NAME}\`"
|
||||
echo "- Data export: $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
echo "- Namespace: ${NAMESPACES[*]}"
|
||||
echo
|
||||
echo "| Namespace | Kind | Nome | File |"
|
||||
echo "|---|---|---|---|"
|
||||
} > "$SUMMARY_FILE"
|
||||
|
||||
for ns in "${NAMESPACES[@]}"; do
|
||||
echo "== Namespace: ${ns} =="
|
||||
|
||||
if ! "${KCTL[@]}" get namespace "$ns" >/dev/null 2>&1; then
|
||||
echo " ATTENZIONE: namespace '${ns}' non trovato, salto." >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
NS_DIR="${OUTPUT_DIR}/${ns}"
|
||||
mkdir -p "$NS_DIR"
|
||||
|
||||
for kind in "${RESOURCE_TYPES[@]}"; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
|
||||
# Il controllo api-resources è omesso: kubectl get <kind> 2>/dev/null || true
|
||||
# gestisce già i resource type non esistenti restituendo NAMES vuoto.
|
||||
NAMES=$("${KCTL[@]}" -n "$ns" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
|
||||
[[ -z "$NAMES" ]] && continue
|
||||
|
||||
KIND_DIR="${NS_DIR}/${kind}"
|
||||
mkdir -p "$KIND_DIR"
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Salta le risorse generate automaticamente (es. default token secret,
|
||||
# ReplicaSet gestiti da Deployment, Pod gestiti da controller superiori)
|
||||
if [[ "$kind" == "secret" && "$name" =~ ^default-token- ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
OUT_FILE="${KIND_DIR}/${name}.yaml"
|
||||
echo " -> ${kind}/${name}"
|
||||
|
||||
if "${KCTL[@]}" -n "$ns" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$OUT_FILE"; then
|
||||
echo "| ${ns} | ${kind} | ${name} | \`${ns}/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE"
|
||||
else
|
||||
echo " ATTENZIONE: export fallito per ${kind}/${name}" >&2
|
||||
rm -f "$OUT_FILE"
|
||||
fi
|
||||
done <<< "$NAMES"
|
||||
done
|
||||
done
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Export risorse cluster-scoped
|
||||
# ------------------------------------------------------------------
|
||||
echo
|
||||
echo "== Risorse cluster-scoped =="
|
||||
|
||||
CLUSTER_DIR="${OUTPUT_DIR}/_cluster-scoped"
|
||||
|
||||
# Cache unica delle risorse disponibili (evita N chiamate a api-resources)
|
||||
_AVAIL_NAMES=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($1)}' || true)
|
||||
_AVAIL_KINDS=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($NF)}' || true)
|
||||
|
||||
resource_type_exists() {
|
||||
local kind="$1"
|
||||
# Controlla sia la colonna NAME (plurale) sia la colonna KIND (singolare) del
|
||||
# risultato di api-resources per gestire plurali irregolari (es. ingress→ingresses).
|
||||
echo "$_AVAIL_NAMES" | grep -qx "${kind}s" \
|
||||
|| echo "$_AVAIL_KINDS" | grep -qx "$kind"
|
||||
}
|
||||
|
||||
export_cluster_resource() {
|
||||
local kind="$1"
|
||||
local name="$2"
|
||||
local kind_dir="${CLUSTER_DIR}/${kind}"
|
||||
mkdir -p "$kind_dir"
|
||||
local out_file="${kind_dir}/${name}.yaml"
|
||||
|
||||
echo " -> ${kind}/${name}"
|
||||
if "${KCTL[@]}" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$out_file"; then
|
||||
echo "| (cluster) | ${kind} | ${name} | \`_cluster-scoped/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE"
|
||||
else
|
||||
echo " ATTENZIONE: export fallito per ${kind}/${name}" >&2
|
||||
rm -f "$out_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Risorse globali: esportate sempre, senza filtro namespace ---
|
||||
for kind in "${CLUSTER_RESOURCE_TYPES_GLOBAL[@]}"; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
resource_type_exists "$kind" || continue
|
||||
|
||||
NAMES=$("${KCTL[@]}" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
|
||||
[[ -z "$NAMES" ]] && continue
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
export_cluster_resource "$kind" "$name"
|
||||
done <<< "$NAMES"
|
||||
done
|
||||
|
||||
# Filtra gli item di un array JSON kubectl (-o json) tenendo solo quelli
|
||||
# per cui `jq_path` (letto tramite kubectl jsonpath-like con yq/python) ha
|
||||
# un valore presente nella lista NAMESPACES. Stampa i nomi corrispondenti.
|
||||
# $1 = json completo (stdin) $2 = "subjects" | "claimref"
|
||||
filter_by_namespace_field() {
|
||||
local mode="$1"
|
||||
if [[ "$USE_YQ" == true ]]; then
|
||||
if [[ "$mode" == "subjects" ]]; then
|
||||
yq eval '.items[] | select((.subjects // []) | any_c(.namespace == env(NS_MATCH))) | .metadata.name' -
|
||||
else
|
||||
yq eval '.items[] | select(.spec.claimRef.namespace == env(NS_MATCH)) | .metadata.name' -
|
||||
fi
|
||||
else
|
||||
python3 "$_PY_FILTER" "$mode"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- ClusterRoleBinding: solo quelli con subject in uno dei namespace indicati ---
|
||||
if resource_type_exists clusterrolebinding; then
|
||||
MATCHED_CRB=""
|
||||
for ns in "${NAMESPACES[@]}"; do
|
||||
RESULT=$("${KCTL[@]}" get clusterrolebinding -o json 2>/dev/null \
|
||||
| NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field subjects || true)
|
||||
MATCHED_CRB="${MATCHED_CRB}
|
||||
${RESULT}"
|
||||
done
|
||||
MATCHED_CRB=$(echo "$MATCHED_CRB" | sort -u | sed '/^$/d')
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
export_cluster_resource clusterrolebinding "$name"
|
||||
done <<< "$MATCHED_CRB"
|
||||
fi
|
||||
|
||||
# --- PersistentVolume: solo quelli con claimRef in uno dei namespace indicati ---
|
||||
if resource_type_exists persistentvolume; then
|
||||
MATCHED_PV=""
|
||||
for ns in "${NAMESPACES[@]}"; do
|
||||
RESULT=$("${KCTL[@]}" get persistentvolume -o json 2>/dev/null \
|
||||
| NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field claimref || true)
|
||||
MATCHED_PV="${MATCHED_PV}
|
||||
${RESULT}"
|
||||
done
|
||||
MATCHED_PV=$(echo "$MATCHED_PV" | sort -u | sed '/^$/d')
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
export_cluster_resource persistentvolume "$name"
|
||||
done <<< "$MATCHED_PV"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Export completato in: ${OUTPUT_DIR}"
|
||||
echo "Riepilogo: ${SUMMARY_FILE}"
|
||||
Reference in New Issue
Block a user