diff --git a/z2/.gitignore b/z2/.gitignore new file mode 100644 index 0000000..6c96855 --- /dev/null +++ b/z2/.gitignore @@ -0,0 +1,47 @@ + +.DS_Store +Thumbs.db + + +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + + +node_modules/ +package-lock.json +.pnpm-debug.log +.npmrc + + +build/ +dist/ + + +*.env +docker-compose.override.yml +docker-compose*.log +.dockerignore + + +.vscode/ +.idea/ +*.sw? + + +.DS_Store + + +*~ + + +*.lnk + + +tmp/ +temp/ + + +.cache/ +.history/ diff --git a/z2/README.md b/z2/README.md new file mode 100644 index 0000000..0e19ecf --- /dev/null +++ b/z2/README.md @@ -0,0 +1,131 @@ +# Battleship Web Application + +## 1. Podmienky na nasadenie a spustenie + +Na spustenie tejto aplikácie je potrebný nasledujúci softvér: +- **Docker** (nainštalovaný a správne nakonfigurovaný) +- **Docker Compose** (verzia 3.8 a vyššia) +- **Bash** (pre spúšťanie skriptov) + +Odporúčané prostredie: Linux alebo Windows s nainštalovaným WSL2. + +## 2. Opis aplikácie + +Táto webová aplikácia predstavuje jednoduchú hru Battleship. Aplikácia je rozdelená na tri hlavné služby: +- **Frontend:** Webové rozhranie, kde hráči zadávajú svoje mená, umiestňujú svoje lode a hrajú hru. +- **Backend:** Serverová logika, ktorá spracováva požiadavky od frontendu, komunikuje s databázou a ukladá výsledky hier. +- **Databáza (MySQL):** Ukladá trvalé dáta – informácie o hrách, výsledkoch a nastaveniach. Dáta sú uchovávané v persistent volume, aby prežili reštarty kontajnerov. + +## 3. Opis virtuálnych sietí a pomenovaných zväzkov + +### Virtuálna sieť +- **app-net:** + Všetky kontajnery (frontend, backend, MySQL) bežia v rámci jednej Docker siete s názvom `app-net`. + - Ak sieť nie je vytvorená, skripty ju vytvoria. + - Sieť umožňuje vzájomnú komunikáciu kontajnerov pomocou mien služieb (napr. backend pristupuje k databáze pomocou mena `mysql`). + +### Pomenované zväzky +- **db_data:** + Tento persistent volume je pripojený k službe MySQL a ukladá dáta (obsah databázy) na ceste `/var/lib/mysql`. + - Zabezpečuje, že aj pri reštarte alebo odstránení kontajnera sa dáta nevymažú, pokiaľ ich zámselne neodstránite. + +## 4. Opis konfigurácie kontajnerov + +### MySQL +- **Image:** `mysql:8.0.30` +- **Premenné prostredia:** + - `MYSQL_ROOT_PASSWORD=somepassword` + - `MYSQL_DATABASE=battleship` +- **Port:** 3306 (mapovaný na localhost:3306) +- **Persistent Volume:** `db_data` pripojený na `/var/lib/mysql` +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +### Backend +- **Build:** Dockerfile v priečinku `./backend` +- **Port:** 4000 (mapovaný na localhost:4000) +- **Environment Variables:** + - `DB_HOST=mysql` (meno služby MySQL v rámci docker-compose) + - `DB_PORT=3306` + - `DB_USER=root` + - `DB_PASSWORD=somepassword` + - `DB_NAME=battleship` +- **Závislosť:** Závisí od služby MySQL (nastavené v `depends_on`) +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +### Frontend +- **Build:** Dockerfile v priečinku `./frontend` +- **Port:** 80 (mapovaný na localhost:3001) +- **Závislosť:** Závisí od backendu (nastavené v `depends_on`) +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +## 5. Zoznam použitých kontajnerov a ich stručný opis + +- **battleship_mysql:** + Beží MySQL databáza, ktorá ukladá trvalé dáta aplikácie. Používa persistent volume `db_data`. + +- **battleship_backend:** + Node.js aplikácia, ktorá poskytuje API pre hru, komunikuje s databázou a spracováva hernú logiku. + +- **battleship_frontend:** + Webové rozhranie aplikácie (React), prostredníctvom ktorého hráči interagujú s hrou. + +## 6. Návod ako pripraviť, spustiť, pozastaviť a vymazať aplikáciu + +Súčasťou projektu sú štyri skripty: + +### Príprava aplikácie +**prepare-app.sh** +Tento skript: +- Vytvorí externú sieť `app-net` (ak ešte neexistuje). +- Zostaví Docker obrazy pre MySQL, backend a frontend prostredníctvom docker-compose. + +Spustenie: +```bash +./prepare-app.sh +``` + +### Spustenie aplikácie +**start-app.sh** +Tento skript: +- Spustí všetky kontajnery (MySQL, backend, frontend) pomocou `docker-compose up -d`. +- Vypíše informácie o dostupnosti aplikácie. + +Spustenie: +```bash +./start-app.sh +``` + +### Pozastavenie aplikácie +**stop-app.sh** +Tento skript: +- Pozastaví všetky služby, pričom dáta v databáze ostanú zachované (persistent volume). + +Spustenie: +```bash +./stop-app.sh +``` + +### Odstránenie aplikácie +**remove-app.sh** +Tento skript: +- Odstráni všetky vytvorené kontajnery, obrazy a persistent volume (ak sa použije parameter `-v`). + +Spustenie: +```bash +./remove-app.sh +``` + +## 7. Návod ako si pozrieť aplikáciu na webovom prehliadači + +### Frontend: +Otvorte webový prehliadač a zadajte: [http://localhost:3001](http://localhost:3001) + +### Backend API: +API je dostupné na: [http://localhost:4000](http://localhost:4000) + +### MySQL: +Pripojte sa k databáze na `localhost:3306` pomocou MySQL klienta. + diff --git a/z2/backend/Dockerfile b/z2/backend/Dockerfile new file mode 100644 index 0000000..7d22d72 --- /dev/null +++ b/z2/backend/Dockerfile @@ -0,0 +1,20 @@ + +FROM node:14-alpine + + +WORKDIR /app + + +COPY package*.json ./ + + +RUN npm install + + +COPY . . + + +EXPOSE 4000 + + +CMD ["npm", "start"] diff --git a/z2/backend/index.js b/z2/backend/index.js new file mode 100644 index 0000000..6874362 --- /dev/null +++ b/z2/backend/index.js @@ -0,0 +1,200 @@ +const express = require('express'); +const cors = require('cors'); +const mysql = require('mysql2/promise'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +(async () => { + console.log("Initializing database connection..."); + + try { + const pool = await mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'somepassword', + database: process.env.DB_NAME || 'battleship', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + + + console.log("Successfully connected to MySQL!"); + + + const conn = await pool.getConnection(); + const [rows] = await conn.query("SELECT VERSION() AS mysql_version"); + console.log(`MySQL Version: ${rows[0].mysql_version}`); + conn.release(); + + + console.log("Checking or creating tables..."); + await pool.execute(` + CREATE TABLE IF NOT EXISTS games ( + id INT AUTO_INCREMENT PRIMARY KEY, + player1 VARCHAR(255), + player2 VARCHAR(255), + player1_ships JSON, + player2_ships JSON, + current_turn INT DEFAULT 1, + game_state JSON NOT NULL + ) + `); + console.log("Table 'games' is ready!"); + + await pool.execute(` + CREATE TABLE IF NOT EXISTS game_results ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT, + winner VARCHAR(255), + loser VARCHAR(255), + moves INT + ) + `); + console.log("Table 'game_results' is ready!"); + +app.get('/api/results', async (req, res) => { + try { + const [results] = await pool.execute('SELECT * FROM game_results ORDER BY id DESC'); + res.json({ results }); + } catch (err) { + console.error("Error fetching results:", err); + res.status(500).json({ error: "Database error" }); + } +}); + + + app.post('/api/new-game', async (req, res) => { + const { player1, player2 } = req.body; + if (!player1 || !player2) { + console.warn("Missing player names in request."); + return res.status(400).json({ error: 'Player names required' }); + } + + const gameState = { + boardSize: 5, + hits: [], + misses: [], + gameOver: false + }; + + try { + console.log(`Creating new game for ${player1} vs ${player2}...`); + const [result] = await pool.execute( + 'INSERT INTO games (player1, player2, game_state) VALUES (?, ?, ?)', + [player1, player2, JSON.stringify(gameState)] + ); + console.log(`Game created with ID: ${result.insertId}`); + res.json({ gameId: result.insertId, message: 'Game created' }); + } catch (err) { + console.error('Error creating game:', err); + res.status(500).json({ error: 'Database error' }); + } + }); + + + app.post('/api/place-ships', async (req, res) => { + const { gameId, player, ships } = req.body; + if (!gameId || !player || !ships) { + console.warn("Missing parameters for placing ships."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Placing ships for player ${player} in game ID: ${gameId}...`); + const [rows] = await pool.execute('SELECT * FROM games WHERE id = ?', [gameId]); + if (rows.length === 0) { + console.warn(`Game with ID ${gameId} not found.`); + return res.status(404).json({ error: 'Game not found' }); + } + + let updateField = player === 1 ? 'player1_ships' : 'player2_ships'; + await pool.execute(`UPDATE games SET ${updateField} = ? WHERE id = ?`, [JSON.stringify(ships), gameId]); + console.log(`Ships placed for player ${player} in game ID: ${gameId}`); + + res.json({ message: `Ships placed for player ${player}` }); + } catch (err) { + console.error('Error placing ships:', err); + res.status(500).json({ error: 'Database error' }); + } + }); + + // 🎯 Атака + app.post('/api/attack', async (req, res) => { + const { gameId, row, col, player } = req.body; + if (!gameId || typeof row === 'undefined' || typeof col === 'undefined' || !player) { + console.warn("Missing attack parameters in request."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Player ${player} attacks (${row}, ${col}) in game ID: ${gameId}`); + const [rows] = await pool.execute('SELECT * FROM games WHERE id = ?', [gameId]); + if (rows.length === 0) { + console.warn(`Game with ID ${gameId} not found.`); + return res.status(404).json({ error: 'Game not found' }); + } + + let game = rows[0]; + let state = JSON.parse(game.game_state); + let opponentShips = player === 1 ? JSON.parse(game.player2_ships || "[]") : JSON.parse(game.player1_ships || "[]"); + + const hit = opponentShips.some(ship => ship.row === row && ship.col === col); + + if (hit) { + console.log(`HIT! Player ${player} hit an opponent's ship at (${row}, ${col})`); + state.hits.push({ row, col }); + if (state.hits.length === opponentShips.length) { + state.gameOver = true; + console.log(`Player ${player} has won the game!`); + } + } else { + console.log(`MISS! Player ${player} attacked (${row}, ${col}) but missed.`); + state.misses.push({ row, col }); + } + + state.current_turn = player === 1 ? 2 : 1; + + await pool.execute('UPDATE games SET game_state = ? WHERE id = ?', [JSON.stringify(state), gameId]); + + res.json({ hit, state }); + } catch (err) { + console.error("Attack error:", err); + res.status(500).json({ error: 'Database error' }); + } + }); + + app.post('/api/save-result', async (req, res) => { + const { gameId, winner, loser, moves } = req.body; + + if (!gameId || !winner || !loser || !moves) { + console.warn("Missing game result parameters."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Saving result: Winner - ${winner}, Loser - ${loser}, Moves - ${moves}`); + await pool.execute( + `INSERT INTO game_results (game_id, winner, loser, moves) VALUES (?, ?, ?, ?)`, + [gameId, winner, loser, moves] + ); + + console.log(`Game result saved for game ID ${gameId}`); + res.json({ message: "Game result saved" }); + } catch (err) { + console.error("Error saving game result:", err); + res.status(500).json({ error: "Database error" }); + } + }); + + const port = 4000; + app.listen(port, () => { + console.log(`Backend started on port ${port}`); + }); + + } catch (err) { + console.error("Database connection failed!", err); + } +})(); diff --git a/z2/backend/package.json b/z2/backend/package.json new file mode 100644 index 0000000..ff56f5c --- /dev/null +++ b/z2/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "battleship-backend", + "version": "1.0.0", + "description": "Backend for Battleship game", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "mysql2": "^3.12.0", + "pg": "^8.9.0" + } +} diff --git a/z2/deployment.yaml b/z2/deployment.yaml new file mode 100644 index 0000000..3e591cb --- /dev/null +++ b/z2/deployment.yaml @@ -0,0 +1,59 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: battleship-backend + namespace: battleship-app + labels: + app: battleship-backend +spec: + replicas: 1 + selector: + matchLabels: + app: battleship-backend + template: + metadata: + labels: + app: battleship-backend + spec: + containers: + - name: battleship-backend + image: battleship-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4000 + env: + - name: DB_HOST + value: mysql + - name: DB_PORT + value: "3306" + - name: DB_USER + value: root + - name: DB_PASSWORD + value: somepassword + - name: DB_NAME + value: battleship +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: battleship-frontend + namespace: battleship-app + labels: + app: battleship-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: battleship-frontend + template: + metadata: + labels: + app: battleship-frontend + spec: + containers: + - name: battleship-frontend + image: battleship-frontend:latest + imagePullPolicy: Never + ports: + - containerPort: 80 diff --git a/z2/docker-compose.yml b/z2/docker-compose.yml new file mode 100644 index 0000000..a3756c0 --- /dev/null +++ b/z2/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0.30 + container_name: battleship_mysql + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=somepassword + - MYSQL_DATABASE=battleship + ports: + - "3306:3306" + networks: + - app-net + volumes: + - db_data:/var/lib/mysql + + backend: + build: ./backend + container_name: battleship_backend + restart: unless-stopped + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=somepassword + - DB_NAME=battleship + ports: + - "4000:4000" + networks: + - app-net + depends_on: + - mysql + + frontend: + build: ./frontend + container_name: battleship_frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "3001:80" + networks: + - app-net + +networks: + app-net: + driver: bridge + +volumes: + db_data: diff --git a/z2/frontend/.gitignore b/z2/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/z2/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/z2/frontend/Dockerfile b/z2/frontend/Dockerfile new file mode 100644 index 0000000..5336955 --- /dev/null +++ b/z2/frontend/Dockerfile @@ -0,0 +1,28 @@ +# --- Build stage --- + FROM node:18-alpine AS build + + WORKDIR /app + + + COPY package*.json ./ + + + RUN npm install + + + COPY . . + + + RUN npm run build + + + FROM nginx:alpine + + + COPY --from=build /app/dist /usr/share/nginx/html + + + EXPOSE 80 + + CMD ["nginx", "-g", "daemon off;"] + \ No newline at end of file diff --git a/z2/frontend/README.md b/z2/frontend/README.md new file mode 100644 index 0000000..aa575c9 --- /dev/null +++ b/z2/frontend/README.md @@ -0,0 +1,110 @@ +# Battleship Application on Kubernetes + +## Overview +This application implements a Battleship game system composed of three main components: +- **Backend:** Handles game logic and communicates with the database. +- **Frontend:** Provides the web user interface for playing the game. +- **MySQL Database:** Stores game data persistently. + +The application is deployed in a Kubernetes cluster using Deployments, a StatefulSet with persistent storage, and Services for network exposure. + +## Containers Used +- **battleship-backend:** + - **Description:** Contains the backend logic for the game. + - **Port:** Listens on port 4000. + - **Configuration:** Uses environment variables to connect to the MySQL database. + +- **battleship-frontend:** + - **Description:** Hosts the web user interface for the game. + - **Port:** Listens on port 80. + - **Exposure:** Exposed via a NodePort (30001). + +- **MySQL (battleship-mysql):** + - **Description:** Runs a MySQL database (version 8.0.30) in a StatefulSet. + - **Port:** Uses port 3306. + - **Persistence:** Uses a PersistentVolume (PV) and PersistentVolumeClaim (PVC) for data storage. + +## Kubernetes Objects +- **Namespace:** + - **battleship-app** + All objects (Deployments, StatefulSet, Services, PV, PVC) are created within this namespace. + +- **Deployments:** + - **battleship-backend:** Manages the backend pods. + - **battleship-frontend:** Manages the frontend pods. + +- **StatefulSet:** + - **battleship-mysql:** Manages the MySQL pods with stable identities and persistent storage. It includes: + - **PersistentVolume (mysql-pv):** Provides 1Gi storage (using a hostPath at `/mnt/data/mysql`). + - **PersistentVolumeClaim (mysql-pvc):** Claims the persistent storage for MySQL. + +- **Services:** + - **MySQL Service:** Exposes the MySQL StatefulSet on port 3306. + - **Backend Service:** Exposes the backend on port 4000 (via NodePort 30000). + - **Frontend Service:** Exposes the frontend on port 80 (via NodePort 30001). + +## Virtual Networks and Named Volumes +- **Virtual Networks:** + Kubernetes provides internal networking for the cluster, and Services offer stable endpoints for communication between pods. + *Note:* In the original Docker Compose configuration, a network named `app-net` was defined; in Kubernetes, the cluster’s internal network is used. + +- **Named Volumes:** + The MySQL StatefulSet uses a PersistentVolume and a PersistentVolumeClaim to ensure data persistence. This setup guarantees that data (e.g., the game database) survives pod restarts or rescheduling. + +## Container Configuration +- **Environment Variables:** + - **Backend Container:** + Configured with variables such as `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, and `DB_NAME` to connect to the MySQL database. + - **MySQL Container:** + Configured with variables `MYSQL_ROOT_PASSWORD` and `MYSQL_DATABASE` to initialize the database. + +- **Ports:** + - **Backend:** Container port 4000, exposed on NodePort 30000. + - **Frontend:** Container port 80, exposed on NodePort 30001. + - **MySQL:** Container port 3306, exposed internally on port 3306. + +- **Image Pull Policy:** + The backend and frontend Deployments use `imagePullPolicy: Never` assuming images are built locally. + +## How to Prepare, Run, Pause, and Remove the Application + +### Preparation +1. **Build Docker Images:** + Execute the `prepare-app.sh` script to build the Docker images for the backend and frontend: + ```bash + ./prepare-app.sh + ``` + +### Running the Application +1. **Deploy the Application:** + Use the `start-app.sh` script to deploy the namespace, Deployments, StatefulSet, and Services: + ```bash + ./start-app.sh + ``` + +### Pausing the Application +1. **Scale Down the Application:** + If you need to temporarily pause the application without deleting objects (thus preserving the data), use: + ```bash + ./stop-app.sh + ``` + *Note:* This script scales the Deployments and StatefulSet to 0 replicas. + +### Removing the Application +1. **Delete All Kubernetes Objects:** + To completely remove the application—including Deployments, StatefulSet, Services, and the Namespace—execute: + ```bash + ./remove-app.sh + ``` + *Important:* Deleting the objects may also remove the associated PersistentVolume depending on its reclaim policy. To preserve data, consider setting the PersistentVolume's reclaim policy to `Retain`. + +## How to Access the Application in a Web Browser +- **Frontend Access:** + The frontend is exposed via a NodePort on port **30001**. + Open a web browser and navigate to: + ``` + http://:30001 + ``` + Replace `` with the IP address of one of your Kubernetes nodes. + + diff --git a/z2/frontend/eslint.config.js b/z2/frontend/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/z2/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/z2/frontend/index.html b/z2/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/z2/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/z2/frontend/package.json b/z2/frontend/package.json new file mode 100644 index 0000000..902fd98 --- /dev/null +++ b/z2/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } +} diff --git a/z2/frontend/public/vite.svg b/z2/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/z2/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/z2/frontend/src/App.css b/z2/frontend/src/App.css new file mode 100644 index 0000000..2e22a36 --- /dev/null +++ b/z2/frontend/src/App.css @@ -0,0 +1,42 @@ +.app-container { + font-family: 'Arial', sans-serif; + text-align: center; + padding: 20px; + background-color: #f0f8ff; + min-height: 100vh; +} + +.game-controls { + margin: 20px 0; +} + +.btn { + padding: 10px 20px; + font-size: 16px; + background-color: #0077cc; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +.btn:hover { + background-color: #005fa3; +} + +.game-board { + margin-top: 20px; +} + +.game-message { + margin-top: 20px; + font-size: 18px; + color: #333; +} + +.game-over { + font-size: 24px; + color: red; + margin-top: 10px; +} diff --git a/z2/frontend/src/App.jsx b/z2/frontend/src/App.jsx new file mode 100644 index 0000000..b97fc75 --- /dev/null +++ b/z2/frontend/src/App.jsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import PlayerForm from './components/PlayerForm'; +import PlacementBoard from './components/PlacementBoard'; +import GameBoard from './components/GameBoard'; + +function App() { + const [step, setStep] = useState(0); + const [gameId, setGameId] = useState(null); + const [player1Name, setPlayer1Name] = useState(''); + const [player2Name, setPlayer2Name] = useState(''); + const [player1Ships, setPlayer1Ships] = useState([]); + const [player2Ships, setPlayer2Ships] = useState([]); + + const handlePlayersSubmit = async (p1, p2) => { + setPlayer1Name(p1); + setPlayer2Name(p2); + + try { + const response = await fetch('http://localhost:30000/api/new-game', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ player1: p1, player2: p2 }) + }); + + const data = await response.json(); + if (data.gameId) { + setGameId(data.gameId); + setStep(1); + } else { + console.error('Game creation error:', data); + } + } catch (error) { + console.error('Network error:', error); + } + }; + + const handlePlacementDone = async (ships) => { + if (!gameId) { + console.error('Error: gameId is missing!'); + return; + } + + try { + const response = await fetch('http://localhost:30000/api/place-ships', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gameId, player: step, ships }) + }); + + const data = await response.json(); + if (data.message) { + console.log(`Ships of player ${step} saved on server.`); + if (step === 1) { + setPlayer1Ships(ships); + setStep(2); + } else if (step === 2) { + setPlayer2Ships(ships); + setStep(3); + } + } else { + console.error('Error saving ships:', data); + } + } catch (error) { + console.error('Network error when saving ships:', error); + } + }; + + + const resetGame = () => { + setStep(0); + setGameId(null); + setPlayer1Name(''); + setPlayer2Name(''); + setPlayer1Ships([]); + setPlayer2Ships([]); + }; + + return ( +
+ {step === 0 && } + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} +
+ ); +} + +export default App; diff --git a/z2/frontend/src/assets/react.svg b/z2/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/z2/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/z2/frontend/src/components/Board.css b/z2/frontend/src/components/Board.css new file mode 100644 index 0000000..3deddb7 --- /dev/null +++ b/z2/frontend/src/components/Board.css @@ -0,0 +1,9 @@ +.board { + display: inline-block; + border: 2px solid #333; + } + + .board-row { + display: flex; + } + \ No newline at end of file diff --git a/z2/frontend/src/components/Board.jsx b/z2/frontend/src/components/Board.jsx new file mode 100644 index 0000000..9111972 --- /dev/null +++ b/z2/frontend/src/components/Board.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Cell from './Cell'; +import './Board.css'; + +function Board({ + boardSize, + ships = [], + hits = [], + misses = [], + sunkShips = [], + onCellClick, + hideShips = false +}) { + const rows = Array.from({ length: boardSize }, (_, i) => i); + const cols = Array.from({ length: boardSize }, (_, i) => i); + + return ( +
+ {rows.map((r) => ( +
+ {cols.map((c) => { + const hasShip = ships.some((s) => s.row === r && s.col === c); + const isHit = hits.some((h) => h.row === r && h.col === c); + const isMiss = misses.some((m) => m.row === r && m.col === c); + const isSunk = sunkShips.some((s) => s.row === r && s.col === c); + + return ( + + ); + })} +
+ ))} +
+ ); +} + +export default Board; diff --git a/z2/frontend/src/components/Cell.css b/z2/frontend/src/components/Cell.css new file mode 100644 index 0000000..c5c5ee2 --- /dev/null +++ b/z2/frontend/src/components/Cell.css @@ -0,0 +1,24 @@ +.cell { + width: 30px; + height: 30px; + border: 1px solid black; + display: inline-block; + background-color: white; + } + + .cell.hit { + background-color: red !important; + } + + .cell.miss { + background-color: green !important; + } + + .cell.sunk { + background-color: black !important; + } + + .cell.has-ship { + background-color: blue; + } + \ No newline at end of file diff --git a/z2/frontend/src/components/Cell.jsx b/z2/frontend/src/components/Cell.jsx new file mode 100644 index 0000000..b7fdffb --- /dev/null +++ b/z2/frontend/src/components/Cell.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import './Cell.css'; + +function Cell({ row, col, hasShip, isHit, isMiss, isSunk, onClick }) { + let className = 'cell'; + + if (isSunk) { + className += ' sunk'; + } else if (isHit) { + className += ' hit'; + } else if (isMiss) { + className += ' miss'; + } else if (hasShip) { + className += ' has-ship'; + } + + const handleClick = () => { + if (onClick) onClick(row, col); + }; + + return
; +} + +export default Cell; diff --git a/z2/frontend/src/components/GameBoard.css b/z2/frontend/src/components/GameBoard.css new file mode 100644 index 0000000..f1d4ecf --- /dev/null +++ b/z2/frontend/src/components/GameBoard.css @@ -0,0 +1,66 @@ +.game-board { + display: flex; + flex-direction: column; + align-items: center; + margin: 2rem auto; + max-width: 1000px; + padding: 2rem; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + font-family: 'Arial', sans-serif; +} + +.turn-title { + margin-bottom: 1.5rem; + font-size: 2rem; + color: #333; +} + +.winner-section { + text-align: center; + margin-bottom: 1.5rem; +} + +.winner-section h2 { + font-size: 2rem; + color: #28a745; + margin-bottom: 1rem; +} + +.new-game-button { + padding: 1rem 2rem; + font-size: 1.3rem; + border: none; + border-radius: 8px; + background-color: #007bff; + color: #fff; + cursor: pointer; + transition: background-color 0.3s; +} + +.new-game-button:hover { + background-color: #0056b3; +} + +.boards-container { + display: flex; + flex-wrap: wrap; + gap: 2rem; + justify-content: center; + width: 100%; +} + +.board-wrapper { + flex: 1; + min-width: 300px; + display: flex; + flex-direction: column; + align-items: center; +} + +.board-wrapper h3 { + margin-bottom: 1rem; + font-size: 1.5rem; + color: #007bff; +} diff --git a/z2/frontend/src/components/GameBoard.jsx b/z2/frontend/src/components/GameBoard.jsx new file mode 100644 index 0000000..ba23cea --- /dev/null +++ b/z2/frontend/src/components/GameBoard.jsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect } from 'react'; +import Board from './Board'; +import './GameBoard.css'; + +function checkSunkShip(ships, hits) { + const sunkShipsArrays = ships.filter((ship) => + ship.every((cell) => + hits.some((hit) => hit.row === cell.row && hit.col === cell.col) + ) + ); + + sunkShipsArrays.forEach((ship) => { + const size = ship.length; + if (size === 4) { + console.log('Battleship (4 cells) is sunk!'); + } else if (size === 3) { + console.log('Cruiser (3 cells) is sunk!'); + } else if (size === 2) { + console.log('Destroyer (2 cells) is sunk!'); + } else if (size === 1) { + console.log('Submarine (1 cell) is sunk!'); + } + }); + + return sunkShipsArrays.flat(); +} + +function GameBoard({ gameId, player1Name, player2Name, player1Ships, player2Ships, onNewGame }) { + const [currentPlayer, setCurrentPlayer] = useState(1); + const [hitsP1, setHitsP1] = useState([]); + const [missesP1, setMissesP1] = useState([]); + const [hitsP2, setHitsP2] = useState([]); + const [missesP2, setMissesP2] = useState([]); + const [sunkShipsP1, setSunkShipsP1] = useState([]); + const [sunkShipsP2, setSunkShipsP2] = useState([]); + const [winner, setWinner] = useState(null); + + useEffect(() => { + console.log('player1Ships:', JSON.stringify(player1Ships, null, 2)); + console.log('player2Ships:', JSON.stringify(player2Ships, null, 2)); + }, []); + + const saveGameResult = async (winnerName, loserName) => { + try { + const resp = await fetch('http://localhost:30000/api/save-result', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + gameId, + winner: winnerName, + loser: loserName, + moves: hitsP1.length + hitsP2.length + }) + }); + const data = await resp.json(); + console.log('Game result saved:', data); + } catch (err) { + console.error('Error saving game result:', err); + } + }; + + useEffect(() => { + const totalCellsP1 = player1Ships.flat().length; + const totalCellsP2 = player2Ships.flat().length; + + if (hitsP1.length === totalCellsP2) { + setWinner(player1Name); + saveGameResult(player1Name, player2Name); + } else if (hitsP2.length === totalCellsP1) { + setWinner(player2Name); + saveGameResult(player2Name, player1Name); + } + }, [hitsP1, hitsP2, player1Ships, player2Ships, player1Name, player2Name]); + + useEffect(() => { + const sunkForP1 = checkSunkShip(player1Ships, hitsP2); + const sunkForP2 = checkSunkShip(player2Ships, hitsP1); + + setSunkShipsP1(sunkForP1); + setSunkShipsP2(sunkForP2); + }, [hitsP1, hitsP2, player1Ships, player2Ships]); + + const handleAttack = (row, col, opponent) => { + if (winner) return; + + let alreadyAttacked; + if (opponent === 2) { + alreadyAttacked = [...hitsP1, ...missesP1].some( + (cell) => cell.row === row && cell.col === col + ); + } else { + alreadyAttacked = [...hitsP2, ...missesP2].some( + (cell) => cell.row === row && cell.col === col + ); + } + + if (alreadyAttacked) { + alert('You have already attacked this cell!'); + return; + } + + let isHit = false; + if (opponent === 2) { + isHit = player2Ships.flat().some((cell) => cell.row === row && cell.col === col); + if (isHit) { + setHitsP1([...hitsP1, { row, col }]); + console.log(`Player1 hit a ship at [${row},${col}] on Player2's board`); + } else { + setMissesP1([...missesP1, { row, col }]); + console.log(`Player1 missed at [${row},${col}] on Player2's board`); + } + } else { + isHit = player1Ships.flat().some((cell) => cell.row === row && cell.col === col); + if (isHit) { + setHitsP2([...hitsP2, { row, col }]); + console.log(`Player2 hit a ship at [${row},${col}] on Player1's board`); + } else { + setMissesP2([...missesP2, { row, col }]); + console.log(`Player2 missed at [${row},${col}] on Player1's board`); + } + } + + if (!isHit) { + setCurrentPlayer(currentPlayer === 1 ? 2 : 1); + } + }; + + return ( +
+ {winner ? ( +
+

Winner: {winner}!

+ +
+ ) : ( +

Current turn: {currentPlayer === 1 ? player1Name : player2Name}

+ )} + +
+
+

{player1Name}'s Board

+ handleAttack(r, c, 1) : null + } + hideShips={true} + /> +
+ +
+

