diff --git a/romanova_adelina_lab_3/README.md b/romanova_adelina_lab_3/README.md new file mode 100644 index 0000000..cc971f5 --- /dev/null +++ b/romanova_adelina_lab_3/README.md @@ -0,0 +1,283 @@ +# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами + +## Задание + +#### Цель: + +Изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API. + +#### Задачи: + +* Создать 2 микросервиса, реализующих CRUD на связанных сущностях. +* Реализовать механизм синхронного обмена сообщениями между микросервисами. +* Реализовать шлюз на основе прозрачного прокси-сервера nginx. + +### Сервисы: + 1. ```product_service``` - сервис, отвечающий за продукты + + 2. ```service_service``` - сервис, отвечающий за услуги, которые содержат в себе продукты + +#### Связь между сервисами: + ```service (single) <- product (many)``` + +## Как запустить программу: +``` +docker compose up +``` + +## Файловая иерархия + +``` +Лаба 3/ +|-- service_service/ +| |-- Dockerfile +| |-- requirements.txt +| |-- service_service.py +|-- product_service/ +| |-- Dockerfile +| |-- requirements.txt +| |-- product_service.py +|-- nginx/ +| |-- Dockerfile +| |-- nginx.conf +|-- docker-compose.yml +``` + +## Описание работы: + +Для разработки приложений был выбран язык программирования - ```python``` + +#### Синхронный обмен сообщениями +`product_service` будет отправлять http-запрос на `service_service` при определенных crud операциях. + +#### Операции CRUD для услуги + +Просмотр списка имеющихся услуг: +``` +@app.route('/all', methods=['GET']) +def show_service_ids(): + return service_ids +``` + +Добавление услуги: +``` +@app.route('/add/', methods=['POST']) +def create_service_id(name): + if request.method == 'POST': + if len(service_ids) == 0: + id = 1 + else: + id = service_ids[-1]['id'] + 1 + service_id = {'id': id, 'name': name} + service_ids.append(service_id) + return jsonify(service_id) +``` + +Поиск услуги по ID: +``` +@app.route('/get/', methods=['GET']) +def get_service_id(id): + found_service = next((service_id for service_id in service_ids if service_id['id'] == id), None) + if found_service: + return jsonify(found_service) + else: + return jsonify({'error': 'service_id not found'}) +``` + +Обновление услуги: +``` +@app.route('/upd/_', methods=['PUT']) +def update_service_id(id, name): + service = next((service_id for service_id in service_ids if service_id['id'] == id), None) + if service: + service['name'] = name + return jsonify(service) + else: + return jsonify({'error': 'service_id not found'}) + +``` + +Удаление услуги: +``` +@app.route('/del/', methods=['DELETE']) +def delete_service_id(id): + global service_ids + service_ids = [service_id for service_id in service_ids if service_id['id'] != id] + return jsonify({'result': True}) + +if __name__ == '__main__': + app.run(port=5001) + +``` + +#### Операции CRUD для продукта + +Просмотр списка имеющихся продуктов: +``` +@app.route('/all', methods=['GET']) +def show_products(): + return products +``` + +Добавление продукта: +``` +@app.route('/add/_', methods=['POST']) +def create_product(service_id, product): + product = {'id': len(products) + 1, + 'service_id': service_id, + 'product': product, + 'action': 'create'} + products.append(product) + requests.post("http://service-service:5001/event", json=product) + + return jsonify(product) +``` + +Поиск продукта по ID: +``` +@app.route('/get/', methods=['GET']) +def get_product(product_id): + found_product = next((product for product in products if product['id'] == product_id), None) + if found_product: + read_product = {'id': found_product['id'], + 'service_id': found_product['service_id'], + 'product': found_product['product']} + return jsonify(read_product) + else: + return jsonify({'error': 'product not found'}) +``` + +Обновление продукта: +``` +@app.route('/upd//', methods=['PUT']) +def update_product(product_id, new_product): + product = next((prod for prod in products if prod['id'] == product_id), None) + if product: + product['product'] = new_product + product['action'] = 'update' + requests.post("http://service-service:5001/event", json=product) + return jsonify({'message': 'Product updated successfully'}) + else: + return jsonify({'error': 'Product not found'}) + +``` + +Удаление продукта: +``` +@app.route('/product/del_', methods=['DELETE']) +def delete_product(product_id): + global products + deleted_product = next((product for product in products if product['id'] == product_id), None) + products = [product for product in products if product['id'] != product_id] + deleted_product['action'] = 'delete' + + requests.post("http://service-service:5001/event", json=deleted_product) + + return jsonify({'result': True}) + +``` + +### Docker-compose + + +Конфигурационный файл ```docker-сompose``` представляет собой многоконтейнерное приложение с тремя сервисами: ```service-service```, ```product-service``` и ```nginx```. Эти сервисы подключены к собственной сети типа "```bridge```" с именем ```my_network``` + +``` +version: '3.8' + +services: + service-service: + build: + context: ./service_service + ports: + - "5001:5001" + networks: + - my_network + restart: always + + product-service: + build: + context: ./product_service + ports: + - "5002:5002" + networks: + - my_network + restart: always + + nginx: + image: nginx:latest + ports: + - "80:80" + depends_on: + - service-service + - product-service + networks: + - my_network + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + restart: always + +networks: + my_network: + driver: bridge +``` + +### Nginx + + +Этот файл представляет собой конфигурацию для ```Nginx```, который является веб-сервером и обратным прокси: + +``` +events { + worker_connections 1024; +} + +http { + server { + listen 80; + listen [::]:80; + server_name localhost; + + location /service-service/ { + proxy_pass http://service-service:5001/; + 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 /product-service/ { + proxy_pass http://product-service:5002/; + 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; + } + } +} + +``` +### Dockerfile микросервисов + +Dockerfile для услуг +``` +FROM python:3.11 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . /app +CMD ["gunicorn", "--bind", "0.0.0.0:5001", "service_service:app"] +``` + +Dockerfile для продуктов +``` +FROM python:3.11 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . /app +CMD ["gunicorn", "--bind", "0.0.0.0:5002", "product_service:app"] +``` + +# Youtube +https://www.youtube.com/watch?v=Sf0CkkcuveY \ No newline at end of file diff --git a/romanova_adelina_lab_3/docker-compose.yaml b/romanova_adelina_lab_3/docker-compose.yaml new file mode 100644 index 0000000..7dbcc78 --- /dev/null +++ b/romanova_adelina_lab_3/docker-compose.yaml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + service-service: + build: + context: ./service_service + ports: + - "5001:5001" + networks: + - my_network + restart: always + + product-service: + build: + context: ./product_service + ports: + - "5002:5002" + networks: + - my_network + restart: always + + nginx: + image: nginx:latest + ports: + - "80:80" + depends_on: + - service-service + - product-service + networks: + - my_network + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + restart: always + +networks: + my_network: + driver: bridge \ No newline at end of file diff --git a/romanova_adelina_lab_3/nginx/Dockerfile b/romanova_adelina_lab_3/nginx/Dockerfile new file mode 100644 index 0000000..1434a93 --- /dev/null +++ b/romanova_adelina_lab_3/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx +COPY nginx.conf /etc/nginx/nginx.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/romanova_adelina_lab_3/nginx/nginx.conf b/romanova_adelina_lab_3/nginx/nginx.conf new file mode 100644 index 0000000..95f432b --- /dev/null +++ b/romanova_adelina_lab_3/nginx/nginx.conf @@ -0,0 +1,27 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + listen [::]:80; + server_name localhost; + + location /service-service/ { + proxy_pass http://service-service:5001/; + 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 /product-service/ { + proxy_pass http://product-service:5002/; + 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/romanova_adelina_lab_3/product_service/Dockerfile b/romanova_adelina_lab_3/product_service/Dockerfile new file mode 100644 index 0000000..397e88f --- /dev/null +++ b/romanova_adelina_lab_3/product_service/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . /app +CMD ["gunicorn", "--bind", "0.0.0.0:5002", "product_service:app"] \ No newline at end of file diff --git a/romanova_adelina_lab_3/product_service/product_service.py b/romanova_adelina_lab_3/product_service/product_service.py new file mode 100644 index 0000000..8b77316 --- /dev/null +++ b/romanova_adelina_lab_3/product_service/product_service.py @@ -0,0 +1,60 @@ +# product_service.py +from flask import Flask, request, jsonify +import requests + +app = Flask(__name__) + +products = [] + +@app.route('/all', methods=['GET']) +def show_products(): + return products + +@app.route('/add/_', methods=['POST']) +def create_product(service_id, product): + product = {'id': len(products) + 1, + 'service_id': service_id, + 'product': product, + 'action': 'create'} + products.append(product) + requests.post("http://service-service:5001/event", json=product) + + return jsonify(product) + +@app.route('/upd//', methods=['PUT']) +def update_product(product_id, new_product): + product = next((prod for prod in products if prod['id'] == product_id), None) + if product: + product['product'] = new_product + product['action'] = 'update' + requests.post("http://service-service:5001/event", json=product) + return jsonify({'message': 'Product updated successfully'}) + else: + return jsonify({'error': 'Product not found'}) + +@app.route('/get/', methods=['GET']) +def get_product(product_id): + found_product = next((product for product in products if product['id'] == product_id), None) + if found_product: + read_product = {'id': found_product['id'], + 'service_id': found_product['service_id'], + 'product': found_product['product']} + return jsonify(read_product) + else: + return jsonify({'error': 'product not found'}) + +@app.route('/product/del_', methods=['DELETE']) +def delete_product(product_id): + global products + deleted_product = next((product for product in products if product['id'] == product_id), None) + products = [product for product in products if product['id'] != product_id] + deleted_product['action'] = 'delete' + + requests.post("http://service-service:5001/event", json=deleted_product) + + return jsonify({'result': True}) + + + +if __name__ == '__main__': + app.run(port=5002) diff --git a/romanova_adelina_lab_3/product_service/requirements.txt b/romanova_adelina_lab_3/product_service/requirements.txt new file mode 100644 index 0000000..3d70f32 --- /dev/null +++ b/romanova_adelina_lab_3/product_service/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +requests==2.31.0 +gunicorn==21.2.0 \ No newline at end of file diff --git a/romanova_adelina_lab_3/service_service/Dockerfile b/romanova_adelina_lab_3/service_service/Dockerfile new file mode 100644 index 0000000..a7fdbdd --- /dev/null +++ b/romanova_adelina_lab_3/service_service/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . /app +CMD ["gunicorn", "--bind", "0.0.0.0:5001", "service_service:app"] \ No newline at end of file diff --git a/romanova_adelina_lab_3/service_service/requirements.txt b/romanova_adelina_lab_3/service_service/requirements.txt new file mode 100644 index 0000000..3d70f32 --- /dev/null +++ b/romanova_adelina_lab_3/service_service/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +requests==2.31.0 +gunicorn==21.2.0 \ No newline at end of file diff --git a/romanova_adelina_lab_3/service_service/service_service.py b/romanova_adelina_lab_3/service_service/service_service.py new file mode 100644 index 0000000..4d90a23 --- /dev/null +++ b/romanova_adelina_lab_3/service_service/service_service.py @@ -0,0 +1,87 @@ +# service_id_service.py +import json +import time +from flask import Flask, request, jsonify + +app = Flask(__name__) +service_ids = [] + +@app.route('/event', methods=['POST']) +def event(): + data = request.get_json() + print(data) + input_service_id = data.get('service_id') + + if data['action'] == 'create': + service = next((service_id for service_id in service_ids if service_id['id'] == input_service_id), None) + if service: + product_id = len(service.get('products', [])) + 1 + product = {'id': product_id, 'product': data['product']} + service.setdefault('products', []).append(product) + print(f"Product created for service_id {input_service_id}: {product}") + else: + print(f"service_id not found for product creation: {input_service_id}") + + elif data['action'] == 'update': + service = next((service_id for service_id in service_ids if service_id['id'] == input_service_id), None) + if service: + product_id = data.get('id') + product = next((product for product in service['products'] if product['id'] == product_id), None) + if product: + product['product'] = data['product'] + print(f"Product updated for service_id {input_service_id}: {product}") + else: + print(f"Product not found for update: {product_id}") + else: + print(f"service_id not found for product update: {input_service_id}") + + elif data['action'] == 'delete': + service = next((service_id for service_id in service_ids if service_id['id'] == input_service_id), None) + if service: + product_id = service.get('id') + service['products'] = [product for product in service.get('products', []) if product['id'] != product_id] + print(f"Product deleted for service_id {input_service_id}: {product_id}") + else: + print(f"service_id not found for product deletion: {input_service_id}") + return jsonify({'result': True}) + +@app.route('/all', methods=['GET']) +def show_service_ids(): + return service_ids + +@app.route('/add/', methods=['POST']) +def create_service_id(name): + if request.method == 'POST': + if len(service_ids) == 0: + id = 1 + else: + id = service_ids[-1]['id'] + 1 + service_id = {'id': id, 'name': name} + service_ids.append(service_id) + return jsonify(service_id) + +@app.route('/get/', methods=['GET']) +def get_service_id(id): + found_service = next((service_id for service_id in service_ids if service_id['id'] == id), None) + if found_service: + return jsonify(found_service) + else: + return jsonify({'error': 'service_id not found'}) + +@app.route('/upd/_', methods=['PUT']) +def update_service_id(id, name): + service = next((service_id for service_id in service_ids if service_id['id'] == id), None) + if service: + service['name'] = name + return jsonify(service) + else: + return jsonify({'error': 'service_id not found'}) + +@app.route('/del/', methods=['DELETE']) +def delete_service_id(id): + global service_ids + service_ids = [service_id for service_id in service_ids if service_id['id'] != id] + return jsonify({'result': True}) + +if __name__ == '__main__': + app.run(port=5001)