diff --git a/backup/backup-k8s-apps.sh b/backup/backup-k8s-apps.sh index 7120c27..7b059ce 100755 --- a/backup/backup-k8s-apps.sh +++ b/backup/backup-k8s-apps.sh @@ -26,6 +26,26 @@ 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 # -------------------------------------------------- @@ -132,7 +152,9 @@ log_status() { # -------------------------------------------------- check_resources() { 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}') if [ "$avail_mb" -lt 1536 ]; then echo " ❌ RAM available ${avail_mb}MB < 1536MB threshold — aborting." diff --git a/backup/restore-k8s-apps.sh b/backup/restore-k8s-apps.sh index 33039a4..21e2bc4 100755 --- a/backup/restore-k8s-apps.sh +++ b/backup/restore-k8s-apps.sh @@ -36,6 +36,26 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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" VALID_APPS="frappe odoo nextcloud mautic n8n" @@ -98,7 +118,9 @@ echo "=========================================" check_resources() { 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}') if [ "$avail_mb" -lt 1536 ] || [ "$avail_gb" -lt 10 ]; then 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 - # ---- 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 echo -n " 🔑 Applying Secret ... " kubectl apply -f "$APP_DIR/secret.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; } fi - if [ -f "$APP_DIR/manifests.yaml" ]; then - echo -n " 📄 Applying manifests ... " - kubectl apply -f "$APP_DIR/manifests.yaml" &>/dev/null && echo "✅" || { echo "⚠️ FAILED"; app_ok=false; } - fi + # NOTE: manifests.yaml (Deployment/Service/ConfigMap/Ingress specs) is + # captured by backup-k8s-apps.sh but deliberately NOT re-applied here. + # Same-cluster restore-in-place never needs to reconcile those specs — + # 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 ---- prior_replicas=$(kubectl get deployment "$app_deploy" -n "$ns" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo 1) diff --git a/k8s/management-platform-deployment.yaml b/k8s/management-platform-deployment.yaml index 981d0bf..f0f44b9 100644 --- a/k8s/management-platform-deployment.yaml +++ b/k8s/management-platform-deployment.yaml @@ -22,7 +22,7 @@ spec: app.kubernetes.io/managed-by: navitrends-pfe-migration owner: ameni-boukattaya spec: - serviceAccountName: management-platform-viewer-sa + serviceAccountName: management-platform-sa containers: - name: management-platform image: management-platform:latest @@ -47,6 +47,9 @@ spec: - mountPath: /app/config.py name: config-py readOnly: true + - mountPath: /usr/local/bin/k3s + name: k3s-bin + readOnly: true volumes: - name: docker-sock hostPath: @@ -68,3 +71,7 @@ spec: hostPath: path: /root/management-platform/config.py type: File + - name: k3s-bin + hostPath: + path: /usr/local/bin/k3s + type: File diff --git a/management-platform-backup-rbac.yaml b/management-platform-backup-rbac.yaml new file mode 100644 index 0000000..b869c66 --- /dev/null +++ b/management-platform-backup-rbac.yaml @@ -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/ 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 diff --git a/management-platform-viewer-rbac.yaml b/management-platform-viewer-rbac.yaml index 3647c8b..a0556a5 100644 --- a/management-platform-viewer-rbac.yaml +++ b/management-platform-viewer-rbac.yaml @@ -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 kind: ClusterRole metadata: @@ -36,7 +27,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -53,7 +44,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -70,7 +61,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -87,7 +78,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -104,7 +95,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -121,7 +112,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -138,7 +129,7 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform --- apiVersion: rbac.authorization.k8s.io/v1 @@ -155,5 +146,5 @@ roleRef: name: management-platform-viewer-role subjects: - kind: ServiceAccount - name: management-platform-viewer-sa + name: management-platform-sa namespace: management-platform