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 +156 -0
- nc_db/k8s/postgres/base/init/01-create-schema.sql +8 -0
- nc_db/k8s/postgres/base/init/02-grants.sql +16 -0
- nc_db/k8s/postgres/base/init/10-healthcheck.sql +10 -0
- nc_db/k8s/postgres/base/kustomization.yaml +31 -0
- nc_db/k8s/postgres/base/namespace.yaml +4 -0
- nc_db/k8s/postgres/base/pg-app-bootstrap.yaml +50 -0
- nc_db/k8s/postgres/base/service.yaml +12 -0
- nc_db/k8s/postgres/base/statefulset.yaml +88 -0
- nc_db/k8s/postgres/overlays/ephemeral/kustomization.yaml +7 -0
- nc_db/k8s/postgres/overlays/ephemeral/patch-ephemeral-emptydir.yaml +15 -0
- nc_db/k8s/postgres/overlays/persistent/kustomization.yaml +7 -0
- nc_db/k8s/postgres/overlays/persistent/patch-persistent-storageclass.yaml +17 -0
- nc_db/k8s/scripts/full_pod_reset.py +178 -0
- nc_db/src/db/DbConfig.py +103 -0
- nc_db/src/db/data_types.py +7 -0
- nc_db/src/db/db.py +330 -0
- nc_db/src/db/queries.py +151 -0
- nc_db/src/db/test_file.json +13 -0
- nc_db/src/tests/conftest.py +69 -0
- nc_db/src/tests/test_db.py +434 -0
- nc_db-0.1.3.dist-info/METADATA +174 -0
- nc_db-0.1.3.dist-info/RECORD +24 -0
- nc_db-0.1.3.dist-info/WHEEL +4 -0
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,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,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,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,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,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,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)
|