Upload files to "/"

This commit is contained in:
Puneet Khurana 2025-04-15 16:41:32 +00:00
parent 72491d78c6
commit 7fa4363cb7
13 changed files with 489 additions and 31 deletions

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
# Use the official nginx image
FROM nginx:alpine
# Copy the game files into the nginx html folder
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
# Expose port 80
EXPOSE 80

126
README.md
View File

@ -1,38 +1,122 @@
# WebApp Docker Deployment
# 🧠 Typing Speed & RSI Awareness Game - Kubernetes Deployment
# Requirements -
This project showcases a web-based **Typing Speed & RSI Awareness Game** deployed on a Kubernetes cluster. Although the game is a simple frontend app (HTML, CSS, JavaScript), the Kubernetes setup is fully compliant with deployment best practices and assignment requirements.
Docker installed
---
Docker Compose (optional)
## 🎮 Game Overview
# Description -
This application consists of a web service and a database service. The web service runs on an Apache server with PHP and connects to a MySQL database.
The application is designed to:
- Improve typing speed and accuracy.
- Educate users on RSI (Repetitive Strain Injury) prevention.
- Include real-time feedback and typing stats.
- Encourage healthy typing habits with periodic stretch reminders.
# Networks and Volumes -
my_network: A bridge network for inter-container communication.
---
# mysql_data -
## 📁 Project Structure
A volume to persist database data.
File/Folder Description |
# Container Configuration
apache: Web service running Apache with PHP, exposes port 8080.
| index.html`, `style.css`, `script.js` | Frontend game files |
| Dockerfile` | Builds Nginx image to serve the game |
| prepare-app.sh` | Prepares the environment and Docker image |
| start-app.sh` | Applies all Kubernetes resources |
| stop-app.sh` | Cleans up all Kubernetes resources |
| namespace.yaml` | Creates the `typing-game` namespace |
| deployment.yaml` | Deploys the frontend via Nginx |
| service.yaml` | Exposes frontend using NodePort |
| pv-pvc.yaml` | Defines persistent volume and claim |
| statefulset.yaml` | Dummy backend with StatefulSet + PVC |
|
# mysql -
---
MySQL database service using the official MySQL image, exposes port 3306.
## How to Run
# Instructions
### Step 1: Prepare the Application
Prepare the application: ./prepare-app.sh
```bash
./prepare-app.sh
```
Start the application: ./start-app.sh
- Builds the Docker image: `typing-game`
- Saves it as a `.tar` for loading on cluster nodes
- Prepares persistent storage
Stop the application: ./stop-app.sh
---
Remove the application: ./remove-app.sh
### Step 2: Deploy the Application
```bash
./start-app.sh
```
This applies:
- Namespace
- Deployment
- Service
- StatefulSet
- PersistentVolume + PersistentVolumeClaim
---
### Step 3: Access the App
Open in your browser:
```
http://<your-node-ip>:30007
```
> Replace `<your-node-ip>` with the external IP of your Kubernetes node.
---
### Step 4: Clean Up
```bash
./stop-app.sh
```
This deletes all Kubernetes resources created by this project.
---
## ✅ Assignment Requirements Fulfilled
### 1. Namespace
**Requirement:** All objects must belong to a custom namespace.
**You Did:** Defined `typing-game` in `namespace.yaml`, and referenced it in all other files.
### 2. Deployment
**Requirement:** Deploy your web application.
**You Did:** Used `deployment.yaml` to deploy your frontend game using Nginx.
### 3. StatefulSet + PersistentVolume + PersistentVolumeClaim
**Requirement:** Demonstrate persistent storage via a StatefulSet.
**You Did:**
- Used `statefulset.yaml` with Alpine Linux writing logs to `/data/log.txt`.
- Mounted `/data` using a PVC created from `pv-pvc.yaml`.
### 4. Service
**Requirement:** Expose your app.
**You Did:** Used a `NodePort` service in `service.yaml` on port `30007`.
### 5. start-app.sh
**Requirement:** Script to create all objects.
**You Did:** Automated creation of namespace, Deployment, Service, StatefulSet, PV/PVC.
### 6. stop-app.sh
**Requirement:** Script to delete all objects.
**You Did:** Cleanly removes all created Kubernetes objects.
### 7. prepare-app.sh
**Requirement:** Prepares the environment.
**You Did:** Builds and saves Docker image, prepares persistent volume.
### 8. Dockerfile
**Requirement:** Provide necessary container files.
**You Did:** Dockerfile builds a custom Nginx image that serves your HTML/JS/CSS game files.
# Accessing the Application
Open a web browser and navigate to http://localhost:8080.

