Merge pull request 'blokhin_dmitriy_lab_3' (#436) from blokhin_dmitriy_lab_3 into main
Reviewed-on: #436
This commit was merged in pull request #436.
This commit is contained in:
71
blokhin_dmitriy_lab_3/README.md
Normal file
71
blokhin_dmitriy_lab_3/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
|
||||
|
||||
## Описание
|
||||
|
||||
Целью лабораторной работы было изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля REST API. Работа реализована в виде системы из двух микросервисов, соединённых через шлюз nginx.
|
||||
|
||||
## Что делает программа
|
||||
|
||||
Программа состоит из трёх основных компонентов:
|
||||
|
||||
1. `user-service`: Микросервис, отвечающий за управление данными пользователей (например, продавцов автомобилей). Реализует CRUD-операции (создание, чтение, обновление, удаление) для сущности `User`.
|
||||
2. `listing-service`: Микросервис, отвечающий за управление объявлениями о продаже автомобилей. Реализует CRUD-операции для сущности `Listing`. Связан с `user-service` отношением "один ко многим" (один пользователь может создать много объявлений).
|
||||
3. `nginx-gateway`: Шлюз, реализованный с помощью nginx. Принимает внешние запросы и направляет их к соответствующим микросервисам. Запросы к `/users/` перенаправляются в `user-service`, а запросы к `/listings/` — в `listing-service`.
|
||||
|
||||
*Синхронный обмен:* При выполнении запроса `GET /listings/{uuid}` (получение деталей конкретного объявления) `listing-service` выполняет синхронный HTTP-запрос к `user-service`, чтобы получить информацию о пользователе, связанном с этим объявлением, и включает её в свой ответ.
|
||||
|
||||
## Как запустить лабораторную работу
|
||||
|
||||
1. Убедитесь, что Docker Desktop запущен
|
||||
2. Перейдите в директорию проекта: `cd .\blokhin_dmitriy_lab_3\`
|
||||
3. Выполните команду: `docker compose up --build`
|
||||
4. После успешного запуска система будет доступна через шлюз nginx на порту `8080` хоста.
|
||||
|
||||
## Какие технологии использовались
|
||||
|
||||
`Python`: Язык программирования для реализации логики микросервисов.
|
||||
`FastAPI`: Фреймворк для создания веб-API на Python. Автоматически генерирует документацию Swagger UI.
|
||||
`Pydantic`: Библиотека для валидации данных, используемая FastAPI.
|
||||
`httpx`: Библиотека для выполнения HTTP-запросов из `listing-service` в `user-service`.
|
||||
`Nginx`: Веб-сервер, использованный в качестве API-шлюза для маршрутизации запросов.
|
||||
`Docker`: Технология контейнеризации для изоляции и запуска микросервисов.
|
||||
`Docker Compose`: Инструмент для определения и запуска многоконтейнерных Docker-приложений.
|
||||
|
||||
## Примеры взаимодействия через шлюз (порт 8080)
|
||||
|
||||
После запуска вы можете тестировать API через шлюз, например, с помощью `curl` или Postman.
|
||||
|
||||
* **Создать пользователя:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Иванов Иван", "contact_info": "ivanov@example.com"}'
|
||||
```
|
||||
* **Создать объявление (используйте `uuid` созданного пользователя):**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/listings \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"make": "Toyota", "model": "Camry", "year": 2020, "price": 1500000, "user_uuid": "UUID_ПОЛЬЗОВАТЕЛЯ"}'
|
||||
```
|
||||
* **Получить список пользователей:**
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/users
|
||||
```
|
||||
* **Получить список объявлений:**
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/listings
|
||||
```
|
||||
* **Получить объявление с деталями пользователя (демонстрация синхронного вызова):**
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/listings/UUID_ОБЪЯВЛЕНИЯ
|
||||
```
|
||||
|
||||
## Доступ к Swagger UI (напрямую, не через шлюз)
|
||||
|
||||
Для удобного просмотра и тестирования API каждого сервиса напрямую:
|
||||
|
||||
* `user-service` Swagger UI: `http://localhost:8001/docs`
|
||||
* `listing-service` Swagger UI: `http://localhost:8002/docs`
|
||||
|
||||
|
||||
Ссылка на видео:https://rutube.ru/video/private/60f1554c3716ec930f74a52d63100199/?p=lbZ6GVrBua9TRxDAyL1Uyw
|
||||
32
blokhin_dmitriy_lab_3/docker-compose.yml
Normal file
32
blokhin_dmitriy_lab_3/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
user-service:
|
||||
build:
|
||||
context: ./user-service # Путь к папке с кодом user-service
|
||||
dockerfile: Dockerfile # Убедитесь, что Dockerfile создан для user-service
|
||||
ports:
|
||||
- "8001:8001" # Для отладки, необязательно
|
||||
# environment:
|
||||
# - DATABASE_URL=sqlite:///./test.db # Пример переменной окружения
|
||||
|
||||
listing-service:
|
||||
build:
|
||||
context: ./listing-service # Путь к папке с кодом listing-service
|
||||
dockerfile: Dockerfile # Убедитесь, что Dockerfile создан для listing-service
|
||||
ports:
|
||||
- "8002:8002" # Для отладки, необязательно
|
||||
# environment:
|
||||
# - USER_SERVICE_URL=http://user-service:8001 # URL для вызова user-service
|
||||
depends_on:
|
||||
- user-service # listing-service зависит от user-service
|
||||
|
||||
nginx-gateway:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80" # Внешний порт для доступа к API через шлюз
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf # Монтируем наш конфиг
|
||||
depends_on:
|
||||
- user-service
|
||||
- listing-service
|
||||
10
blokhin_dmitriy_lab_3/listing-service/Dockerfile
Normal file
10
blokhin_dmitriy_lab_3/listing-service/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
154
blokhin_dmitriy_lab_3/listing-service/app.py
Normal file
154
blokhin_dmitriy_lab_3/listing-service/app.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
import httpx # Импортируем httpx для асинхронного HTTP-запроса
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Инициализируем FastAPI приложение с информацией о документации
|
||||
app = FastAPI(
|
||||
title="Listing Service API",
|
||||
description="API for managing car listings",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# --- Модели Pydantic ---
|
||||
class UserInfo(BaseModel):
|
||||
uuid: str
|
||||
name: str
|
||||
contact_info: str
|
||||
registration_date: datetime
|
||||
|
||||
class ListingBase(BaseModel):
|
||||
make: str
|
||||
model: str
|
||||
year: int
|
||||
price: float
|
||||
user_uuid: str
|
||||
|
||||
class ListingCreate(ListingBase):
|
||||
pass
|
||||
|
||||
class ListingUpdate(ListingBase):
|
||||
pass
|
||||
|
||||
class Listing(ListingBase):
|
||||
uuid: str
|
||||
created_at: datetime
|
||||
|
||||
class ListingDetail(Listing):
|
||||
user_info: Optional[UserInfo] = None
|
||||
|
||||
# --- Хранилище данных (в памяти) ---
|
||||
listings_db: List[Listing] = []
|
||||
|
||||
# --- CRUD операции ---
|
||||
def get_listing_by_uuid(listing_uuid: str) -> Optional[Listing]:
|
||||
for listing in listings_db:
|
||||
if listing.uuid == listing_uuid:
|
||||
return listing
|
||||
return None
|
||||
|
||||
# --- Роуты API ---
|
||||
@app.get("/", response_model=List[Listing])
|
||||
def get_listings():
|
||||
"""
|
||||
Get a list of all listings.
|
||||
Note: This does not include user details.
|
||||
"""
|
||||
return listings_db
|
||||
|
||||
# Исправленная функция: теперь она async
|
||||
@app.get("/{listing_uuid}", response_model=ListingDetail)
|
||||
async def get_listing(listing_uuid: str): # <-- Добавлен async
|
||||
"""
|
||||
Get a listing by its UUID.
|
||||
This call performs a synchronous request to the User Service to fetch user details.
|
||||
The 'user_info' field will contain user details if the user exists, otherwise it will be null.
|
||||
"""
|
||||
listing = get_listing_by_uuid(listing_uuid)
|
||||
if not listing:
|
||||
raise HTTPException(status_code=404, detail="Listing not found")
|
||||
|
||||
# Синхронный вызов user-service для получения информации о пользователе
|
||||
user_info = None
|
||||
try:
|
||||
# Используем имя сервиса из docker-compose.yml
|
||||
# Создаём асинхронный клиент и используем его внутри async функции
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://user-service:8001/{listing.user_uuid}") # <-- await
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
user_info = UserInfo(**user_data) # Создаем объект из JSON
|
||||
else:
|
||||
# Логика обработки ошибки, если пользователь не найден или сервис недоступен
|
||||
print(f"Warning: Could not fetch user info for UUID {listing.user_uuid}. Status: {response.status_code}")
|
||||
except httpx.RequestError as exc:
|
||||
# Логика обработки ошибки соединения
|
||||
print(f"Error fetching user info: {exc}")
|
||||
|
||||
# Возвращаем детали объявления, включая информацию о пользователе
|
||||
return ListingDetail(
|
||||
uuid=listing.uuid,
|
||||
make=listing.make,
|
||||
model=listing.model,
|
||||
year=listing.year,
|
||||
price=listing.price,
|
||||
user_uuid=listing.user_uuid,
|
||||
created_at=listing.created_at,
|
||||
user_info=user_info
|
||||
)
|
||||
|
||||
|
||||
@app.post("/", response_model=Listing)
|
||||
def create_listing(listing_data: ListingCreate): # <-- Исправлено: listing_data: ListingCreate
|
||||
"""
|
||||
Create a new listing.
|
||||
"""
|
||||
listing_uuid = str(uuid.uuid4())
|
||||
created_at = datetime.now()
|
||||
listing = Listing(
|
||||
uuid=listing_uuid,
|
||||
make=listing_data.make, # <-- Исправлено: listing_data.make
|
||||
model=listing_data.model,
|
||||
year=listing_data.year,
|
||||
price=listing_data.price,
|
||||
user_uuid=listing_data.user_uuid,
|
||||
created_at=created_at
|
||||
)
|
||||
listings_db.append(listing)
|
||||
return listing
|
||||
|
||||
@app.put("/{listing_uuid}", response_model=Listing)
|
||||
def update_listing(listing_uuid: str, listing_data: ListingUpdate): # <-- Исправлено: listing_data: ListingUpdate
|
||||
"""
|
||||
Update an existing listing by its UUID.
|
||||
"""
|
||||
listing = get_listing_by_uuid(listing_uuid)
|
||||
if not listing:
|
||||
raise HTTPException(status_code=404, detail="Listing not found")
|
||||
# Обновляем атрибуты
|
||||
listing.make = listing_data.make # <-- Исправлено: listing_data.make
|
||||
listing.model = listing_data.model
|
||||
listing.year = listing_data.year
|
||||
listing.price = listing_data.price
|
||||
listing.user_uuid = listing_data.user_uuid
|
||||
# created_at остается неизменным
|
||||
return listing
|
||||
|
||||
@app.delete("/{listing_uuid}")
|
||||
def delete_listing(listing_uuid: str):
|
||||
"""
|
||||
Delete a listing by its UUID.
|
||||
"""
|
||||
global listings_db
|
||||
initial_len = len(listings_db)
|
||||
listings_db = [l for l in listings_db if l.uuid != listing_uuid]
|
||||
if len(listings_db) == initial_len:
|
||||
raise HTTPException(status_code=404, detail="Listing not found")
|
||||
return {"message": "Listing deleted successfully"}
|
||||
|
||||
# Точка входа для запуска с помощью uvicorn (если запускается напрямую)
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
3
blokhin_dmitriy_lab_3/listing-service/requirements.txt
Normal file
3
blokhin_dmitriy_lab_3/listing-service/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn[standard]==0.32.0
|
||||
httpx==0.27.2
|
||||
41
blokhin_dmitriy_lab_3/nginx.conf
Normal file
41
blokhin_dmitriy_lab_3/nginx.conf
Normal file
@@ -0,0 +1,41 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Указываем upstream для каждого микросервиса
|
||||
upstream user_service {
|
||||
server user-service:8001; # Имя сервиса из docker-compose.yml
|
||||
}
|
||||
|
||||
upstream listing_service {
|
||||
server listing-service:8002; # Имя сервиса из docker-compose.yml
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80; # Порт, на котором слушает nginx
|
||||
|
||||
# Маршрутизация запросов к user-service
|
||||
location /users {
|
||||
proxy_pass http://user_service; # Передаем запрос на upstream
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Маршрутизация запросов к listing-service
|
||||
location /listings {
|
||||
proxy_pass http://listing_service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Дополнительно: можно настроить корень для отладки
|
||||
location / {
|
||||
return 404; # Или перенаправить на документацию
|
||||
}
|
||||
}
|
||||
}
|
||||
10
blokhin_dmitriy_lab_3/user-service/Dockerfile
Normal file
10
blokhin_dmitriy_lab_3/user-service/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
102
blokhin_dmitriy_lab_3/user-service/app.py
Normal file
102
blokhin_dmitriy_lab_3/user-service/app.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Инициализируем FastAPI приложение с информацией о документации
|
||||
app = FastAPI(
|
||||
title="User Service API",
|
||||
description="API for managing users (sellers)",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# --- Модели Pydantic ---
|
||||
class UserBase(BaseModel):
|
||||
name: str
|
||||
contact_info: str
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
|
||||
class UserUpdate(UserBase):
|
||||
pass
|
||||
|
||||
class User(UserBase):
|
||||
uuid: str
|
||||
registration_date: datetime
|
||||
|
||||
# --- Хранилище данных (в памяти) ---
|
||||
users_db: List[User] = []
|
||||
|
||||
# --- CRUD операции ---
|
||||
def get_user_by_uuid(user_uuid: str) -> Optional[User]:
|
||||
for user in users_db:
|
||||
if user.uuid == user_uuid:
|
||||
return user
|
||||
return None
|
||||
|
||||
# --- Роуты API ---
|
||||
@app.get("/", response_model=List[User])
|
||||
def get_users():
|
||||
"""
|
||||
Get a list of all users.
|
||||
"""
|
||||
return users_db
|
||||
|
||||
@app.get("/{user_uuid}", response_model=User)
|
||||
def get_user(user_uuid: str):
|
||||
"""
|
||||
Get a user by its UUID.
|
||||
"""
|
||||
user = get_user_by_uuid(user_uuid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
@app.post("/", response_model=User)
|
||||
def create_user(user_data: UserCreate):
|
||||
"""
|
||||
Create a new user.
|
||||
"""
|
||||
user_uuid = str(uuid.uuid4())
|
||||
registration_date = datetime.now()
|
||||
user = User(
|
||||
uuid=user_uuid,
|
||||
name=user_data.name,
|
||||
contact_info=user_data.contact_info,
|
||||
registration_date=registration_date
|
||||
)
|
||||
users_db.append(user)
|
||||
return user
|
||||
|
||||
@app.put("/{user_uuid}", response_model=User)
|
||||
def update_user(user_uuid: str, user_data: UserUpdate):
|
||||
"""
|
||||
Update an existing user by its UUID.
|
||||
"""
|
||||
user = get_user_by_uuid(user_uuid)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
# Обновляем атрибуты
|
||||
user.name = user_data.name
|
||||
user.contact_info = user_data.contact_info
|
||||
# registration_date остается неизменным
|
||||
return user
|
||||
|
||||
@app.delete("/{user_uuid}")
|
||||
def delete_user(user_uuid: str):
|
||||
"""
|
||||
Delete a user by its UUID.
|
||||
"""
|
||||
global users_db
|
||||
initial_len = len(users_db)
|
||||
users_db = [u for u in users_db if u.uuid != user_uuid]
|
||||
if len(users_db) == initial_len:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
# Точка входа для запуска с помощью uvicorn (если запускается напрямую)
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
2
blokhin_dmitriy_lab_3/user-service/requirements.txt
Normal file
2
blokhin_dmitriy_lab_3/user-service/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
fastapi
|
||||
uvicorn[standard]==0.32.0
|
||||
Reference in New Issue
Block a user