Skip to content

Commit

Permalink
docs: vercel example portal to private postgres db
Browse files Browse the repository at this point in the history
  • Loading branch information
snandam committed Dec 18, 2024
1 parent 86753fb commit dd82b09
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/command/portals/vercel/example-2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.ticket
.vercel
73 changes: 73 additions & 0 deletions examples/command/portals/vercel/example-2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Access private AWS RDS PostgreSQL Databasefrom Vercel

![Architecture](./diagram.png)

## AWS

### Setup a private RDS PostgreSQL Database

- [Follow AWS Guide](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html) to setup a private RDS PostgreSQL Database

- Create a database, table and insert some data

```sql
CREATE TABLE public.products
(
id integer primary key,
product_name text,
price numeric
);

INSERT INTO products (id, product_name, price)
VALUES
(1, "Ergonomic Keyboard", 38.30),
(2, "Wireless Mouse", 93.30),
(3, "USB Hub", 47.63)'
```
### Setup an Ockam Outlet Node
- [Follow Ockam Guide](https://docs.ockam.io/reference/command/guides/aws-marketplace/ockam-node-for-amazon-rds-postgrese) to setup an Ockam Outlet Node in AWS.
## Vercel: Setup Vercel Serverless functions to access the private database.
### Download latest ockam binary
- Download ockam binary `x86_64-unknown-linux-gnu` from the [Ockam](https://github.com/build-trust/ockam/releases) github repository and place it in the `data/linux-x86_64` directory. Rename the binary to `ockam`.
### Create an enrollment ticket for the Vercel function
```sh
# Generate an inlet ticket for the Vercel function.
# Values for --usage-count and --expires-in are setup to allow to be used 100 counts for 24 hours.
ockam project ticket --expires-in 24h --usage-count 100 \
--attribute amazon-rds-postgresql-inlet \
> "vercel-inlet.ticket"
```
### Setup a Vercel project and add the inlet ticket as a secret
- Select `Project Settings`, `Environments`, `Production
- Click `Add Environment Variable`
- Select `Sensitive`
- Add `OCKAM_RDS_INLET_ENROLLMENT_TICKET` as the name
- Add the value of `vercel-inlet.ticket` as the value
- Click `Add`
- Follow the same steps to add `DB_PASSWORD` as the environment variable
### Deploy the Vercel function
- Setup vercel cli and select the project
- Deploy the function inside the `api` directory
```sh
vercel --prod
```
### Test the Vercel function
- Use the `/api` endpoint to get the products
- Use the `/api/update` endpoint to update the products
222 changes: 222 additions & 0 deletions examples/command/portals/vercel/example-2/api/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""
Simple API handler for accessing PostgreSQL via Ockam secure channel
"""

import json
import os
import subprocess
import threading
import time
from http.server import BaseHTTPRequestHandler
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from decimal import Decimal

# Constants
#TODO: Use ockam_inmemory testing. Cleanup after testing
OCKAM_BINARY_PATH = '/var/task/data/linux-x86_64/ockam' if os.environ.get('VERCEL_ENV') == 'production' else 'ockam'
MAX_RETRIES = 60
RETRY_DELAY = 1 # seconds
DB_PORT = 15432
DB_TIMEOUT = 3 # seconds

# Track if Ockam node creation has been attempted
NODE_INITIALIZED = False

def is_production() -> bool:
"""Check if the environment is production."""
return os.environ.get('VERCEL_ENV') == 'production'

def get_ockam_version() -> str:
"""Get the installed Ockam version."""
try:
ockam_path = OCKAM_BINARY_PATH
if is_production() and not os.path.exists(ockam_path):
return f"Error: Ockam binary not found at {os.path.abspath(ockam_path)}"

result = subprocess.run([ockam_path, '--version'],
capture_output=True,
text=True)

return result.stdout.strip() if result.returncode == 0 else f"Error running ockam: {result.stderr}"
except Exception as e:
return f"Error: {str(e)} (CWD: {os.getcwd()})"

class handler(BaseHTTPRequestHandler):
"""Handler for PostgreSQL database requests via Ockam secure channel."""

def create_ockam_node(self, enrollment_ticket: str) -> bool:
"""Initialize Ockam node with the provided enrollment ticket."""
if not is_production():
return False

global NODE_INITIALIZED
request_id = self.headers.get('x-vercel-id', os.urandom(8).hex())

print(f"[{request_id}] Starting with NODE_INITIALIZED = {NODE_INITIALIZED}")

if not NODE_INITIALIZED:
NODE_INITIALIZED = True

def run_ockam_node():
try:
config = '''{
"tcp-inlet": {
"from": "127.0.0.1:15432",
"via": "postgresql",
"allow": "amazon-rds-postgresql-outlet"
}
}'''

print(f"[{request_id}] Starting ockam node with config: {config}", flush=True)

subprocess.Popen([
OCKAM_BINARY_PATH,
'node',
'create',
'--configuration',
config,
'--enrollment-ticket',
enrollment_ticket.strip()
], env={
**os.environ,
'OCKAM_HOME': '/tmp',
'OCKAM_OPENTELEMETRY_EXPORT': 'false',
'OCKAM_DISABLE_UPGRADE_CHECK': 'true'
})

except Exception as e:
print(f"[{request_id}] Error in node creation: {str(e)}", flush=True)

thread = threading.Thread(target=run_ockam_node)
thread.daemon = True
print(f"[{request_id}] Starting ockam node thread", flush=True)
thread.start()

return True

def get_db_connection(self, request_id: str):
"""Establish database connection with retries."""
db_password = os.environ.get('DB_PASSWORD')
if not db_password:
raise ValueError("DB_PASSWORD environment variable is not set")

for retry in range(MAX_RETRIES):
try:
conn = psycopg2.connect(
host="127.0.0.1",
port=DB_PORT,
database="testdb",
user="postgresuser",
password=db_password,
connect_timeout=DB_TIMEOUT
)
print(f"[{request_id}] Connected to PostgreSQL successfully after {retry} retries")
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
return conn, None

except Exception as e:
print(f"[{request_id}] Attempt {retry + 1} failed: {str(e)}")
if retry < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)

return None, f"Failed to connect after {MAX_RETRIES} attempts"

def handle_select(self, request_id: str) -> dict:
"""Handle GET requests for product data."""
conn, error = self.get_db_connection(request_id)
if error:
return {"status": "error", "message": f"Database connection failed: {error}"}

try:
cur = conn.cursor()
cur.execute("SELECT * FROM products;")
rows = cur.fetchall()
formatted_rows = [
{
'id': row[0],
'product_name': row[1],
'price': round(float(row[2]), 2) if isinstance(row[2], Decimal) else row[2]
}
for row in rows
]

cur.close()
conn.close()

return {"status": "success", "data": formatted_rows}
except Exception as e:
return {"status": "error", "message": str(e)}

def handle_update(self, request_id: str) -> dict:
"""Handle requests to update product prices."""
conn, error = self.get_db_connection(request_id)
if error:
return {"status": "error", "message": f"Database connection failed: {error}"}

try:
cur = conn.cursor()
cur.execute("UPDATE products SET price = (random() * 90 + 10)::numeric(10,2);")

# Get updated data
cur.execute("SELECT * FROM products;")
rows = cur.fetchall()
formatted_rows = [
{
'id': row[0],
'product_name': row[1],
'price': round(float(row[2]), 2) if isinstance(row[2], Decimal) else row[2]
}
for row in rows
]

cur.close()
conn.close()

return {
"status": "success",
"message": "Prices updated successfully",
"data": formatted_rows
}
except Exception as e:
return {"status": "error", "message": str(e)}

def do_GET(self):
"""Handle incoming GET requests."""
try:
# Verify Ockam installation
version_result = get_ockam_version()
if 'Error' in version_result:
self._send_error(500, version_result)
return

# Verify enrollment ticket
enrollment_ticket = os.environ.get('OCKAM_RDS_INLET_ENROLLMENT_TICKET')
if not enrollment_ticket:
self._send_error(500, 'OCKAM_RDS_INLET_ENROLLMENT_TICKET not configured')
return

# Initialize Ockam node
if not self.create_ockam_node(enrollment_ticket):
self._send_error(500, 'Failed to initialize Ockam node')
return

# Handle the request
request_id = self.headers.get('x-vercel-id', os.urandom(8).hex())
result = self.handle_update(request_id) if self.path == '/api/update' else self.handle_select(request_id)

self._send_response(200, result)

except Exception as e:
self._send_error(500, str(e))

def _send_response(self, status: int, data: dict):
"""Helper method to send JSON responses."""
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())

def _send_error(self, status: int, message: str):
"""Helper method to send error responses."""
self._send_response(status, {'error': message})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/command/portals/vercel/example-2/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask==3.0.3
psycopg2-binary==2.9.9
19 changes: 19 additions & 0 deletions examples/command/portals/vercel/example-2/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"redirects": [
{
"source": "/",
"destination": "/api"
}
],
"rewrites": [
{
"source": "/api/update",
"destination": "/api"
}
],
"functions": {
"api/**/*": {
"maxDuration": 300
}
}
}

0 comments on commit dd82b09

Please sign in to comment.