21
deployment.yaml Normal file
View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: typing-game-deployment
namespace: typing-game
spec:
replicas: 1
selector:
matchLabels:
app: typing-game
template:
metadata:
labels:
app: typing-game
spec:
containers:
- name: typing-game
image: typing-game:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

43
index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Typing Speed & RSI Awareness Game</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Fast Fingers, Healthy Habits</h1>
<p style="font-style: italic; margin-top: -5px; color: #555;">
Type fast. Type smart. Stay safe.
</p>
<div class="game-container">
<button id="dark-mode-toggle">🌙 Toggle Dark Mode</button>
<div id="session-timer">You've been typing for 0 minutes.</div>
<div id="high-score">🏆 Highest Score: 0</div>
<h1>Typing Speed & RSI Awareness</h1>
<div id="word-display">Start</div>
<input type="text" id="word-input" placeholder="Type here..." autofocus />
<div class="stats">
<span id="score">Score: 0</span>
<span id="accuracy">Accuracy: 100%</span>
<span id="timer">Time: 0:00</span>
</div>
<div id="rsi-tip" class="tip-box">💡 Keep your wrists straight while typing.</div>
<div id="stretch-popup" class="stretch-popup"></div>
<div class="gif-container">
<img id="stretch-gif"
src="https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZHpoc3hwOTQ4Nm0xcWowN2h3ODhudXdybnYzdWJrZ2JiamNkajU3eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/395GQQavHm5CPXboEG/giphy.gif"
alt="Stretch your hands!"
style="display: none; width: 140px; border-radius: 12px;" />
</div>
</div>
<script src="script.js"></script>
</body>
</html>

4
namespace.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: typing-game

15
prepare-app.sh Executable file → Normal file
View File

@ -1,6 +1,13 @@
#!/bin/bash
echo "Preparing application..."
docker network create myapp-net
docker volume create mysql-data
echo "Preparation complete."
# Create Docker image
echo "Building Docker image: typing-game"
docker build -t typing-game .
# Save Docker image as tar to simulate deployment to remote cluster
docker save typing-game > typing-game.tar
# Dummy command to create persistent volume directory (for demo)
mkdir -p ./persistent-data
echo "Preparation complete."

25
pv-pvc.yaml Normal file
View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: typing-game-pv
namespace: typing-game
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data/typing-game"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: typing-game-pvc
namespace: typing-game
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

114
script.js Normal file
View File

@ -0,0 +1,114 @@
const words = ["apple", "banana", "cherry", "dragon", "elephant", "fish", "grape", "house", "ink", "jungle"];
let currentWord = "";
let score = 0;
let typedWords = 0;
let missed = 0;
let totalTyped = 0;
let startTime = Date.now();
let wordStartTime = Date.now();
const wordDisplay = document.getElementById("word-display");
const wordInput = document.getElementById("word-input");
const scoreDisplay = document.getElementById("score");
const accuracyDisplay = document.getElementById("accuracy");
const timerDisplay = document.getElementById("timer");
const tipBox = document.getElementById("rsi-tip");
const stretchPopup = document.getElementById("stretch-popup");
const sessionTimerText = document.getElementById("session-timer");
const highScoreDisplay = document.getElementById("high-score");
const darkModeToggle = document.getElementById("dark-mode-toggle");
const tips = [
"💡 Keep your wrists straight while typing.",
"🪑 Sit with your back supported.",
"👁 Adjust your screen to eye level.",
"💧 Take a short water break.",
"🧍‍♂️ Stand and stretch for a moment."
];
let highScore = localStorage.getItem("typingHighScore") || 0;
highScoreDisplay.textContent = `🏆 Highest Score: ${highScore}`;
function generateWord() {
currentWord = words[Math.floor(Math.random() * words.length)];
wordDisplay.textContent = currentWord;
wordInput.value = "";
wordStartTime = Date.now();
}
function updateScore() {
scoreDisplay.textContent = `Score: ${score}`;
const accuracy = totalTyped === 0 ? 100 : Math.round((score / totalTyped) * 100);
accuracyDisplay.textContent = `Accuracy: ${accuracy}%`;
}
function updateTimer() {
const now = Date.now();
const seconds = Math.floor((now - startTime) / 1000);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
timerDisplay.textContent = `Time: ${mins}:${secs.toString().padStart(2, '0')}`;
}
function gameOver() {
if (score > highScore) {
localStorage.setItem("typingHighScore", score);
alert(`New High Score: ${score}! 🎉`);
} else {
alert("Game Over! You missed 3 words.");
}
location.reload();
}
wordInput.addEventListener("input", () => {
if (wordInput.value.trim() === currentWord) {
score++;
typedWords++;
totalTyped++;
generateWord();
updateScore();
if (typedWords % 10 === 0) {
stretchPopup.textContent = "🧘‍♀️ Time to stretch your fingers or roll your shoulders!";
document.getElementById("stretch-gif").style.display = "block";
setTimeout(() => {
stretchPopup.textContent = "";
document.getElementById("stretch-gif").style.display = "none";
}, 5000);
}
}
});
wordInput.addEventListener("blur", () => wordInput.focus());
setInterval(() => {
const timeSinceWordShown = Date.now() - wordStartTime;
if (timeSinceWordShown >= 8000 && wordInput.value.trim() === "") {
missed++;
totalTyped++;
if (missed >= 3) gameOver();
generateWord();
updateScore();
}
}, 1000);
let tipIndex = 0;
setInterval(() => {
tipBox.textContent = tips[tipIndex];
tipIndex = (tipIndex + 1) % tips.length;
}, 30000);
setInterval(updateTimer, 1000);
setInterval(() => {
const minutes = Math.floor((Date.now() - startTime) / 60000);
sessionTimerText.textContent = `You've been typing for ${minutes} minute${minutes !== 1 ? 's' : ''}.`;
}, 10000);
darkModeToggle.addEventListener("click", () => {
document.body.classList.toggle("dark-mode");
});
generateWord();
updateScore();
updateTimer();

