k8s nfs backup: exclude namespaces from scaling, isolate per-run state, harden failure handling
- Add EXCLUDED_NAMESPACES (default kube-system,kube-public,kube-node-lease,default); excluded folders are backed up but never scaled - Use a per-run state dir via mktemp -d so stale .state files from an earlier run can't leak into restores - Partial folder-backup failure now exits 0 (only zero successes fails); retention runs even on partial failure - Remove dead SCALE_TIMEOUT_SECONDS config; document SCALE_WAIT_SECONDS - Add .gitattributes to keep *.sh at LF - Extend test harness to 44 passing assertions covering exclusions, per-run state, partial/all-failed/empty scenarios
This commit is contained in:
@@ -19,6 +19,7 @@ BACKUP_PASSWORD="${BACKUP_PASSWORD:-}"
|
||||
KUBECTL_BIN="${KUBECTL_BIN:-kubectl}"
|
||||
KUBE_CONTEXT="${KUBE_CONTEXT:-}"
|
||||
WORKLOAD_KINDS="${WORKLOAD_KINDS:-deployment,statefulset,replicaset}"
|
||||
EXCLUDED_NAMESPACES="${EXCLUDED_NAMESPACES-kube-system,kube-public,kube-node-lease,default}"
|
||||
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-nfs-backup}"
|
||||
ARCHIVE_TS_FORMAT="${ARCHIVE_TS_FORMAT:-%Y%m%d_%H%M%S}"
|
||||
SEVENZ_METHOD="${SEVENZ_METHOD:-lzma2}"
|
||||
@@ -26,7 +27,6 @@ SEVENZ_LEVEL="${SEVENZ_LEVEL:-9}"
|
||||
SEVENZ_HEADER_ENCRYPT="${SEVENZ_HEADER_ENCRYPT:-on}"
|
||||
SEVENZ_THREADS="${SEVENZ_THREADS:-on}"
|
||||
SEVENZ_BIN="${SEVENZ_BIN:-7z}"
|
||||
SCALE_TIMEOUT_SECONDS="${SCALE_TIMEOUT_SECONDS:-600}"
|
||||
SCALE_RETRY_COUNT="${SCALE_RETRY_COUNT:-3}"
|
||||
SCALE_RETRY_DELAY_SECONDS="${SCALE_RETRY_DELAY_SECONDS:-5}"
|
||||
SCALE_WAIT_SECONDS="${SCALE_WAIT_SECONDS:-30}"
|
||||
@@ -37,6 +37,7 @@ NOTIFY_TITLE="${NOTIFY_TITLE:-Kubernetes}"
|
||||
NOTIFY_ASSET="${NOTIFY_ASSET:-kube config}"
|
||||
NOTIFY_CALLER="${NOTIFY_CALLER:-Kubernetes config backup}"
|
||||
CLEANUP_KEEP_COUNT="${CLEANUP_KEEP_COUNT:-4}"
|
||||
RUN_STATE_DIR=""
|
||||
|
||||
KUBECTL_ARGS=()
|
||||
if [[ -n "$KUBE_CONTEXT" ]]; then
|
||||
@@ -45,6 +46,7 @@ fi
|
||||
|
||||
declare -a WORKLOAD_KIND_LIST=()
|
||||
declare -A NAMESPACE_MAP=()
|
||||
declare -A EXCLUDED_NAMESPACE_MAP=()
|
||||
|
||||
total_folders=0
|
||||
mapped_folders=0
|
||||
@@ -75,9 +77,23 @@ parse_workload_kinds() {
|
||||
fi
|
||||
}
|
||||
|
||||
parse_excluded_namespaces() {
|
||||
local raw
|
||||
local cleaned
|
||||
local namespace
|
||||
IFS=',' read -r -a raw <<< "$EXCLUDED_NAMESPACES"
|
||||
for namespace in "${raw[@]}"; do
|
||||
cleaned="$(trim "${namespace,,}")"
|
||||
if [[ -n "$cleaned" ]]; then
|
||||
EXCLUDED_NAMESPACE_MAP["$cleaned"]=1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
validate_inputs() {
|
||||
require_cmd "$KUBECTL_BIN"
|
||||
require_cmd "$SEVENZ_BIN"
|
||||
require_cmd "mktemp"
|
||||
if [[ -n "$NOTIFY_SUCCESS_URL" || -n "$NOTIFY_FAILURE_URL" ]]; then
|
||||
require_cmd "curl"
|
||||
fi
|
||||
@@ -96,6 +112,10 @@ validate_inputs() {
|
||||
|
||||
mkdir -p "$BACKUP_OUTPUT_PATH"
|
||||
mkdir -p "$TMP_STATE_DIR"
|
||||
if ! RUN_STATE_DIR="$(mktemp -d "${TMP_STATE_DIR%/}/run-XXXXXXXXXX")"; then
|
||||
die "Unable to create a per-run state directory under ${TMP_STATE_DIR}"
|
||||
fi
|
||||
log_debug "Using isolated run state directory: ${RUN_STATE_DIR}"
|
||||
|
||||
if ! _kubectl get namespaces >/dev/null 2>&1; then
|
||||
die "Unable to list Kubernetes namespaces with ${KUBECTL_BIN}"
|
||||
@@ -112,6 +132,9 @@ load_namespaces() {
|
||||
|
||||
namespace_for_folder() {
|
||||
local folder_name="${1:?folder name required}"
|
||||
if [[ -n "${EXCLUDED_NAMESPACE_MAP[$folder_name]:-}" ]]; then
|
||||
return 2
|
||||
fi
|
||||
if [[ -n "${NAMESPACE_MAP[$folder_name]:-}" ]]; then
|
||||
printf '%s' "$folder_name"
|
||||
return 0
|
||||
@@ -121,7 +144,7 @@ namespace_for_folder() {
|
||||
|
||||
state_file_for_namespace() {
|
||||
local namespace="${1:?namespace required}"
|
||||
printf '%s/%s.state' "$TMP_STATE_DIR" "$namespace"
|
||||
printf '%s/%s.state' "$RUN_STATE_DIR" "$namespace"
|
||||
}
|
||||
|
||||
capture_replicas_state() {
|
||||
@@ -207,7 +230,7 @@ restore_namespace_replicas() {
|
||||
}
|
||||
|
||||
restore_all_remaining() {
|
||||
if [[ -z "${TMP_STATE_DIR:-}" ]] || [[ ! -d "${TMP_STATE_DIR:-}" ]]; then
|
||||
if [[ -z "${RUN_STATE_DIR:-}" ]] || [[ ! -d "${RUN_STATE_DIR:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -216,7 +239,7 @@ restore_all_remaining() {
|
||||
local kind name replicas
|
||||
local found=0
|
||||
|
||||
for state_file in "${TMP_STATE_DIR}"/*.state; do
|
||||
for state_file in "${RUN_STATE_DIR}"/*.state; do
|
||||
[[ -f "$state_file" ]] || continue
|
||||
found=1
|
||||
namespace="$(basename "$state_file" .state)"
|
||||
@@ -237,6 +260,10 @@ restore_all_remaining() {
|
||||
if [[ "$found" -eq 1 ]]; then
|
||||
log_info "EXIT cleanup complete - all remaining workloads restored"
|
||||
fi
|
||||
|
||||
if ! rmdir -- "$RUN_STATE_DIR" 2>/dev/null; then
|
||||
log_warn "Run state directory is not empty; leaving it in place: ${RUN_STATE_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_path_for_run() {
|
||||
@@ -393,6 +420,7 @@ process_folder() {
|
||||
local namespace=""
|
||||
local state_file=""
|
||||
local has_mapping=0
|
||||
local namespace_match_status=0
|
||||
|
||||
folder_name="$(basename "$folder_path")"
|
||||
total_folders=$((total_folders + 1))
|
||||
@@ -410,8 +438,13 @@ process_folder() {
|
||||
log_info "Waiting ${SCALE_WAIT_SECONDS} seconds for namespace '${namespace}' to scale down..."
|
||||
sleep "$SCALE_WAIT_SECONDS"
|
||||
else
|
||||
namespace_match_status=$?
|
||||
unmapped_folders=$((unmapped_folders + 1))
|
||||
log_warn "No namespace matched folder '${folder_name}'. Running backup only."
|
||||
if (( namespace_match_status == 2 )); then
|
||||
log_warn "Folder '${folder_name}' matches an excluded namespace; skipping Kubernetes scale actions and running backup only."
|
||||
else
|
||||
log_warn "No namespace matched folder '${folder_name}'. Running backup only."
|
||||
fi
|
||||
fi
|
||||
|
||||
if backup_folder "$folder_path" "$archive_path"; then
|
||||
@@ -436,6 +469,7 @@ print_summary() {
|
||||
|
||||
main() {
|
||||
parse_workload_kinds
|
||||
parse_excluded_namespaces
|
||||
validate_inputs
|
||||
load_namespaces
|
||||
|
||||
@@ -467,12 +501,20 @@ main() {
|
||||
local size_mb
|
||||
size_mb="$(backup_size_mb "$total_backup_size_bytes")"
|
||||
|
||||
if (( backup_successes == 0 )); then
|
||||
send_backup_failure_notification \
|
||||
"$NOTIFY_FAILURE_URL" \
|
||||
"No NFS folders were backed up successfully" \
|
||||
"$size_mb" || true
|
||||
die "No folder backups succeeded" 1
|
||||
fi
|
||||
|
||||
if (( backup_failures > 0 )); then
|
||||
send_backup_failure_notification \
|
||||
"$NOTIFY_FAILURE_URL" \
|
||||
"Error while backing up NFS folders" \
|
||||
"Some NFS folders failed to back up" \
|
||||
"$size_mb" || true
|
||||
die "One or more folder backups failed" 1
|
||||
log_warn "Some folder backups failed; continuing because ${backup_successes} folder backup(s) succeeded"
|
||||
fi
|
||||
|
||||
cleanup_archives || true
|
||||
|
||||
Reference in New Issue
Block a user