{player2Name}'s Board

+ handleAttack(r, c, 2) : null + } + hideShips={true} + /> +
+
+
+ ); +} + +export default GameBoard; diff --git a/z2/frontend/src/components/Header.css b/z2/frontend/src/components/Header.css new file mode 100644 index 0000000..37e0c60 --- /dev/null +++ b/z2/frontend/src/components/Header.css @@ -0,0 +1,12 @@ +.header { + background-color: #0077cc; + color: white; + padding: 20px 0; + margin-bottom: 20px; + } + + .header h1 { + margin: 0; + font-size: 2.5em; + } + \ No newline at end of file diff --git a/z2/frontend/src/components/Header.jsx b/z2/frontend/src/components/Header.jsx new file mode 100644 index 0000000..bf0127c --- /dev/null +++ b/z2/frontend/src/components/Header.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import './Header.css'; + +const Header = ({ title }) => { + return ( +
+

{title}

+
+ ); +}; + +export default Header; diff --git a/z2/frontend/src/components/PlacementBoard.css b/z2/frontend/src/components/PlacementBoard.css new file mode 100644 index 0000000..fba4856 --- /dev/null +++ b/z2/frontend/src/components/PlacementBoard.css @@ -0,0 +1,73 @@ +.placement-board { + display: flex; + flex-direction: column; + align-items: center; + margin: 2rem auto; + max-width: 800px; + text-align: center; + background: #ffffff; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.placement-board h2 { + margin-bottom: 1.5rem; + font-size: 2rem; + color: #333; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin-bottom: 1.5rem; +} + +.control-group { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.ship-label, +.orientation-label { + font-size: 1.1rem; + margin-bottom: 0.5rem; + color: #555; + font-weight: 600; +} + +.ship-select, +.orientation-select { + font-size: 1.1rem; + padding: 0.6rem; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #f9f9f9; +} + +.ship-select { + width: 220px; +} + +.orientation-select { + width: 150px; +} + +.finish-button { + margin-top: 1.5rem; + padding: 0.8rem 1.5rem; + font-size: 1.2rem; + cursor: pointer; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 6px; + transition: background-color 0.3s; +} + +.finish-button:hover { + background-color: #218838; +} diff --git a/z2/frontend/src/components/PlacementBoard.jsx b/z2/frontend/src/components/PlacementBoard.jsx new file mode 100644 index 0000000..3541053 --- /dev/null +++ b/z2/frontend/src/components/PlacementBoard.jsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import Board from './Board'; +import './PlacementBoard.css'; + +const SHIPS_LIMIT = { + 'Battleship (4 cells)': { size: 4, max: 1 }, + 'Cruiser (3 cells)': { size: 3, max: 2 }, + 'Destroyer (2 cells)': { size: 2, max: 3 }, + 'Submarine (1 cell)': { size: 1, max: 4 } +}; + +function PlacementBoard({ playerName, onPlacementDone }) { + const [ships, setShips] = useState([]); + const [selectedShipType, setSelectedShipType] = useState('Battleship (4 cells)'); + const [shipCounts, setShipCounts] = useState({}); + const [orientation, setOrientation] = useState('horizontal'); + + const isOverlap = (newShip) => { + for (const ship of ships) { + for (const cell of ship) { + for (const newCell of newShip) { + if (cell.row === newCell.row && cell.col === newCell.col) { + return true; + } + } + } + } + return false; + }; + + const handleCellClick = (row, col) => { + const shipData = SHIPS_LIMIT[selectedShipType]; + if (!shipData) { + alert('Invalid ship type!'); + return; + } + + const currentCount = shipCounts[selectedShipType] || 0; + if (currentCount >= shipData.max) { + alert(`You can only place ${shipData.max} ${selectedShipType}!`); + return; + } + + let newShip = []; + if (orientation === 'horizontal') { + if (col + shipData.size > 10) { + alert('Ship is out of bounds horizontally!'); + return; + } + for (let i = 0; i < shipData.size; i++) { + newShip.push({ row, col: col + i }); + } + } else { + if (row + shipData.size > 10) { + alert('Ship is out of bounds vertically!'); + return; + } + for (let i = 0; i < shipData.size; i++) { + newShip.push({ row: row + i, col }); + } + } + + if (isOverlap(newShip)) { + alert('Ships cannot overlap!'); + return; + } + + setShips([...ships, newShip]); + setShipCounts({ + ...shipCounts, + [selectedShipType]: currentCount + 1 + }); + }; + + const handleDone = () => { + + if ( + Object.keys(SHIPS_LIMIT).some( + (type) => (shipCounts[type] || 0) < SHIPS_LIMIT[type].max + ) + ) { + alert('You must place all required ships!'); + return; + } + onPlacementDone(ships); + }; + + return ( +
+

Ship Placement: {playerName}

+ +
+
+ + +
+ +
+ + +
+
+ + + + +
+ ); +} + +export default PlacementBoard; diff --git a/z2/frontend/src/components/PlayerForm.css b/z2/frontend/src/components/PlayerForm.css new file mode 100644 index 0000000..2a1a51a --- /dev/null +++ b/z2/frontend/src/components/PlayerForm.css @@ -0,0 +1,123 @@ +.player-form-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + background: #f7f9fc; + min-height: 100vh; + font-family: 'Arial', sans-serif; +} + +.player-form-card, +.results-card { + background: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 400px; + margin-bottom: 2rem; + padding: 2rem; +} + +.form-title { + text-align: center; + margin-bottom: 1.5rem; + font-size: 1.8rem; + color: #333; +} + +.player-form { + display: flex; + flex-direction: column; +} + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 1.2rem; +} + +.form-group label { + margin-bottom: 0.5rem; + font-size: 1.1rem; + color: #555; +} + +.form-group input { + padding: 0.8rem; + font-size: 1.1rem; + border: 1px solid #ccc; + border-radius: 4px; + transition: border-color 0.3s; +} + +.form-group input:focus { + border-color: #007bff; + outline: none; +} + +.submit-button { + padding: 0.8rem; + font-size: 1.2rem; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +.submit-button:hover { + background-color: #0056b3; +} + +.results-section { + width: 400px; + text-align: center; +} + +.toggle-results-button { + padding: 0.8rem; + font-size: 1.2rem; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + margin-bottom: 1rem; + transition: background-color 0.3s; +} + +.toggle-results-button:hover { + background-color: #218838; +} + +.results-title { + font-size: 1.6rem; + text-align: center; + margin-bottom: 1rem; + color: #333; +} + +.results-table { + width: 100%; + border-collapse: collapse; +} + +.results-table th, +.results-table td { + border: 1px solid #ddd; + padding: 0.8rem; + font-size: 1.1rem; + text-align: center; +} + +.results-table th { + background-color: #007bff; + color: #fff; +} + +.no-results { + font-size: 1.1rem; + text-align: center; + color: #777; +} diff --git a/z2/frontend/src/components/PlayerForm.jsx b/z2/frontend/src/components/PlayerForm.jsx new file mode 100644 index 0000000..93cade1 --- /dev/null +++ b/z2/frontend/src/components/PlayerForm.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import './PlayerForm.css'; + +function PlayerForm({ onSubmit }) { + const [p1, setP1] = useState(''); + const [p2, setP2] = useState(''); + const [results, setResults] = useState([]); + const [showResults, setShowResults] = useState(false); + + const handleSubmit = (e) => { + e.preventDefault(); + if (p1.trim() === '' || p2.trim() === '') { + alert('Please enter both player names!'); + console.log('Error: Missing player names'); + return; + } + console.log('Starting new game with players:', p1, p2); + onSubmit(p1, p2); + }; + + const fetchResults = () => { + console.log('Fetching previous game results...'); + fetch('http://localhost:30000/api/results') + .then((resp) => resp.json()) + .then((data) => { + console.log('Fetched results:', data.results); + setResults(data.results); + }) + .catch((err) => console.error('Error fetching results:', err)); + }; + + const handleToggleResults = () => { + if (!showResults) { + fetchResults(); + } + setShowResults(!showResults); + }; + + return ( +
+
+

