Add completed Kubernetes deployment for Task Manager (z2)
This commit is contained in:
parent
9758b6e31c
commit
29f7042de4
381
z2/README.md
Normal file
381
z2/README.md
Normal 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
20
z2/api/Dockerfile
Normal 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
18
z2/api/db.js
Normal 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
15
z2/api/package.json
Normal 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
161
z2/api/server.js
Normal 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
44
z2/configmap-secret.yaml
Normal 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
17
z2/db/init.sql
Normal 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
158
z2/deployment.yaml
Normal 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
19
z2/frontend/Dockerfile
Normal 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
35
z2/frontend/nginx.conf
Normal 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
234
z2/frontend/public/app.js
Normal 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);
|
||||||
113
z2/frontend/public/index.html
Normal file
113
z2/frontend/public/index.html
Normal 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 — Kubernetes Deployment • PostgreSQL • Redis • Node.js • Nginx</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
331
z2/frontend/public/style.css
Normal file
331
z2/frontend/public/style.css
Normal 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
11
z2/namespace.yaml
Normal 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
53
z2/prepare-app.sh
Executable 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
11
z2/remove-app.sh
Executable 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
110
z2/service.yaml
Normal 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
67
z2/start-app.sh
Executable 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
220
z2/statefulset.yaml
Normal 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
37
z2/stop-app.sh
Executable 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 "============================================="
|
||||||
Loading…
Reference in New Issue
Block a user