Skip to content

Commit b986626

Browse files
committed
Add Kubernetes auth and workspace deployment support
1 parent 99bb278 commit b986626

32 files changed

Lines changed: 843 additions & 70 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ AI_SQL_ANALYST_QUERY_LOG_PATH=./data/query_log.jsonl
77
AI_SQL_ANALYST_MAX_QUERY_ROWS=100
88
AI_SQL_ANALYST_DATABASE_BACKEND=sqlite
99
AI_SQL_ANALYST_POSTGRES_DSN=postgresql://ai_sql:ai_sql_password@localhost:5432/ai_sql_analyst
10+
AI_SQL_ANALYST_API_KEYS=dev-api-key
11+
AI_SQL_ANALYST_BROWSER_API_KEY=dev-api-key
1012
OPENAI_API_KEY=
1113
AI_SQL_ANALYST_MODEL=gpt-5-mini

.github/workflows/ai-sql-analyst-ci.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,48 @@ jobs:
3434

3535
- name: Run text-to-SQL evals
3636
run: python manage.py evals
37+
38+
postgres-integration:
39+
runs-on: ubuntu-latest
40+
services:
41+
postgres:
42+
image: postgres:16-alpine
43+
env:
44+
POSTGRES_DB: ai_sql_analyst
45+
POSTGRES_USER: ai_sql
46+
POSTGRES_PASSWORD: ai_sql_password
47+
ports:
48+
- 5432:5432
49+
options: >-
50+
--health-cmd "pg_isready -U ai_sql -d ai_sql_analyst"
51+
--health-interval 5s
52+
--health-timeout 5s
53+
--health-retries 10
54+
55+
defaults:
56+
run:
57+
working-directory: ai-sql-analyst
58+
59+
env:
60+
AI_SQL_ANALYST_DATABASE_BACKEND: postgres
61+
AI_SQL_ANALYST_POSTGRES_DSN: postgresql://ai_sql:ai_sql_password@localhost:5432/ai_sql_analyst
62+
AI_SQL_ANALYST_API_KEYS: dev-api-key
63+
AI_SQL_ANALYST_BROWSER_API_KEY: dev-api-key
64+
65+
steps:
66+
- name: Check out repository
67+
uses: actions/checkout@v4
68+
69+
- name: Set up Python
70+
uses: actions/setup-python@v5
71+
with:
72+
python-version: "3.13"
73+
74+
- name: Install dependencies
75+
run: pip install -r requirements.txt
76+
77+
- name: Initialize Postgres schema
78+
run: python manage.py init-db
79+
80+
- name: Run evals against Postgres
81+
run: python manage.py evals

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ data/*.db
22
data/*.jsonl
33
__pycache__/
44
.pytest_cache/
5+
.terraform/
6+
*.tfstate
7+
*.tfstate.*
8+
*.tfvars

README.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ It is designed to show:
1010
- Observable analytics workflows
1111
- PostgreSQL-backed application architecture
1212
- Dockerized local infrastructure
13+
- Kubernetes manifests with probes, services, secrets, and autoscaling
14+
- Terraform Kubernetes deployment option
1315
- Automated tests and text-to-SQL evals
1416

1517
## What It Does
@@ -39,9 +41,11 @@ It then:
3941
- Heuristic fallback when no OpenAI API key is configured
4042
- Analyst console served by the API
4143
- Query history and aggregate system metrics
44+
- API key authentication
45+
- Workspace-scoped SQL guardrails for multi-tenant analytics
4246
- Built-in SQL evaluation suite
4347
- Automated tests for API behavior and guardrails
44-
- CI workflow for tests and evals
48+
- CI workflow for tests, evals, and Postgres integration
4549

4650
## Project Structure
4751

@@ -64,6 +68,10 @@ ai-sql-analyst/
6468
index.html
6569
styles.css
6670
tests/
71+
k8s/
72+
base/
73+
terraform/
74+
kubernetes/
6775
evals.py
6876
manage.py
6977
docker-compose.yml
@@ -117,6 +125,8 @@ Key options:
117125
```bash
118126
AI_SQL_ANALYST_DATABASE_BACKEND=sqlite
119127
AI_SQL_ANALYST_POSTGRES_DSN=postgresql://ai_sql:ai_sql_password@localhost:5432/ai_sql_analyst
128+
AI_SQL_ANALYST_API_KEYS=dev-api-key
129+
AI_SQL_ANALYST_BROWSER_API_KEY=dev-api-key
120130
OPENAI_API_KEY=your-key-here
121131
```
122132

@@ -152,10 +162,17 @@ Request:
152162

153163
```json
154164
{
155-
"question": "Show the top 5 customers by revenue"
165+
"question": "Show the top 5 customers by revenue",
166+
"workspace_id": "demo"
156167
}
157168
```
158169

170+
Protected API endpoints require:
171+
172+
```text
173+
X-API-Key: dev-api-key
174+
```
175+
159176
### `GET /history`
160177

161178
Returns recent query log entries.
@@ -184,6 +201,42 @@ python ai-sql-analyst/evals.py
184201

185202
The GitHub Actions workflow in `.github/workflows/ai-sql-analyst-ci.yml` runs both tests and evals.
186203

204+
## Kubernetes
205+
206+
The Kubernetes base manifests live in `k8s/base`.
207+
208+
Apply with Kustomize:
209+
210+
```bash
211+
kubectl apply -k ai-sql-analyst/k8s/base
212+
```
213+
214+
The base includes:
215+
- Namespace
216+
- ConfigMap
217+
- Secret example
218+
- PostgreSQL StatefulSet and Service
219+
- API Deployment and Service
220+
- Readiness and liveness probes
221+
- HorizontalPodAutoscaler
222+
- Optional ingress example
223+
224+
For a real environment, replace `k8s/base/secret.example.yaml` with a sealed secret, external secret, or cluster-managed secret.
225+
226+
## Terraform
227+
228+
The Terraform Kubernetes example lives in `terraform/kubernetes`.
229+
230+
```bash
231+
cd ai-sql-analyst/terraform/kubernetes
232+
terraform init
233+
terraform plan \
234+
-var="api_keys=replace-me" \
235+
-var="postgres_password=replace-me"
236+
```
237+
238+
This provisions the namespace, app config, secret, PostgreSQL StatefulSet, and API Deployment/Service into an existing Kubernetes cluster.
239+
187240
Response shape:
188241

189242
```json
@@ -222,6 +275,6 @@ This keeps the project business-facing and makes the SQL portfolio signal much c
222275

223276
## Next Up
224277

225-
- Role-based query scopes
278+
- Role-based workspace permissions
226279
- Retrieval over metric definitions and BI docs
227-
- Cloud deployment and infrastructure as code
280+
- Cloud deployment walkthrough

ai_sql_analyst/auth.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from fastapi import Header, HTTPException, status
6+
7+
from ai_sql_analyst.config import settings
8+
9+
10+
@dataclass(frozen=True, slots=True)
11+
class Principal:
12+
api_key: str
13+
workspace_id: str = "demo"
14+
15+
16+
def require_api_key(x_api_key: str = Header(default="")) -> Principal:
17+
allowed_keys = settings.allowed_api_keys()
18+
if not allowed_keys:
19+
return Principal(api_key="", workspace_id="demo")
20+
if x_api_key not in allowed_keys:
21+
raise HTTPException(
22+
status_code=status.HTTP_401_UNAUTHORIZED,
23+
detail="Missing or invalid X-API-Key header.",
24+
)
25+
return Principal(api_key=x_api_key, workspace_id="demo")

ai_sql_analyst/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ class Settings(BaseSettings):
3434
default="postgresql://ai_sql:ai_sql_password@localhost:5432/ai_sql_analyst",
3535
alias="AI_SQL_ANALYST_POSTGRES_DSN",
3636
)
37+
api_keys: str = Field(default="dev-api-key", alias="AI_SQL_ANALYST_API_KEYS")
38+
browser_api_key: str = Field(default="dev-api-key", alias="AI_SQL_ANALYST_BROWSER_API_KEY")
3739
openai_api_key: str = Field(default="", alias="OPENAI_API_KEY")
3840
model_name: str = Field(default="gpt-5-mini", alias="AI_SQL_ANALYST_MODEL")
3941

42+
def allowed_api_keys(self) -> set[str]:
43+
return {key.strip() for key in self.api_keys.split(",") if key.strip()}
44+
4045

4146
settings = Settings()

ai_sql_analyst/db/migrations.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
SQLITE_SCHEMA = """
55
CREATE TABLE IF NOT EXISTS customers (
66
customer_id INTEGER PRIMARY KEY,
7+
workspace_id TEXT NOT NULL DEFAULT 'demo',
78
customer_name TEXT NOT NULL,
89
segment TEXT NOT NULL,
910
region TEXT NOT NULL,
@@ -12,6 +13,7 @@
1213
1314
CREATE TABLE IF NOT EXISTS invoices (
1415
invoice_id INTEGER PRIMARY KEY,
16+
workspace_id TEXT NOT NULL DEFAULT 'demo',
1517
customer_id INTEGER NOT NULL,
1618
invoice_month TEXT NOT NULL,
1719
amount_usd REAL NOT NULL,
@@ -21,6 +23,7 @@
2123
2224
CREATE TABLE IF NOT EXISTS support_tickets (
2325
ticket_id INTEGER PRIMARY KEY,
26+
workspace_id TEXT NOT NULL DEFAULT 'demo',
2427
customer_id INTEGER NOT NULL,
2528
created_at TEXT NOT NULL,
2629
priority TEXT NOT NULL,
@@ -34,6 +37,7 @@
3437
POSTGRES_SCHEMA = """
3538
CREATE TABLE IF NOT EXISTS customers (
3639
customer_id INTEGER PRIMARY KEY,
40+
workspace_id TEXT NOT NULL DEFAULT 'demo',
3741
customer_name TEXT NOT NULL,
3842
segment TEXT NOT NULL,
3943
region TEXT NOT NULL,
@@ -42,6 +46,7 @@
4246
4347
CREATE TABLE IF NOT EXISTS invoices (
4448
invoice_id INTEGER PRIMARY KEY,
49+
workspace_id TEXT NOT NULL DEFAULT 'demo',
4550
customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
4651
invoice_month DATE NOT NULL,
4752
amount_usd NUMERIC(12, 2) NOT NULL,
@@ -50,6 +55,7 @@
5055
5156
CREATE TABLE IF NOT EXISTS support_tickets (
5257
ticket_id INTEGER PRIMARY KEY,
58+
workspace_id TEXT NOT NULL DEFAULT 'demo',
5359
customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
5460
created_at DATE NOT NULL,
5561
priority TEXT NOT NULL,
@@ -58,7 +64,9 @@
5864
);
5965
6066
CREATE INDEX IF NOT EXISTS idx_invoices_customer_id ON invoices(customer_id);
67+
CREATE INDEX IF NOT EXISTS idx_invoices_workspace_id ON invoices(workspace_id);
6168
CREATE INDEX IF NOT EXISTS idx_invoices_month ON invoices(invoice_month);
6269
CREATE INDEX IF NOT EXISTS idx_support_tickets_customer_id ON support_tickets(customer_id);
70+
CREATE INDEX IF NOT EXISTS idx_support_tickets_workspace_id ON support_tickets(workspace_id);
6371
CREATE INDEX IF NOT EXISTS idx_support_tickets_priority ON support_tickets(priority);
6472
"""

ai_sql_analyst/db/seeds.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
from __future__ import annotations
22

33
CUSTOMERS = [
4-
(1, "Northstar Health", "Enterprise", "Midwest", "2024-01-15"),
5-
(2, "Blue River Logistics", "Mid-Market", "South", "2024-02-20"),
6-
(3, "Summit Retail Group", "SMB", "West", "2024-03-08"),
7-
(4, "Atlas Insurance", "Enterprise", "Northeast", "2024-04-11"),
8-
(5, "Brightline Energy", "Mid-Market", "West", "2024-05-03"),
4+
(1, "demo", "Northstar Health", "Enterprise", "Midwest", "2024-01-15"),
5+
(2, "demo", "Blue River Logistics", "Mid-Market", "South", "2024-02-20"),
6+
(3, "demo", "Summit Retail Group", "SMB", "West", "2024-03-08"),
7+
(4, "demo", "Atlas Insurance", "Enterprise", "Northeast", "2024-04-11"),
8+
(5, "demo", "Brightline Energy", "Mid-Market", "West", "2024-05-03"),
99
]
1010

1111
INVOICES = [
12-
(1, 1, "2026-01-01", 12400.0, "Enterprise Pro"),
13-
(2, 1, "2026-02-01", 12400.0, "Enterprise Pro"),
14-
(3, 1, "2026-03-01", 11800.0, "Enterprise Pro"),
15-
(4, 2, "2026-01-01", 7300.0, "Growth"),
16-
(5, 2, "2026-02-01", 7300.0, "Growth"),
17-
(6, 2, "2026-03-01", 6900.0, "Growth"),
18-
(7, 3, "2026-01-01", 2100.0, "Starter"),
19-
(8, 3, "2026-02-01", 2400.0, "Starter"),
20-
(9, 3, "2026-03-01", 2400.0, "Starter"),
21-
(10, 4, "2026-01-01", 9800.0, "Enterprise Pro"),
22-
(11, 4, "2026-02-01", 9800.0, "Enterprise Pro"),
23-
(12, 4, "2026-03-01", 10200.0, "Enterprise Pro"),
24-
(13, 5, "2026-01-01", 5800.0, "Growth"),
25-
(14, 5, "2026-02-01", 5800.0, "Growth"),
26-
(15, 5, "2026-03-01", 5200.0, "Growth"),
12+
(1, "demo", 1, "2026-01-01", 12400.0, "Enterprise Pro"),
13+
(2, "demo", 1, "2026-02-01", 12400.0, "Enterprise Pro"),
14+
(3, "demo", 1, "2026-03-01", 11800.0, "Enterprise Pro"),
15+
(4, "demo", 2, "2026-01-01", 7300.0, "Growth"),
16+
(5, "demo", 2, "2026-02-01", 7300.0, "Growth"),
17+
(6, "demo", 2, "2026-03-01", 6900.0, "Growth"),
18+
(7, "demo", 3, "2026-01-01", 2100.0, "Starter"),
19+
(8, "demo", 3, "2026-02-01", 2400.0, "Starter"),
20+
(9, "demo", 3, "2026-03-01", 2400.0, "Starter"),
21+
(10, "demo", 4, "2026-01-01", 9800.0, "Enterprise Pro"),
22+
(11, "demo", 4, "2026-02-01", 9800.0, "Enterprise Pro"),
23+
(12, "demo", 4, "2026-03-01", 10200.0, "Enterprise Pro"),
24+
(13, "demo", 5, "2026-01-01", 5800.0, "Growth"),
25+
(14, "demo", 5, "2026-02-01", 5800.0, "Growth"),
26+
(15, "demo", 5, "2026-03-01", 5200.0, "Growth"),
2727
]
2828

2929
SUPPORT_TICKETS = [
30-
(1, 1, "2026-03-03", "High", "Closed", 14.0),
31-
(2, 1, "2026-03-18", "Medium", "Closed", 8.0),
32-
(3, 2, "2026-03-06", "High", "Closed", 19.0),
33-
(4, 2, "2026-03-27", "Low", "Open", 36.0),
34-
(5, 3, "2026-03-10", "Medium", "Closed", 6.0),
35-
(6, 4, "2026-03-11", "Critical", "Closed", 22.0),
36-
(7, 4, "2026-03-19", "High", "Open", 41.0),
37-
(8, 5, "2026-03-20", "Medium", "Closed", 9.0),
38-
(9, 5, "2026-03-22", "Low", "Closed", 5.0),
30+
(1, "demo", 1, "2026-03-03", "High", "Closed", 14.0),
31+
(2, "demo", 1, "2026-03-18", "Medium", "Closed", 8.0),
32+
(3, "demo", 2, "2026-03-06", "High", "Closed", 19.0),
33+
(4, "demo", 2, "2026-03-27", "Low", "Open", 36.0),
34+
(5, "demo", 3, "2026-03-10", "Medium", "Closed", 6.0),
35+
(6, "demo", 4, "2026-03-11", "Critical", "Closed", 22.0),
36+
(7, "demo", 4, "2026-03-19", "High", "Open", 41.0),
37+
(8, "demo", 5, "2026-03-20", "Medium", "Closed", 9.0),
38+
(9, "demo", 5, "2026-03-22", "Low", "Closed", 5.0),
3939
]

0 commit comments

Comments
 (0)