fix: resolve CouchDB deployment and init job issues
- Remove problematic ERL_FLAGS from CouchDB StatefulSet - Copy setup-couchdb.js to backend/scripts/ for Docker image inclusion - Update init job to use full DNS name and add timeout/retry logic - Fix script path in init job command 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
472
backend/scripts/setup-couchdb.js
Normal file
472
backend/scripts/setup-couchdb.js
Normal file
@@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Setup module path to include backend node_modules
|
||||
const path = require('path');
|
||||
const backendPath = path.join(__dirname, '..', 'backend');
|
||||
process.env.NODE_PATH = path.join(backendPath, 'node_modules') + ':' + (process.env.NODE_PATH || '');
|
||||
require('module').Module._initPaths();
|
||||
|
||||
const Nano = require('nano');
|
||||
require('dotenv').config({ path: path.join(backendPath, '.env') });
|
||||
|
||||
// Configuration
|
||||
const COUCHDB_URL = process.env.COUCHDB_URL || 'http://localhost:5984';
|
||||
const COUCHDB_USER = process.env.COUCHDB_USER || 'admin';
|
||||
const COUCHDB_PASSWORD = process.env.COUCHDB_PASSWORD || 'admin';
|
||||
const COUCHDB_DB_NAME = process.env.COUCHDB_DB_NAME || 'adopt-a-street';
|
||||
|
||||
class CouchDBSetup {
|
||||
constructor() {
|
||||
this.nano = Nano({
|
||||
url: COUCHDB_URL,
|
||||
auth: { username: COUCHDB_USER, password: COUCHDB_PASSWORD }
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('🚀 Initializing CouchDB setup...');
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
await this.nano.info();
|
||||
console.log('✅ Connected to CouchDB');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to CouchDB:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async createDatabase() {
|
||||
console.log(`📦 Creating database: ${COUCHDB_DB_NAME}`);
|
||||
|
||||
try {
|
||||
await this.nano.db.create(COUCHDB_DB_NAME);
|
||||
console.log('✅ Database created successfully');
|
||||
} catch (error) {
|
||||
if (error.statusCode === 412) {
|
||||
console.log('ℹ️ Database already exists');
|
||||
} else {
|
||||
console.error('❌ Failed to create database:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createIndexes() {
|
||||
console.log('🔍 Creating indexes...');
|
||||
|
||||
const db = this.nano.use(COUCHDB_DB_NAME);
|
||||
|
||||
// Create design document with indexes
|
||||
const designDoc = {
|
||||
_id: '_design/adopt-a-street',
|
||||
views: {
|
||||
users_by_email: {
|
||||
map: "function(doc) { if (doc.type === 'user') { emit(doc.email, doc); } }"
|
||||
},
|
||||
streets_by_location: {
|
||||
map: "function(doc) { if (doc.type === 'street') { emit(doc.location, doc); } }"
|
||||
},
|
||||
by_user: {
|
||||
map: "function(doc) { if (doc.user && doc.user.userId) { emit(doc.user.userId, doc); } }"
|
||||
},
|
||||
users_by_points: {
|
||||
map: "function(doc) { if (doc.type === 'user') { emit(doc.points, doc); } }"
|
||||
},
|
||||
posts_by_date: {
|
||||
map: "function(doc) { if (doc.type === 'post') { emit(doc.createdAt, doc); } }"
|
||||
},
|
||||
streets_by_status: {
|
||||
map: "function(doc) { if (doc.type === 'street') { emit(doc.status, doc); } }"
|
||||
},
|
||||
events_by_date_status: {
|
||||
map: "function(doc) { if (doc.type === 'event') { emit([doc.date, doc.status], doc); } }"
|
||||
},
|
||||
comments_by_post: {
|
||||
map: "function(doc) { if (doc.type === 'comment' && doc.post && doc.post.postId) { emit(doc.post.postId, doc); } }"
|
||||
},
|
||||
transactions_by_user_date: {
|
||||
map: "function(doc) { if (doc.type === 'point_transaction' && doc.user && doc.user.userId) { emit([doc.user.userId, doc.createdAt], doc); } }"
|
||||
}
|
||||
},
|
||||
indexes: {
|
||||
users_by_email: {
|
||||
index: {
|
||||
fields: ["type", "email"]
|
||||
},
|
||||
name: "user-by-email",
|
||||
type: "json"
|
||||
},
|
||||
streets_by_location: {
|
||||
index: {
|
||||
fields: ["type", "location"]
|
||||
},
|
||||
name: "streets-by-location",
|
||||
type: "json"
|
||||
},
|
||||
by_user: {
|
||||
index: {
|
||||
fields: ["type", "user.userId"]
|
||||
},
|
||||
name: "by-user",
|
||||
type: "json"
|
||||
},
|
||||
users_by_points: {
|
||||
index: {
|
||||
fields: ["type", "points"]
|
||||
},
|
||||
name: "users-by-points",
|
||||
type: "json"
|
||||
},
|
||||
posts_by_date: {
|
||||
index: {
|
||||
fields: ["type", "createdAt"]
|
||||
},
|
||||
name: "posts-by-date",
|
||||
type: "json"
|
||||
},
|
||||
streets_by_status: {
|
||||
index: {
|
||||
fields: ["type", "status"]
|
||||
},
|
||||
name: "streets-by-status",
|
||||
type: "json"
|
||||
},
|
||||
events_by_date_status: {
|
||||
index: {
|
||||
fields: ["type", "date", "status"]
|
||||
},
|
||||
name: "events-by-date-status",
|
||||
type: "json"
|
||||
},
|
||||
comments_by_post: {
|
||||
index: {
|
||||
fields: ["type", "post.postId"]
|
||||
},
|
||||
name: "comments-by-post",
|
||||
type: "json"
|
||||
},
|
||||
transactions_by_user_date: {
|
||||
index: {
|
||||
fields: ["type", "user.userId", "createdAt"]
|
||||
},
|
||||
name: "transactions-by-user-date",
|
||||
type: "json"
|
||||
}
|
||||
},
|
||||
language: "javascript"
|
||||
};
|
||||
|
||||
try {
|
||||
await db.insert(designDoc);
|
||||
console.log('✅ Design document and indexes created successfully');
|
||||
} catch (error) {
|
||||
if (error.statusCode === 409) {
|
||||
// Document already exists, update it
|
||||
const existing = await db.get('_design/adopt-a-street');
|
||||
designDoc._rev = existing._rev;
|
||||
await db.insert(designDoc);
|
||||
console.log('✅ Design document and indexes updated successfully');
|
||||
} else {
|
||||
console.error('❌ Failed to create indexes:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createSecurityDocument() {
|
||||
console.log('🔒 Setting up security document...');
|
||||
|
||||
const db = this.nano.use(COUCHDB_DB_NAME);
|
||||
|
||||
const securityDoc = {
|
||||
admins: {
|
||||
names: [COUCHDB_USER],
|
||||
roles: []
|
||||
},
|
||||
members: {
|
||||
names: [],
|
||||
roles: []
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await db.insert(securityDoc, '_security');
|
||||
console.log('✅ Security document created successfully');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to create security document:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async seedBadges() {
|
||||
console.log('🏆 Seeding badges...');
|
||||
|
||||
const db = this.nano.use(COUCHDB_DB_NAME);
|
||||
|
||||
// Check if badges already exist
|
||||
try {
|
||||
const existingBadges = await db.find({
|
||||
selector: { type: 'badge' },
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (existingBadges.docs.length > 0) {
|
||||
console.log('ℹ️ Badges already exist, skipping seeding');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with seeding
|
||||
}
|
||||
|
||||
const badges = [
|
||||
// Street Adoption Badges
|
||||
{
|
||||
_id: 'badge_first_adoption',
|
||||
type: 'badge',
|
||||
name: 'First Adoption',
|
||||
description: 'Adopted your first street',
|
||||
icon: '🏡',
|
||||
criteria: { type: 'street_adoptions', threshold: 1 },
|
||||
rarity: 'common',
|
||||
order: 1,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_street_adopter',
|
||||
type: 'badge',
|
||||
name: 'Street Adopter',
|
||||
description: 'Adopted 5 streets',
|
||||
icon: '🏘️',
|
||||
criteria: { type: 'street_adoptions', threshold: 5 },
|
||||
rarity: 'rare',
|
||||
order: 2,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_neighborhood_champion',
|
||||
type: 'badge',
|
||||
name: 'Neighborhood Champion',
|
||||
description: 'Adopted 10 streets',
|
||||
icon: '🌆',
|
||||
criteria: { type: 'street_adoptions', threshold: 10 },
|
||||
rarity: 'epic',
|
||||
order: 3,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_city_guardian',
|
||||
type: 'badge',
|
||||
name: 'City Guardian',
|
||||
description: 'Adopted 25 streets',
|
||||
icon: '🏙️',
|
||||
criteria: { type: 'street_adoptions', threshold: 25 },
|
||||
rarity: 'legendary',
|
||||
order: 4,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
// Task Completion Badges
|
||||
{
|
||||
_id: 'badge_first_task',
|
||||
type: 'badge',
|
||||
name: 'First Task',
|
||||
description: 'Completed your first task',
|
||||
icon: '✅',
|
||||
criteria: { type: 'task_completions', threshold: 1 },
|
||||
rarity: 'common',
|
||||
order: 5,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_task_master',
|
||||
type: 'badge',
|
||||
name: 'Task Master',
|
||||
description: 'Completed 10 tasks',
|
||||
icon: '🎯',
|
||||
criteria: { type: 'task_completions', threshold: 10 },
|
||||
rarity: 'rare',
|
||||
order: 6,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_dedicated_worker',
|
||||
type: 'badge',
|
||||
name: 'Dedicated Worker',
|
||||
description: 'Completed 50 tasks',
|
||||
icon: '🛠️',
|
||||
criteria: { type: 'task_completions', threshold: 50 },
|
||||
rarity: 'epic',
|
||||
order: 7,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
_id: 'badge_maintenance_legend',
|
||||
type: 'badge',
|
||||
name: 'Maintenance Legend',
|
||||
description: 'Completed 100 tasks',
|
||||
icon: '⚡',
|
||||
criteria: { type: 'task_completions', threshold: 100 },
|
||||
rarity: 'legendary',
|
||||
order: 8,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
const results = await db.bulk({ docs: badges });
|
||||
const successCount = results.filter(r => !r.error).length;
|
||||
console.log(`✅ Successfully seeded ${successCount} badges`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to seed badges:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async verifySetup() {
|
||||
console.log('🔍 Verifying setup...');
|
||||
|
||||
const db = this.nano.use(COUCHDB_DB_NAME);
|
||||
|
||||
try {
|
||||
// Check database info
|
||||
const info = await db.info();
|
||||
console.log(`✅ Database "${info.db_name}" is ready`);
|
||||
console.log(` - Doc count: ${info.doc_count}`);
|
||||
console.log(` - Update seq: ${info.update_seq}`);
|
||||
|
||||
// Check indexes
|
||||
const designDoc = await db.get('_design/adopt-a-street');
|
||||
const indexCount = Object.keys(designDoc.indexes || {}).length;
|
||||
console.log(`✅ ${indexCount} indexes created`);
|
||||
|
||||
// Check badges
|
||||
const badges = await db.find({
|
||||
selector: { type: 'badge' },
|
||||
fields: ['name']
|
||||
});
|
||||
console.log(`✅ ${badges.docs.length} badges available`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Setup verification failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async seedAdminUser() {
|
||||
console.log('👤 Seeding admin user...');
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.log('⚠️ ADMIN_EMAIL or ADMIN_PASSWORD not set, skipping admin user creation');
|
||||
return;
|
||||
}
|
||||
|
||||
const db = this.nano.use(COUCHDB_DB_NAME);
|
||||
|
||||
// Check if admin user already exists
|
||||
try {
|
||||
const existing = await db.find({
|
||||
selector: { type: 'user', email: ADMIN_EMAIL },
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (existing.docs.length > 0) {
|
||||
const user = existing.docs[0];
|
||||
if (!user.isAdmin) {
|
||||
user.isAdmin = true;
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await db.insert(user);
|
||||
console.log('✅ Existing user promoted to admin');
|
||||
} else {
|
||||
console.log('ℹ️ Admin user already exists');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with creation
|
||||
}
|
||||
|
||||
// Create new admin user with hashed password
|
||||
const bcrypt = require('bcryptjs');
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, salt);
|
||||
|
||||
const adminUser = {
|
||||
_id: `user_admin_${Date.now()}`,
|
||||
type: 'user',
|
||||
name: 'Administrator',
|
||||
email: ADMIN_EMAIL,
|
||||
password: hashedPassword,
|
||||
isAdmin: true,
|
||||
isPremium: true,
|
||||
points: 0,
|
||||
avatar: null,
|
||||
profilePicture: null,
|
||||
bio: 'System Administrator',
|
||||
location: '',
|
||||
website: '',
|
||||
social: { twitter: '', github: '', linkedin: '' },
|
||||
privacySettings: { profileVisibility: 'private' },
|
||||
preferences: { emailNotifications: true, pushNotifications: true, theme: 'light' },
|
||||
adoptedStreets: [],
|
||||
completedTasks: [],
|
||||
posts: [],
|
||||
events: [],
|
||||
earnedBadges: [],
|
||||
stats: { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0 },
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await db.insert(adminUser);
|
||||
console.log(`✅ Admin user created: ${ADMIN_EMAIL}`);
|
||||
}
|
||||
|
||||
async run() {
|
||||
try {
|
||||
await this.initialize();
|
||||
await this.createDatabase();
|
||||
await this.createIndexes();
|
||||
await this.createSecurityDocument();
|
||||
await this.seedBadges();
|
||||
await this.seedAdminUser();
|
||||
await this.verifySetup();
|
||||
|
||||
console.log('\n🎉 CouchDB setup completed successfully!');
|
||||
console.log(`\n📋 Connection Details:`);
|
||||
console.log(` URL: ${COUCHDB_URL}`);
|
||||
console.log(` Database: ${COUCHDB_DB_NAME}`);
|
||||
console.log(` User: ${COUCHDB_USER}`);
|
||||
console.log(`\n🌐 Access CouchDB at: ${COUCHDB_URL}/_utils`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Setup failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run setup if called directly
|
||||
if (require.main === module) {
|
||||
const setup = new CouchDBSetup();
|
||||
setup.run().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = CouchDBSetup;
|
||||
@@ -20,8 +20,15 @@ spec:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until curl -f -s http://adopt-a-street-couchdb:5984/_up > /dev/null 2>&1; do
|
||||
echo "Waiting for CouchDB to be ready..."
|
||||
COUNT=0
|
||||
MAX_ATTEMPTS=60
|
||||
until curl -f -s http://adopt-a-street-couchdb.adopt-a-street.svc.cluster.local:5984/_up > /dev/null 2>&1; do
|
||||
COUNT=$((COUNT+1))
|
||||
if [ $COUNT -ge $MAX_ATTEMPTS ]; then
|
||||
echo "Timeout waiting for CouchDB after $MAX_ATTEMPTS attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for CouchDB to be ready... (attempt $COUNT/$MAX_ATTEMPTS)"
|
||||
sleep 3
|
||||
done
|
||||
echo "CouchDB is ready!"
|
||||
|
||||
@@ -45,6 +45,21 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- arm64 # Pi 5 architecture
|
||||
initContainers:
|
||||
- name: setup-config
|
||||
image: busybox:1.36
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "[chttpd]" > /opt/couchdb/etc/local.d/bind.ini
|
||||
echo "bind_address = 0.0.0.0" >> /opt/couchdb/etc/local.d/bind.ini
|
||||
cat /opt/couchdb/etc/local.d/bind.ini
|
||||
volumeMounts:
|
||||
- name: couchdb-data
|
||||
mountPath: /opt/couchdb/data
|
||||
- name: local-config
|
||||
mountPath: /opt/couchdb/etc/local.d
|
||||
containers:
|
||||
- name: couchdb
|
||||
image: couchdb:3.3
|
||||
@@ -71,30 +86,39 @@ spec:
|
||||
key: COUCHDB_SECRET
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
volumeMounts:
|
||||
- name: couchdb-data
|
||||
mountPath: /opt/couchdb/data
|
||||
- name: local-config
|
||||
mountPath: /opt/couchdb/etc/local.d
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /_up
|
||||
port: 5984
|
||||
exec:
|
||||
command:
|
||||
- curl
|
||||
- -f
|
||||
- http://localhost:5984/_up
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /_up
|
||||
port: 5984
|
||||
exec:
|
||||
command:
|
||||
- curl
|
||||
- -f
|
||||
- http://localhost:5984/_up
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
volumes:
|
||||
- name: local-config
|
||||
emptyDir: {}
|
||||
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
|
||||
Reference in New Issue
Block a user