Add compose demo app for VeilStream preview testing
web (node) renders the seeded customers table plus container hostname. db (postgres:16-alpine) seeds five PII-shaped rows from db/init.sql.
This commit is contained in:
commit
47fe3c33e6
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
TOS.txt
|
||||
node_modules/
|
||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# veilstream-demo
|
||||
|
||||
Throwaway app for testing VeilStream preview environments.
|
||||
|
||||
Two services:
|
||||
|
||||
- `web` - Node, no framework. Renders the `customers` table plus the container
|
||||
hostname, so you can tell one preview environment from another.
|
||||
- `db` - `postgres:16-alpine`, seeded from `db/init.sql` with five rows of
|
||||
deliberately PII-shaped data (name, email, phone) to give the sanitization
|
||||
features something to act on.
|
||||
|
||||
Primary service is `web` on port 3000. `/healthz` returns `ok`.
|
||||
|
||||
Run locally:
|
||||
|
||||
docker compose up --build
|
||||
|
||||
Then open
|
||||
|
||||
http://localhost:3000
|
||||
16
db/init.sql
Normal file
16
db/init.sql
Normal file
@ -0,0 +1,16 @@
|
||||
CREATE TABLE customers (
|
||||
id serial PRIMARY KEY,
|
||||
full_name text NOT NULL,
|
||||
email text NOT NULL,
|
||||
phone text NOT NULL,
|
||||
city text NOT NULL,
|
||||
plan text NOT NULL,
|
||||
mrr_cents integer NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO customers (full_name, email, phone, city, plan, mrr_cents) VALUES
|
||||
('Ada Lovelace', 'ada@example.com', '+1 250 555 0101', 'Victoria', 'pro', 9900),
|
||||
('Grace Hopper', 'grace@example.com', '+1 250 555 0102', 'Nanaimo', 'team', 29900),
|
||||
('Alan Turing', 'alan@example.com', '+1 604 555 0103', 'Vancouver', 'free', 0),
|
||||
('Katherine Johnson','kj@example.com', '+1 250 555 0104', 'Sidney', 'pro', 9900),
|
||||
('Edsger Dijkstra', 'edsger@example.com', '+1 778 555 0105', 'Victoria', 'enterprise',99900);
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: ./web
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://demo:demo@db:5432/demo
|
||||
APP_LABEL: "VeilStream demo"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: demo
|
||||
POSTGRES_PASSWORD: demo
|
||||
POSTGRES_DB: demo
|
||||
volumes:
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U demo -d demo"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
7
web/Dockerfile
Normal file
7
web/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY server.js ./
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
9
web/package.json
Normal file
9
web/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "veilstream-demo-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"pg": "^8.13.1"
|
||||
}
|
||||
}
|
||||
72
web/server.js
Normal file
72
web/server.js
Normal file
@ -0,0 +1,72 @@
|
||||
const http = require("http");
|
||||
const os = require("os");
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const label = process.env.APP_LABEL || "VeilStream demo";
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
|
||||
const esc = (s) =>
|
||||
String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||
|
||||
async function page() {
|
||||
let rows = [];
|
||||
let dbError = null;
|
||||
try {
|
||||
const r = await pool.query(
|
||||
"SELECT id, full_name, email, phone, city, plan, mrr_cents FROM customers ORDER BY id"
|
||||
);
|
||||
rows = r.rows;
|
||||
} catch (err) {
|
||||
dbError = err.message;
|
||||
}
|
||||
|
||||
const body = dbError
|
||||
? `<p class="err">Database unreachable: ${esc(dbError)}</p>`
|
||||
: `<table>
|
||||
<tr><th>id</th><th>name</th><th>email</th><th>phone</th><th>city</th><th>plan</th><th>mrr</th></tr>
|
||||
${rows
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr><td>${r.id}</td><td>${esc(r.full_name)}</td><td>${esc(r.email)}</td><td>${esc(
|
||||
r.phone
|
||||
)}</td><td>${esc(r.city)}</td><td>${esc(r.plan)}</td><td>$${(r.mrr_cents / 100).toFixed(
|
||||
2
|
||||
)}</td></tr>`
|
||||
)
|
||||
.join("\n ")}
|
||||
</table>
|
||||
<p class="meta">${rows.length} rows. If these values look like real people, the seed was not sanitized.</p>`;
|
||||
|
||||
return `<!doctype html>
|
||||
<html><head><meta charset="utf-8"><title>${esc(label)}</title>
|
||||
<style>
|
||||
body{font:15px/1.5 -apple-system,system-ui,sans-serif;margin:2rem auto;max-width:52rem;padding:0 1rem}
|
||||
table{border-collapse:collapse;width:100%;margin:1rem 0}
|
||||
th,td{border:1px solid #ccc;padding:.4rem .6rem;text-align:left}
|
||||
th{background:#f4f4f4}
|
||||
.meta{color:#666}
|
||||
.err{color:#b00}
|
||||
dl{display:grid;grid-template-columns:max-content 1fr;gap:.2rem 1rem}
|
||||
dt{color:#666}
|
||||
</style></head><body>
|
||||
<h1>${esc(label)}</h1>
|
||||
<dl>
|
||||
<dt>hostname</dt><dd>${esc(os.hostname())}</dd>
|
||||
<dt>served at</dt><dd>${new Date().toISOString()}</dd>
|
||||
<dt>node</dt><dd>${esc(process.version)}</dd>
|
||||
</dl>
|
||||
${body}
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
http
|
||||
.createServer(async (req, res) => {
|
||||
if (req.url === "/healthz") {
|
||||
res.writeHead(200, { "content-type": "text/plain" });
|
||||
return res.end("ok\n");
|
||||
}
|
||||
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
res.end(await page());
|
||||
})
|
||||
.listen(port, "0.0.0.0", () => console.log(`listening on ${port}`));
|
||||
Loading…
Reference in New Issue
Block a user