feat: harden bootstrap and add tests

This commit is contained in:
2026-06-28 16:43:03 -05:00
parent 01ea9e391d
commit 991e73fcff
39 changed files with 3399 additions and 459 deletions
+36 -2
View File
@@ -29,6 +29,42 @@ spec:
secretKeyRef:
name: la-huasca-db-secrets
key: DATABASE_URL
- name: BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_ADMIN_PASSWORD
- name: BOOTSTRAP_USER_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_USER_PASSWORD
- name: BOOTSTRAP_WIFI_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_WIFI_PASSWORD
- name: BOOTSTRAP_TAGLINE
valueFrom:
configMapKeyRef:
name: la-huasca-bootstrap-config
key: BOOTSTRAP_TAGLINE
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: 200m
@@ -56,8 +92,6 @@ kind: Ingress
metadata:
name: la-huasca-ts
namespace: lahuasca
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
spec:
ingressClassName: traefik
tls:
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: la-huasca-bootstrap-config
namespace: lahuasca
data:
BOOTSTRAP_TAGLINE: "Hotel · Restaurant · Andean Highlands"
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: la-huasca-bootstrap-secrets
namespace: lahuasca
type: Opaque
stringData:
BOOTSTRAP_ADMIN_PASSWORD: "CHANGE_ME_ADMIN_PASSWORD"
BOOTSTRAP_USER_PASSWORD: "CHANGE_ME_USER_PASSWORD"
BOOTSTRAP_WIFI_PASSWORD: "CHANGE_ME_WIFI_PASSWORD"
+1 -1
View File
@@ -3,7 +3,7 @@
"kind": "Deployment",
"metadata": {
"name": "la-huasca-ts",
"namespace": "la-huasca"
"namespace": "lahuasca"
},
"spec": {
"replicas": 1,
+1 -1
View File
@@ -3,7 +3,7 @@
"kind": "Ingress",
"metadata": {
"name": "la-huasca-ts",
"namespace": "la-huasca",
"namespace": "lahuasca",
"annotations": {
"traefik.ingress.kubernetes.io/router.entrypoints": "web,websecure"
}
+220
View File
@@ -0,0 +1,220 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: la-huasca-bootstrap-config
namespace: lahuasca
data:
BOOTSTRAP_TAGLINE: Hotel · Restaurant · Andean Highlands
schema.sql: |
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')),
comments_disabled BOOLEAN DEFAULT FALSE,
first_name VARCHAR(100),
last_name VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS images (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
data TEXT NOT NULL,
category VARCHAR(100) DEFAULT 'other',
room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL,
location_target VARCHAR(100) DEFAULT 'gallery',
uploaded_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reservations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
date DATE NOT NULL,
time TIME NOT NULL,
guests INTEGER NOT NULL,
table_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS coupons (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
code VARCHAR(50) NOT NULL,
discount VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS offers (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS benefits (
id SERIAL PRIMARY KEY,
text VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS config (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS rooms (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS menu_items (
id SERIAL PRIMARY KEY,
category VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
is_daily_special BOOLEAN DEFAULT FALSE,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS places (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
distance_km NUMERIC(8,2),
visit_time VARCHAR(100),
suggestion TEXT,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reviews (
id SERIAL PRIMARY KEY,
author_name VARCHAR(255) NOT NULL,
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
text TEXT NOT NULL,
approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS slides (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
subtitle TEXT,
image_id INTEGER REFERENCES images(id) ON DELETE SET NULL,
image_url VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS room_comments (
id SERIAL PRIMARY KEY,
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
photo_url VARCHAR(500),
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
first_name VARCHAR(100) NOT NULL,
last_initial VARCHAR(10),
text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
deleted_by_admin BOOLEAN DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS social_events (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price VARCHAR(50),
date DATE,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS social_event_reservations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE,
guests INTEGER NOT NULL,
notes TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, social_event_id)
);
CREATE TABLE IF NOT EXISTS calendar_events (
id SERIAL PRIMARY KEY,
date DATE NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
type VARCHAR(100) NOT NULL,
description TEXT,
color VARCHAR(20) NOT NULL
);
CREATE TABLE IF NOT EXISTS user_sessions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);
seed.sh: |
#!/bin/sh
set -eu
: "${BOOTSTRAP_ADMIN_USERNAME:?missing BOOTSTRAP_ADMIN_USERNAME}"
: "${BOOTSTRAP_ADMIN_PASSWORD:?missing BOOTSTRAP_ADMIN_PASSWORD}"
: "${BOOTSTRAP_USER_USERNAME:?missing BOOTSTRAP_USER_USERNAME}"
: "${BOOTSTRAP_USER_PASSWORD:?missing BOOTSTRAP_USER_PASSWORD}"
: "${BOOTSTRAP_WIFI_PASSWORD:?missing BOOTSTRAP_WIFI_PASSWORD}"
: "${BOOTSTRAP_TAGLINE:?missing BOOTSTRAP_TAGLINE}"
psql -h "${DB_HOST}" -p "${DB_PORT}" -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL'
INSERT INTO users (username, password_hash, role)
VALUES
('${BOOTSTRAP_ADMIN_USERNAME}', '${BOOTSTRAP_ADMIN_PASSWORD}', 'admin'),
('${BOOTSTRAP_USER_USERNAME}', '${BOOTSTRAP_USER_PASSWORD}', 'user')
ON CONFLICT (username) DO NOTHING;
INSERT INTO config (key, value)
VALUES
('wifi_password', '${BOOTSTRAP_WIFI_PASSWORD}'),
('tagline', '${BOOTSTRAP_TAGLINE}')
ON CONFLICT (key) DO NOTHING;
INSERT INTO offers (title, description, active, sort_order)
VALUES
('Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', true, 1),
('Sunset Dinner', 'Three-course menu on the terrace every Friday.', true, 2)
ON CONFLICT DO NOTHING;
INSERT INTO benefits (text)
VALUES
('Priority booking for peak weekends'),
('Late checkout on availability'),
('Free welcome cocktail'),
('Member-only events and tastings')
ON CONFLICT DO NOTHING;
SQL
+87 -233
View File
@@ -4,239 +4,93 @@ metadata:
name: init-database
namespace: lahuasca
spec:
backoffLimit: 4
template:
spec:
containers:
- name: init-db
image: postgres:15
command:
- sh
- -c
- |
apk add --no-cache curl
# Wait for postgres to be ready
until pg_isready -h postgres.lahuasca.svc.cluster.local -U lahuasca; do
echo "Waiting for postgres..."
sleep 2
done
# Create tables and seed data
psql -h postgres.lahuasca.svc.cluster.local -U lahuasca -d lahuasca << 'EOF'
-- Create all tables
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')),
comments_disabled BOOLEAN DEFAULT FALSE,
first_name VARCHAR(100),
last_name VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS images (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
data TEXT NOT NULL,
category VARCHAR(100) DEFAULT 'other',
room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL,
location_target VARCHAR(100) DEFAULT 'gallery',
uploaded_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reservations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
date DATE NOT NULL,
time TIME NOT NULL,
guests INTEGER NOT NULL,
table_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS coupons (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
code VARCHAR(50) NOT NULL,
discount VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS offers (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS benefits (
id SERIAL PRIMARY KEY,
text VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS config (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS rooms (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS menu_items (
id SERIAL PRIMARY KEY,
category VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
is_daily_special BOOLEAN DEFAULT FALSE,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS places (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
distance_km NUMERIC(8,2),
visit_time VARCHAR(100),
suggestion TEXT,
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reviews (
id SERIAL PRIMARY KEY,
author_name VARCHAR(255) NOT NULL,
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
text TEXT NOT NULL,
approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS slides (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
subtitle TEXT,
image_id INTEGER REFERENCES images(id) ON DELETE SET NULL,
image_url VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS room_comments (
id SERIAL PRIMARY KEY,
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
photo_url VARCHAR(500),
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
first_name VARCHAR(100) NOT NULL,
last_initial VARCHAR(10),
text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
deleted_by_admin BOOLEAN DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS social_events (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price VARCHAR(50),
date DATE,
photos TEXT[] DEFAULT '{}',
featured_photo VARCHAR(500),
active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS social_event_reservations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE,
guests INTEGER NOT NULL,
notes TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, social_event_id)
);
CREATE TABLE IF NOT EXISTS calendar_events (
id SERIAL PRIMARY KEY,
date DATE NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
type VARCHAR(100) NOT NULL,
description TEXT,
color VARCHAR(20) NOT NULL
);
CREATE TABLE IF NOT EXISTS user_sessions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Add missing columns if they don't exist
ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);
-- Insert initial data
-- Default admin user (password: admin)
INSERT INTO users (username, password_hash, role) VALUES
('admin', '$2a$10$8K1p/a0dhrxiowP.dnkgNORTWgdEDHn5L2/xjpEWuC.QQv4rKO9jO', 'admin')
ON CONFLICT (username) DO NOTHING;
-- Default config values
INSERT INTO config (key, value) VALUES
('wifi_password', 'LaHuascaGuest2026'),
('tagline', 'Hotel · Restaurant · Andean Highlands')
ON CONFLICT (key) DO NOTHING;
-- Sample offers
INSERT INTO offers (title, description, active, sort_order) VALUES
('Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', true, 1),
('Sunset Dinner', 'Three-course menu on the terrace every Friday.', true, 2)
ON CONFLICT DO NOTHING;
-- Sample benefits
INSERT INTO benefits (text) VALUES
('Priority booking for peak weekends'),
('Late checkout on availability'),
('Free welcome cocktail'),
('Member-only events and tastings')
ON CONFLICT DO NOTHING;
EOF
echo "Database initialization completed!"
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-db-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: la-huasca-db-secrets
key: POSTGRES_USER
restartPolicy: Never
backoffLimit: 4
containers:
- name: init-db
image: postgres:15
command:
- sh
- -ec
- |
until pg_isready -h "${DB_HOST}" -p "${DB_PORT}" -U "$POSTGRES_USER"; do
echo "Waiting for postgres..."
sleep 2
done
psql \
-h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
-f /bootstrap/schema.sql
sh /bootstrap/seed.sh
echo "Database initialization completed!"
env:
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-db-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: la-huasca-db-secrets
key: POSTGRES_USER
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: la-huasca-db-secrets
key: POSTGRES_DB
- name: BOOTSTRAP_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_ADMIN_USERNAME
- name: BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_ADMIN_PASSWORD
- name: BOOTSTRAP_USER_USERNAME
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_USER_USERNAME
- name: BOOTSTRAP_USER_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_USER_PASSWORD
- name: BOOTSTRAP_WIFI_PASSWORD
valueFrom:
secretKeyRef:
name: la-huasca-bootstrap-secrets
key: BOOTSTRAP_WIFI_PASSWORD
- name: BOOTSTRAP_TAGLINE
valueFrom:
configMapKeyRef:
name: la-huasca-bootstrap-config
key: BOOTSTRAP_TAGLINE
volumeMounts:
- name: bootstrap
mountPath: /bootstrap
readOnly: true
volumes:
- name: bootstrap
configMap:
name: la-huasca-bootstrap-config
defaultMode: 0755
items:
- key: schema.sql
path: schema.sql
- key: seed.sh
path: seed.sh
+1 -1
View File
@@ -2,7 +2,7 @@ apiVersion: v1
kind: Secret
metadata:
name: la-huasca-registry
namespace: ${NAMESPACE}
namespace: lahuasca
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
+1 -1
View File
@@ -3,7 +3,7 @@
"kind": "Service",
"metadata": {
"name": "la-huasca-ts",
"namespace": "la-huasca"
"namespace": "lahuasca"
},
"spec": {
"ports": [