Enter Player Names

+
+
+ + setP1(e.target.value)} + placeholder="Enter Player 1 name" + required + /> +
+
+ + setP2(e.target.value)} + placeholder="Enter Player 2 name" + required + /> +
+ +
+
+ +
+ + {showResults && ( +
+

Previous Game Results

+ {results.length > 0 ? ( + + + + + + + + + + {results.map((result, index) => ( + + + + + + ))} + +
WinnerLoserShots
{result.winner}{result.loser}{result.moves}
+ ) : ( +

No previous results available.

+ )} +
+ )} +
+
+ ); +} + +export default PlayerForm; diff --git a/z2/frontend/src/index.css b/z2/frontend/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/z2/frontend/src/main.jsx b/z2/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/z2/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/z2/frontend/vite.config.js b/z2/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/z2/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/z2/namespace.yaml b/z2/namespace.yaml new file mode 100644 index 0000000..6a1748b --- /dev/null +++ b/z2/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: battleship-app diff --git a/z2/prepare-app.sh b/z2/prepare-app.sh new file mode 100644 index 0000000..92c2617 --- /dev/null +++ b/z2/prepare-app.sh @@ -0,0 +1,8 @@ +#!/bin/bash +echo "Building Docker image for backend..." +docker build -t battleship-backend ./backend + +echo "Building Docker image for frontend..." +docker build -t battleship-frontend ./frontend + +echo "Docker images built." diff --git a/z2/remove-app.sh b/z2/remove-app.sh new file mode 100644 index 0000000..26739de --- /dev/null +++ b/z2/remove-app.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo "Deleting Services for MySQL, backend, and frontend..." +kubectl delete -f service.yaml -n battleship-app + +echo "Deleting StatefulSet for MySQL..." +kubectl delete -f statefulset.yaml -n battleship-app + +echo "Deleting Deployments for backend and frontend..." +kubectl delete -f deployment.yaml -n battleship-app + +echo "Deleting namespace battleship-app (and all objects within it)..." +kubectl delete -f namespace.yaml + +echo "Application completely removed from Kubernetes." diff --git a/z2/service.yaml b/z2/service.yaml new file mode 100644 index 0000000..22022c2 --- /dev/null +++ b/z2/service.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: battleship-app +spec: + ports: + - port: 3306 + targetPort: 3306 + selector: + app: battleship-mysql +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: battleship-app +spec: + type: NodePort + ports: + - port: 4000 + targetPort: 4000 + nodePort: 30000 + selector: + app: battleship-backend +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: battleship-app +spec: + type: NodePort + ports: + - port: 80 + targetPort: 80 + nodePort: 30001 + selector: + app: battleship-frontend diff --git a/z2/start-app.sh b/z2/start-app.sh new file mode 100644 index 0000000..c57fd99 --- /dev/null +++ b/z2/start-app.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo "Creating namespace battleship-app..." +kubectl apply -f namespace.yaml + +echo "Creating Deployment for backend and frontend..." +kubectl apply -f deployment.yaml -n battleship-app + +echo "Creating StatefulSet for MySQL..." +kubectl apply -f statefulset.yaml -n battleship-app + +echo "Creating Service for MySQL, backend, and frontend..." +kubectl apply -f service.yaml -n battleship-app + +echo "Application deployed in Kubernetes." diff --git a/z2/statefulset.yaml b/z2/statefulset.yaml new file mode 100644 index 0000000..ea29ea3 --- /dev/null +++ b/z2/statefulset.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mysql-pv + namespace: battleship-app +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /mnt/data/mysql +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: battleship-app +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: battleship-mysql + namespace: battleship-app +spec: + serviceName: "mysql" + replicas: 1 + selector: + matchLabels: + app: battleship-mysql + template: + metadata: + labels: + app: battleship-mysql + spec: + containers: + - name: battleship-mysql + image: mysql:8.0.30 + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + value: somepassword + - name: MYSQL_DATABASE + value: battleship + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-pvc diff --git a/z2/stop-app.sh b/z2/stop-app.sh new file mode 100644 index 0000000..7d4d729 --- /dev/null +++ b/z2/stop-app.sh @@ -0,0 +1,13 @@ +#!/bin/bash +echo "Stopping the application by scaling down to 0 replicas..." + +echo "Scaling down the Deployment for backend to 0..." +kubectl scale deployment battleship-backend -n battleship-app --replicas=0 + +echo "Scaling down the Deployment for frontend to 0..." +kubectl scale deployment battleship-frontend -n battleship-app --replicas=0 + +echo "Scaling down the StatefulSet for MySQL to 0..." +kubectl scale statefulset battleship-mysql -n battleship-app --replicas=0 + +echo "Application stopped, data (e.g., database) is preserved."