Add MongoDB integration with CRUD UI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Integrated MongoDB 7.0 with Mongoose ODM - Added CRUD API endpoints (GET, POST, PUT, DELETE) - Created Family model with validation - Added database seeding script with initial data - Implemented Add Family modal form in frontend - Updated docker-compose with MongoDB service - Updated Helm chart to v0.3.0 with MongoDB StatefulSet - Updated documentation with MongoDB setup instructions
This commit is contained in:
parent
f3da9b66c3
commit
02074aa4a6
240
README.md
240
README.md
@ -16,55 +16,86 @@ A full-stack web application for mapping and searching family locations in Yemen
|
|||||||
- 🔍 **Family Search**: Search for families by name with autocomplete suggestions
|
- 🔍 **Family Search**: Search for families by name with autocomplete suggestions
|
||||||
- 🗺️ **Interactive Map**: Leaflet-based map with multiple tile layer options
|
- 🗺️ **Interactive Map**: Leaflet-based map with multiple tile layer options
|
||||||
- 📍 **Location Markers**: View family locations with city information
|
- 📍 **Location Markers**: View family locations with city information
|
||||||
- 🎨 **Modern UI**: Clean and responsive design
|
- ➕ **Add Families**: Admin UI to add new family locations to the database
|
||||||
|
- 💾 **MongoDB Database**: Real database persistence with full CRUD operations
|
||||||
|
- 🎨 **Modern UI**: Clean and responsive design with modal forms
|
||||||
- 🐳 **Docker Ready**: Containerized for easy deployment
|
- 🐳 **Docker Ready**: Containerized for easy deployment
|
||||||
|
- ☸️ **Kubernetes Ready**: Helm chart for production deployment
|
||||||
- 💚 **Health Checks**: Built-in health monitoring
|
- 💚 **Health Checks**: Built-in health monitoring
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
**Backend:**
|
**Backend:**
|
||||||
- Node.js
|
- Node.js 20 Alpine
|
||||||
- Express.js
|
- Express.js 4.18
|
||||||
- JSON data storage
|
- MongoDB 7.0
|
||||||
|
- Mongoose ODM
|
||||||
|
- CORS enabled
|
||||||
|
|
||||||
**Frontend:**
|
**Frontend:**
|
||||||
- HTML5/CSS3
|
- HTML5/CSS3
|
||||||
- JavaScript (ES6+)
|
- Vanilla JavaScript (ES6+)
|
||||||
- Leaflet.js (interactive maps)
|
- Leaflet.js (interactive maps)
|
||||||
- Fuse.js (fuzzy search)
|
- Fuse.js (fuzzy search)
|
||||||
|
- Nginx (production server)
|
||||||
|
|
||||||
**DevOps:**
|
**DevOps:**
|
||||||
- Docker
|
- Docker & Docker Compose
|
||||||
- Docker Compose
|
- Kubernetes & Helm Charts
|
||||||
|
- Woodpecker CI/CD
|
||||||
|
- Harbor Registry
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure with MongoDB
|
||||||
|
|
||||||
```
|
|
||||||
oramap/
|
|
||||||
├── backend/
|
|
||||||
│ ├── server.js # Express server
|
|
||||||
│ ├── package.json # Backend dependencies
|
│ ├── package.json # Backend dependencies
|
||||||
│ ├── Dockerfile # Backend container image
|
│ ├── Dockerfile # Backend container image
|
||||||
│ └── data/
|
│ ├── config/
|
||||||
│ └── families.json # Family location data
|
│ │ └── database.js # MongoDB connection
|
||||||
|
│ ├── models/
|
||||||
|
│ │ └── Family.js # Mongoose schema
|
||||||
|
│ └── scripts/
|
||||||
|
│ └── seed.js # Database seeding script
|
||||||
├── frontend/
|
├── frontend/
|
||||||
│ ├── Dockerfile # Frontend Nginx container
|
│ ├── Dockerfile # Frontend Nginx container
|
||||||
│ ├── nginx.conf # Nginx configuration
|
│ ├── nginx.conf # Nginx configuration
|
||||||
│ └── public/
|
│ └── public/
|
||||||
│ ├── index.html # Frontend HTML
|
│ ├── index.html # Frontend HTML with modal form
|
||||||
│ ├── script.js # Frontend JavaScript
|
│ ├── script.js # Frontend JavaScript with CRUD
|
||||||
│ └── style.css # Styles
|
│ └── style.css # Styles including modal
|
||||||
├── public/
|
├── oramap/ # Helm Chart
|
||||||
│ ├── index.html # Shared frontend files
|
│ ├── Chart.yaml # Chart metadata (v0.3.0)
|
||||||
|
│ ├── values.yaml # Configuration values
|
||||||
|
│ ├── README.md # Helm documentation
|
||||||
|
│ └── templates/
|
||||||
|
│ ├── deployment.yaml # Backend & Frontend deployments
|
||||||
|
│ ├── service.yaml # Services
|
||||||
|
│ ├── ingress.yaml # Ingress rules
|
||||||
|
│ ├── configmap.yaml # Nginx config
|
||||||
|
│ ├── mongodb-statefulset.yaml # MongoDB StatefulSet
|
||||||
|
│ └── mongodb-service.yaml # MongoDB service
|
||||||
|
├── old/ # Legacy files
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── data/families.json
|
||||||
|
├── Dockerfile # Monolith Docker build
|
||||||
|
├── docker-compose.yml # Monolith deployment
|
||||||
|
├── docker-compose.microservices.yml # Microservices with MongoDB
|
||||||
│ ├── script.js
|
│ ├── script.js
|
||||||
│ └── style.css
|
│ └── style.css
|
||||||
├── Dockerfile # Monolith Docker build
|
├── Dockerfile # Monolith Docker build
|
||||||
├── docker-compose.yml # Monolith deployment
|
├── docker-compose.yml # Monolith deployment
|
||||||
├── docker-compose.microservices.yml # Microservices deployment
|
├── MongoDB**: Database for family locations (Port 27017)
|
||||||
├── .woodpecker.yaml # CI/CD pipeline config
|
- **Backend**: Express API server with MongoDB integration (Internal Port 3000)
|
||||||
└── .dockerignore # Docker ignore rules
|
- **Frontend**: Nginx serving static files with API proxy (Port 80)
|
||||||
```
|
- Nginx proxies `/api/*` requests to backend
|
||||||
|
- Best for: Production, scalability, CI/CD pipelines
|
||||||
|
- Use: `docker-compose -f docker-compose.microservices.yml up`
|
||||||
|
|
||||||
|
### 3. **Kubernetes Mode** (Enterprise)
|
||||||
|
- Helm chart deployment with StatefulSet for MongoDB
|
||||||
|
- Persistent storage for database
|
||||||
|
- Horizontal scaling for backend/frontend
|
||||||
|
- Ingress with TLS support
|
||||||
|
- See: `oramap/README.md` for Helm chart documentation
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
The application supports two deployment modes:
|
The application supports two deployment modes:
|
||||||
@ -89,18 +120,33 @@ The application supports two deployment modes:
|
|||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|
||||||
1. **Install backend dependencies:**
|
1. **Start MongoDB (optional - uses local instance if not running):**
|
||||||
|
```bash
|
||||||
|
# Using Docker
|
||||||
|
docker run -d -p 27017:27017 --name mongodb mongo:7.0
|
||||||
|
|
||||||
|
# Or install MongoDB locally
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install backend dependencies:**
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Start the server:**
|
3. **Seed initial data:**
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm run seed
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Open your browser:**
|
4. **Start the server:**
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
# Or for development with auto-reload:
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Open your browser:**
|
||||||
Navigate to `http://localhost:3000`
|
Navigate to `http://localhost:3000`
|
||||||
|
|
||||||
## 🐳 Docker Deployment
|
## 🐳 Docker Deployment
|
||||||
@ -121,22 +167,30 @@ docker-compose down
|
|||||||
|
|
||||||
### Microservices Mode (Recommended for Production)
|
### Microservices Mode (Recommended for Production)
|
||||||
|
|
||||||
1. **Build and start:**
|
1. **Build and start all services (Backend + Frontend + MongoDB):**
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.microservices.yml up -d
|
docker-compose -f docker-compose.microservices.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Access:** http://localhost (Port 80)
|
2. **Seed the database:**
|
||||||
|
```bash
|
||||||
|
docker exec -it oramap-backend npm run seed
|
||||||
|
```
|
||||||
|
|
||||||
3. **View logs:**
|
3. **Access:** http://localhost (Port 80)
|
||||||
|
|
||||||
|
4. **View logs:**
|
||||||
```bash
|
```bash
|
||||||
docker logs oramap-frontend
|
docker logs oramap-frontend
|
||||||
docker logs oramap-backend
|
docker logs oramap-backend
|
||||||
|
docker logs oramap-mongo
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Stop:**
|
5. **Stop:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.microservices.yml down
|
docker-compose -f docker-compose.microservices.yml down
|
||||||
|
# To remove volumes:
|
||||||
|
docker-compose -f docker-compose.microservices.yml down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Docker directly
|
### Using Docker directly
|
||||||
@ -184,36 +238,144 @@ cd backend
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
|
||||||
|
**Seed initial data:**
|
||||||
|
```bash
|
||||||
|
npm run seed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Force re-seed (clears existing data):**
|
||||||
|
```bash
|
||||||
|
npm run seed:force
|
||||||
|
```
|
||||||
|
|
||||||
### Adding New Families
|
### Adding New Families
|
||||||
|
|
||||||
Edit `backend/data/families.json` and add entries in the following format:
|
Use the web UI "Add Family" button, or make API calls:
|
||||||
|
|
||||||
```json
|
```bash
|
||||||
{
|
curl -X POST http://localhost:3000/api/families \
|
||||||
"family": "Family Name (Hebrew)",
|
-H "Content-Type: application/json" \
|
||||||
"city": "City Name (Hebrew)",
|
-d '{
|
||||||
|
"family": "Family Name",
|
||||||
|
"city": "City Name",
|
||||||
"lat": 15.3545,
|
"lat": 15.3545,
|
||||||
"lng": 44.2064
|
"lng": 44.2064
|
||||||
}
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📡 API Endpoints
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
### Get All Families
|
||||||
|
```
|
||||||
|
GET /api/families
|
||||||
|
```
|
||||||
|
Returns all families sorted by name.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/families
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"_id": "507f1f77bcf86cd799439011",
|
||||||
|
"family": "משפחת כפה",
|
||||||
|
"city": "צנעא",
|
||||||
|
"lat": 15.3545,
|
||||||
|
"lng": 44.2064,
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### Search Families
|
### Search Families
|
||||||
```
|
```
|
||||||
GET /api/search?family={familyName}
|
GET /api/search?family={familyName}
|
||||||
```
|
```
|
||||||
Returns matching family records with location data.
|
Returns matching family records using MongoDB text search.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
curl "http://localhost:3000/api/search?family=Kafe"
|
curl "http://localhost:3000/api/search?family=Kafe"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Create Family
|
||||||
|
```
|
||||||
|
POST /api/families
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
Creates a new family location.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"family": "משפחת דוד",
|
||||||
|
"city": "עדן",
|
||||||
|
"lat": 12.7855,
|
||||||
|
"lng": 45.0187
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "507f1f77bcf86cd799439011",
|
||||||
|
"family": "משפחת דוד",
|
||||||
|
"city": "עדן",
|
||||||
|
"lat": 12.7855,
|
||||||
|
"lng": 45.0187,
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Family
|
||||||
|
```
|
||||||
|
PUT /api/families/:id
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
Updates an existing family.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"family": "משפחת כהן",
|
||||||
|
"city": "צנעא",
|
||||||
|
"lat": 15.3695,
|
||||||
|
"lng": 44.1910
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Family
|
||||||
|
```
|
||||||
|
DELETE /api/families/:id
|
||||||
|
```
|
||||||
|
Deletes a family by ID.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:3000/api/families/507f1f77bcf86cd799439011
|
||||||
|
```
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
```
|
```
|
||||||
GET /api/health
|
GET /api/health
|
||||||
```
|
```
|
||||||
|
Returns server and database health status.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z",
|
||||||
|
"database": "connected"
|
||||||
|
}
|
||||||
|
```
|
||||||
Returns server health status.
|
Returns server health status.
|
||||||
|
|
||||||
**Example Response:**
|
**Example Response:**
|
||||||
|
|||||||
20
backend/config/database.js
Normal file
20
backend/config/database.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const connectDB = async () => {
|
||||||
|
try {
|
||||||
|
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap';
|
||||||
|
|
||||||
|
await mongoose.connect(mongoURI, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ MongoDB connected successfully');
|
||||||
|
console.log(`📍 Database: ${mongoose.connection.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ MongoDB connection error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = connectDB;
|
||||||
33
backend/models/Family.js
Normal file
33
backend/models/Family.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const familySchema = new mongoose.Schema({
|
||||||
|
family: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
lat: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: -90,
|
||||||
|
max: 90
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: -180,
|
||||||
|
max: 180
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Index for faster searches
|
||||||
|
familySchema.index({ family: 'text', city: 'text' });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Family', familySchema);
|
||||||
@ -5,13 +5,16 @@
|
|||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "node server.js"
|
"dev": "node server.js",
|
||||||
|
"seed": "node scripts/seed.js",
|
||||||
|
"seed:force": "node scripts/seed.js --force"
|
||||||
},
|
},
|
||||||
"keywords": ["map", "family", "leaflet", "express"],
|
"keywords": ["map", "family", "leaflet", "express"],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"cors": "^2.8.5"
|
"cors": "^2.8.5",
|
||||||
|
"mongoose": "^7.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
backend/scripts/seed.js
Normal file
59
backend/scripts/seed.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Family = require('../models/Family');
|
||||||
|
|
||||||
|
const initialData = [
|
||||||
|
{ "family": "Kafe (קאפח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Shiheb (שחב-שבח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Manakhah (מנאכה)", "lat": 15.3019, "lng": 43.5983 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Dhamar (ד'מאר)", "lat": 14.5424, "lng": 44.4056 },
|
||||||
|
{ "family": "Salumi (סלומי-שלומי)", "city": "Al Kafla (אל קפלה)", "lat": 16.0240, "lng": 43.9790 },
|
||||||
|
{ "family": "Afgin (עפג'ין)", "city": "Sa'dah (צעדה)", "lat": 16.9402, "lng": 43.7639 },
|
||||||
|
{ "family": "Eraki (עראקי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 }
|
||||||
|
];
|
||||||
|
|
||||||
|
async function seedDatabase() {
|
||||||
|
try {
|
||||||
|
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap';
|
||||||
|
|
||||||
|
await mongoose.connect(mongoURI, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Connected to MongoDB');
|
||||||
|
|
||||||
|
// Check if data already exists
|
||||||
|
const existingCount = await Family.countDocuments();
|
||||||
|
|
||||||
|
if (existingCount > 0) {
|
||||||
|
console.log(`ℹ️ Database already has ${existingCount} families`);
|
||||||
|
const answer = process.argv.includes('--force');
|
||||||
|
|
||||||
|
if (answer) {
|
||||||
|
console.log('🗑️ Clearing existing data...');
|
||||||
|
await Family.deleteMany({});
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Skipping seed. Use --force flag to override existing data');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert initial data
|
||||||
|
console.log('📝 Seeding database...');
|
||||||
|
const result = await Family.insertMany(initialData);
|
||||||
|
|
||||||
|
console.log(`✅ Successfully seeded ${result.length} families`);
|
||||||
|
console.log('\nAdded families:');
|
||||||
|
result.forEach(family => {
|
||||||
|
console.log(` - ${family.family} | ${family.city}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Seed error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedDatabase();
|
||||||
@ -1,28 +1,156 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const app = express();
|
const connectDB = require('./config/database');
|
||||||
const families = require('./data/families.json');
|
const Family = require('./models/Family');
|
||||||
|
|
||||||
// Serve static files from the public directory
|
const app = express();
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
|
||||||
|
// Enable CORS for frontend
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
// Body parser middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Connect to MongoDB
|
||||||
|
connectDB();
|
||||||
|
|
||||||
|
// Serve static files from the public directory (for standalone mode)
|
||||||
|
if (process.env.SERVE_STATIC === 'true') {
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
}
|
||||||
|
|
||||||
// API endpoint for family search
|
// API endpoint for family search
|
||||||
app.get('/api/search', (req, res) => {
|
app.get('/api/search', async (req, res) => {
|
||||||
|
try {
|
||||||
const query = req.query.family?.toLowerCase();
|
const query = req.query.family?.toLowerCase();
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return res.json([]);
|
return res.json([]);
|
||||||
}
|
}
|
||||||
const matches = families.filter(fam => fam.family.toLowerCase().includes(query));
|
|
||||||
|
// Search by family name (case-insensitive)
|
||||||
|
const matches = await Family.find({
|
||||||
|
family: { $regex: query, $options: 'i' }
|
||||||
|
}).select('-__v -createdAt -updatedAt');
|
||||||
|
|
||||||
res.json(matches);
|
res.json(matches);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
res.status(500).json({ error: 'Search failed', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all families
|
||||||
|
app.get('/api/families', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const families = await Family.find()
|
||||||
|
.select('-__v -createdAt -updatedAt')
|
||||||
|
.sort({ family: 1 });
|
||||||
|
|
||||||
|
res.json(families);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get families error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch families', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create new family
|
||||||
|
app.post('/api/families', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { family, city, lat, lng } = req.body;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!family || !city || lat === undefined || lng === undefined) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields',
|
||||||
|
required: ['family', 'city', 'lat', 'lng']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate coordinates
|
||||||
|
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid coordinates',
|
||||||
|
message: 'Latitude must be between -90 and 90, Longitude between -180 and 180'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFamily = new Family({ family, city, lat, lng });
|
||||||
|
await newFamily.save();
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Family added successfully',
|
||||||
|
family: newFamily
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create family error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create family', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update family
|
||||||
|
app.put('/api/families/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { family, city, lat, lng } = req.body;
|
||||||
|
|
||||||
|
const updatedFamily = await Family.findByIdAndUpdate(
|
||||||
|
id,
|
||||||
|
{ family, city, lat, lng },
|
||||||
|
{ new: true, runValidators: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedFamily) {
|
||||||
|
return res.status(404).json({ error: 'Family not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Family updated successfully',
|
||||||
|
family: updatedFamily
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update family error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update family', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete family
|
||||||
|
app.delete('/api/families/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const deletedFamily = await Family.findByIdAndDelete(id);
|
||||||
|
|
||||||
|
if (!deletedFamily) {
|
||||||
|
return res.status(404).json({ error: 'Family not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Family deleted successfully',
|
||||||
|
family: deletedFamily
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete family error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete family', message: error.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
const mongoose = require('mongoose');
|
||||||
|
const dbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected';
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
database: dbStatus
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
app.listen(port, '0.0.0.0', () => {
|
app.listen(port, '0.0.0.0', () => {
|
||||||
console.log(`🗺️ Ora Map Server running at http://localhost:${port}`);
|
console.log(`🗺️ Ora Map Backend API running at http://localhost:${port}`);
|
||||||
console.log(`📍 API endpoint: http://localhost:${port}/api/search`);
|
console.log(`📍 Search endpoint: http://localhost:${port}/api/search`);
|
||||||
|
console.log(`💚 Health endpoint: http://localhost:${port}/api/health`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,26 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# MongoDB Database Service
|
||||||
|
mongo:
|
||||||
|
image: mongo:7.0
|
||||||
|
container_name: oramap-mongo
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_DATABASE=oramap
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- oramap-network
|
||||||
|
|
||||||
# Backend API Service
|
# Backend API Service
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@ -11,6 +31,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- MONGODB_URI=mongodb://mongo:27017/oramap
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
@ -46,3 +70,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
oramap-network:
|
oramap-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
@ -12,9 +12,41 @@
|
|||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<input type="text" id="searchInput" placeholder="Enter family name..." />
|
<input type="text" id="searchInput" placeholder="Enter family name..." />
|
||||||
<button onclick="searchFamily()">Search</button>
|
<button onclick="searchFamily()">Search</button>
|
||||||
|
<button onclick="toggleAddFamilyForm()" class="add-btn">➕ Add Family</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Add Family Form Modal -->
|
||||||
|
<div id="addFamilyModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="toggleAddFamilyForm()">×</span>
|
||||||
|
<h2>Add New Family Location</h2>
|
||||||
|
<form id="addFamilyForm" onsubmit="addFamily(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="familyName">Family Name:</label>
|
||||||
|
<input type="text" id="familyName" required placeholder="e.g., Cohen (כהן)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cityName">City:</label>
|
||||||
|
<input type="text" id="cityName" required placeholder="e.g., Jerusalem (ירושלים)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="latitude">Latitude:</label>
|
||||||
|
<input type="number" id="latitude" required step="0.0001" min="-90" max="90" placeholder="e.g., 31.7683" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="longitude">Longitude:</label>
|
||||||
|
<input type="number" id="longitude" required step="0.0001" min="-180" max="180" placeholder="e.g., 35.2137" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Add Family</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="toggleAddFamilyForm()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="formMessage" class="form-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -122,3 +122,88 @@ L.Control.Logo = L.Control.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
L.control.logo({ position: 'bottomleft' }).addTo(map);
|
L.control.logo({ position: 'bottomleft' }).addTo(map);
|
||||||
|
|
||||||
|
// Add Family Form Functions
|
||||||
|
function toggleAddFamilyForm() {
|
||||||
|
const modal = document.getElementById('addFamilyModal');
|
||||||
|
const form = document.getElementById('addFamilyForm');
|
||||||
|
const message = document.getElementById('formMessage');
|
||||||
|
|
||||||
|
if (modal.style.display === 'block') {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
form.reset();
|
||||||
|
message.textContent = '';
|
||||||
|
message.className = 'form-message';
|
||||||
|
} else {
|
||||||
|
modal.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFamily(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const familyName = document.getElementById('familyName').value.trim();
|
||||||
|
const cityName = document.getElementById('cityName').value.trim();
|
||||||
|
const latitude = parseFloat(document.getElementById('latitude').value);
|
||||||
|
const longitude = parseFloat(document.getElementById('longitude').value);
|
||||||
|
|
||||||
|
const messageEl = document.getElementById('formMessage');
|
||||||
|
messageEl.textContent = 'Adding family...';
|
||||||
|
messageEl.className = 'form-message info';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/families', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
family: familyName,
|
||||||
|
city: cityName,
|
||||||
|
lat: latitude,
|
||||||
|
lng: longitude
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
messageEl.textContent = '✅ Family added successfully!';
|
||||||
|
messageEl.className = 'form-message success';
|
||||||
|
|
||||||
|
// Add marker to map
|
||||||
|
L.marker([latitude, longitude]).addTo(map)
|
||||||
|
.bindPopup(`<strong>${familyName}</strong><br>City: ${cityName}`)
|
||||||
|
.openPopup();
|
||||||
|
|
||||||
|
map.setView([latitude, longitude], 10);
|
||||||
|
|
||||||
|
// Reset form after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toggleAddFamilyForm();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
messageEl.textContent = `❌ Error: ${data.error || data.message}`;
|
||||||
|
messageEl.className = 'form-message error';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Add family error:', error);
|
||||||
|
messageEl.textContent = '❌ Failed to add family. Please try again.';
|
||||||
|
messageEl.className = 'form-message error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('addFamilyModal');
|
||||||
|
if (event.target === modal) {
|
||||||
|
toggleAddFamilyForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to trigger search
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
searchFamily();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -96,3 +96,143 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background-color: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-message {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-message.info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Family button */
|
||||||
|
.btn-add-family {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-family:hover {
|
||||||
|
background-color: #229954;
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: oramap
|
name: oramap
|
||||||
description: Ora Map - Family Location Mapping Application (Microservices Architecture)
|
description: Ora Map - Family Location Mapping Application with MongoDB (Microservices Architecture)
|
||||||
|
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
# Chart version
|
# Chart version
|
||||||
version: 0.2.0
|
version: 0.3.0
|
||||||
|
|
||||||
# Application version
|
# Application version
|
||||||
appVersion: "1.0.0"
|
appVersion: "1.0.0"
|
||||||
|
|||||||
@ -4,10 +4,11 @@ Helm chart for deploying Ora Map application in Kubernetes with microservices ar
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
This chart deploys two main components:
|
This chart deploys three main components:
|
||||||
|
|
||||||
- **Backend**: Node.js Express API server (Port 3000)
|
- **Backend**: Node.js Express API server with MongoDB integration (Port 3000)
|
||||||
- **Frontend**: Nginx serving static files (Port 80)
|
- **Frontend**: Nginx serving static files with API proxy (Port 80)
|
||||||
|
- **MongoDB**: Database for storing family location data (Port 27017)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -62,6 +63,13 @@ The following table lists the configurable parameters and their default values:
|
|||||||
| `ingress.enabled` | Enable ingress | `true` |
|
| `ingress.enabled` | Enable ingress | `true` |
|
||||||
| `ingress.className` | Ingress class name | `traefik` |
|
| `ingress.className` | Ingress class name | `traefik` |
|
||||||
| `ingress.hosts[0].host` | Ingress hostname | `oramap.dvirlabs.com` |
|
| `ingress.hosts[0].host` | Ingress hostname | `oramap.dvirlabs.com` |
|
||||||
|
| `mongodb.enabled` | Enable MongoDB deployment | `true` |
|
||||||
|
| `mongodb.image.repository` | MongoDB image repository | `mongo` |
|
||||||
|
| `mongodb.image.tag` | MongoDB image tag | `7.0` |
|
||||||
|
| `mongodb.persistence.enabled` | Enable persistent storage | `true` |
|
||||||
|
| `mongodb.persistence.size` | PVC size | `5Gi` |
|
||||||
|
| `mongodb.persistence.storageClass` | Storage class name | `""` (default) |
|
||||||
|
| `mongodb.database` | Database name | `oramap` |
|
||||||
|
|
||||||
### Example Custom Values
|
### Example Custom Values
|
||||||
|
|
||||||
@ -70,6 +78,12 @@ The following table lists the configurable parameters and their default values:
|
|||||||
backend:
|
backend:
|
||||||
image:
|
image:
|
||||||
tag: "v1.0.0"
|
tag: "v1.0.0"
|
||||||
|
mongodb:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 10Gi
|
||||||
|
storageClass: "fast-ssd"
|
||||||
|
|
||||||
replicaCount: 3
|
replicaCount: 3
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
@ -107,13 +121,55 @@ ingress:
|
|||||||
### Services
|
### Services
|
||||||
|
|
||||||
- **Backend Service**: Internal ClusterIP service on port 3000
|
- **Backend Service**: Internal ClusterIP service on port 3000
|
||||||
- **Frontend Service**: ClusterIP service on port 80 (exposed via Ingress)
|
- **Frontend Service**: ClusterIP
|
||||||
|
|
||||||
### Ingress
|
### MongoDB StatefulSet
|
||||||
|
|
||||||
- Routes external traffic to frontend service
|
- Persistent database storage for family data
|
||||||
- TLS/SSL termination support
|
- Health checks using mongosh
|
||||||
- Configurable hostname and paths
|
- Configurable storage size and class
|
||||||
|
- Automatic PVC creation
|
||||||
|
|
||||||
|
## Database Setup
|
||||||
|
|
||||||
|
### Initial Data Seeding
|
||||||
|
|
||||||
|
After deployment, seed the database with initial family data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access the backend pod
|
||||||
|
kubectl exec -it deployment/oramap-backend -- sh
|
||||||
|
|
||||||
|
# Run the seed script
|
||||||
|
npm run seed
|
||||||
|
|
||||||
|
# Or force re-seed
|
||||||
|
npm run seed:force
|
||||||
|
```
|
||||||
|
|
||||||
|
### MongoDB Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Port-forward to MongoDB
|
||||||
|
kubectl port-forward svc/oramap-mongodb 27017:27017
|
||||||
|
|
||||||
|
# Check StatefulSet
|
||||||
|
kubectl get statefulset oramap-mongodb
|
||||||
|
kubectl get pvc
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend logs
|
||||||
|
kubectl logs -l app=oramap-backend
|
||||||
|
|
||||||
|
# Frontend logs
|
||||||
|
kubectl logs -l app=oramap-frontend
|
||||||
|
|
||||||
|
# MongoDB logs
|
||||||
|
kubectl logs oramap-mongodb-0
|
||||||
|
```
|
||||||
|
|
||||||
## CI/CD Integration
|
## CI/CD Integration
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,10 @@ spec:
|
|||||||
value: "production"
|
value: "production"
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: "{{ .Values.backend.containerPort }}"
|
value: "{{ .Values.backend.containerPort }}"
|
||||||
|
{{- if .Values.mongodb.enabled }}
|
||||||
|
- name: MONGODB_URI
|
||||||
|
value: "mongodb://{{ include \"oramap.fullname\" . }}-mongodb:27017/{{ .Values.mongodb.database }}"
|
||||||
|
{{- end }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /api/health
|
path: /api/health
|
||||||
|
|||||||
20
oramap/templates/mongodb-service.yaml
Normal file
20
oramap/templates/mongodb-service.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{{- if .Values.mongodb.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "oramap.fullname" . }}-mongodb
|
||||||
|
labels:
|
||||||
|
{{- include "oramap.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: database
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
clusterIP: None
|
||||||
|
ports:
|
||||||
|
- port: 27017
|
||||||
|
targetPort: 27017
|
||||||
|
protocol: TCP
|
||||||
|
name: mongodb
|
||||||
|
selector:
|
||||||
|
{{- include "oramap.selectorLabels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: database
|
||||||
|
{{- end }}
|
||||||
75
oramap/templates/mongodb-statefulset.yaml
Normal file
75
oramap/templates/mongodb-statefulset.yaml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{{- if .Values.mongodb.enabled }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: {{ include "oramap.fullname" . }}-mongodb
|
||||||
|
labels:
|
||||||
|
{{- include "oramap.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: database
|
||||||
|
spec:
|
||||||
|
serviceName: {{ include "oramap.fullname" . }}-mongodb
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "oramap.selectorLabels" . | nindent 6 }}
|
||||||
|
app.kubernetes.io/component: database
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "oramap.selectorLabels" . | nindent 8 }}
|
||||||
|
app.kubernetes.io/component: database
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mongodb
|
||||||
|
image: "{{ .Values.mongodb.image.repository }}:{{ .Values.mongodb.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.mongodb.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: mongodb
|
||||||
|
containerPort: 27017
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: MONGO_INITDB_DATABASE
|
||||||
|
value: {{ .Values.mongodb.database }}
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- mongosh
|
||||||
|
- --eval
|
||||||
|
- "db.adminCommand('ping')"
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- mongosh
|
||||||
|
- --eval
|
||||||
|
- "db.adminCommand('ping')"
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.mongodb.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data/db
|
||||||
|
{{- if .Values.mongodb.persistence.enabled }}
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
{{- if .Values.mongodb.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.mongodb.persistence.storageClass }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.mongodb.persistence.size }}
|
||||||
|
{{- else }}
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@ -41,6 +41,26 @@ service:
|
|||||||
port: 80
|
port: 80
|
||||||
targetPort: 80
|
targetPort: 80
|
||||||
|
|
||||||
|
# MongoDB configuration
|
||||||
|
mongodb:
|
||||||
|
enabled: true
|
||||||
|
image:
|
||||||
|
repository: mongo
|
||||||
|
tag: "7.0"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: ""
|
||||||
|
size: 5Gi
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
database: oramap
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
className: "traefik"
|
className: "traefik"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user