diff --git a/backend/Dockerfile b/backend/Dockerfile index fda6591..84bdb5a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,6 +8,12 @@ # migrate One-shot Drizzle migrations (used as a k8s initContainer) ARG NODE_VERSION=22 +# The backend's dependency tree has unresolved peer conflicts (e.g. +# x402-stellar wants @stellar/stellar-sdk@^12 while the app uses ^15), so npm +# needs --legacy-peer-deps until that is fixed upstream +# (tracked in https://github.com/SynapsVault/backend/issues). Override with +# --build-arg NPM_FLAGS= once the upstream peer dependency issue is resolved. +ARG NPM_FLAGS=--legacy-peer-deps # Once upstream peer dependency conflicts (x402-stellar vs @stellar/stellar-sdk) # are resolved, drop --legacy-peer-deps by passing --build-arg NPM_FLAGS="" or # leaving NPM_FLAGS empty when the lockfile is clean. diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 93b865c..a31915e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,6 +3,10 @@ # that repository; this file lives in the deploy repo. ARG NODE_VERSION=22 +# The frontend repo has no lockfile and conflicting peer dependencies, which +# crash npm's resolver; --legacy-peer-deps works around it until fixed upstream +# (tracked in https://github.com/SynapsVault/frontend/issues). +ARG NPM_FLAGS=--legacy-peer-deps # Once the upstream lockfile and peer dependencies are resolved, drop the # --legacy-peer-deps flag (override with --build-arg NPM_FLAGS=--legacy-peer-deps if needed). ARG NPM_FLAGS="" diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml index 2d2a92f..3b520ba 100644 --- a/k8s/backend-deployment.yaml +++ b/k8s/backend-deployment.yaml @@ -29,6 +29,31 @@ spec: runAsGroup: 1000 seccompProfile: type: RuntimeDefault + initContainers: + # Applies pending Drizzle migrations before the new version starts. + - name: migrate + image: backend-migrate + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: backend-config + - secretRef: + name: backend-secret + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: tmp + mountPath: /tmp + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" containers: - name: backend image: backend @@ -44,8 +69,12 @@ spec: name: backend-secret securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true capabilities: drop: ["ALL"] + volumeMounts: + - name: tmp + mountPath: /tmp startupProbe: httpGet: path: /health/live @@ -76,3 +105,6 @@ spec: limits: cpu: "1000m" memory: "512Mi" + volumes: + - name: tmp + emptyDir: {} diff --git a/k8s/frontend-deployment.yaml b/k8s/frontend-deployment.yaml index 3538abc..4f19be9 100644 --- a/k8s/frontend-deployment.yaml +++ b/k8s/frontend-deployment.yaml @@ -19,6 +19,12 @@ spec: labels: app: frontend spec: + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + seccompProfile: + type: RuntimeDefault containers: - name: frontend image: frontend @@ -29,6 +35,16 @@ spec: protocol: TCP securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + - name: tmp + mountPath: /tmp startupProbe: httpGet: path: / @@ -59,6 +75,13 @@ spec: limits: cpu: "250m" memory: "128Mi" + volumes: + - name: nginx-cache + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index 4e7f22e..3e61e66 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -13,6 +13,7 @@ resources: - backend-deployment.yaml - backend-service.yaml - frontend-deployment.yaml + - pdb.yaml - ingress.yaml labels: diff --git a/k8s/pdb.yaml b/k8s/pdb.yaml new file mode 100644 index 0000000..51ddfda --- /dev/null +++ b/k8s/pdb.yaml @@ -0,0 +1,23 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: backend + labels: + app: backend +spec: + minAvailable: 1 + selector: + matchLabels: + app: backend +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: frontend + labels: + app: frontend +spec: + minAvailable: 1 + selector: + matchLabels: + app: frontend diff --git a/scripts/restore.sh b/scripts/restore.sh index 43795d0..6a744dc 100644 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -14,6 +14,11 @@ # from the backup came back, then drop it # --work-dir Working directory for downloads (default: mktemp -d) # --keep-work-dir Do not delete the working directory on exit +# --post-restore-sql Optional SQL script to run against Postgres after +# restoring (aliases $CONTRACT_STATE_FILE). Note: this +# is a PostgreSQL .sql script, NOT the on-chain JSON +# snapshots under contracts/ in the backup archive. +# --skip-contract-state Do not apply CONTRACT_STATE_FILE / --post-restore-sql # --skip-contract-state Do not re-apply CONTRACT_STATE_FILE after restore # -y, --yes Skip interactive confirmation when restoring over a # non-throwaway target database @@ -32,7 +37,10 @@ # BACKUP_ENCRYPTION_KEY gpg passphrase used by backup.sh # BACKUP_AGE_IDENTITY_FILE age identity file, for age-encrypted backups # DATABASE_URL Default target database connection string -# CONTRACT_STATE_FILE Optional SQL file to apply after restore +# CONTRACT_STATE_FILE Optional post-restore PostgreSQL .sql file +# (e.g. to update contract ID rows in DB; +# for repointing env/ConfigMap contract IDs, +# use scripts/rollback-contracts.cjs instead) # # Exit codes: # 0 success @@ -56,6 +64,7 @@ die() { } usage() { + sed -n '2,47p' "$0" | sed 's/^# \{0,1\}//' sed -n '2,43p' "$0" | sed 's/^# \{0,1\}//' } @@ -69,6 +78,7 @@ ASSUME_YES="false" WORK_DIR="" KEEP_WORK_DIR="false" SKIP_CONTRACT_STATE="false" +POST_RESTORE_SQL_ARG="" while [[ $# -gt 0 ]]; do case "$1" in @@ -99,6 +109,11 @@ while [[ $# -gt 0 ]]; do KEEP_WORK_DIR="true" shift ;; + --post-restore-sql) + [[ $# -ge 2 ]] || die 1 "--post-restore-sql requires a path to a .sql file" + POST_RESTORE_SQL_ARG="$2" + shift 2 + ;; --skip-contract-state) SKIP_CONTRACT_STATE="true" shift @@ -349,16 +364,20 @@ restore_into() { apply_contract_state() { local url="$1" if [[ "$SKIP_CONTRACT_STATE" == "true" ]]; then - log "skipping contract state re-application (--skip-contract-state)" + log "skipping post-restore SQL application (--skip-contract-state)" return 0 fi - if [[ -z "$CONTRACT_STATE_FILE" ]]; then - log "no CONTRACT_STATE_FILE configured; skipping contract state re-application" + local sql_file="${POST_RESTORE_SQL_ARG:-${CONTRACT_STATE_FILE:-}}" + if [[ -z "$sql_file" ]]; then + log "no CONTRACT_STATE_FILE / --post-restore-sql configured; skipping post-restore SQL" return 0 fi - [[ -f "$CONTRACT_STATE_FILE" ]] || die 4 "contract state file not found: $CONTRACT_STATE_FILE" - log "re-applying contract state from $CONTRACT_STATE_FILE" - psql_exec "$url" -f "$CONTRACT_STATE_FILE" || die 4 "failed to re-apply contract state" + [[ -f "$sql_file" ]] || die 4 "post-restore SQL file not found: $sql_file" + if [[ "$sql_file" == *.json ]]; then + die 1 "CONTRACT_STATE_FILE ($sql_file) must be a PostgreSQL .sql script, not a JSON contract snapshot (use scripts/rollback-contracts.cjs to repoint on-chain contract IDs)" + fi + log "applying post-restore SQL from $sql_file" + psql_exec "$url" -f "$sql_file" || die 4 "failed to apply post-restore SQL from $sql_file" } # ---------------------------------------------------------------------------