Compare commits
2 Commits
8255527254
...
a6fb9e5e18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6fb9e5e18 | ||
|
|
f3f08c3ef6 |
@@ -26,6 +26,26 @@
|
|||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
|
# When run inside the management-platform pod, only the static k3s binary is
|
||||||
|
# hostPath-mounted (no separate kubectl binary) — fall back to `k3s kubectl`.
|
||||||
|
# IMPORTANT: this pod also has a pre-existing rw hostPath mount of /root (for
|
||||||
|
# unrelated file-browsing features), which means the host's own root-owned
|
||||||
|
# ~/.kube/config (full cluster-admin) is silently visible inside the pod too.
|
||||||
|
# `--kubeconfig=/dev/null` is REQUIRED here to stop k3s kubectl from picking
|
||||||
|
# that up as a base config — without it, every command below silently runs
|
||||||
|
# as cluster-admin instead of the scoped management-platform-sa token,
|
||||||
|
# defeating the RBAC in management-platform-backup-rbac.yaml entirely.
|
||||||
|
if ! command -v kubectl &>/dev/null && command -v k3s &>/dev/null; then
|
||||||
|
kubectl() {
|
||||||
|
k3s kubectl \
|
||||||
|
--kubeconfig=/dev/null \
|
||||||
|
--server=https://kubernetes.default.svc \
|
||||||
|
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
|
||||||
|
--token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Parse --apps
|
# Parse --apps
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -132,7 +152,9 @@ log_status() {
|
|||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
check_resources() {
|
check_resources() {
|
||||||
local avail_mb avail_gb
|
local avail_mb avail_gb
|
||||||
avail_mb=$(free -m | awk '/^Mem:/{print $7}')
|
# /proc/meminfo instead of `free` — `free` isn't present in the
|
||||||
|
# management-platform pod's minimal image, /proc/meminfo always is.
|
||||||
|
avail_mb=$(awk '/^MemAvailable:/{printf "%d", $2/1024}' /proc/meminfo)
|
||||||
avail_gb=$(df -BG / | awk 'NR==2{gsub("G","",$4); print $4}')
|
avail_gb=$(df -BG / | awk 'NR==2{gsub("G","",$4); print $4}')
|
||||||
if [ "$avail_mb" -lt 1536 ]; then
|
if [ "$avail_mb" -lt 1536 ]; then
|
||||||
echo " ❌ RAM available ${avail_mb}MB < 1536MB threshold — aborting."
|
echo " ❌ RAM available ${avail_mb}MB < 1536MB threshold — aborting."
|
||||||
|
|||||||
@@ -36,6 +36,26 @@ set -uo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# When run inside the management-platform pod, only the static k3s binary is
|
||||||
|
# hostPath-mounted (no separate kubectl binary) — fall back to `k3s kubectl`.
|
||||||
|
# IMPORTANT: this pod also has a pre-existing rw hostPath mount of /root (for
|
||||||
|
# unrelated file-browsing features), which means the host's own root-owned
|
||||||
|
# ~/.kube/config (full cluster-admin) is silently visible inside the pod too.
|
||||||
|
# `--kubeconfig=/dev/null` is REQUIRED here to stop k3s kubectl from picking
|
||||||
|
# that up as a base config — without it, every command below silently runs
|
||||||
|
# as cluster-admin instead of the scoped management-platform-sa token,
|
||||||
|
# defeating the RBAC in management-platform-backup-rbac.yaml entirely.
|
||||||
|
if ! command -v kubectl &>/dev/null && command -v k3s &>/dev/null; then
|
||||||
|
kubectl() {
|
||||||
|
k3s kubectl \
|
||||||
|
--kubeconfig=/dev/null \
|
||||||
|
--server=https://kubernetes.default.svc \
|
||||||
|
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
|
||||||
|
--token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
APPS_FILTER="all"
|
APPS_FILTER="all"
|
||||||
VALID_APPS="frappe odoo nextcloud mautic n8n"
|
VALID_APPS="frappe odoo nextcloud mautic n8n"
|
||||||
|
|
||||||
@@ -98,7 +118,9 @@ echo "========================================="
|
|||||||
|
|
||||||
check_resources() {
|
check_resources() {
|
||||||
local avail_mb avail_gb
|
local avail_mb avail_gb
|
||||||
avail_mb=$(free -m | awk '/^Mem:/{print $7}')
|
# /proc/meminfo instead of `free` — `free` isn't present in the
|
||||||
|
# management-platform pod's minimal image, /proc/meminfo always is.
|
||||||
|
avail_mb=$(awk '/^MemAvailable:/{printf "%d", $2/1024}' /proc/meminfo)
|
||||||
avail_gb=$(df -BG / | awk 'NR==2{gsub("G","",$4); print $4}')
|
avail_gb=$(df -BG / | awk 'NR==2{gsub("G","",$4); print $4}')
|
||||||
if [ "$avail_mb" -lt 1536 ] || [ "$avail_gb" -lt 10 ]; then
|
if [ "$avail_mb" -lt 1536 ] || [ "$avail_gb" -lt 10 ]; then
|
||||||
echo " ❌ Resource safety threshold hit (RAM=${avail_mb}MB, Disk=${avail_gb}GB) — aborting."
|
echo " ❌ Resource safety threshold hit (RAM=${avail_mb}MB, Disk=${avail_gb}GB) — aborting."
|
||||||
@@ -139,15 +161,21 @@ for app in $ALL_APPS; do
|
|||||||
|
|
||||||
app_ok=true
|
app_ok=true
|
||||||
|
|
||||||
# ---- 1. Secret + manifests (apply — idempotent) ----
|
# ---- 1. Secret (apply — idempotent update, no create: every Secret
|
||||||
|
# restored here already exists in a same-cluster restore) ----
|
||||||
if [ -f "$APP_DIR/secret.yaml" ]; then
|
if [ -f "$APP_DIR/secret.yaml" ]; then
|
||||||
echo -n " 🔑 Applying Secret ... "
|
echo -n " 🔑 Applying Secret ... "
|
||||||
kubectl apply -f "$APP_DIR/secret.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; }
|
kubectl apply -f "$APP_DIR/secret.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; }
|
||||||
fi
|
fi
|
||||||
if [ -f "$APP_DIR/manifests.yaml" ]; then
|
# NOTE: manifests.yaml (Deployment/Service/ConfigMap/Ingress specs) is
|
||||||
echo -n " 📄 Applying manifests ... "
|
# captured by backup-k8s-apps.sh but deliberately NOT re-applied here.
|
||||||
kubectl apply -f "$APP_DIR/manifests.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; }
|
# Same-cluster restore-in-place never needs to reconcile those specs —
|
||||||
fi
|
# only Secret values, PVC data, and DB content actually change — and
|
||||||
|
# the pod's RBAC (management-platform-backup-role) intentionally grants
|
||||||
|
# no write on Deployments/Services/ConfigMaps/Ingresses beyond the
|
||||||
|
# deployments/scale subresource used in step 2/4 below. manifests.yaml
|
||||||
|
# is kept in every backup purely as a captured reference for a future
|
||||||
|
# fresh-cluster/DR restore path, which is explicitly out of scope today.
|
||||||
|
|
||||||
# ---- 2. Scale app down ----
|
# ---- 2. Scale app down ----
|
||||||
prior_replicas=$(kubectl get deployment "$app_deploy" -n "$ns" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo 1)
|
prior_replicas=$(kubectl get deployment "$app_deploy" -n "$ns" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo 1)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ spec:
|
|||||||
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
owner: ameni-boukattaya
|
owner: ameni-boukattaya
|
||||||
spec:
|
spec:
|
||||||
serviceAccountName: management-platform-viewer-sa
|
serviceAccountName: management-platform-sa
|
||||||
containers:
|
containers:
|
||||||
- name: management-platform
|
- name: management-platform
|
||||||
image: management-platform:latest
|
image: management-platform:latest
|
||||||
@@ -47,6 +47,9 @@ spec:
|
|||||||
- mountPath: /app/config.py
|
- mountPath: /app/config.py
|
||||||
name: config-py
|
name: config-py
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
- mountPath: /usr/local/bin/k3s
|
||||||
|
name: k3s-bin
|
||||||
|
readOnly: true
|
||||||
volumes:
|
volumes:
|
||||||
- name: docker-sock
|
- name: docker-sock
|
||||||
hostPath:
|
hostPath:
|
||||||
@@ -68,3 +71,7 @@ spec:
|
|||||||
hostPath:
|
hostPath:
|
||||||
path: /root/management-platform/config.py
|
path: /root/management-platform/config.py
|
||||||
type: File
|
type: File
|
||||||
|
- name: k3s-bin
|
||||||
|
hostPath:
|
||||||
|
path: /usr/local/bin/k3s
|
||||||
|
type: File
|
||||||
|
|||||||
149
management-platform-backup-rbac.yaml
Normal file
149
management-platform-backup-rbac.yaml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-role
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
rules:
|
||||||
|
# Read + resolve exec targets (deploy/<name> shorthand needs get on the
|
||||||
|
# Deployment plus get/list on the Pods it selects).
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods", "persistentvolumeclaims"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
# Read-only manifest capture for backup (`kubectl get deployment,service,
|
||||||
|
# configmap,ingress -o yaml`). Services and Deployments are already
|
||||||
|
# readable via management-platform-viewer-role (this SA also holds that
|
||||||
|
# binding — see management-platform-viewer-rbac.yaml); configmaps and
|
||||||
|
# ingresses are not, so they're granted explicitly here. Read-only, no
|
||||||
|
# write — matches "no write beyond scale" for everything but Secrets.
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["configmaps"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["networking.k8s.io"]
|
||||||
|
resources: ["ingresses"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
# Run pg_dump/mysqldump/psql/mysql/tar inside existing DB and app pods,
|
||||||
|
# and inside the short-lived loader pod restore creates for PVC repopulation.
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods/exec"]
|
||||||
|
verbs: ["create"]
|
||||||
|
# Create/delete ONLY the short-lived alpine loader pod restore uses to
|
||||||
|
# repopulate a PVC after the app Deployment is scaled to 0 (no running
|
||||||
|
# app pod exists at that point to exec into — see restore-k8s-apps.sh
|
||||||
|
# step 3). Does not grant any write on the app/DB Deployments' own pods.
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["create", "delete"]
|
||||||
|
# Scale only — deliberately NOT "update"/"patch" on the deployments
|
||||||
|
# resource itself, so this role cannot change a Deployment's image,
|
||||||
|
# env, mounts, etc. Same-cluster restore never reconciles Deployment/
|
||||||
|
# Service/ConfigMap/Ingress specs; those are captured in each backup's
|
||||||
|
# manifests.yaml for reference but are not re-applied by restore-k8s-apps.sh.
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments/scale"]
|
||||||
|
verbs: ["get", "update", "patch"]
|
||||||
|
# Read DB credentials for pg_dump/mysqldump/psql/mysql (backup + restore),
|
||||||
|
# and roll a Secret's values back to what's in the backup during restore
|
||||||
|
# (kubectl apply -f secret.yaml). No create/delete — every Secret this
|
||||||
|
# role touches already exists; restore only ever updates in place.
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get", "list", "update", "patch"]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-binding
|
||||||
|
namespace: n8n
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: management-platform-backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-binding
|
||||||
|
namespace: odoo
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: management-platform-backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-binding
|
||||||
|
namespace: mautic
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: management-platform-backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-binding
|
||||||
|
namespace: nextcloud
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: management-platform-backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: management-platform-backup-binding
|
||||||
|
namespace: erpnext
|
||||||
|
labels:
|
||||||
|
owner: ameni-boukattaya
|
||||||
|
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: management-platform-backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: management-platform-sa
|
||||||
|
namespace: management-platform
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: management-platform-viewer-sa
|
|
||||||
namespace: management-platform
|
|
||||||
labels:
|
|
||||||
owner: ameni-boukattaya
|
|
||||||
app.kubernetes.io/managed-by: navitrends-pfe-migration
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
metadata:
|
metadata:
|
||||||
@@ -36,7 +27,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -53,7 +44,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -70,7 +61,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -87,7 +78,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -104,7 +95,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -121,7 +112,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -138,7 +129,7 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
@@ -155,5 +146,5 @@ roleRef:
|
|||||||
name: management-platform-viewer-role
|
name: management-platform-viewer-role
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: management-platform-viewer-sa
|
name: management-platform-sa
|
||||||
namespace: management-platform
|
namespace: management-platform
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ def api_backup_run():
|
|||||||
'message': 'Manual backup can only be triggered from the main server platform.'
|
'message': 'Manual backup can only be triggered from the main server platform.'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
script = '/root/backup-myapps.sh'
|
script = '/root/CloudOps/backup/backup-k8s-apps.sh'
|
||||||
if not os.path.exists(script):
|
if not os.path.exists(script):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -818,11 +818,14 @@ def restore_start():
|
|||||||
if not os.path.exists(backup_path):
|
if not os.path.exists(backup_path):
|
||||||
return jsonify({'error': f'Not found: {backup_path}'}), 400
|
return jsonify({'error': f'Not found: {backup_path}'}), 400
|
||||||
|
|
||||||
restore_script_local = os.path.join(
|
# Absolute /root path (not image-relative like the old restore-myapps.sh
|
||||||
os.path.dirname(os.path.abspath(__file__)), 'restore-myapps.sh'
|
# was) — backup/restore-k8s-apps.sh lives in the CloudOps repo's backup/
|
||||||
)
|
# folder, a sibling of platform/, so it's never baked into the Docker
|
||||||
|
# image; it's reached the same way api_backup_run's script is, via the
|
||||||
|
# pod's existing rw hostPath mount of /root.
|
||||||
|
restore_script_local = '/root/CloudOps/backup/restore-k8s-apps.sh'
|
||||||
if not os.path.exists(restore_script_local):
|
if not os.path.exists(restore_script_local):
|
||||||
return jsonify({'error': f'restore-myapps.sh not found at {restore_script_local}'}), 500
|
return jsonify({'error': f'restore-k8s-apps.sh not found at {restore_script_local}'}), 500
|
||||||
|
|
||||||
if target == 'local':
|
if target == 'local':
|
||||||
hostname = os.uname().nodename
|
hostname = os.uname().nodename
|
||||||
@@ -835,9 +838,9 @@ def restore_start():
|
|||||||
f"mkdir -p {session_dir} && "
|
f"mkdir -p {session_dir} && "
|
||||||
f"echo 'Extracting backup...' && "
|
f"echo 'Extracting backup...' && "
|
||||||
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
||||||
f"cp {restore_script_local} {session_dir}/restore-myapps.sh && "
|
f"cp {restore_script_local} {session_dir}/restore-k8s-apps.sh && "
|
||||||
f"chmod +x {session_dir}/restore-myapps.sh && "
|
f"chmod +x {session_dir}/restore-k8s-apps.sh && "
|
||||||
f"cd {session_dir} && bash restore-myapps.sh{apps_flag} ; "
|
f"cd {session_dir} && bash restore-k8s-apps.sh{apps_flag} ; "
|
||||||
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -865,12 +868,12 @@ def restore_start():
|
|||||||
f"echo 'Copying backup archive...' && "
|
f"echo 'Copying backup archive...' && "
|
||||||
f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && "
|
f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && "
|
||||||
f"echo 'Copying restore script...' && "
|
f"echo 'Copying restore script...' && "
|
||||||
f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-myapps.sh && "
|
f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-k8s-apps.sh && "
|
||||||
f"echo 'Running restore on {remote_ip}:{remote_port}...' && "
|
f"echo 'Running restore on {remote_ip}:{remote_port}...' && "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
||||||
f"'set -e && cd {remote_dest} && "
|
f"'set -e && cd {remote_dest} && "
|
||||||
f"tar -xzf {backup_file} --strip-components=1 && "
|
f"tar -xzf {backup_file} --strip-components=1 && "
|
||||||
f"chmod +x restore-myapps.sh && bash restore-myapps.sh{apps_flag}' ; "
|
f"chmod +x restore-k8s-apps.sh && bash restore-k8s-apps.sh{apps_flag}' ; "
|
||||||
f"EXIT=$? ; "
|
f"EXIT=$? ; "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
||||||
f"exit $EXIT"
|
f"exit $EXIT"
|
||||||
|
|||||||
@@ -56,8 +56,12 @@ def _ssh_main(remote_cmd, timeout=30):
|
|||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_local_backups():
|
def get_local_backups():
|
||||||
|
# myapps-backup-* is the legacy Docker-era lineage; myapps-k8s-backup-*
|
||||||
|
# is the k8s-native one (backup-k8s-apps.sh). Listed together, sorted
|
||||||
|
# by mtime, so history from both stays visible in the UI.
|
||||||
stdout, _ = _ssh_main(
|
stdout, _ = _ssh_main(
|
||||||
"ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20"
|
"ls -t /root/backups/myapps-backup-*.tar.gz "
|
||||||
|
"/root/backups/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20"
|
||||||
)
|
)
|
||||||
files = []
|
files = []
|
||||||
if stdout:
|
if stdout:
|
||||||
@@ -77,7 +81,8 @@ def get_vm_backups():
|
|||||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
||||||
f"-o BatchMode=yes "
|
f"-o BatchMode=yes "
|
||||||
f"{VM_USER}@{VM_HOST} "
|
f"{VM_USER}@{VM_HOST} "
|
||||||
f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz 2>/dev/null | head -20'"
|
f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz "
|
||||||
|
f"/backups/cloudproject/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20'"
|
||||||
)
|
)
|
||||||
stdout, _ = _run(cmd, timeout=25)
|
stdout, _ = _run(cmd, timeout=25)
|
||||||
if stdout:
|
if stdout:
|
||||||
@@ -90,7 +95,10 @@ def get_vm_backups():
|
|||||||
else:
|
else:
|
||||||
backup_dir = '/backups/cloudproject'
|
backup_dir = '/backups/cloudproject'
|
||||||
if os.path.exists(backup_dir):
|
if os.path.exists(backup_dir):
|
||||||
files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
files = (
|
||||||
|
glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
||||||
|
+ glob.glob(f'{backup_dir}/myapps-k8s-backup-*.tar.gz')
|
||||||
|
)
|
||||||
files.sort(key=os.path.getmtime, reverse=True)
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
vm_backups = [os.path.basename(f) for f in files[:20]]
|
vm_backups = [os.path.basename(f) for f in files[:20]]
|
||||||
return vm_backups
|
return vm_backups
|
||||||
@@ -408,7 +416,11 @@ def _sha256_file(path):
|
|||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def delete_backup(backup_file, source='local'):
|
def delete_backup(backup_file, source='local'):
|
||||||
if not re.match(r'^myapps-backup-\d{8}_\d{6}\.tar\.gz$', backup_file):
|
# Accepts both the legacy Docker-era lineage (myapps-backup-*) and the
|
||||||
|
# k8s-native one (myapps-k8s-backup-*, from backup-k8s-apps.sh). Still a
|
||||||
|
# tight anchor match — this guards against path traversal in a filename
|
||||||
|
# that ends up interpolated into a shell/filesystem path below.
|
||||||
|
if not re.match(r'^myapps-(k8s-)?backup-\d{8}_\d{6}\.tar\.gz$', backup_file):
|
||||||
return False, f'Invalid backup filename: {backup_file}'
|
return False, f'Invalid backup filename: {backup_file}'
|
||||||
|
|
||||||
if source == 'local':
|
if source == 'local':
|
||||||
|
|||||||
Reference in New Issue
Block a user