Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions GornyhIS/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
.idea/
105 changes: 105 additions & 0 deletions GornyhIS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

# 🌐 URL Checker (Microservices Project)


Сервис для проверки доступности веб-сайтов, построенный на микросервисной архитектуре с использованием Docker.

## 🏗 Архитектура системы

Проект разделен на независимые слои для обеспечения отказоустойчивости и возможности масштабирования.

```mermaid
graph TD
User((Пользователь)) -->|HTTP:80| Nginx[Nginx Gateway]

subgraph "Frontend Layer"
Nginx -->|Static HTML/JS| FE[Frontend Container]
end

subgraph "Backend Layer"
Nginx -->|/api/*| BE[Backend Flask API]
BE -->|Отправка задачи| Redis[(Redis Broker)]
end

subgraph "Worker Layer (Long Task Processor)"
Worker[Celery Worker] -->|Слушает очередь| Redis
Worker -->|Проверка URL| Internet((Internet))
Internet -.->|Результат| Worker
Worker -->|Запись результата| Redis
end

BE -.->|Опрос статуса| Redis
```

## 🛠 Технологический стек
- Gateway: Nginx (Reverse Proxy)
- Frontend: HTML5 / JavaScript (Vanilla) / Nginx
- Backend: Python 3.11 / Flask / Gunicorn (WSGI)
- Task Queue: Celery + Redis
- Worker: Python 3.11 / Requests
- Orchestration: Docker Compose

## 🚀 Быстрый запуск
Все компоненты запускаются одной командой из корневой директории `GornyhIS`

```bash
docker compose up --build
```

После запуска приложение доступно по адресу:
```text
http://localhost
```

## 📂 Структура проекта
```text
/backend — API для приема заявок и отдачи статуса задач.
/worker — Сервис, выполняющий реальные HTTP-запросы.
/frontend — Простой UI для взаимодействия с пользователем.
nginx.conf — Правила маршрутизации трафика.
docker-compose.yml — Описание связи всех контейнеров.
```
## ✨ Особенности реализации
- **Выбор протокола**: пользователь выбирает `http://` или `https://` через выпадающий список.
- **Определение IP**: при проверке сайта автоматически определяется IP-адрес сервера через DNS.
- **Отображение результата**: на экран выводится:
- Статус доступности (✅/❌)
- HTTP-статус код
- IP-адрес целевого сервера
- **Асинхронная обработка**:
- Задача отправляется в очередь через Redis
- Backend сразу возвращает `task_id`
- Frontend опрашивает статус через polling

## 📝 Как это работает
1. Пользователь выбирает протокол (`http` или `https`) и вводит домен (например, `google.com`).
2. Frontend формирует полный URL и отправляет POST-запрос на `/api/check`.
3. Backend создаёт задачу в Celery, возвращает `task_id`.
4. Celery Worker забирает задачу, определяет IP-адрес, делает HTTP-запрос.
5. Результат сохраняется в Redis.
6. Frontend периодически опрашивает `/api/status/<task_id>`, пока не получит результат.
7. Отображается статус, код ответа и IP-адрес.

## ✅ Тестирование
Юнит-тесты Worker:
```bash
cd worker
python -m pytest test_tasks.py -v --cov=tasks --cov-report=term
```

Тесты проверяют:
- Успешный ответ с IP
- Ошибку DNS (неизвестный хост)
- Ошибку подключения (таймаут, отказ соединения)

Юнит-тесты Backend:
```bash
cd backend
python -m pytest test_app.py -v
```
- Он проверяет корректность HTTP-маршрутов и взаимодействие с Celery
- не запускает реальных задач

---

Проект полностью рабочий, модульный и готов к демонстрации 💯
14 changes: 14 additions & 0 deletions GornyhIS/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.11-slim

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем зависимости и устанавливаем их
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Копируем исходный код
COPY *.py .

# Запускаем через Gunicorn (WSGI)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
32 changes: 32 additions & 0 deletions GornyhIS/backend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# backend/app.py
from flask import Flask, request, jsonify
from celery import Celery
import os

app = Flask(__name__)
celery = Celery('tasks', broker=os.getenv('REDIS_URL'), backend=os.getenv('REDIS_URL'))

@app.route('/api/check', methods=['POST'])
def check_url():
data = request.get_json()
if not data or 'url' not in data:
return jsonify({"error": "URL is required"}), 400
task = celery.send_task('tasks.check_availability', args=[data['url']])
return jsonify({"task_id": task.id}), 202

@app.route('/api/status/<task_id>', methods=['GET'])
def get_status(task_id):
res = celery.AsyncResult(task_id)
response_data = {"status": res.status}

if res.ready():
result = res.result
# Передаём весь результат, включая URL, IP, статус и online
response_data["result"] = result
else:
response_data["result"] = None # или можно добавить "processing"

