diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..668ba510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ + +Samples and Hints/Problem 3 /README.md +Samples and Hints/Problem 3 /README.md +Samples and Hints/Problem 3 /README.md +Samples and Hints/Problem 4/README.md +Samples and Hints/Problem 3 /README.md +Samples and Hints/Problem 1/README.md +Samples and Hints/Problem 2/README.md +README.md +Samples and Hints/Problem 1/README.md +Samples and Hints/Problem 2/README.md +Samples and Hints/Problem 3 /README.md +Samples and Hints/Problem 4/README.md +Samples and Hints/Problem 5/README.md diff --git a/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/main.py b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/main.py new file mode 100644 index 00000000..5e59703d --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/main.py @@ -0,0 +1,63 @@ +from fastapi import FastAPI, Depends +from tasks import start_container, stop_container +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "postgresql://mot:mypassword@localhost:5432/mydb" +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(bind=engine) + +app = FastAPI() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@app.post("/assign") +async def assign_challenge(team_id, challenge_id, db=Depends(get_db)): + image_name = "python:3.12" + task = start_container.delay(image_name, team_id, challenge_id) + result = task.get(timeout=20) + + create_table_sql = text(""" + CREATE TABLE IF NOT EXISTS assignments ( + id SERIAL PRIMARY KEY, + team_id VARCHAR(50), + challenge_id VARCHAR(50), + container_id VARCHAR(100), + container_address VARCHAR(100) + ) + """) + db.execute(create_table_sql) + + insert_sql = text(""" + INSERT INTO assignments (team_id, challenge_id, container_id, container_address) + VALUES (:team_id, :challenge_id, :container_id, :container_address) + """) + db.execute(insert_sql, { + "team_id": team_id, + "challenge_id": challenge_id, + "container_id": result['container_id'], + "container_address": result['container_address'] + }) + db.commit() + + return {"status": "assigned", "container_address": result['container_address']} + +@app.post("/remove") +async def remove_challenge(team_id, challenge_id, db=Depends(get_db)): + query = text("SELECT container_id FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + container = db.execute(query, {"team_id": team_id, "challenge_id": challenge_id}).fetchone() + if not container: + return {"status": "not found"} + + stop_container.delay(container[0]) + + delete_query = text("DELETE FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + db.execute(delete_query, {"team_id": team_id, "challenge_id": challenge_id}) + db.commit() + + return {"status": "removed"} diff --git a/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/requirements.txt b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/requirements.txt new file mode 100644 index 00000000..fe12e83d --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/requirements.txt @@ -0,0 +1,6 @@ +fastapi +sqlalchemy +psycopg2-binary +celery +redis +docker \ No newline at end of file diff --git a/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/tasks.py b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/tasks.py new file mode 100644 index 00000000..cd361ed7 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem4_WebAPI/Implmentation/tasks.py @@ -0,0 +1,24 @@ +from celery import Celery +import docker + +celery_app = Celery( + 'tasks', + broker='redis://localhost:6379/0', + backend='redis://localhost:6379/0' +) + +client = docker.from_env() + +@celery_app.task +def start_container(image_name, team_id, challenge_id): + container = client.containers.run(image_name, detach=True, ports={'80/tcp': None}) + container.reload() + container_ip = container.attrs['NetworkSettings']['IPAddress'] + return {'container_id': container.id, 'container_address': container_ip} + +@celery_app.task +def stop_container(container_id): + container = client.containers.get(container_id) + container.stop() + container.remove() + return {'status': 'removed'} diff --git a/Chatraei-Raoufimanesh/Problem4_WebAPI/README.markdown b/Chatraei-Raoufimanesh/Problem4_WebAPI/README.markdown new file mode 100644 index 00000000..3b8896ed --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem4_WebAPI/README.markdown @@ -0,0 +1,466 @@ +# Web API for Team Challenge Management + +I developed a Web API to manage Capture The Flag (CTF) challenge containers for different teams, as part of my assignment. This document explains how I built the API, including the purpose of each endpoint, the database schema, how I configured Celery with Redis, and instructions to set it up and run it. I used Docker containers for both Redis and PostgreSQL to ensure a consistent environment. + +--- + +## Overview of My API + +I built this API using **FastAPI**, a Python web framework, to manage CTF challenge containers for teams. The API assigns and removes Docker containers based on team IDs and challenge IDs. Here's what my API does: + +- Stores assignment data in a **PostgreSQL** database running in a Docker container. +- Uses **Celery** with **Redis** (also in a Docker container) to handle container management tasks asynchronously. +- Interacts with **Docker** to start and stop containers. +- Returns container addresses in responses so teams can access their challenges. + +My API meets all the assignment requirements by: + +- Assigning containers to teams based on team and challenge IDs. +- Removing containers when requested. +- Updating the database to track assignments. +- Processing container operations asynchronously using Celery and Redis. + +--- + + +## API Endpoints + +I implemented two endpoints to manage CTF containers: + +### 1. POST /assign + +**What It Does**: This endpoint assigns a Docker container for a CTF challenge to a team. + +- **Input**: + - `team_id`: A string to identify the team. + - `challenge_id`: A string to identify the challenge. +- **How It Works**: + - Starts a Docker container using the `python:3.12` image through a Celery task. + - Saves the team ID, challenge ID, container ID, and container IP address in the database. + - Returns the container's IP address and a status message. +- **Response**: + + ```json + { + "status": "assigned", + "container_address": "" + } + ``` + +### 2. POST /remove + +**What It Does**: This endpoint removes a container assigned to a team for a specific challenge. + +- **Input**: + - `team_id`: A string to identify the team. + - `challenge_id`: A string to identify the challenge. +- **How It Works**: + - Checks the database for the container ID linked to the team and challenge. + - Stops and removes the container using a Celery task. + - Deletes the assignment from the database. + - Returns a status message. +- **Response**: + + ```json + { + "status": "removed" + } + ``` + + If no assignment exists: + + ```json + { + "status": "not found" + } + ``` + +--- + +## Database Schema + +I used a **PostgreSQL** database (running in a Docker container) with one table called `assignments` to store container assignment details. + +### Table: assignments + +| Column | Data Type | Description | +| --- | --- | --- | +| `id` | SERIAL | Auto-incremented primary key. | +| `team_id` | VARCHAR(50) | The team's identifier. | +| `challenge_id` | VARCHAR(50) | The challenge's identifier. | +| `container_id` | VARCHAR(100) | The Docker container's unique ID. | +| `container_address` | VARCHAR(100) | The container's IP address. | + +I designed the table to be created automatically when the `/assign` endpoint is called, using a `CREATE TABLE IF NOT EXISTS` statement to avoid errors if the table already exists. + +--- + +## My Celery and Redis Configuration + +I used **Celery** for asynchronous task processing and **Redis** (in a Docker container) as the message broker and result backend. + +### How I Configured It + +- **Broker**: Redis at `redis://localhost:6379/0` handles task queuing. +- **Backend**: Redis at `redis://localhost:6379/0` stores task results. +- **Tasks**: + - `start_container`: Starts a Docker container and returns its ID and IP address. + - `stop_container`: Stops and removes a Docker container by its ID. + +### How It Works + +- When I call the `/assign` endpoint, it queues a `start_container` task in Celery. The API waits up to 20 seconds for the task to finish and gets the container details. +- When I call the `/remove` endpoint, it queues a `stop_container` task to stop and remove the container. This runs asynchronously, so the API doesn't wait for it to complete. + +--- + +## Explanation of My Code + +### main.py + +This file contains my FastAPI application and database logic. + +#### Imports + +```python +from fastapi import FastAPI, Depends +from tasks import start_container, stop_container +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +``` + +- `FastAPI` and `Depends`: For building the API and managing dependencies. +- `start_container`, `stop_container`: My Celery tasks from `tasks.py`. +- `create_engine`, `text`, `sessionmaker`: SQLAlchemy tools for database operations. + +#### Database Setup + +```python +DATABASE_URL = "postgresql://mot:mypassword@localhost:5432/mydb" +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(bind=engine) +``` + +- `DATABASE_URL`: Connects to my PostgreSQL container (`user:password@host:port/dbname`). +- `create_engine`: Sets up the database connection. +- `SessionLocal`: Creates a session factory for database interactions. + +#### Dependency Injection + +```python +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() +``` + +- I created `get_db` to provide a database session for each request and close it afterward. + +#### Endpoint: /assign + +```python +@app.post("/assign") +async def assign_challenge(team_id, challenge_id, db=Depends(get_db)): + image_name = "python:3.12" + task = start_container.delay(image_name, team_id, challenge_id) + result = task.get(timeout=20) +``` + +- **What It Does**: Assigns a container for a challenge to a team. +- **How It Works**: + - Queues the `start_container` Celery task with the `python:3.12` image. + - Waits up to 20 seconds for the task to return the container ID and IP address. + +```python + create_table_sql = text(""" + CREATE TABLE IF NOT EXISTS assignments ( + id SERIAL PRIMARY KEY, + team_id VARCHAR(50), + challenge_id VARCHAR(50), + container_id VARCHAR(100), + container_address VARCHAR(100) + ) + """) + db.execute(create_table_sql) +``` + +- Creates the `assignments` table if it doesn't exist, ensuring my database is ready. + +```python + insert_sql = text(""" + INSERT INTO assignments (team_id, challenge_id, container_id, container_address) + VALUES (:team_id, :challenge_id, :container_id, :container_address) + """) + db.execute(insert_sql, { + "team_id": team_id, + "challenge_id": challenge_id, + "container_id": result['container_id'], + "container_address": result['container_address'] + }) + db.commit() +``` + +- Inserts the assignment details into the database and commits the changes. + +```python + return {"status": "assigned", "container_address": result['container_address']} +``` + +- Returns the assignment status and container IP address. + +#### Endpoint: /remove + +```python +@app.post("/remove") +async def remove_challenge(team_id, challenge_id, db=Depends(get_db)): + query = text("SELECT container_id FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + container = db.execute(query, {"team_id": team_id, "challenge_id": challenge_id}).fetchone() + if not container: + return {"status": "not found"} +``` + +- **What It Does**: Removes a container for a team and challenge. +- **How It Works**: + - Queries the database for the container ID. + - Returns "not found" if no assignment exists. + +```python + stop_container.delay(container[0]) +``` + +- Queues the `stop_container` task to stop and remove the container. + +```python + delete_query = text("DELETE FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + db.execute(delete_query, {"team_id": team_id, "challenge_id": challenge_id}) + db.commit() +``` + +- Deletes the assignment from the database and commits. + +```python + return {"status": "removed"} +``` + +- Returns a confirmation of removal. + +### tasks.py + +This file contains my Celery tasks for Docker container management. + +#### Imports and Setup + +```python +from celery import Celery +import docker + +celery_app = Celery( + 'tasks', + broker='redis://localhost:6379/0', + backend='redis://localhost:6379/0' +) +client = docker.from_env() +``` + +- `Celery`: Sets up my Celery instance named `tasks`. +- `broker` and `backend`: Use my Redis container at `localhost:6379/0`. +- `docker.from_env()`: Connects to my Docker daemon. + +#### Task: start_container + +```python +@celery_app.task +def start_container(image_name, team_id, challenge_id): + container = client.containers.run(image_name, detach=True, ports={'80/tcp': None}) + container.reload() + container_ip = container.attrs['NetworkSettings']['IPAddress'] + return {'container_id': container.id, 'container_address': container_ip} +``` + +- **What It Does**: Starts a Docker container and returns its ID and IP address. +- **How It Works**: + - Runs a container in detached mode with the given image. + - Reloads the container to get updated attributes. + - Extracts the container's IP address. + - Returns the container ID and IP address. + +#### Task: stop_container + +```python +@celery_app.task +def stop_container(container_id): + container = client.containers.get(container_id) + container.stop() + container.remove() + return {'status': 'removed'} +``` + +- **What It Does**: Stops and removes a Docker container. +- **How It Works**: + - Gets the container by ID. + - Stops and removes it. + - Returns a confirmation status. + +--- + +## How to Set Up and Run My API + +I designed my API to run with Redis and PostgreSQL in Docker containers. Here's how to set it up: + +### What You Need + +- **Python 3.12**: Install Python 3.12 or later. +- **Docker**: Install Docker and ensure the daemon is running. +- **Python Packages**: + + ```bash + pip install fastapi uvicorn sqlalchemy psycopg2-binary celery docker + ``` + +### Step-by-Step Setup + +1. **Run PostgreSQL in a Docker Container**: + + ```bash + docker run -d --name postgres -p 5432:5432 -e POSTGRES_USER=mot -e POSTGRES_PASSWORD=mypassword -e POSTGRES_DB=mydb postgres:latest + ``` + + - This starts PostgreSQL with user `mot`, password `mypassword`, and database `mydb`. + +2. **Run Redis in a Docker Container**: + + ```bash + docker run -d --name redis -p 6379:6379 redis:latest + ``` + + - This starts Redis on `localhost:6379`. + +3. **Save My Code**: + + - Create a project directory. + - Save my `main.py` and `tasks.py` files in it. + +4. **Pull the Python Image**: + + ```bash + docker pull python:3.12 + ``` + + - Ensures the `python:3.12` image is available for containers. + +5. **Start the Celery Worker**: + + - In the project directory, run: + + ```bash + celery -A tasks.celery_app worker --loglevel=info + ``` + +6. **Start the FastAPI Server**: + + - In another terminal, in the project directory, run: + + ```bash + uvicorn main:app --reload + ``` + + - My API will be available at `http://localhost:8000`. + +--- + +## Testing API with Postman + +I tested my API using Postman to ensure it works as expected. + +### Test 1: Assign a Container + +1. **Request**: + - Method: POST + - URL: `http://localhost:8000/assign` + - Body (JSON): + + ```json + { + "team_id": "team1", + "challenge_id": "challenge1" + } + ``` +2. **Expected Response**: + + ```json + { + "status": "assigned", + "container_address": "" + } + ``` +3. **Verification**: + - Check running containers: + + ```bash + docker ps + ``` + - Query the database: + + ```bash + docker exec -it postgres psql -U mot -d mydb -c "SELECT * FROM assignments;" + ``` + +### Test 2: Remove a Container + +1. **Request**: + - Method: POST + - URL: `http://localhost:8000/remove` + - Body (JSON): + + ```json + { + "team_id": "team1", + "challenge_id": "challenge1" + } + ``` +2. **Expected Response**: + + ```json + { + "status": "removed" + } + ``` +3. **Verification**: + - Check stopped containers: + + ```bash + docker ps -a + ``` + - Query the database to confirm deletion: + + ```bash + docker exec -it postgres psql -U mot -d mydb -c "SELECT * FROM assignments;" + ``` + +### Test 3: Remove Non-Existent Assignment + +1. **Request**: + - Method: POST + - URL: `http://localhost:8000/remove` + - Body (JSON): + + ```json + { + "team_id": "team2", + "challenge_id": "challenge2" + } + ``` +2. **Expected Response**: + + ```json + { + "status": "not found" + } + ``` + + +## Explaination video + +The video is avalable in https://iutbox.iut.ac.ir/index.php/s/iGPckqFZkxKJZcW \ No newline at end of file diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/Dockerfile b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/Dockerfile new file mode 100644 index 00000000..5eb99a46 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/docker-compose.yml b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/docker-compose.yml new file mode 100644 index 00000000..7dd605c2 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.9" + +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: mot + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + redis: + image: redis:7 + ports: + - "6379:6379" + + celery_worker: + build: . + command: celery -A tasks worker --loglevel=info + depends_on: + - redis + - postgres + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + + web: + build: . + command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload + ports: + - "8000:8000" + depends_on: + - celery_worker + - redis + - postgres + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + pgdata: diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/frozen.txt b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/frozen.txt new file mode 100644 index 00000000..c7af5085 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/frozen.txt @@ -0,0 +1,61 @@ +attrs==23.2.0 +Automat==22.10.0 +Babel==2.10.3 +bcrypt==3.2.2 +blinker==1.7.0 +certifi==2023.11.17 +chardet==5.2.0 +click==8.1.6 +cloud-init==24.3.1 +colorama==0.4.6 +command-not-found==0.3 +configobj==5.0.8 +constantly==23.10.4 +cryptography==41.0.7 +dbus-python==1.3.2 +distro==1.9.0 +distro-info==1.7+build1 +httplib2==0.20.4 +hyperlink==21.0.0 +idna==3.6 +incremental==22.10.0 +Jinja2==3.1.2 +jsonpatch==1.32 +jsonpointer==2.0 +jsonschema==4.10.3 +launchpadlib==1.11.0 +lazr.restfulclient==0.14.6 +lazr.uri==1.0.6 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +netifaces==0.11.0 +oauthlib==3.2.2 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycurl==7.45.3 +Pygments==2.17.2 +PyGObject==3.48.2 +PyHamcrest==2.1.0 +PyJWT==2.7.0 +pyOpenSSL==23.2.0 +pyparsing==3.1.1 +pyrsistent==0.20.0 +pyserial==3.5 +python-apt==2.7.7+ubuntu3 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.31.0 +rich==13.7.1 +service-identity==24.1.0 +setuptools==68.1.2 +six==1.16.0 +systemd-python==235 +Twisted==24.3.0 +typing_extensions==4.10.0 +ubuntu-pro-client==8001 +unattended-upgrades==0.1 +urllib3==2.0.7 +wadllib==1.3.6 +wheel==0.42.0 +zope.interface==6.1 diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/main.py b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/main.py new file mode 100644 index 00000000..5e59703d --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/main.py @@ -0,0 +1,63 @@ +from fastapi import FastAPI, Depends +from tasks import start_container, stop_container +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "postgresql://mot:mypassword@localhost:5432/mydb" +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(bind=engine) + +app = FastAPI() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@app.post("/assign") +async def assign_challenge(team_id, challenge_id, db=Depends(get_db)): + image_name = "python:3.12" + task = start_container.delay(image_name, team_id, challenge_id) + result = task.get(timeout=20) + + create_table_sql = text(""" + CREATE TABLE IF NOT EXISTS assignments ( + id SERIAL PRIMARY KEY, + team_id VARCHAR(50), + challenge_id VARCHAR(50), + container_id VARCHAR(100), + container_address VARCHAR(100) + ) + """) + db.execute(create_table_sql) + + insert_sql = text(""" + INSERT INTO assignments (team_id, challenge_id, container_id, container_address) + VALUES (:team_id, :challenge_id, :container_id, :container_address) + """) + db.execute(insert_sql, { + "team_id": team_id, + "challenge_id": challenge_id, + "container_id": result['container_id'], + "container_address": result['container_address'] + }) + db.commit() + + return {"status": "assigned", "container_address": result['container_address']} + +@app.post("/remove") +async def remove_challenge(team_id, challenge_id, db=Depends(get_db)): + query = text("SELECT container_id FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + container = db.execute(query, {"team_id": team_id, "challenge_id": challenge_id}).fetchone() + if not container: + return {"status": "not found"} + + stop_container.delay(container[0]) + + delete_query = text("DELETE FROM assignments WHERE team_id=:team_id AND challenge_id=:challenge_id") + db.execute(delete_query, {"team_id": team_id, "challenge_id": challenge_id}) + db.commit() + + return {"status": "removed"} diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/requirements.txt b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/requirements.txt new file mode 100644 index 00000000..23ae0898 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/requirements.txt @@ -0,0 +1,7 @@ +fastapi +sqlalchemy +psycopg2-binary +celery +redis +docker +uvicorn[standard] diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/tasks.py b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/tasks.py new file mode 100644 index 00000000..cd361ed7 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/Implementation/tasks.py @@ -0,0 +1,24 @@ +from celery import Celery +import docker + +celery_app = Celery( + 'tasks', + broker='redis://localhost:6379/0', + backend='redis://localhost:6379/0' +) + +client = docker.from_env() + +@celery_app.task +def start_container(image_name, team_id, challenge_id): + container = client.containers.run(image_name, detach=True, ports={'80/tcp': None}) + container.reload() + container_ip = container.attrs['NetworkSettings']['IPAddress'] + return {'container_id': container.id, 'container_address': container_ip} + +@celery_app.task +def stop_container(container_id): + container = client.containers.get(container_id) + container.stop() + container.remove() + return {'status': 'removed'} diff --git a/Chatraei-Raoufimanesh/Problem5_DockerCompose/README.markdown b/Chatraei-Raoufimanesh/Problem5_DockerCompose/README.markdown new file mode 100644 index 00000000..36e5b600 --- /dev/null +++ b/Chatraei-Raoufimanesh/Problem5_DockerCompose/README.markdown @@ -0,0 +1,133 @@ +# My Docker Compose Integration for CTF Challenge Management API + +For my assignment, I integrated my Web API for managing Capture The Flag (CTF) challenge containers into a Docker Compose setup. This report explains how I created a `docker-compose.yml` file to orchestrate all services, ensured they work together, and set up persistent data storage. I also describe how the services are connected, provide instructions to start and use the system, and outline my plan for a demonstration video. This builds on my previous work with the FastAPI-based API, PostgreSQL database, Celery, Redis, and Docker container management. + +--- + +## Overview of My Docker Compose Setup + +I created a Docker Compose setup to run all components of my CTF challenge management system in a cohesive environment. The system includes: + +- **PostgreSQL**: Stores assignment data in a database. +- **Redis**: Acts as the message broker and result backend for Celery. +- **Celery Worker**: Handles asynchronous tasks for starting and stopping Docker containers. +- **Web API**: A FastAPI application that exposes endpoints to assign and remove containers. +- **CTF Challenge Containers**: Docker containers (using `python:3.12`) created dynamically for teams. + +My Docker Compose setup ensures all services can communicate, uses volume mounts for persistent data, and starts seamlessly with a single command. I tested the system to confirm that I can assign and remove challenges using Postman, with all components working together. + +--- + +## My `docker-compose.yml` File + +I wrote a `docker-compose.yml` file to define and configure all services. Below is the file I created: + +```yaml +version: "3.9" + +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: mot + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + redis: + image: redis:7 + ports: + - "6379:6379" + + celery_worker: + build: . + command: celery -A tasks worker --loglevel=info + depends_on: + - redis + - postgres + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + + web: + build: . + command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload + ports: + - "8000:8000" + depends_on: + - celery_worker + - redis + - postgres + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + pgdata: +``` + +### Explanation of `docker-compose.yml` + +- **Services**: + - **postgres**: Runs PostgreSQL 15 with a user `mot`, password `mypassword`, and database `mydb`. I mapped port `5432` and used a volume `pgdata` for persistent storage. + - **redis**: Runs Redis 7 and exposes port `6379` for Celery communication. + - **celery_worker**: Builds from my `Dockerfile`, runs the Celery worker, and depends on `redis` and `postgres`. I mounted the project directory and Docker socket to allow container management. + - **web**: Builds from my `Dockerfile`, runs the FastAPI server with Uvicorn, and exposes port `8000`. It depends on all other services and uses the same volume mounts. +- **Volumes**: I created a `pgdata` volume to ensure PostgreSQL data persists across container restarts. +- **Networking**: Docker Compose automatically creates a default bridge network, allowing all services to communicate using their service names (e.g., `postgres`, `redis`). + +--- + +## How the Services Are Connected + +I designed the services to work together seamlessly: + +- **Web API and PostgreSQL**: My FastAPI application (`main.py`) connects to the PostgreSQL service using the connection string `postgresql://mot:mypassword@postgres:5432/mydb`. The service name `postgres` resolves to the container's IP within the Docker network. +- **Web API and Celery**: The API queues tasks (e.g., `start_container`, `stop_container`) using Celery, which communicates with the `redis` service as the message broker. +- **Celery and Redis**: The Celery worker (`celery_worker` service) uses Redis (`redis://redis:6379/0`) for task queuing and result storage. The service name `redis` resolves within the Docker network. +- **Celery and Docker**: The `celery_worker` and `web` services mount the Docker socket (`/var/run/docker.sock`) to interact with the Docker daemon on the host, allowing them to start and stop CTF challenge containers. +- **CTF Containers**: When I assign a challenge, Celery starts a `python:3.12` container, and its IP address is stored in the PostgreSQL database. These containers run on the default Docker network, accessible by their IP addresses. + +The `depends_on` directives ensure that services start in the correct order: `postgres` and `redis` start first, followed by `celery_worker`, and finally `web`. The default Docker bridge network enables communication between all services. + +--- + +## Starting and Using My System + +### Prerequisites + +- **Docker and Docker Compose**: I installed Docker and Docker Compose on my machine. +- **Project Files**: + - `Dockerfile`: Builds the image for my `web` and `celery_worker` services. + - `main.py`: My FastAPI application. + - `tasks.py`: My Celery tasks for container management. + - `docker-compose.yml`: Orchestrates all services. + - `requirements.txt`: Lists Python dependencies (e.g., `fastapi`, `uvicorn`, `sqlalchemy`, `psycopg2-binary`, `celery`, `docker`). + +### My `Dockerfile` + +```dockerfile +FROM python:3.12 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +- **Purpose**: Builds a Docker image for the `web` and `celery_worker` services. +- **Details**: + - Uses `python:3.12` as the base image. + - Sets `/app` as the working directory. + - Installs dependencies from `requirements.txt`. + - Copies my project files. + - Specifies a default command for the `web` service (overridden for `celery_worker` in `docker-compose.yml`). + +## Video report available at \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index e6388ed1..00000000 --- a/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Cloud Assignment - -This repository contains the **Cloud Assignment** for containerizing and managing CTF challenges using Docker, Redis, Celery, FastAPI, PostgreSQL, and NGINX. - -## 🚀 Submission Instructions - -Please follow these steps to complete and submit your assignment: - -1. **Fork this repository**: - - -2. **Create a folder with your full name** in the root of the project. - Example: -``` - MohamadMahdiReisi/ - ├── Problem1_PostgreSQL/ - ├── Problem2_Redis/ - ├── Problem3_Celery/ - ├── Problem4_WebAPI/ - ├── Problem5_NGINX/ - └── Problem6_DockerCompose/ -``` - -4. **Add all your documents, source code, and configuration files** (e.g., Dockerfiles, `docker-compose.yml`, Python scripts, etc.) to the correct folder for each question. - -5. **Upload required videos** (demonstration videos) to **IUT Box** or any cloud storage, and **add the video links** inside a `README.md` file within each problem folder. - -6. **Write your explanations and answers** as a Markdown file (`README.md`) in each problem folder. Include: -- Description of what you did -- Steps to run your solution -- Reasoning behind any decisions or tools you chose -- Screenshots if needed -- Link to your demonstration video - -7. **Open a Pull Request (PR)** back to the original repository