Add completed Kubernetes deployment for Task Manager (z2)

This commit is contained in:
Gopikanta Shill 2026-04-29 11:35:30 +02:00
parent 9758b6e31c
commit 29f7042de4
20 changed files with 2055 additions and 0 deletions

381
z2/README.md Normal file
View File

@ -0,0 +1,381 @@
# Task Manager — Kubernetes Deployment
A full-stack task management web application deployed on **Kubernetes**, extending the Docker Compose version (z1) with Kubernetes-native orchestration, scaling, and persistent storage.
## Table of Contents
- [Description](#description)
- [Prerequisites](#prerequisites)
- [Architecture](#architecture)
- [Kubernetes Objects](#kubernetes-objects)
- [Containers Used](#containers-used)
- [Networking](#networking)
- [Persistent Volumes](#persistent-volumes)
- [Container Configuration](#container-configuration)
- [Quick Start](#quick-start)
- [Usage Instructions](#usage-instructions)
- [Viewing the Application](#viewing-the-application)
- [Example Workflow](#example-workflow)
- [File Structure](#file-structure)
- [Sources](#sources)
- [Use of Artificial Intelligence](#use-of-artificial-intelligence)
---
## Description
**Task Manager** is a full-stack web application for creating, managing, and tracking tasks. Users can:
- **Create tasks** with a title and optional description
- **Mark tasks** as completed or reopen them
- **Delete tasks** they no longer need
- **Filter tasks** by status (All / Active / Completed)
- **View statistics** including total, active, and completed task counts
- **Manage the database** via Adminer web interface
The application consists of a dark-themed Nginx frontend, a Node.js/Express REST API backend, PostgreSQL for persistent task storage, and Redis for API response caching — all orchestrated by Kubernetes.
---
## Prerequisites
| Software | Minimum Version | Purpose |
|----------|----------------|---------|
| **Linux** | Any modern distribution | Host OS |
| **Docker** | 20.10+ | Build container images |
| **kubectl** | 1.25+ | Kubernetes CLI |
| **Minikube** or **kind** | Latest | Local Kubernetes cluster |
| **bash** | 4.0+ | Running management scripts |
### Verify installation:
```bash
docker --version
kubectl version --client
minikube version # or: kind version
```
### Start a local cluster (if not already running):
```bash
minikube start
# or
kind create cluster
```
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Namespace: taskmanager │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Kubernetes Cluster DNS │ │
│ │ │ │
│ │ NodePort :30080 NodePort :30081 │ │
│ │ │ │ │ │
│ │ ┌────▼──────────┐ ┌────────▼──────────┐ │ │
│ │ │ Service │ │ Service │ │ │
│ │ │ (frontend) │ │ (adminer) │ │ │
│ │ └────┬──────────┘ └────────┬──────────┘ │ │
│ │ │ │ │ │
│ │ ┌────▼──────────┐ ┌────────▼──────────┐ │ │
│ │ │ Deployment │ │ Deployment │ │ │
│ │ │ (Nginx ×1) │ │ (Adminer ×1) │ │ │
│ │ └────┬──────────┘ └────────┬──────────┘ │ │
│ │ │ /api/ proxy │ │ │
│ │ ┌────▼──────────┐ │ │ │
│ │ │ Service │ │ │ │
│ │ │ (api:3000) │ │ │ │
│ │ └────┬──────────┘ │ │ │
│ │ │ │ │ │
│ │ ┌────▼──────────┐ ┌────────▼──────────┐ │ │
│ │ │ Deployment │ │ StatefulSet │ │ │
│ │ │ (API ×2) │ │ (Postgres ×1) │ │ │
│ │ └───┬───────────┘ └────────┬──────────┘ │ │
│ │ │ │ PVC → PV │ │
│ │ ┌───▼──────────┐ │ /mnt/.../postgres │ │
│ │ │ StatefulSet │ │ │ │
│ │ │ (Redis ×1) │─────────────►│ │ │
│ │ └──────────────┘ │ │ │
│ │ PVC → PV │ │ │
│ │ /mnt/.../redis │ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## Kubernetes Objects
| Object | Name | File | Description |
|--------|------|------|-------------|
| **Namespace** | `taskmanager` | `namespace.yaml` | Isolates all objects from other cluster workloads |
| **Secret** | `postgres-secret` | `configmap-secret.yaml` | Stores base64-encoded DB username and password |
| **ConfigMap** | `postgres-init` | `configmap-secret.yaml` | Contains `init.sql` to create the tasks table on first run |
| **PersistentVolume** | `postgres-pv` | `statefulset.yaml` | 1 Gi host-path volume for PostgreSQL data |
| **PersistentVolumeClaim** | `postgres-pvc` | `statefulset.yaml` | Claims `postgres-pv` for the postgres StatefulSet |
| **PersistentVolume** | `redis-pv` | `statefulset.yaml` | 500 Mi host-path volume for Redis AOF data |
| **PersistentVolumeClaim** | `redis-pvc` | `statefulset.yaml` | Claims `redis-pv` for the redis StatefulSet |
| **StatefulSet** | `postgres` | `statefulset.yaml` | Manages the PostgreSQL 16 database pod with stable identity |
| **StatefulSet** | `redis` | `statefulset.yaml` | Manages the Redis 7 cache pod with stable identity |
| **Deployment** | `taskmanager-api` | `deployment.yaml` | Runs 2 replicas of the Node.js REST API |
| **Deployment** | `taskmanager-frontend` | `deployment.yaml` | Runs 1 replica of the Nginx frontend |
| **Deployment** | `taskmanager-adminer` | `deployment.yaml` | Runs 1 replica of Adminer (DB web UI) |
| **Service** | `postgres` | `service.yaml` | ClusterIP — internal access to PostgreSQL on port 5432 |
| **Service** | `redis` | `service.yaml` | ClusterIP — internal access to Redis on port 6379 |
| **Service** | `taskmanager-api` | `service.yaml` | ClusterIP — internal access to API on port 3000 |
| **Service** | `taskmanager-frontend` | `service.yaml` | NodePort — exposes UI at port 30080 |
| **Service** | `taskmanager-adminer` | `service.yaml` | NodePort — exposes Adminer at port 30081 |
---
## Containers Used
### 1. `taskmanager-frontend` (Custom — Nginx)
- **Image:** Built from `frontend/Dockerfile` using `nginx:alpine`
- **Role:** Serves the static HTML/CSS/JS UI and reverse-proxies `/api/` requests to `taskmanager-api:3000` using Kubernetes DNS resolution
- **Port:** 80 (internal), exposed at NodePort 30080
### 2. `taskmanager-api` (Custom — Node.js)
- **Image:** Built from `api/Dockerfile` using `node:20-alpine`
- **Role:** REST API providing full CRUD operations for tasks. Uses PostgreSQL for data and Redis for caching (30 s TTL). 2 replicas for availability
- **Port:** 3000 (ClusterIP)
### 3. `postgres` (Official — `postgres:16-alpine`)
- **Role:** Primary relational database. The `init.sql` ConfigMap is mounted into `/docker-entrypoint-initdb.d/` so the schema and sample data are created automatically the first time
- **Port:** 5432 (ClusterIP)
### 4. `redis` (Official — `redis:7-alpine`)
- **Role:** In-memory cache for task list API responses. Configured with append-only persistence (`--appendonly yes`) so the cache survives pod restarts
- **Port:** 6379 (ClusterIP)
### 5. `adminer` (Official — `adminer:latest`)
- **Role:** Web-based database management interface for browsing, querying, and managing the PostgreSQL database directly in a browser
- **Port:** 8080 (internal), exposed at NodePort 30081
---
## Networking
Kubernetes uses a **flat cluster network** — every pod can reach every other pod by its Service DNS name inside the same namespace.
| Service Name | Type | Port | Accessible From |
|-------------|------|------|----------------|
| `postgres` | ClusterIP | 5432 | API pods only (internal) |
| `redis` | ClusterIP | 6379 | API pods only (internal) |
| `taskmanager-api` | ClusterIP | 3000 | Frontend pods (Nginx proxy) |
| `taskmanager-frontend` | NodePort | 80 → 30080 | External (browser) |
| `taskmanager-adminer` | NodePort | 8080 → 30081 | External (browser) |
**DNS resolution example:** Inside the cluster, Nginx resolves `taskmanager-api` to the API Service IP automatically through Kubernetes DNS (`kube-dns`). The database and cache are not reachable from outside the cluster.
---
## Persistent Volumes
| Volume Name | Type | Capacity | Mount Path | Purpose |
|------------|------|----------|-----------|---------|
| `postgres-pv` | hostPath | 1 Gi | `/mnt/taskmanager/postgres` | PostgreSQL data directory |
| `redis-pv` | hostPath | 500 Mi | `/mnt/taskmanager/redis` | Redis append-only file |
**Data persistence:** Stopping the application by scaling to zero (`./stop-app.sh`) or restarting pods does **not** delete the host directories or PVs. Only `./remove-app.sh` deletes them.
---
## Container Configuration
All containers are configured using **environment variables**, **Secrets**, and **ConfigMaps**:
- **Secrets:** `postgres-secret` provides the database username and password to both the `postgres` StatefulSet and `taskmanager-api` Deployment, avoiding plain-text credentials in YAML files
- **ConfigMap:** `postgres-init` provides the `init.sql` script mounted as a file into the postgres container
- **Resource limits:** Every container has `requests` and `limits` defined to prevent resource starvation
- **Readiness/liveness probes:** All containers have health checks so Kubernetes only routes traffic to healthy pods and automatically restarts crashed pods
- **Restart policy:** Kubernetes restarts failed pods by default (managed by the Deployment/StatefulSet controllers)
- **StatefulSets** are used for PostgreSQL and Redis because they need a stable network identity and persistent storage. The API and frontend use Deployments because they are stateless and benefit from rolling updates
---
## Quick Start
```bash
# 1. Start a local Kubernetes cluster (if not already running)
minikube start
# 2. Prepare — build Docker images and create host directories
./prepare-app.sh
# 3. Deploy to Kubernetes
./start-app.sh
# 4. Open the app in your browser
# Task Manager: http://<NODE_IP>:30080
# Adminer: http://<NODE_IP>:30081
```
For Minikube, get the node IP with:
```bash
minikube ip
```
---
## Usage Instructions
### Preparing the application
```bash
./prepare-app.sh
```
Builds the custom Docker images (`taskmanager-api:latest`, `taskmanager-frontend:latest`), creates the host-path directories used by the PersistentVolumes, and **automatically loads the images into Minikube** if it detects a running Minikube cluster (preventing `ImagePullBackOff` errors).
### Starting the application
```bash
./start-app.sh
```
Applies all Kubernetes manifests in the correct dependency order and waits for StatefulSets and Deployments to be ready. Prints the access URLs at the end.
### Stopping and removing the application
```bash
./stop-app.sh
```
Deletes **all** Kubernetes objects (Deployments, StatefulSets, Services, PVCs, Secrets, ConfigMap, Namespace) and the PersistentVolumes and host directories.
⚠️ **All task data will be lost.** Run `./prepare-app.sh` then `./start-app.sh` to start fresh.
### Pausing without data loss
```bash
# Scale all workloads to 0 — objects and PVs remain, data is preserved
kubectl scale deployment --all --replicas=0 -n taskmanager
kubectl scale statefulset --all --replicas=0 -n taskmanager
# Resume
./start-app.sh
```
---
## Viewing the Application
### Task Manager (Main UI)
- **URL:** `http://<NODE_IP>:30080`
- For Minikube: `http://$(minikube ip):30080`
- Features: Create, complete, delete, and filter tasks
### Adminer (Database Management)
- **URL:** `http://<NODE_IP>:30081`
- **Login credentials:**
- System: `PostgreSQL`
- Server: `postgres`
- Username: `taskuser`
- Password: `taskpass`
- Database: `taskmanager`
---
## Example Workflow
```bash
# Start a Minikube cluster
$ minikube start
# Prepare images and host directories
$ ./prepare-app.sh
=============================================
Preparing Task Manager for Kubernetes...
=============================================
[1/2] Building Docker images...
✓ taskmanager-api:latest built
✓ taskmanager-frontend:latest built
[2/2] Creating host directories for PersistentVolumes...
✓ /mnt/taskmanager/postgres created
✓ /mnt/taskmanager/redis created
=============================================
✓ Preparation complete!
Run ./start-app.sh to deploy to Kubernetes
=============================================
# Deploy the application
$ ./start-app.sh
=============================================
Starting Task Manager on Kubernetes...
=============================================
[1/6] Creating Namespace...
[2/6] Applying Secrets and ConfigMaps...
[3/6] Applying PersistentVolumes, PVCs and StatefulSets...
[4/6] Waiting for postgres StatefulSet to be Ready...
Waiting for redis StatefulSet to be Ready...
[5/6] Applying Deployments...
[6/6] Applying Services...
Waiting for API deployment to be ready...
=============================================
✓ Task Manager is running!
🌐 Task Manager: http://192.168.49.2:30080
🗄️ Adminer (DB): http://192.168.49.2:30081
=============================================
# Stop (data preserved)
$ ./stop-app.sh
✓ Application stopped. Data is preserved in PersistentVolumes.
# Start again — all tasks still there
$ ./start-app.sh
# Remove everything
$ ./remove-app.sh
✓ Application completely removed.
```
---
## File Structure
```
z2/
├── namespace.yaml # Namespace: taskmanager
├── configmap-secret.yaml # Secret (DB credentials) + ConfigMap (init.sql)
├── statefulset.yaml # PV, PVC, StatefulSet for postgres and redis
├── deployment.yaml # Deployments for API, frontend, adminer
├── service.yaml # Services (ClusterIP + NodePort)
├── prepare-app.sh # Build images, create host directories
├── start-app.sh # Deploy all Kubernetes objects
├── stop-app.sh # Scale to zero (pause)
├── remove-app.sh # Delete everything
├── README.md # This documentation
├── api/
│ ├── Dockerfile # Node.js 20 Alpine image
│ ├── package.json # Node dependencies
│ ├── server.js # Express REST API
│ └── db.js # PostgreSQL connection pool
├── frontend/
│ ├── Dockerfile # Nginx Alpine image
│ ├── nginx.conf # Nginx config (proxies /api/ → taskmanager-api)
│ └── public/
│ ├── index.html # Task Manager SPA
│ ├── style.css # Dark theme CSS
│ └── app.js # Frontend JavaScript
└── db/
└── init.sql # Database schema + sample data
```
---
## Sources
1. **Kubernetes Documentation** — [https://kubernetes.io/docs/](https://kubernetes.io/docs/)
2. **kubectl Reference** — [https://kubernetes.io/docs/reference/kubectl/](https://kubernetes.io/docs/reference/kubectl/)
3. **Kubernetes: StatefulSets** — [https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
4. **Kubernetes: Persistent Volumes** — [https://kubernetes.io/docs/concepts/storage/persistent-volumes/](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
5. **Kubernetes: Secrets** — [https://kubernetes.io/docs/concepts/configuration/secret/](https://kubernetes.io/docs/concepts/configuration/secret/)
6. **Minikube Documentation** — [https://minikube.sigs.k8s.io/docs/](https://minikube.sigs.k8s.io/docs/)
7. **PostgreSQL Docker Image** — [https://hub.docker.com/_/postgres](https://hub.docker.com/_/postgres)
8. **Redis Docker Image** — [https://hub.docker.com/_/redis](https://hub.docker.com/_/redis)
9. **Adminer Docker Image** — [https://hub.docker.com/_/adminer](https://hub.docker.com/_/adminer)
10. **Node.js Docker Image** — [https://hub.docker.com/_/node](https://hub.docker.com/_/node)
11. **Nginx Docker Image** — [https://hub.docker.com/_/nginx](https://hub.docker.com/_/nginx)
12. **Express.js Documentation** — [https://expressjs.com/](https://expressjs.com/)
---
## Use of Artificial Intelligence
Artificial intelligence tools such as ChatGPT and Claude were used as a support tool during development for understanding Kubernetes concepts, writing YAML manifests, and debugging configuration issues. All implementation, testing, and integration were performed independently.

20
z2/api/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM node:20-alpine
WORKDIR /app
# Copy package files and install dependencies
COPY package.json ./
RUN npm install --production
# Copy application source
COPY . .
# Expose API port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
# Start the application
CMD ["node", "server.js"]

18
z2/api/db.js Normal file
View File

@ -0,0 +1,18 @@
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.POSTGRES_HOST || 'postgres',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
database: process.env.POSTGRES_DB || 'taskmanager',
user: process.env.POSTGRES_USER || 'taskuser',
password: process.env.POSTGRES_PASSWORD || 'taskpass',
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
pool.on('error', (err) => {
console.error('Unexpected PostgreSQL pool error:', err);
});
module.exports = pool;

15
z2/api/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "taskmanager-api",
"version": "1.0.0",
"description": "REST API for Task Manager - Kubernetes deployment",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"pg": "^8.11.3",
"redis": "^4.6.10"
}
}

161
z2/api/server.js Normal file
View File

@ -0,0 +1,161 @@
const express = require('express');
const cors = require('cors');
const pool = require('./db');
const { createClient } = require('redis');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Redis client
let redisClient;
const CACHE_TTL = 30; // seconds
async function initRedis() {
try {
redisClient = createClient({
url: `redis://${process.env.REDIS_HOST || 'redis'}:${process.env.REDIS_PORT || 6379}`
});
redisClient.on('error', (err) => console.error('Redis error:', err));
await redisClient.connect();
console.log('Connected to Redis');
} catch (err) {
console.error('Failed to connect to Redis:', err.message);
redisClient = null;
}
}
// Helper: invalidate cache
async function invalidateCache() {
if (redisClient && redisClient.isOpen) {
try {
await redisClient.del('tasks:all');
} catch (err) {
console.error('Cache invalidation error:', err.message);
}
}
}
// Health check
app.get('/api/health', async (req, res) => {
try {
await pool.query('SELECT 1');
const redisOk = redisClient && redisClient.isOpen;
res.json({
status: 'healthy',
database: 'connected',
cache: redisOk ? 'connected' : 'disconnected',
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(503).json({ status: 'unhealthy', error: err.message });
}
});
// GET /api/tasks — List all tasks (with Redis caching)
app.get('/api/tasks', async (req, res) => {
try {
if (redisClient && redisClient.isOpen) {
const cached = await redisClient.get('tasks:all');
if (cached) {
return res.json({ tasks: JSON.parse(cached), source: 'cache' });
}
}
const result = await pool.query(
'SELECT * FROM tasks ORDER BY created_at DESC'
);
if (redisClient && redisClient.isOpen) {
await redisClient.setEx('tasks:all', CACHE_TTL, JSON.stringify(result.rows));
}
res.json({ tasks: result.rows, source: 'database' });
} catch (err) {
console.error('Error fetching tasks:', err);
res.status(500).json({ error: 'Failed to fetch tasks' });
}
});
// POST /api/tasks — Create a new task
app.post('/api/tasks', async (req, res) => {
try {
const { title, description } = req.body;
if (!title || title.trim() === '') {
return res.status(400).json({ error: 'Title is required' });
}
const result = await pool.query(
'INSERT INTO tasks (title, description) VALUES ($1, $2) RETURNING *',
[title.trim(), (description || '').trim()]
);
await invalidateCache();
res.status(201).json({ task: result.rows[0] });
} catch (err) {
console.error('Error creating task:', err);
res.status(500).json({ error: 'Failed to create task' });
}
});
// PUT /api/tasks/:id — Update a task
app.put('/api/tasks/:id', async (req, res) => {
try {
const { id } = req.params;
const { title, description, completed } = req.body;
const result = await pool.query(
`UPDATE tasks
SET title = COALESCE($1, title),
description = COALESCE($2, description),
completed = COALESCE($3, completed),
updated_at = CURRENT_TIMESTAMP
WHERE id = $4 RETURNING *`,
[title, description, completed, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Task not found' });
}
await invalidateCache();
res.json({ task: result.rows[0] });
} catch (err) {
console.error('Error updating task:', err);
res.status(500).json({ error: 'Failed to update task' });
}
});
// DELETE /api/tasks/:id — Delete a task
app.delete('/api/tasks/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(
'DELETE FROM tasks WHERE id = $1 RETURNING *',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Task not found' });
}
await invalidateCache();
res.json({ message: 'Task deleted', task: result.rows[0] });
} catch (err) {
console.error('Error deleting task:', err);
res.status(500).json({ error: 'Failed to delete task' });
}
});
// Start server
async function start() {
await initRedis();
app.listen(PORT, '0.0.0.0', () => {
console.log(`Task Manager API running on port ${PORT}`);
});
}
start();

44
z2/configmap-secret.yaml Normal file
View File

@ -0,0 +1,44 @@
# =============================================
# Secret — PostgreSQL credentials
# Base64-encoded username and password.
# taskuser → dGFza3VzZXI=
# taskpass → dGFza3Bhc3M=
# =============================================
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: taskmanager
type: Opaque
data:
username: dGFza3VzZXI=
password: dGFza3Bhc3M=
---
# =============================================
# ConfigMap — PostgreSQL init.sql
# Mounted into the postgres container so the
# tasks table and sample data are created the
# first time the database starts.
# =============================================
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-init
namespace: taskmanager
data:
init.sql: |
-- Task Manager Database Initialization
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO tasks (title, description, completed) VALUES
('Welcome to Task Manager', 'This is a sample task. You can edit or delete it.', FALSE),
('Try creating a new task', 'Click the "Add Task" button to create your own tasks.', FALSE),
('Mark tasks as complete', 'Click the checkbox to mark a task as done.', TRUE);

17
z2/db/init.sql Normal file
View File

@ -0,0 +1,17 @@
-- Task Manager Database Initialization
-- Creates the tasks table for the application
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample tasks so the app isn't empty on first load
INSERT INTO tasks (title, description, completed) VALUES
('Welcome to Task Manager', 'This is a sample task. You can edit or delete it.', FALSE),
('Try creating a new task', 'Click the "Add Task" button to create your own tasks.', FALSE),
('Mark tasks as complete', 'Click the checkbox to mark a task as done.', TRUE);

158
z2/deployment.yaml Normal file
View File

@ -0,0 +1,158 @@
# =============================================
# Deployment — Node.js REST API
# Stateless application layer that handles all
# CRUD operations for tasks. Reads environment
# variables for DB and Redis connection info.
# =============================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskmanager-api
namespace: taskmanager
labels:
app: taskmanager-api
spec:
replicas: 2
selector:
matchLabels:
app: taskmanager-api
template:
metadata:
labels:
app: taskmanager-api
spec:
containers:
- name: api
image: taskmanager-api:latest
# Use local image — do not pull from registry
imagePullPolicy: Never
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
- name: POSTGRES_HOST
value: postgres
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_DB
value: taskmanager
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: REDIS_HOST
value: redis
- name: REDIS_PORT
value: "6379"
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 15
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
# =============================================
# Deployment — Nginx Frontend
# Serves the static HTML/CSS/JS task manager
# UI and reverse-proxies /api/ calls to the
# API service inside the cluster.
# =============================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskmanager-frontend
namespace: taskmanager
labels:
app: taskmanager-frontend
spec:
replicas: 1
selector:
matchLabels:
app: taskmanager-frontend
template:
metadata:
labels:
app: taskmanager-frontend
spec:
containers:
- name: frontend
image: taskmanager-frontend:latest
imagePullPolicy: Never
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
---
# =============================================
# Deployment — Adminer
# Lightweight web interface for browsing the
# PostgreSQL database directly in a browser.
# =============================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: taskmanager-adminer
namespace: taskmanager
labels:
app: taskmanager-adminer
spec:
replicas: 1
selector:
matchLabels:
app: taskmanager-adminer
template:
metadata:
labels:
app: taskmanager-adminer
spec:
containers:
- name: adminer
image: adminer:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: ADMINER_DEFAULT_SERVER
value: postgres
- name: ADMINER_DESIGN
value: dracula
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"

19
z2/frontend/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM nginx:alpine
# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy static frontend files
COPY public/ /usr/share/nginx/html/
# Expose HTTP port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]

35
z2/frontend/nginx.conf Normal file
View File

@ -0,0 +1,35 @@
server {
listen 80;
server_name localhost;
# Serve static frontend files
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Reverse proxy API requests to the Node.js backend Service
# In Kubernetes, the Service name "taskmanager-api" resolves via
# cluster DNS to the API pods on port 3000.
location /api/ {
proxy_pass http://taskmanager-api:3000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 256;
}

234
z2/frontend/public/app.js Normal file
View File

@ -0,0 +1,234 @@
// ===== Task Manager Frontend Application =====
const API_BASE = '/api';
let tasks = [];
let currentFilter = 'all';
// ===== DOM Elements =====
const taskList = document.getElementById('task-list');
const emptyState = document.getElementById('empty-state');
const loadingState = document.getElementById('loading-state');
const addTaskForm = document.getElementById('add-task-form');
const taskTitleInput = document.getElementById('task-title');
const taskDescInput = document.getElementById('task-description');
const filterTabs = document.querySelectorAll('.filter-tab');
const toast = document.getElementById('toast');
// ===== API Functions =====
async function apiRequest(url, options = {}) {
try {
const response = await fetch(`${API_BASE}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'TypeError') {
throw new Error('Cannot connect to server. Is the API running?');
}
throw err;
}
}
async function fetchTasks() {
const data = await apiRequest('/tasks');
tasks = data.tasks || [];
const sourceEl = document.querySelector('#stat-source .stat-value');
if (sourceEl) {
sourceEl.textContent = data.source === 'cache' ? '⚡ Cache' : '🗄️ DB';
}
return tasks;
}
async function createTask(title, description) {
const data = await apiRequest('/tasks', {
method: 'POST',
body: JSON.stringify({ title, description }),
});
return data.task;
}
async function updateTask(id, updates) {
const data = await apiRequest(`/tasks/${id}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
return data.task;
}
async function deleteTask(id) {
await apiRequest(`/tasks/${id}`, { method: 'DELETE' });
}
// ===== UI Rendering =====
function getFilteredTasks() {
switch (currentFilter) {
case 'active': return tasks.filter(t => !t.completed);
case 'completed': return tasks.filter(t => t.completed);
default: return tasks;
}
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
function renderTasks() {
const filtered = getFilteredTasks();
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const active = total - completed;
document.querySelector('#stat-total .stat-value').textContent = total;
document.querySelector('#stat-active .stat-value').textContent = active;
document.querySelector('#stat-completed .stat-value').textContent = completed;
loadingState.style.display = 'none';
if (filtered.length === 0) {
taskList.innerHTML = '';
emptyState.style.display = 'block';
emptyState.querySelector('p').textContent = tasks.length > 0
? `No ${currentFilter} tasks.`
: 'No tasks yet. Add your first task above!';
return;
}
emptyState.style.display = 'none';
taskList.innerHTML = filtered.map(task => `
<div class="task-item ${task.completed ? 'completed' : ''}" data-id="${task.id}">
<label class="task-checkbox">
<input type="checkbox"
${task.completed ? 'checked' : ''}
onchange="handleToggle(${task.id}, this.checked)"
aria-label="Mark task as ${task.completed ? 'incomplete' : 'complete'}">
<span class="checkmark">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</span>
</label>
<div class="task-content">
<div class="task-title">${escapeHtml(task.title)}</div>
${task.description ? `<div class="task-description">${escapeHtml(task.description)}</div>` : ''}
<div class="task-meta">${formatDate(task.created_at)}</div>
</div>
<div class="task-actions">
<button class="btn-icon" onclick="handleDelete(${task.id})" title="Delete task" aria-label="Delete task">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>
</button>
</div>
</div>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ===== Toast Notifications =====
let toastTimeout;
function showToast(message, type = 'info') {
clearTimeout(toastTimeout);
toast.textContent = message;
toast.className = `toast ${type} show`;
toastTimeout = setTimeout(() => { toast.classList.remove('show'); }, 3000);
}
// ===== Event Handlers =====
async function handleToggle(id, completed) {
try {
await updateTask(id, { completed });
await fetchTasks();
renderTasks();
showToast(completed ? '✅ Task completed!' : '🔄 Task reopened', 'success');
} catch (err) {
showToast(err.message, 'error');
await fetchTasks();
renderTasks();
}
}
async function handleDelete(id) {
try {
await deleteTask(id);
const el = document.querySelector(`.task-item[data-id="${id}"]`);
if (el) {
el.style.transform = 'translateX(100px)';
el.style.opacity = '0';
await new Promise(r => setTimeout(r, 250));
}
await fetchTasks();
renderTasks();
showToast('🗑️ Task deleted', 'success');
} catch (err) {
showToast(err.message, 'error');
}
}
// ===== Form Submit =====
addTaskForm.addEventListener('submit', async (e) => {
e.preventDefault();
const title = taskTitleInput.value.trim();
const description = taskDescInput.value.trim();
if (!title) return;
try {
await createTask(title, description);
taskTitleInput.value = '';
taskDescInput.value = '';
taskTitleInput.focus();
await fetchTasks();
renderTasks();
showToast('✨ Task created!', 'success');
} catch (err) {
showToast(err.message, 'error');
}
});
// ===== Filter Tabs =====
filterTabs.forEach(tab => {
tab.addEventListener('click', () => {
filterTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFilter = tab.dataset.filter;
renderTasks();
});
});
// ===== Initialize =====
async function init() {
loadingState.style.display = 'block';
emptyState.style.display = 'none';
try {
await fetchTasks();
renderTasks();
} catch (err) {
loadingState.style.display = 'none';
showToast('Failed to load tasks: ' + err.message, 'error');
emptyState.style.display = 'block';
emptyState.querySelector('p').textContent = 'Unable to connect to the server.';
}
}
init();
// Auto-refresh every 30 seconds
setInterval(async () => {
try { await fetchTasks(); renderTasks(); } catch (_) {}
}, 30000);

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Task Manager - A modern task management web application deployed on Kubernetes">
<title>Task Manager</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<div class="header-content">
<div class="logo">
<div class="logo-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
</div>
<h1>Task Manager</h1>
</div>
<div class="header-stats">
<div class="stat" id="stat-total">
<span class="stat-value">0</span>
<span class="stat-label">Total</span>
</div>
<div class="stat" id="stat-active">
<span class="stat-value">0</span>
<span class="stat-label">Active</span>
</div>
<div class="stat" id="stat-completed">
<span class="stat-value">0</span>
<span class="stat-label">Done</span>
</div>
<div class="stat" id="stat-source">
<span class="stat-value"></span>
<span class="stat-label">Source</span>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Add Task Form -->
<section class="add-task-section">
<form id="add-task-form" class="add-task-form">
<div class="form-row">
<div class="input-group input-group-title">
<input type="text" id="task-title" placeholder="What needs to be done?" required autocomplete="off">
</div>
<div class="input-group input-group-desc">
<input type="text" id="task-description" placeholder="Description (optional)" autocomplete="off">
</div>
<button type="submit" class="btn btn-add" id="btn-add-task">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span>Add Task</span>
</button>
</div>
</form>
</section>
<!-- Filter Tabs -->
<section class="filter-section">
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">All</button>
<button class="filter-tab" data-filter="active">Active</button>
<button class="filter-tab" data-filter="completed">Completed</button>
</div>
</section>
<!-- Task List -->
<section class="task-list-section">
<div id="task-list" class="task-list">
<!-- Tasks will be rendered here -->
</div>
<div id="empty-state" class="empty-state" style="display: none;">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
<p>No tasks yet. Add your first task above!</p>
</div>
<div id="loading-state" class="loading-state">
<div class="spinner"></div>
<p>Loading tasks...</p>
</div>
</section>
</main>
<!-- Footer -->
<footer class="app-footer">
<p>Task Manager &mdash; Kubernetes Deployment &bull; PostgreSQL &bull; Redis &bull; Node.js &bull; Nginx</p>
</footer>
</div>
<!-- Toast Notification -->
<div id="toast" class="toast"></div>
<script src="app.js"></script>
</body>
</html>

View File

@ -0,0 +1,331 @@
/* ===== CSS Variables & Reset ===== */
:root {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--bg-card: #16213e;
--bg-card-hover: #1a2744;
--bg-input: #0d1b36;
--border-color: #2a2a4a;
--border-focus: #6c63ff;
--text-primary: #e8e8f0;
--text-secondary: #9898b0;
--text-muted: #6868880;
--accent: #6c63ff;
--accent-hover: #5a52e0;
--accent-glow: rgba(108, 99, 255, 0.3);
--success: #2ecc71;
--success-bg: rgba(46, 204, 113, 0.12);
--danger: #e74c3c;
--danger-hover: #c0392b;
--warning: #f39c12;
--gradient-1: linear-gradient(135deg, #6c63ff 0%, #3f8efc 100%);
--gradient-2: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
--shadow-glow: 0 0 20px rgba(108, 99, 255, 0.15);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.app-container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
padding: 32px 0 24px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 32px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 48px;
height: 48px;
background: var(--gradient-1);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
box-shadow: var(--shadow-glow);
}
.logo h1 {
font-size: 1.75rem;
font-weight: 700;
background: var(--gradient-1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-stats {
display: flex;
gap: 20px;
}
.stat {
text-align: center;
padding: 8px 16px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
min-width: 70px;
transition: var(--transition);
}
.stat:hover {
border-color: var(--accent);
box-shadow: var(--shadow-glow);
}
.stat-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
color: var(--accent);
}
.stat-label {
display: block;
font-size: 0.7rem;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.add-task-section { margin-bottom: 24px; }
.add-task-form {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
box-shadow: var(--shadow-md);
transition: var(--transition);
}
.add-task-form:focus-within {
border-color: var(--accent);
box-shadow: var(--shadow-glow), var(--shadow-md);
}
.form-row { display: flex; gap: 12px; align-items: stretch; }
.input-group { flex: 1; }
.input-group-title { flex: 2; }
.input-group input {
width: 100%;
padding: 12px 16px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: inherit;
font-size: 0.95rem;
transition: var(--transition);
outline: none;
}
.input-group input::placeholder { color: var(--text-secondary); }
.input-group input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.btn-add { background: var(--gradient-1); color: white; box-shadow: var(--shadow-sm); }
.btn-add:hover { transform: translateY(-1px); box-shadow: var(--shadow-glow), var(--shadow-md); }
.btn-add:active { transform: translateY(0); }
.filter-section { margin-bottom: 20px; }
.filter-tabs {
display: flex;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
padding: 4px;
border: 1px solid var(--border-color);
gap: 4px;
}
.filter-tab {
flex: 1;
padding: 10px 16px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-family: inherit;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.filter-tab:hover { color: var(--text-primary); background: rgba(108, 99, 255, 0.08); }
.filter-tab.active { background: var(--accent); color: white; box-shadow: var(--shadow-sm); }
.task-list { display: flex; flex-direction: column; gap: 12px; }
.task-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
transition: var(--transition);
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.task-item:hover {
background: var(--bg-card-hover);
border-color: rgba(108, 99, 255, 0.3);
box-shadow: var(--shadow-sm);
transform: translateX(4px);
}
.task-item.completed { opacity: 0.6; }
.task-item.completed .task-title { text-decoration: line-through; color: var(--text-secondary); }
.task-checkbox { position: relative; width: 24px; height: 24px; flex-shrink: 0; }
.task-checkbox input { position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer; z-index: 1; }
.task-checkbox .checkmark {
width: 24px; height: 24px;
border: 2px solid var(--border-color);
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
transition: var(--transition);
}
.task-checkbox input:hover + .checkmark { border-color: var(--accent); }
.task-checkbox input:checked + .checkmark { background: var(--success); border-color: var(--success); }
.task-checkbox .checkmark svg { opacity: 0; transform: scale(0.5); transition: var(--transition); color: white; }
.task-checkbox input:checked + .checkmark svg { opacity: 1; transform: scale(1); }
.task-content { flex: 1; min-width: 0; }
.task-title { font-size: 1rem; font-weight: 500; color: var(--text-primary); margin-bottom: 2px; word-break: break-word; }
.task-description { font-size: 0.85rem; color: var(--text-secondary); word-break: break-word; }
.task-meta { font-size: 0.7rem; color: var(--text-muted); margin-top: 4px; }
.task-actions { display: flex; gap: 8px; flex-shrink: 0; opacity: 0; transition: var(--transition); }
.task-item:hover .task-actions { opacity: 1; }
.btn-icon {
width: 36px; height: 36px;
border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-secondary);
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: var(--transition);
}
.btn-icon:hover { background: rgba(231, 76, 60, 0.15); color: var(--danger); }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.empty-state svg { margin-bottom: 16px; opacity: 0.3; }
.empty-state p { font-size: 1rem; }
.loading-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.spinner {
width: 40px; height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
margin: 0 auto 16px;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.toast {
position: fixed; bottom: 24px; right: 24px;
padding: 14px 24px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.9rem; font-weight: 500;
box-shadow: var(--shadow-lg);
transform: translateY(100px); opacity: 0;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000; max-width: 360px;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.success { border-left: 4px solid var(--success); }
.toast.error { border-left: 4px solid var(--danger); }
.toast.info { border-left: 4px solid var(--accent); }
.app-footer { margin-top: auto; padding: 24px 0; border-top: 1px solid var(--border-color); text-align: center; }
.app-footer p { font-size: 0.8rem; color: var(--text-secondary); }
@media (max-width: 640px) {
.header-content { flex-direction: column; align-items: flex-start; }
.header-stats { width: 100%; justify-content: space-between; }
.stat { min-width: 0; flex: 1; padding: 6px 8px; }
.form-row { flex-direction: column; }
.btn-add { width: 100%; justify-content: center; }
.task-actions { opacity: 1; }
.logo h1 { font-size: 1.4rem; }
}

11
z2/namespace.yaml Normal file
View File

@ -0,0 +1,11 @@
# =============================================
# Namespace — taskmanager
# Isolates all Task Manager objects from other
# workloads running in the same cluster.
# =============================================
apiVersion: v1
kind: Namespace
metadata:
name: taskmanager
labels:
app: taskmanager

53
z2/prepare-app.sh Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# =============================================================================
# prepare-app.sh — Build Docker images and create host-path directories
#
# This script:
# 1. Builds the custom Docker images (api, frontend) from source.
# 2. Creates the host directories used by PersistentVolumes.
#
# Prerequisites: Docker installed and running, kubectl configured.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "============================================="
echo " Preparing Task Manager for Kubernetes..."
echo "============================================="
echo ""
# ---- Step 1: Build Docker images ----------------------------------------
echo "[1/2] Building Docker images..."
echo " Building taskmanager-api..."
docker build -t taskmanager-api:latest "$SCRIPT_DIR/api"
echo " ✓ taskmanager-api:latest built"
echo " Building taskmanager-frontend..."
docker build -t taskmanager-frontend:latest "$SCRIPT_DIR/frontend"
echo " ✓ taskmanager-frontend:latest built"
# If Minikube is running, load the images into its registry
if command -v minikube &> /dev/null && minikube status &> /dev/null; then
echo ""
echo " Detected Minikube. Loading images into Minikube registry..."
minikube image load taskmanager-api:latest
minikube image load taskmanager-frontend:latest
echo " ✓ Images loaded into Minikube"
fi
# ---- Step 2: Create host directories for PersistentVolumes ---------------
echo ""
echo "[2/2] Creating host directories for PersistentVolumes..."
sudo mkdir -p /mnt/taskmanager/postgres
sudo mkdir -p /mnt/taskmanager/redis
sudo chmod -R 777 /mnt/taskmanager
echo " ✓ /mnt/taskmanager/postgres created"
echo " ✓ /mnt/taskmanager/redis created"
echo ""
echo "============================================="
echo " ✓ Preparation complete!"
echo " Run ./start-app.sh to deploy to Kubernetes"
echo "============================================="

11
z2/remove-app.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# =============================================================================
# remove-app.sh — Alias for stop-app.sh
#
# Provided for convenience. stop-app.sh already performs a full deletion
# of all Kubernetes objects, PersistentVolumes, and host directories.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/stop-app.sh"

110
z2/service.yaml Normal file
View File

@ -0,0 +1,110 @@
# =============================================
# Service — PostgreSQL (ClusterIP)
# Internal DNS name "postgres" used by the API
# to reach the database. Not exposed publicly.
# =============================================
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: taskmanager
labels:
app: postgres
spec:
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
type: ClusterIP
---
# =============================================
# Service — Redis (ClusterIP)
# Internal DNS name "redis" used by the API.
# Not exposed publicly.
# =============================================
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: taskmanager
labels:
app: redis
spec:
selector:
app: redis
ports:
- name: redis
port: 6379
targetPort: 6379
type: ClusterIP
---
# =============================================
# Service — Task Manager API (ClusterIP)
# Internal DNS name "taskmanager-api" reachable
# by the Nginx frontend reverse proxy.
# =============================================
apiVersion: v1
kind: Service
metadata:
name: taskmanager-api
namespace: taskmanager
labels:
app: taskmanager-api
spec:
selector:
app: taskmanager-api
ports:
- name: http
port: 3000
targetPort: 3000
type: ClusterIP
---
# =============================================
# Service — Nginx Frontend (NodePort)
# Exposes the UI at http://<NodeIP>:30080
# so the application is reachable from the
# host machine without an Ingress controller.
# =============================================
apiVersion: v1
kind: Service
metadata:
name: taskmanager-frontend
namespace: taskmanager
labels:
app: taskmanager-frontend
spec:
selector:
app: taskmanager-frontend
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
type: NodePort
---
# =============================================
# Service — Adminer (NodePort)
# Exposes the database web UI at port 30081.
# =============================================
apiVersion: v1
kind: Service
metadata:
name: taskmanager-adminer
namespace: taskmanager
labels:
app: taskmanager-adminer
spec:
selector:
app: taskmanager-adminer
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 30081
type: NodePort

67
z2/start-app.sh Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env bash
# =============================================================================
# start-app.sh — Deploy all Kubernetes objects for the Task Manager
#
# Applies manifests in the correct dependency order:
# 1. Namespace
# 2. Secret + ConfigMap
# 3. PersistentVolumes + PersistentVolumeClaims (inside StatefulSet file)
# 4. StatefulSets (postgres, redis)
# 5. Deployments (api, frontend, adminer)
# 6. Services
#
# The script waits for the StatefulSets to be Ready before continuing so
# the API does not fail its readiness probe immediately.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NS="taskmanager"
echo "============================================="
echo " Starting Task Manager on Kubernetes..."
echo "============================================="
echo ""
echo "[1/6] Creating Namespace..."
kubectl apply -f "$SCRIPT_DIR/namespace.yaml"
echo "[2/6] Applying Secrets and ConfigMaps..."
kubectl apply -f "$SCRIPT_DIR/configmap-secret.yaml"
echo "[3/6] Applying PersistentVolumes, PVCs and StatefulSets..."
kubectl apply -f "$SCRIPT_DIR/statefulset.yaml"
echo "[4/6] Waiting for postgres StatefulSet to be Ready..."
kubectl rollout status statefulset/postgres -n $NS --timeout=120s
echo " Waiting for redis StatefulSet to be Ready..."
kubectl rollout status statefulset/redis -n $NS --timeout=60s
echo "[5/6] Applying Deployments..."
kubectl apply -f "$SCRIPT_DIR/deployment.yaml"
echo "[6/6] Applying Services..."
kubectl apply -f "$SCRIPT_DIR/service.yaml"
echo ""
echo "Waiting for API deployment to be ready..."
kubectl rollout status deployment/taskmanager-api -n $NS --timeout=120s
# Determine the node IP for NodePort access
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "localhost")
echo ""
echo "============================================="
echo " ✓ Task Manager is running!"
echo ""
echo " 🌐 Task Manager: http://${NODE_IP}:30080"
echo " 🗄️ Adminer (DB): http://${NODE_IP}:30081"
echo ""
echo " Adminer login:"
echo " System: PostgreSQL"
echo " Server: postgres"
echo " Username: taskuser"
echo " Password: taskpass"
echo " Database: taskmanager"
echo "============================================="

220
z2/statefulset.yaml Normal file
View File

@ -0,0 +1,220 @@
# =============================================
# PersistentVolume — PostgreSQL data storage
# Host-path volume that stores database files.
# =============================================
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
labels:
app: postgres
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/taskmanager/postgres
---
# =============================================
# PersistentVolumeClaim — PostgreSQL
# Binds to postgres-pv for database storage.
# =============================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: taskmanager
labels:
app: postgres
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 1Gi
---
# =============================================
# PersistentVolume — Redis data storage
# Host-path volume for Redis append-only file.
# =============================================
apiVersion: v1
kind: PersistentVolume
metadata:
name: redis-pv
labels:
app: redis
spec:
capacity:
storage: 500Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/taskmanager/redis
---
# =============================================
# PersistentVolumeClaim — Redis
# Binds to redis-pv for cache persistence.
# =============================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-pvc
namespace: taskmanager
labels:
app: redis
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 500Mi
---
# =============================================
# StatefulSet — PostgreSQL 16
# Manages the primary relational database.
# Uses the PVC above for persistent storage.
# The init.sql ConfigMap is mounted so the DB
# schema is created automatically on first run.
# =============================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: taskmanager
labels:
app: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: taskmanager
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
- name: init-sql
mountPath: /docker-entrypoint-initdb.d/init.sql
subPath: init.sql
readinessProbe:
exec:
command:
- pg_isready
- -U
- taskuser
- -d
- taskmanager
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command:
- pg_isready
- -U
- taskuser
- -d
- taskmanager
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
- name: init-sql
configMap:
name: postgres-init
---
# =============================================
# StatefulSet — Redis 7
# Manages the in-memory caching layer.
# Uses the redis-pvc for AOF persistence.
# =============================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: taskmanager
labels:
app: redis
spec:
serviceName: redis
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
args: ["redis-server", "--appendonly", "yes"]
ports:
- containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-pvc

37
z2/stop-app.sh Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# =============================================================================
# stop-app.sh — Delete all Kubernetes objects for the Task Manager
#
# Drops Deployments, StatefulSets, Services, Secrets, ConfigMaps, PVCs,
# and the Namespace. PersistentVolumes (cluster-scoped) and the host-path
# directories are also removed.
#
# ⚠️ WARNING: All task data stored in the PersistentVolumes will be LOST.
# To pause the app without losing data, scale to 0 manually:
# kubectl scale deployment --all --replicas=0 -n taskmanager
# kubectl scale statefulset --all --replicas=0 -n taskmanager
# =============================================================================
set -euo pipefail
NS="taskmanager"
echo "============================================="
echo " Stopping (dropping) Task Manager..."
echo "============================================="
echo ""
echo "[1/3] Deleting namespace '$NS' and all objects inside it..."
kubectl delete namespace $NS --ignore-not-found=true
echo "[2/3] Deleting cluster-scoped PersistentVolumes..."
kubectl delete pv postgres-pv redis-pv --ignore-not-found=true
echo "[3/3] Removing host-path directories..."
sudo rm -rf /mnt/taskmanager 2>/dev/null || true
echo ""
echo "============================================="
echo " ✓ All Kubernetes objects removed."
echo " Run ./prepare-app.sh then ./start-app.sh"
echo " to redeploy the application."
echo "============================================="