From ee99ff035e1565bafacad973f6c1946fe4e95b67 Mon Sep 17 00:00:00 2001 From: allllen4a Date: Tue, 7 Oct 2025 11:50:31 +0400 Subject: [PATCH] zhirnova_alyona_lab_3 --- zhirnova_alyona_lab_3/Docker-compose.yaml | 27 ++++ zhirnova_alyona_lab_3/README.md | 70 ++++++++++ .../cartoon_service/Dockerfile | 11 ++ .../cartoon_service/cartoon_service.py | 94 ++++++++++++++ zhirnova_alyona_lab_3/nginx.conf | 49 +++++++ .../producer_service/Dockerfile | 11 ++ .../producer_service/producer_service.py | 122 ++++++++++++++++++ zhirnova_alyona_lab_3/requirements.txt | 3 + 8 files changed, 387 insertions(+) create mode 100644 zhirnova_alyona_lab_3/Docker-compose.yaml create mode 100644 zhirnova_alyona_lab_3/README.md create mode 100644 zhirnova_alyona_lab_3/cartoon_service/Dockerfile create mode 100644 zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py create mode 100644 zhirnova_alyona_lab_3/nginx.conf create mode 100644 zhirnova_alyona_lab_3/producer_service/Dockerfile create mode 100644 zhirnova_alyona_lab_3/producer_service/producer_service.py create mode 100644 zhirnova_alyona_lab_3/requirements.txt diff --git a/zhirnova_alyona_lab_3/Docker-compose.yaml b/zhirnova_alyona_lab_3/Docker-compose.yaml new file mode 100644 index 0000000..93f5af7 --- /dev/null +++ b/zhirnova_alyona_lab_3/Docker-compose.yaml @@ -0,0 +1,27 @@ +services: + + producer_service: + container_name: producer_service + build: + context: . + dockerfile: ./producer_service/Dockerfile + expose: + - 8080 + + cartoon_service: + container_name: cartoon_service + build: + context: . + dockerfile: ./cartoon_service/Dockerfile + expose: + - 8081 + + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - producer_service + - cartoon_service \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/README.md b/zhirnova_alyona_lab_3/README.md new file mode 100644 index 0000000..82205a4 --- /dev/null +++ b/zhirnova_alyona_lab_3/README.md @@ -0,0 +1,70 @@ +# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами + +## Цель +изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API. + +### Задачи +1. Создать 2 микросервиса, реализующих CRUD на связанных сущностях. +2. Реализовать механизм синхронного обмена сообщениями между микросервисами. +3. Реализовать шлюз на основе прозрачного прокси-сервера nginx. +4. Оформить отчёт в формате Markdown + +### Инструменты для выполнения: +1. Docker +2. Docker Compose +3. Python +4. Postman + +## Компоненты системы: +1. Cartoon Service (cartoon_service/) - сервис для управления мультфильмами +2. Producer Service (producer_service/) - сервис для управления режиссерами +3. Nginx - обратный прокси и балансировщик нагрузки +4. Docker Compose - оркестрация контейнеров + +## API Endpoints: +GET / - Получить все мультфильмы +GET /{cartoon_uuid} - Получить мультфильм по ID +GET /full - Получить все мультфильмы с информацией о продюсерах +GET /full/{cartoon_uuid} - Получить мультфильм с информацией о продюсере +GET /by-producer/{producer_uuid} - Получить мультфильмы по продюсеру +POST / - Создать новый мультфильм +PUT /{cartoon_uuid} - Обновить мультфильм +DELETE /{cartoon_uuid} - Удалить мультфильм + +GET / - Получить всех режиссеров +GET /{producer_uuid} - Получить режиссера по ID +GET /with-cartoons/{producer_uuid} - Получить режиссера с его мультфильмами +GET /check/{producer_uuid} - Проверить существование режиссера +POST / - Создать нового режиссера +PUT /{producer_uuid} - Обновить режиссера +DELETE /{producer_uuid} - Удалить режиссера + +Пример POST http://localhost/cartoon_service/: +```json +{ + "title": "Король Лев", + "year": 1994, + "producer_id": "997aa4c5-ebb2-4794-ba81-e742f9f1fa30" +} +``` + +## Особенности реализации: +1. Микросервисная архитектура - каждый сервис независим и выполняет одну бизнес-функцию +2. Взаимодействие через HTTP - сервисы общаются через REST API вызовы +3. Контейнеризация - все компоненты запускаются в Docker контейнерах +4. Автоматическая сборка - Docker Compose автоматически собирает и запускает все сервисы +5. Единая точка входа - Nginx обеспечивает единый endpoint для всех сервисов + +## Запуск: +1. На компьютере должны быть установлены Docker и Docker Compose +2. Скачайте папку с репозитория +3. Перейдите в директорию с yml +4. Запустите с помощью команды: +`docker-compose up --build` +5. Откройте Postman + +## Что делает данная лабораторная работа: +Проект представляет собой микросервисное приложение для управления базой данных мультфильмов и их режиссеров. Система состоит из двух независимых сервисов, взаимодействующих через REST API, и обратного прокси на базе Nginx для маршрутизации запросов. + +## Видео: +https://rutube.ru/video/private/22f82905c7e737e2863bd9726cedf3e5/?p=FkA762T6Z6K0TDyHZ5Yaeg \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/cartoon_service/Dockerfile b/zhirnova_alyona_lab_3/cartoon_service/Dockerfile new file mode 100644 index 0000000..6478faf --- /dev/null +++ b/zhirnova_alyona_lab_3/cartoon_service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY cartoon_service/cartoon_service.py . + +CMD ["python", "cartoon_service.py"] \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py b/zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py new file mode 100644 index 0000000..2370f1d --- /dev/null +++ b/zhirnova_alyona_lab_3/cartoon_service/cartoon_service.py @@ -0,0 +1,94 @@ +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel +from typing import List, Optional +from uuid import UUID, uuid4 +import requests +import uvicorn + + +app = FastAPI(root_path="/cartoon_service", title="Cartoon Service", description="Service to manage cartoons and their producers", version="1.0.0") + +producers_url = "http://producer_service:8080/" + +class Cartoon(BaseModel): + uuid: UUID + title: str + year: int + producer_id: UUID + +class CartoonCreate(BaseModel): + title: str + year: int + producer_id: UUID + +cartoons: List[Cartoon] = [ + Cartoon(uuid=UUID("89fa1e7a-7e88-445e-a4d8-6d4497ea8f19"), title="Большое путешествие", year=2006, producer_id=UUID("eb815350-c7b9-4446-8434-4c0640c21995")), + Cartoon(uuid=UUID("0351ee11-f11b-4d83-b2c8-1075b0c357dc"), title="Ходячий замок", year=2004, producer_id=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")), + Cartoon(uuid=UUID("dfc17619-7690-47aa-ae8e-6a5068f8ddec"), title="Горбун из Нотр-Дама", year=1996, producer_id=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")), +] + +@app.get("/", response_model=List[Cartoon], summary="Get all cartoons", description="Retrieve a list of all cartoons.") +def get_all_cartoons(): + return cartoons + +@app.get("/full", summary="Get all cartoons with producer info", description="Retrieve all cartoons with additional producer information.") +def get_all_cartoons_with_producers(): + producers = requests.get(producers_url).json() + response = [] + for cartoon in cartoons: + producer_info = next((p for p in producers if p["uuid"] == str(cartoon.producer_id)), None) + if producer_info: + response.append({**cartoon.dict(), "producer_info": producer_info}) + return response + +@app.get("/by-producer/{producer_uuid}", response_model=List[Cartoon], summary="Get cartoons by producer", description="Retrieve all cartoons for a given producer.") +def get_cartoons_by_producer(producer_uuid: UUID): + return [cartoon for cartoon in cartoons if cartoon.producer_id == producer_uuid] + +@app.get("/{cartoon_uuid}", response_model=Cartoon, summary="Get a cartoon by ID", description="Retrieve a single cartoon by its ID.") +def get_cartoon_by_id(cartoon_uuid: UUID): + cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None) + if not cartoon: + raise HTTPException(status_code=404, detail="Cartoon not found") + return cartoon + +@app.get("/full/{cartoon_uuid}", summary="Get cartoon with producer info", description="Retrieve a cartoon with additional producer information.") +def get_cartoon_with_producer_info(cartoon_uuid: UUID): + cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None) + if not cartoon: + raise HTTPException(status_code=404, detail="Cartoon not found") + + producer_info = requests.get(f"{producers_url}{cartoon.producer_id}").json() + return {**cartoon.dict(), "producer_info": producer_info} + +@app.post("/", response_model=Cartoon, summary="Create a new cartoon", description="Add a new cartoon to the database.") +def create_cartoon(cartoon: CartoonCreate): + # Check if producer exists + response = requests.get(f"{producers_url}check/{cartoon.producer_id}") + if response.status_code == 404: + raise HTTPException(status_code=404, detail="Producer not found") + + new_cartoon = Cartoon(uuid=uuid4(), **cartoon.dict()) + cartoons.append(new_cartoon) + return new_cartoon + +@app.put("/{cartoon_uuid}", response_model=Cartoon, summary="Update a cartoon", description="Update an existing cartoon by its ID.") +def update_cartoon(cartoon_uuid: UUID, cartoon_update: CartoonCreate): + cartoon = next((cartoon for cartoon in cartoons if cartoon.uuid == cartoon_uuid), None) + if not cartoon: + raise HTTPException(status_code=404, detail="Cartoon not found") + + cartoon.title = cartoon_update.title + cartoon.year = cartoon_update.year + cartoon.producer_id = cartoon_update.producer_id + return cartoon + +@app.delete("/{cartoon_uuid}", summary="Delete a cartoon", description="Remove a cartoon by its ID.") +def delete_cartoon(cartoon_uuid: UUID): + global cartoons + cartoons = [cartoon for cartoon in cartoons if cartoon.uuid != cartoon_uuid] + return {"detail": "Cartoon deleted successfully"} + + +if __name__ == "__main__": + uvicorn.run("cartoon_service:app", host="0.0.0.0", port=8081, reload=True) \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/nginx.conf b/zhirnova_alyona_lab_3/nginx.conf new file mode 100644 index 0000000..78e1bcb --- /dev/null +++ b/zhirnova_alyona_lab_3/nginx.conf @@ -0,0 +1,49 @@ +events { worker_connections 1024; } + +http { + server { + listen 80; + listen [::]:80; + server_name localhost; + + location /producer_service/ { + proxy_pass http://producer_service:8080/; + 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 /cartoon_service/ { + proxy_pass http://cartoon_service:8081/; + 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 /producer_service_service/docs { + proxy_pass http://producer_service:8080/docs; + } + + location /cartoon_service/docs { + proxy_pass http://cartoon_service:8081/docs; + } + + location /producer_service/openapi.json { + proxy_pass http://producer_service:8080/openapi.json; + 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 /cartoon_service/openapi.json { + proxy_pass http://cartoon_service:8081/openapi.json; + 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; + } + } +} \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/producer_service/Dockerfile b/zhirnova_alyona_lab_3/producer_service/Dockerfile new file mode 100644 index 0000000..4e037b8 --- /dev/null +++ b/zhirnova_alyona_lab_3/producer_service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY producer_service/producer_service.py . + +CMD ["python", "producer_service.py"] \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/producer_service/producer_service.py b/zhirnova_alyona_lab_3/producer_service/producer_service.py new file mode 100644 index 0000000..f2c3741 --- /dev/null +++ b/zhirnova_alyona_lab_3/producer_service/producer_service.py @@ -0,0 +1,122 @@ +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from uuid import UUID, uuid4 +import requests +from typing import List, Optional +import uvicorn + + +app = FastAPI(root_path="/producer_service", title="Producer API", description="API для управления продюсерами", version="1.0.0") + + +class Producer: + def __init__(self, name: str, surname: str, uuid_: Optional[UUID] = None): + self.uuid_ = uuid_ if uuid_ else uuid4() + self.name: str = name + self.surname: str = surname + + def to_dict(self): + return { + "uuid": str(self.uuid_), + "name": self.name, + "surname": self.surname + } + + def to_dict_with_cartoons(self, cartoons: list): + return { + "uuid": str(self.uuid_), + "name": self.name, + "surname": self.surname, + "cartoons": cartoons + } + + +class CreateProducerRequest(BaseModel): + name: str + surname: str + + +class UpdateProducerRequest(BaseModel): + name: Optional[str] + surname: Optional[str] + + +producers: List[Producer] = [ + Producer(name="Хаяо", surname="Миядзаки", uuid_=UUID("997aa4c5-ebb2-4794-ba81-e742f9f1fa30")), + Producer(name="Гари", surname="Труздейл", uuid_=UUID("694827e4-0f93-45a5-8f75-bad7ef2d21fe")), + Producer(name="Стив", surname="Уильямс", uuid_=UUID("eb815350-c7b9-4446-8434-4c0640c21995")) +] + +cartoons_url = "http://cartoon_service:8081/" + + +@app.get("/", response_model=List[dict]) +def get_all(): + """Получение списка всех продюсеров.""" + return [producer.to_dict() for producer in producers] + + +@app.get("/{uuid_}", response_model=dict) +def get_one(uuid_: UUID): + """Получение продюсера по идентификатору.""" + for producer in producers: + if producer.uuid_ == uuid_: + return producer.to_dict() + raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден") + + +@app.get("/with-cartoons/{uuid_}", response_model=dict) +def get_one_with_cartoons(uuid_: UUID): + """Получение продюсера со списком его мультфильмов.""" + for producer in producers: + if producer.uuid_ == uuid_: + response = requests.get(f"{cartoons_url}by-producer/{uuid_}") + if response.status_code != 200: + raise HTTPException(status_code=404, detail="Не удалось получить мультфильмы продюсера") + return producer.to_dict_with_cartoons(response.json()) + raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден") + + +@app.get("/check/{uuid_}", response_class=JSONResponse) +def check_exist(uuid_: UUID): + """Проверка наличия продюсера по идентификатору (для cartoon_service).""" + for producer in producers: + if producer.uuid_ == uuid_: + return JSONResponse(content="", status_code=200) + return JSONResponse(content="", status_code=404) + + +@app.post("/", response_model=dict) +def create(producer: CreateProducerRequest): + """Создание нового продюсера.""" + new_producer = Producer(name=producer.name, surname=producer.surname) + producers.append(new_producer) + return new_producer.to_dict() + + +@app.put("/{uuid_}", response_model=dict) +def update_by_id(uuid_: UUID, producer_update: UpdateProducerRequest): + """Изменение данных продюсера по идентификатору.""" + for producer in producers: + if producer.uuid_ == uuid_: + if producer_update.name is not None: + producer.name = producer_update.name + if producer_update.surname is not None: + producer.surname = producer_update.surname + return producer.to_dict() + raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден") + + +@app.delete("/{uuid_}", response_class=JSONResponse) +def delete(uuid_: UUID): + """Удаление продюсера по идентификатору.""" + for producer in producers: + if producer.uuid_ == uuid_: + producers.remove(producer) + return JSONResponse(content="Продюсер успешно удалён", status_code=200) + raise HTTPException(status_code=404, detail="Продюсер с таким uuid не был найден") + + +if __name__ == "__main__": + uvicorn.run("producer_service:app", host="0.0.0.0", port=8080, reload=True) \ No newline at end of file diff --git a/zhirnova_alyona_lab_3/requirements.txt b/zhirnova_alyona_lab_3/requirements.txt new file mode 100644 index 0000000..7bf9524 --- /dev/null +++ b/zhirnova_alyona_lab_3/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn +requests \ No newline at end of file