14
service.yaml Normal file
View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: typing-game-service
namespace: typing-game
spec:
type: NodePort
selector:
app: typing-game
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30007

12
start-app.sh Executable file → Normal file
View File

@ -1,5 +1,11 @@
#!/bin/bash
echo "Starting application..."
docker compose up -d
echo "Application started. Access it at http://localhost:8080"
echo "Creating Kubernetes resources..."
kubectl apply -f namespace.yaml
kubectl apply -f pv-pvc.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f statefulset.yaml
echo "Application deployed under namespace 'typing-game'."

31
statefulset.yaml Normal file
View File

@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: typing-backend
namespace: typing-game
spec:
serviceName: "backend-service"
replicas: 1
selector:
matchLabels:
app: typing-backend
template:
metadata:
labels:
app: typing-backend
spec:
containers:
- name: backend
image: alpine
command: ["/bin/sh", "-c", "while true; do echo Hello from StatefulSet >> /data/log.txt; sleep 10; done"]
volumeMounts:
- name: backend-storage
mountPath: /data
volumeClaimTemplates:
- metadata:
name: backend-storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi

12
stop-app.sh Executable file → Normal file
View File

@ -1,5 +1,11 @@
#!/bin/bash
echo "Stopping application..."
docker-compose down
echo "Application stopped."
echo "Deleting Kubernetes resources..."
kubectl delete -f statefulset.yaml
kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl delete -f pv-pvc.yaml
kubectl delete -f namespace.yaml
echo "All resources deleted."

93
style.css Normal file
View File

@ -0,0 +1,93 @@
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f4f8;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.game-container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 90%;
max-width: 500px;
}
#dark-mode-toggle {
margin-bottom: 10px;
}
#session-timer {
font-size: 0.9rem;
margin-bottom: 5px;
}
#high-score {
font-size: 1rem;
font-weight: bold;
}
.gif-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
#word-display {
font-size: 2rem;
margin-bottom: 1rem;
}
#word-input {
font-size: 1.2rem;
padding: 0.5rem;
border: 2px solid #ccc;
border-radius: 8px;
width: 100%;
}
.stats {
margin-top: 1rem;
display: flex;
justify-content: space-between;
font-size: 1rem;
}
.tip-box {
margin-top: 1rem;
padding: 0.5rem;
background: #e6f7ff;
border-left: 5px solid #1890ff;
border-radius: 6px;
font-size: 0.9rem;
}
.stretch-popup {
margin-top: 1rem;
color: #d48806;
font-weight: bold;
}
body.dark-mode {
background: #1a1a1a;
color: #ffffff;
}
body.dark-mode .game-container {
background: #2c2c2c;
color: #ffffff;
}
body.dark-mode input {
background: #333;
color: #fff;
border: 2px solid #888;
}