nc-db 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nc_db/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # nc_db
2
+ PostgreSQL schema, lightweight Python DB client, tests, and Kubernetes manifests for the Neural Context project.
3
+
4
+ This repository provides:
5
+ - A thin, typed Python client for JSONB-first tables (insert/update/select/delete) under a configurable schema.
6
+ - Test suite that spins up isolated schemas to validate behavior and edge cases.
7
+ - Kubernetes manifests (Kustomize) to run Postgres locally or in-cluster, plus a safe reset script.
8
+
9
+ ## Contents
10
+
11
+ ```
12
+ src/db/ # Python client: Db, DbConfig, SQL helpers
13
+ src/tests/ # Pytest suite and fixtures (isolated schemas)
14
+ k8s/ # K8s StatefulSet, init SQL, and ops scripts
15
+ pyproject.toml # Poetry + pytest configuration
16
+ .github/ # Issue and PR templates
17
+ README.md # This file (schema diagram below)
18
+ ```
19
+
20
+ ## Prerequisites
21
+
22
+ - Python 3.12+
23
+ - PostgreSQL 15+ available locally, or run via the provided K8s manifests
24
+
25
+ Default development credentials used in examples/tests:
26
+ `host=localhost, port=5432, db=nc-data, user=nc_admin, password=bob`.
27
+
28
+ ## Quick Start
29
+
30
+ 1) Install dependencies with Poetry:
31
+
32
+ ```bash
33
+ poetry install
34
+ ```
35
+
36
+ 2) Ensure Postgres is reachable using the defaults above. You can run it via Kubernetes (see below) or your local Postgres.
37
+
38
+ 3) Smoke test the Python client:
39
+
40
+ ```bash
41
+ poetry run python -c "from src.db.db import Db; print(Db().get('scg', ids=[]))"
42
+ ```
43
+
44
+ ## Running Tests
45
+
46
+ ```bash
47
+ kubectl apply -k k8s/postgres/overlays/ephemeral
48
+ kubectl -n nc_db wait --for=condition=ready pod -l app=nc-data --timeout=90s
49
+ kubectl -n nc_db port-forward svc/postgres 5432:5432
50
+ ```
51
+
52
+ ```bash
53
+ poetry run pytest -q
54
+ ```
55
+
56
+ - To run a single test: `poetry run pytest -k test_update_merge -q`
57
+ - Tests create per-test schemas and tables; they do not require pre-existing app tables.
58
+
59
+ ## Python Client Overview
60
+
61
+ The client favors tables shaped as `(id BIGSERIAL PRIMARY KEY, body JSONB NOT NULL)` and enforces `body.id == id` on writes/reads.
62
+
63
+ Basic usage:
64
+
65
+ ```python
66
+ from nc_db.db.db import Db
67
+ from nc_db.db.DbConfig import DbConfig
68
+
69
+ db = Db(schema="nc", config=DbConfig(
70
+ user="my_user", password="my_password", host="localhost", port=5432, dbname="my_database"
71
+ ))
72
+
73
+ # Insert with or without explicit id
74
+ id1 = db.insert("scg", {"body": {"name": "alpha"}})
75
+ id2 = db.insert("scg", {"body": {"id": 42, "name": "explicit"}})
76
+
77
+ # Update (merge JSON into existing body or replace entirely)
78
+ db.update("scg", ids=[id1], items=[{"body": {"name": "alpha-2"}}], mode="merge")
79
+
80
+ # Read (optionally map rows to models)
81
+ rows = db.get("scg") # [{"id": 1, "body": {...}}, ...]
82
+
83
+ # Delete
84
+ db.delete("scg", [id2])
85
+ ```
86
+
87
+ Key features:
88
+ - Insert trusts a provided id if present; otherwise uses the table sequence and normalizes `body.id`.
89
+ - Update supports `mode="merge"` (default) and `mode="replace"`, always ensuring `body.id` matches the row id.
90
+ - Get returns rows as dictionaries or via a mapper function to your own model types.
91
+ - Delete single or all rows, plus a helper to `TRUNCATE ... RESTART IDENTITY`.
92
+
93
+ ## Configuration
94
+
95
+ Use `DbConfig` or environment variables to control connection pooling and DSN; defaults are suitable for local development.
96
+
97
+ ```python
98
+ from nc_db.db.DbConfig import DbConfig
99
+
100
+ config = DbConfig(
101
+ user="your_username",
102
+ password="your_password",
103
+ host="localhost",
104
+ port=5432,
105
+ dbname="your_database",
106
+ )
107
+ ```
108
+ OR
109
+
110
+ ```bash
111
+ export PGUSER="your_username"
112
+ export PGPASSWORD="your_password"
113
+ export PGHOST=localhost
114
+ export PGPORT=5432
115
+ export PGDATABASE="youd_database"
116
+
117
+ ```
118
+
119
+ ## Kubernetes (Optional)
120
+
121
+ Run a local Postgres via Kustomize overlays.
122
+
123
+ Ephemeral (emptyDir) instance:
124
+
125
+ ```bash
126
+ kubectl apply -k k8s/postgres/overlays/ephemeral
127
+ kubectl -n nc_db wait --for=condition=ready pod -l app=nc-data --timeout=90s
128
+ kubectl -n nc_db port-forward svc/postgres 5432:5432
129
+ ```
130
+
131
+ Persistent storage overlay is also available under `k8s/postgres/overlays/persistent`.
132
+
133
+ To safely reset a StatefulSet (DANGEROUS — wipes data), use:
134
+
135
+ ```bash
136
+ python k8s/scripts/full_pod_reset.py --yes
137
+ ```
138
+
139
+ Review the script before use; it scales down, deletes the PVC (and optionally PV), and scales back up.
140
+
141
+ ## Development Conventions
142
+
143
+ - Python 3.12, type hints, PEP 8; keep imports sorted and code minimal.
144
+ - Modules: snake_case; Classes: CamelCase; variables/functions: snake_case.
145
+ - Tests live in `src/tests`; name files/functions `test_*` and focus on behavior.
146
+ - See `.github/PULL_REQUEST_TEMPLATE.md` for PR expectations.
147
+
148
+ ## Schema Diagram
149
+
150
+ Note: the init SQL currently provisions `scg` and `healthcheck`
151
+
152
+ ## Security & License
153
+
154
+ - Do not commit real secrets. The defaults in code and K8s manifests are for local development only.
155
+ - Use `DbConfig` or environment variables to specify connection details.
156
+ - License: Proprietary (see `pyproject.toml`).
@@ -0,0 +1,8 @@
1
+ CREATE SCHEMA IF NOT EXISTS nc;
2
+
3
+ CREATE TABLE IF NOT EXISTS nc.scg (
4
+ id BIGSERIAL PRIMARY KEY,
5
+ body JSONB NOT NULL
6
+ );
7
+
8
+ CREATE INDEX IF NOT EXISTS idx_data_body_gin on nc.scg USING GIN (body);
@@ -0,0 +1,16 @@
1
+ -- Revoke overly broad PUBLIC defaults and grant to the application role
2
+
3
+ -- Revoke defaults for future objects
4
+ ALTER DEFAULT PRIVILEGES IN SCHEMA "nc" REVOKE ALL ON TABLES FROM PUBLIC;
5
+ ALTER DEFAULT PRIVILEGES IN SCHEMA "nc" REVOKE ALL ON SEQUENCES FROM PUBLIC;
6
+
7
+ -- Revoke on existing objects (no-op if none)
8
+ REVOKE ALL ON ALL TABLES IN SCHEMA nc FROM PUBLIC;
9
+ REVOKE ALL ON ALL SEQUENCES IN SCHEMA nc FROM PUBLIC;
10
+
11
+ -- Grant defaults to nc_app for future objects (owner-dependent)
12
+ ALTER DEFAULT PRIVILEGES IN SCHEMA "nc"
13
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO nc_app;
14
+
15
+ ALTER DEFAULT PRIVILEGES IN SCHEMA "nc"
16
+ GRANT USAGE, SELECT ON SEQUENCES TO nc_app;
@@ -0,0 +1,10 @@
1
+ CREATE SCHEMA IF NOT EXISTS nc;
2
+
3
+ CREATE TABLE IF NOT EXISTS nc.healthcheck (
4
+ id int PRIMARY KEY,
5
+ ok boolean NOT NULL
6
+ );
7
+
8
+ INSERT INTO nc.healthcheck (id, ok)
9
+ VALUES (1, true)
10
+ ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1,31 @@
1
+ apiVersion: kustomize.config.k8s.io/v1beta1
2
+ kind: Kustomization
3
+ namespace: nc_db
4
+
5
+ secretGenerator:
6
+ # Cluster/postgres superuser creds
7
+ - name: postgres-credentials-root
8
+ envs:
9
+ - secrets/postgres.root.env
10
+
11
+ # App/user creds (what your app uses to connect)
12
+ - name: postgres-credentials-app
13
+ envs:
14
+ - secrets/postgres.app.env
15
+
16
+ configMapGenerator:
17
+ - name: pg-init-sql
18
+ files:
19
+ - init/00-create-app-role.sql
20
+ - init/01-create-schema.sql
21
+ - init/02-grants.sql
22
+ - init/10-healthcheck.sql
23
+
24
+ # Keep the suffix hash enabled so rolling updates occur when content changes
25
+ generatorOptions:
26
+ disableNameSuffixHash: false
27
+
28
+ resources:
29
+ - namespace.yaml
30
+ - service.yaml
31
+ - statefulset.yaml
@@ -0,0 +1,4 @@
1
+ apiVersion: v1
2
+ kind: Namespace
3
+ metadata:
4
+ name: nc_db
@@ -0,0 +1,50 @@
1
+ apiVersion: v1
2
+ kind: Secret
3
+ metadata:
4
+ name: pg-app-bootstrap
5
+ namespace: nc_db
6
+ type: Opaque
7
+ stringData:
8
+ 20-create-app-user.sh: |
9
+ #!/bin/bash
10
+ set -euo pipefail
11
+
12
+ echo "[initdb] creating / ensuring application role ${APP_DB_USER}"
13
+
14
+ psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
15
+ DO \$\$
16
+ BEGIN
17
+ IF NOT EXISTS (
18
+ SELECT 1 FROM pg_roles WHERE rolname = '${APP_DB_USER}'
19
+ ) THEN
20
+ EXECUTE format(
21
+ 'CREATE ROLE %I LOGIN PASSWORD %L',
22
+ '${APP_DB_USER}',
23
+ '${APP_DB_PASSWORD}'
24
+ );
25
+ END IF;
26
+ END
27
+ \$\$;
28
+
29
+ -- ensure schema exists
30
+ CREATE SCHEMA IF NOT EXISTS ${APP_DB_SCHEMA};
31
+
32
+ -- make sure app user can actually use the DB
33
+ GRANT CONNECT ON DATABASE "${APP_DB_NAME}" TO ${APP_DB_USER};
34
+
35
+ GRANT USAGE ON SCHEMA ${APP_DB_SCHEMA} TO ${APP_DB_USER};
36
+
37
+ GRANT SELECT, INSERT, UPDATE, DELETE
38
+ ON ALL TABLES IN SCHEMA ${APP_DB_SCHEMA}
39
+ TO ${APP_DB_USER};
40
+
41
+ GRANT USAGE, SELECT
42
+ ON ALL SEQUENCES IN SCHEMA ${APP_DB_SCHEMA}
43
+ TO ${APP_DB_USER};
44
+
45
+ ALTER DEFAULT PRIVILEGES IN SCHEMA ${APP_DB_SCHEMA}
46
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${APP_DB_USER};
47
+
48
+ ALTER DEFAULT PRIVILEGES IN SCHEMA ${APP_DB_SCHEMA}
49
+ GRANT USAGE, SELECT ON SEQUENCES TO ${APP_DB_USER};
50
+ EOSQL
@@ -0,0 +1,12 @@
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: postgres
5
+ spec:
6
+ type: ClusterIP
7
+ selector:
8
+ app: nc-data
9
+ ports:
10
+ - name: pg
11
+ port: 5432
12
+ targetPort: 5432
@@ -0,0 +1,88 @@
1
+ apiVersion: apps/v1
2
+ kind: StatefulSet
3
+ metadata:
4
+ name: postgres
5
+ namespace: nc_db
6
+ spec:
7
+ serviceName: postgres
8
+ replicas: 1
9
+ selector:
10
+ matchLabels:
11
+ app: nc-data
12
+ template:
13
+ metadata:
14
+ labels:
15
+ app: nc-data
16
+ spec:
17
+ containers:
18
+ - name: postgres
19
+ image: postgres:15
20
+ imagePullPolicy: IfNotPresent
21
+ ports:
22
+ - containerPort: 5432
23
+ name: pg
24
+
25
+ # Load secrets into env so init script can read them
26
+ envFrom:
27
+ - secretRef:
28
+ name: postgres-credentials-root # POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
29
+ - secretRef:
30
+ name: postgres-credentials-app # APP_DB_USER, APP_DB_PASSWORD, APP_DB_NAME, APP_DB_SCHEMA
31
+
32
+ # Non-secret config
33
+ env:
34
+ - name: PGDATA
35
+ value: /var/lib/postgresql/data/pgdata
36
+
37
+ # NOTE: lifecycle: postStart was intentionally removed
38
+
39
+ readinessProbe:
40
+ tcpSocket:
41
+ port: 5432
42
+ initialDelaySeconds: 5
43
+ periodSeconds: 5
44
+
45
+ livenessProbe:
46
+ tcpSocket:
47
+ port: 5432
48
+ initialDelaySeconds: 10
49
+ periodSeconds: 10
50
+
51
+ volumeMounts:
52
+ - name: data
53
+ mountPath: /var/lib/postgresql/data
54
+
55
+ # merged init dir: configmap SQL + secret script
56
+ - name: init-bootstrap
57
+ mountPath: /docker-entrypoint-initdb.d
58
+ readOnly: true
59
+
60
+ volumes:
61
+ - name: init-bootstrap
62
+ projected:
63
+ sources:
64
+ - configMap:
65
+ name: pg-init-sql
66
+ items:
67
+ - key: 01-create-schema.sql
68
+ path: 01-create-schema.sql
69
+ - key: 02-grants.sql
70
+ path: 02-grants.sql
71
+ - key: 10-healthcheck.sql
72
+ path: 10-healthcheck.sql
73
+ - secret:
74
+ name: pg-app-bootstrap
75
+ items:
76
+ - key: 20-create-app-user.sh
77
+ path: 20-create-app-user.sh
78
+
79
+ volumeClaimTemplates:
80
+ - metadata:
81
+ name: data
82
+ spec:
83
+ accessModes:
84
+ - ReadWriteOnce
85
+ storageClassName: microk8s-hostpath
86
+ resources:
87
+ requests:
88
+ storage: 5Gi
@@ -0,0 +1,7 @@
1
+ apiVersion: kustomize.config.k8s.io/v1beta1
2
+ kind: Kustomization
3
+ namespace: nc_db
4
+ resources:
5
+ - ../../base
6
+ patches:
7
+ - path: patch-ephemeral-emptydir.yaml
@@ -0,0 +1,15 @@
1
+ apiVersion: apps/v1
2
+ kind: StatefulSet
3
+ metadata:
4
+ name: postgres
5
+ spec:
6
+ template:
7
+ spec:
8
+ volumes:
9
+ - name: init-sql
10
+ configMap:
11
+ name: pg-init-sql
12
+ defaultMode: 0444
13
+ - name: data
14
+ emptyDir: {}
15
+ volumeClaimTemplates: [] # remove PVCs for ephemeral runs
@@ -0,0 +1,7 @@
1
+ apiVersion: kustomize.config.k8s.io/v1beta1
2
+ kind: Kustomization
3
+ namespace: nc_db
4
+ resources:
5
+ - ../../base
6
+ patches:
7
+ - path: patch-persistent-storageclass.yaml
@@ -0,0 +1,17 @@
1
+ apiVersion: apps/v1
2
+ kind: StatefulSet
3
+ metadata:
4
+ name: postgres
5
+ spec:
6
+ volumeClaimTemplates: [] # disable PVC template from base
7
+ template:
8
+ spec:
9
+ volumes:
10
+ - name: data
11
+ hostPath:
12
+ path: /home/chale/nc_db/postgress-store/
13
+ type: DirectoryOrCreate
14
+ - name: init-sql
15
+ configMap:
16
+ name: pg-init-sql
17
+ defaultMode: 0444
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Reset a PostgreSQL StatefulSet (Kubernetes) to a clean state.
4
+
5
+ What it does:
6
+ 1) Scale the StatefulSet down to 0 (graceful stop).
7
+ 2) Delete the data PVC for a given pod ordinal (wipes data).
8
+ 3) (Optional) Delete the backing PV if reclaimPolicy=Retain or you want to force-remove it.
9
+ 4) Scale the StatefulSet back up to 1 (or a target) and wait for readiness.
10
+
11
+ Defaults match your example:
12
+ namespace: nc_db
13
+ statefulset: postgres
14
+ volumeClaimTemplates name: data
15
+ ordinal: 0
16
+ scale-up replicas: 1
17
+
18
+ Usage examples:
19
+ python k8s/scripts/full_pod_reset.py
20
+ python k8s/scripts/full_pod_reset.py --namespace nc_db --statefulset postgres --vct data --ordinal 0 --replicas 1 --delete-pv
21
+ KUBECTL_BIN="microk8s.kubectl" python k8s/scripts/full_pod_reset.py
22
+
23
+ Requires:
24
+ - kubectl (or microk8s.kubectl) in PATH
25
+ - current kube-context pointing at the correct cluster/namespace
26
+ """
27
+
28
+ import argparse
29
+ import os
30
+ import subprocess
31
+ import sys
32
+ import time
33
+ from typing import Optional, Tuple
34
+
35
+
36
+ def run_cmd(cmd: list[str], check: bool = True, capture: bool = True) -> Tuple[int, str]:
37
+ """Run a shell command. Returns (exit_code, stdout). Raises on error if check=True."""
38
+ proc = subprocess.run(
39
+ cmd,
40
+ check=False,
41
+ text=True,
42
+ stdout=subprocess.PIPE if capture else None,
43
+ stderr=subprocess.STDOUT,
44
+ )
45
+ if check and proc.returncode != 0:
46
+ raise RuntimeError(f"Command failed ({proc.returncode}): {' '.join(cmd)}\n{proc.stdout}")
47
+ return proc.returncode, (proc.stdout or "")
48
+
49
+
50
+ def wait_for_pod_gone(kubectl: str, ns: str, pod: str, timeout_s: int = 180) -> None:
51
+ """Wait until the specific pod no longer exists."""
52
+ deadline = time.time() + timeout_s
53
+ while time.time() < deadline:
54
+ rc, _ = run_cmd([kubectl, "-n", ns, "get", "pod", pod], check=False)
55
+ if rc != 0:
56
+ return
57
+ time.sleep(2)
58
+ raise TimeoutError(f"Timed out waiting for pod/{pod} to be deleted")
59
+
60
+
61
+ def wait_for_rollout_ready(kubectl: str, ns: str, sts: str, timeout_s: int = 300) -> None:
62
+ """Wait for StatefulSet rollout to complete."""
63
+ _, out = run_cmd([kubectl, "-n", ns, "rollout", "status", f"statefulset/{sts}", f"--timeout={timeout_s}s"])
64
+ print(out.strip())
65
+
66
+
67
+ def get_pv_name_for_pvc(kubectl: str, ns: str, pvc: str) -> Optional[str]:
68
+ """Return the PV name bound to a PVC (or None if not bound/not found)."""
69
+ rc, out = run_cmd(
70
+ [kubectl, "-n", ns, "get", "pvc", pvc, "-o", "jsonpath={.spec.volumeName}"],
71
+ check=False,
72
+ )
73
+ pv = out.strip()
74
+ if rc != 0 or not pv:
75
+ return None
76
+ return pv
77
+
78
+
79
+ def get_pv_reclaim_policy(kubectl: str, pv: str) -> Optional[str]:
80
+ """Return reclaim policy for a PV (e.g., Retain/Delete/Recycle), or None if not found."""
81
+ rc, out = run_cmd(
82
+ [kubectl, "get", "pv", pv, "-o", "jsonpath={.spec.persistentVolumeReclaimPolicy}"],
83
+ check=False,
84
+ )
85
+ pol = out.strip()
86
+ if rc != 0 or not pol:
87
+ return None
88
+ return pol
89
+
90
+
91
+ def main():
92
+ parser = argparse.ArgumentParser(description="Factory-reset a PostgreSQL StatefulSet (wipe data PVC and restart).")
93
+ parser.add_argument("--namespace", "-n", default="nc_db", help="Kubernetes namespace")
94
+ parser.add_argument("--statefulset", "--sts", default="postgres", dest="statefulset", help="StatefulSet name")
95
+ parser.add_argument("--vct", default="data", help="volumeClaimTemplates name (e.g., 'data')")
96
+ parser.add_argument("--ordinal", type=int, default=0, help="Pod ordinal to reset (e.g., 0 for <sts>-0)")
97
+ parser.add_argument("--replicas", type=int, default=1, help="Replicas to scale up after wipe")
98
+ parser.add_argument("--delete-pv", action="store_true", help="Also delete the bound PV (useful if Retain)")
99
+ parser.add_argument("--kubectl", default=os.environ.get("KUBECTL_BIN", "kubectl"), help="kubectl binary to use")
100
+ parser.add_argument("--yes", "-y", action="store_true", help="Do not prompt for confirmation")
101
+ args = parser.parse_args()
102
+
103
+ ns = args.namespace
104
+ sts = args.statefulset
105
+ vct = args.vct
106
+ ordinal = args.ordinal
107
+ replicas = args.replicas
108
+ kubectl = args.kubectl
109
+
110
+ pod_name = f"{sts}-{ordinal}"
111
+ pvc_name = f"{vct}-{pod_name}"
112
+
113
+ print(f"Namespace: {ns}")
114
+ print(f"StatefulSet: {sts}")
115
+ print(f"Target pod ordinal: {ordinal} -> pod/{pod_name}")
116
+ print(f"PVC to delete: {pvc_name}")
117
+ print(f"Scale up replicas after wipe: {replicas}")
118
+ print(f"Kubectl: {kubectl}")
119
+
120
+ if not args.yes:
121
+ resp = input("Proceed with FULL RESET (this will DELETE data on the PVC)? [y/N]: ").strip().lower()
122
+ if resp not in ("y", "yes"):
123
+ print("Aborted.")
124
+ sys.exit(1)
125
+
126
+ # 1) Scale down to 0
127
+ print("\n[1/5] Scaling StatefulSet down to 0 replicas...")
128
+ run_cmd([kubectl, "-n", ns, "scale", f"statefulset/{sts}", "--replicas=0"])
129
+ print("Waiting for pod to terminate...")
130
+ try:
131
+ wait_for_pod_gone(kubectl, ns, pod_name, timeout_s=180)
132
+ except TimeoutError as e:
133
+ print(f"Warning: {e}. Continuing…")
134
+
135
+ # Resolve PV name BEFORE deleting the PVC
136
+ pv_name = get_pv_name_for_pvc(kubectl, ns, pvc_name)
137
+ if pv_name:
138
+ print(f"PVC {pvc_name} is bound to PV {pv_name}")
139
+ reclaim = get_pv_reclaim_policy(kubectl, pv_name) or "Unknown"
140
+ print(f"PV reclaimPolicy: {reclaim}")
141
+ else:
142
+ print(f"Could not resolve a PV for PVC {pvc_name} (it may not exist or be Pending).")
143
+
144
+ # 2) Delete the PVC (the wipe)
145
+ print(f"\n[2/5] Deleting PVC {pvc_name}...")
146
+ run_cmd([kubectl, "-n", ns, "delete", "pvc", pvc_name, "--ignore-not-found=true"])
147
+
148
+ # 3) Optionally delete the PV
149
+ if args.delete_pv and pv_name:
150
+ print(f"\n[3/5] Deleting PV {pv_name} (because --delete-pv was set)...")
151
+ run_cmd([kubectl, "delete", "pv", pv_name])
152
+ else:
153
+ print("\n[3/5] Skipping PV deletion.")
154
+
155
+ # 4) Scale up
156
+ print(f"\n[4/5] Scaling StatefulSet up to {replicas} replicas...")
157
+ run_cmd([kubectl, "-n", ns, "scale", f"statefulset/{sts}", f"--replicas={replicas}"])
158
+
159
+ # 5) Wait for rollout ready
160
+ print("\n[5/5] Waiting for rollout to complete…")
161
+ wait_for_rollout_ready(kubectl, ns, sts, timeout_s=600)
162
+
163
+ print("\n✅ Reset complete. A new PVC will have been created automatically by the StatefulSet.")
164
+ print("Tip: watch the pod come up with:")
165
+ print(f" {kubectl} -n {ns} get pods -w")
166
+ print("To view logs:")
167
+ print(f" {kubectl} -n {ns} logs statefulset/{sts} --tail=100 -f")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ try:
172
+ main()
173
+ except KeyboardInterrupt:
174
+ print("\nInterrupted.")
175
+ sys.exit(130)
176
+ except Exception as exc:
177
+ print(f"\n❌ Error: {exc}")
178
+ sys.exit(1)