diff --git a/artamonova_tatyana_lab_3/docker-compose.yml b/artamonova_tatyana_lab_3/docker-compose.yml new file mode 100644 index 0000000..c6f1c55 --- /dev/null +++ b/artamonova_tatyana_lab_3/docker-compose.yml @@ -0,0 +1,15 @@ +services: + vacancies-service: + build: ./vacancy-service + ports: + - "5000:5000" + resumes-service: + build: ./resume-service + ports: + - "5001:5001" + gateway: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf diff --git a/artamonova_tatyana_lab_3/nginx.conf b/artamonova_tatyana_lab_3/nginx.conf new file mode 100644 index 0000000..798eb87 --- /dev/null +++ b/artamonova_tatyana_lab_3/nginx.conf @@ -0,0 +1,19 @@ +upstream vacancies { + server vacancies-service:5000; +} + +upstream resumes { + server resumes-service:5001; +} + +server { + listen 80; + + location /vacancies { + proxy_pass http://vacancies; + } + + location /resumes { + proxy_pass http://resumes; + } +} diff --git a/artamonova_tatyana_lab_3/readme.md b/artamonova_tatyana_lab_3/readme.md new file mode 100644 index 0000000..cc63d38 --- /dev/null +++ b/artamonova_tatyana_lab_3/readme.md @@ -0,0 +1,35 @@ +## Лабораторная работа №3 ПИбд-42 Артамоновой Татьяны + +### Цель: +* Реализовать два микросервиса, которые взаимодействуют друг с другом через синхронный обмен сообщениями (HTTP-запросы). Для доступа к микросервисам используется шлюз Nginx, реализованный с помощью Docker Compose. + +### Технологии: + +* Python: Язык программирования для реализации микросервисов. +* Flask: Фреймворк Python для создания веб-приложений, использован для создания REST API микросервисов. +* requests: Библиотека Python для отправки HTTP-запросов, использован для синхронного обмена сообщениями между микросервисами. +* flask_cors: Расширение Flask, которое позволяет микросервисам получать доступ к данным из других доменов. +* Docker: Технология контейнеризации для упаковки и запуска микросервисов. +* Docker Compose: Инструмент для определения и управления многоконтейнерными приложениями, использован для запуска микросервисов и шлюза Nginx. +* Nginx: Сетевой прокси-сервер, использован как шлюз для доступа к микросервисам. + +### Функциональность: + +#### Микросервис vacancies-service: +* Реализует CRUD операции для вакансий (GET, POST, PUT, DELETE). +* Сохраняет данные о вакансиях в памяти (в словаре vacancies). +* Получает информацию о резюме из микросервиса resumes-service через HTTP-запрос. +* Включает информацию о резюме в ответ JSON для вакансии. +#### Микросервис resumes-service: +* Реализует CRUD операции для резюме (GET, POST, PUT, DELETE). +* Сохраняет данные о резюме в памяти (в словаре resumes). +#### Шлюз Nginx: +* Перенаправляет HTTP-запросы на соответствующие микросервисы. +* Предоставляет единую точку входа для доступа к микросервисам. + +### Запуск программы: + +* Запуск команды docker-compose up -d + +### Ссылка на видео: +https://vk.com/artamonovat?z=video212084908_456239358%2Fvideos212084908%2Fpl_212084908_-2 \ No newline at end of file diff --git a/artamonova_tatyana_lab_3/resume-service/Dockerfile b/artamonova_tatyana_lab_3/resume-service/Dockerfile new file mode 100644 index 0000000..723d320 --- /dev/null +++ b/artamonova_tatyana_lab_3/resume-service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.9 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +CMD ["python", "resume.py"] diff --git a/artamonova_tatyana_lab_3/resume-service/requirements.txt b/artamonova_tatyana_lab_3/resume-service/requirements.txt new file mode 100644 index 0000000..0937db4 --- /dev/null +++ b/artamonova_tatyana_lab_3/resume-service/requirements.txt @@ -0,0 +1,3 @@ +flask +requests +flask_cors \ No newline at end of file diff --git a/artamonova_tatyana_lab_3/resume-service/resume.py b/artamonova_tatyana_lab_3/resume-service/resume.py new file mode 100644 index 0000000..060d83f --- /dev/null +++ b/artamonova_tatyana_lab_3/resume-service/resume.py @@ -0,0 +1,79 @@ +import uuid +import json +from flask import Flask, request, jsonify +from flask_cors import CORS + + +class Resume: + def __init__(self, uuid, full_name, skills, experience, phone, email): + self.uuid = uuid + self.full_name = full_name + self.skills = skills + self.experience = experience + self.phone = phone + self.email = email + + +app = Flask(__name__) +CORS(app) + +resumes = {} + + +@app.route("/resumes", methods=["GET"]) +def get_resumes(): + return jsonify([resume.__dict__ for resume in resumes.values()]) + + +@app.route("/resumes/", methods=["GET"]) +def get_resume(resume_uuid): + resume = resumes.get(resume_uuid) + if resume: + return jsonify(resume.__dict__) + else: + return jsonify({"error": "Resume not found"}), 404 + + +@app.route("/resumes", methods=["POST"]) +def create_resume(): + data = request.get_json() + resume_uuid = str(uuid.uuid4()) + resume = Resume( + resume_uuid, + data["full_name"], + data["skills"], + data["experience"], + data["phone"], + data["email"], + ) + resumes[resume_uuid] = resume + return jsonify(resume.__dict__), 201 + + +@app.route("/resumes/", methods=["PUT"]) +def update_resume(resume_uuid): + resume = resumes.get(resume_uuid) + if resume: + data = request.get_json() + resume.full_name = data.get("full_name", resume.full_name) + resume.skills = data.get("skills", resume.skills) + resume.experience = data.get("experience", resume.experience) + resume.phone = data.get("phone", resume.phone) + resume.email = data.get("email", resume.email) + return jsonify(resume.__dict__) + else: + return jsonify({"error": "Resume not found"}), 404 + + +@app.route("/resumes/", methods=["DELETE"]) +def delete_resume(resume_uuid): + resume = resumes.get(resume_uuid) + if resume: + del resumes[resume_uuid] + return "", 200 + else: + return jsonify({"error": "Resume not found"}), 404 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5001) diff --git a/artamonova_tatyana_lab_3/vacancy-service/Dockerfile b/artamonova_tatyana_lab_3/vacancy-service/Dockerfile new file mode 100644 index 0000000..839d1ce --- /dev/null +++ b/artamonova_tatyana_lab_3/vacancy-service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.9 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +CMD ["python", "vacancy.py"] diff --git a/artamonova_tatyana_lab_3/vacancy-service/requirements.txt b/artamonova_tatyana_lab_3/vacancy-service/requirements.txt new file mode 100644 index 0000000..0937db4 --- /dev/null +++ b/artamonova_tatyana_lab_3/vacancy-service/requirements.txt @@ -0,0 +1,3 @@ +flask +requests +flask_cors \ No newline at end of file diff --git a/artamonova_tatyana_lab_3/vacancy-service/vacancy.py b/artamonova_tatyana_lab_3/vacancy-service/vacancy.py new file mode 100644 index 0000000..2cfeece --- /dev/null +++ b/artamonova_tatyana_lab_3/vacancy-service/vacancy.py @@ -0,0 +1,124 @@ +import uuid +import json +from flask import Flask, request, jsonify +from flask_cors import CORS +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +class Vacancy: + def __init__(self, uuid, title, company, description, salary, resume_uuid): + self.uuid = uuid + self.title = title + self.company = company + self.description = description + self.salary = salary + self.resume_uuid = resume_uuid + self.resume_info = None + + def to_dict(self): + if self.resume_info: + return { + "uuid": self.uuid, + "title": self.title, + "company": self.company, + "description": self.description, + "salary": self.salary, + "resume_uuid": self.resume_uuid, + "resume_info": self.resume_info + } + else: + return { + "uuid": self.uuid, + "title": self.title, + "company": self.company, + "description": self.description, + "salary": self.salary, + "resume_uuid": self.resume_uuid + } + +app = Flask(__name__) +CORS(app) + +vacancies = {} + +@app.route("/vacancies", methods=["GET"]) +def get_vacancies(): + return jsonify([vacancy.to_dict() for vacancy in vacancies.values()]) + +@app.route("/vacancies/", methods=["GET"]) +def get_vacancy(vacancy_uuid): + vacancy = vacancies.get(vacancy_uuid) + if vacancy: + if not vacancy.resume_info: + vacancy.resume_info = get_resume_info(vacancy.resume_uuid) + return jsonify(vacancy.to_dict()) + else: + return jsonify({"error": "Vacancy not found"}), 404 + +@app.route("/vacancies", methods=["POST"]) +def create_vacancy(): + data = request.get_json() + vacancy_uuid = str(uuid.uuid4()) + vacancy = Vacancy( + vacancy_uuid, + data["title"], + data["company"], + data["description"], + data["salary"], + data["resume_uuid"], + ) + vacancies[vacancy_uuid] = vacancy + vacancy.resume_info = get_resume_info(vacancy.resume_uuid) + return jsonify(vacancy.to_dict()), 201 + +@app.route("/vacancies/", methods=["PUT"]) +def update_vacancy(vacancy_uuid): + vacancy = vacancies.get(vacancy_uuid) + if vacancy: + data = request.get_json() + vacancy.title = data.get("title", vacancy.title) + vacancy.company = data.get("company", vacancy.company) + vacancy.description = data.get("description", vacancy.description) + vacancy.salary = data.get("salary", vacancy.salary) + vacancy.resume_uuid = data.get("resume_uuid", vacancy.resume_uuid) + vacancy.resume_info = get_resume_info(vacancy.resume_uuid) + return jsonify(vacancy.to_dict()) + else: + return jsonify({"error": "Vacancy not found"}), 404 + +@app.route("/vacancies/", methods=["DELETE"]) +def delete_vacancy(vacancy_uuid): + vacancy = vacancies.get(vacancy_uuid) + if vacancy: + del vacancies[vacancy_uuid] + return "", 200 + else: + return jsonify({"error": "Vacancy not found"}), 404 + +def get_resume_info(resume_uuid): + url = f'http://resumes-service:5001/resumes/{resume_uuid}' + + # Настройка retry механизма + retries = Retry( + total=3, # Максимальное количество повторов + status_forcelist=[429, 500, 502, 503, 504], # Коды статуса, для которых нужно повторить запрос + backoff_factor=0.3, # Время ожидания перед повторной попыткой + ) + adapter = HTTPAdapter(max_retries=retries) + http = requests.Session() + http.mount("https://", adapter) + http.mount("http://", adapter) + + try: + response = http.get(url) + if response.status_code == 200: + return response.json() + else: + return None + except requests.exceptions.RequestException as e: + print(f"Ошибка при запросе к resumes-service: {e}") + return None + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5000)