Upload files to "/"
This commit is contained in:
parent
72491d78c6
commit
7fa4363cb7
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
126
README.md
@ -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
21
deployment.yaml
Normal 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
43
index.html
Normal 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
4
namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: typing-game
|
15
prepare-app.sh
Executable file → Normal file
15
prepare-app.sh
Executable file → Normal 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
25
pv-pvc.yaml
Normal 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
114
script.js
Normal 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
14
service.yaml
Normal 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
12
start-app.sh
Executable file → Normal 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
31
statefulset.yaml
Normal 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
12
stop-app.sh
Executable file → Normal 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
93
style.css
Normal 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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user