return jsonify(response_data)

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
5 changes: 5 additions & 0 deletions GornyhIS/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
flask==3.0.0
celery==5.3.6
redis==5.0.1
gunicorn==21.2.0
pytest
45 changes: 45 additions & 0 deletions GornyhIS/backend/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
from unittest.mock import MagicMock, patch
from app import app

@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client

@patch('app.celery')
def test_check_url_success(mock_celery, client):
# Мокируем Celery-задачу
mock_task = MagicMock()
mock_task.id = 'test-task-id'
mock_celery.send_task.return_value = mock_task

response = client.post('/api/check', json={'url': 'http://example.com'})

assert response.status_code == 202
data = response.get_json()
assert data['task_id'] == 'test-task-id'
mock_celery.send_task.assert_called_once_with('tasks.check_availability', args=['http://example.com'])

@patch('app.celery')
def test_check_url_missing_url(mock_celery, client):
response = client.post('/api/check', json={})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data

@patch('app.celery')
def test_get_status(mock_celery, client):
mock_result = MagicMock()
mock_result.status = 'SUCCESS'
mock_result.result = True
mock_celery.AsyncResult.return_value = mock_result

response = client.get('/api/status/test-task-id')

assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'SUCCESS'
assert data['result'] is True
mock_celery.AsyncResult.assert_called_once_with('test-task-id')
23 changes: 23 additions & 0 deletions GornyhIS/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
services:
nginx:
image: nginx:alpine
ports: ["80:80"]
volumes: ["./nginx.conf:/etc/nginx/nginx.conf:ro"]
depends_on: [frontend, backend]

backend:
build: ./backend
environment:
- REDIS_URL=redis://redis:6379/0

worker:
build: ./worker
environment:
- REDIS_URL=redis://redis:6379/0
depends_on: [redis]

redis:
image: redis:alpine

frontend:
build: ./frontend
9 changes: 9 additions & 0 deletions GornyhIS/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM nginx:alpine

# Удаляем дефолтную страницу nginx
RUN rm /usr/share/nginx/html/*

# Копируем наш интерфейс в папку сервера
COPY index.html /usr/share/nginx/html/index.html

EXPOSE 80
86 changes: 86 additions & 0 deletions GornyhIS/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>URL Checker</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 50px auto; text-align: center; }
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
justify-content: center;
}
select, input, button {
padding: 10px;
font-size: 16px;
}
input { width: 60%; }
button { cursor: pointer; }
#result { margin-top: 20px; font-weight: bold; }
</style>
</head>
<body>
<h2>Проверка доступности сайтов</h2>
<div class="input-group">
<select id="protocol">
<option value="https">https://</option>
<option value="http">http://</option>
</select>
<input type="text" id="urlInput" placeholder="example.com">
</div>
<button onclick="checkUrl()">Проверить</button>
<div id="result"></div>

<script>
async function checkUrl() {
const protocol = document.getElementById('protocol').value;
const domain = document.getElementById('urlInput').value.trim();
const resDiv = document.getElementById('result');

if (!domain) {
resDiv.innerText = "Введите адрес сайта!";
return;
}

// Собираем полный URL
const url = `${protocol}://${domain}`;
resDiv.innerText = "Отправка задачи...";

// 1. Отправляем URL на бэкенд
const response = await fetch('/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url })
});

const data = await response.json();

if (response.status !== 202) {
resDiv.innerText = `Ошибка: ${data.error}`;
return;
}

// 2. Опрашиваем статус задачи
const interval = setInterval(async () => {
const statusRes = await fetch(`/api/status/${data.task_id}`);
const statusData = await statusRes.json();

if (statusData.status === 'SUCCESS') {
clearInterval(interval);
const r = statusData.result;
if (r.online) {
resDiv.innerHTML = `✅ Доступен<br>Статус: ${r.status}<br>IP: ${r.ip}`;
} else {
resDiv.innerHTML = `❌ Недоступен<br><small>${r.error || 'Неизвестная ошибка'}</small>`;
}
} else if (statusData.status === 'FAILURE') {
clearInterval(interval);
resDiv.innerText = `❌ Ошибка выполнения задачи`;
}
}, 1000);
}
</script>
</body>
</html>
28 changes: 28 additions & 0 deletions GornyhIS/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
events {
worker_connections 1024;
}

http {
# Настройка проксирования
server {
listen 80;
server_name localhost;

# 1. Запросы к API (бэкенд)
location /api/ {
# Перенаправляем на сервис 'backend' из docker-compose
proxy_pass http://backend:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 2. Все остальные запросы (фронтенд)
location / {
# Перенаправляем на сервис 'frontend' из docker-compose
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
Binary file added GornyhIS/worker/.coverage
Binary file not shown.
11 changes: 11 additions & 0 deletions GornyhIS/worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY *.py .

# Запуск Celery воркера
CMD ["celery", "-A", "tasks", "worker", "--loglevel=info"]
5 changes: 5 additions & 0 deletions GornyhIS/worker/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
celery==5.3.6
redis==5.0.1
requests==2.31.0
pytest
pytest-cov
Loading