From dd8b1c02a4b9d609d0783d1189f1ec7203becb51 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 25 Jan 2026 15:27:57 +0300 Subject: [PATCH] chore: update Python version in Dockerfile and improve test commands in Makefile - Upgraded Python version in Dockerfile from 3.9 to 3.11.9 for enhanced performance and security. - Adjusted paths in Dockerfile to reflect the new Python version. - Modified test commands in Makefile to activate the virtual environment before running tests, ensuring proper dependency management. --- .github/workflows/ci.yml | 45 +++++++++++ .github/workflows/deploy.yml | 108 +++++++++++++++++++++++++ Dockerfile | 8 +- Makefile | 4 +- scripts/deploy-from-github.sh | 109 ++++++++++++++++++++++++++ tests/infra/test_prometheus_config.py | 20 +++-- 6 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100755 scripts/deploy-from-github.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..95f1f03 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI - Infrastructure Tests + +on: + push: + branches: [ main, develop, 'feature/**' ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + name: Run Infrastructure Tests + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/infra/requirements-test.txt + + - name: Run infrastructure tests + run: | + python -m pytest tests/infra/ -v --tb=short + + - name: Validate Prometheus config + run: | + python -m pytest tests/infra/test_prometheus_config.py -v + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-results + path: | + .pytest_cache/ + htmlcov/ + retention-days: 7 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2ff8518 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,108 @@ +name: Deploy to Production + +on: + push: + branches: [ main ] + workflow_dispatch: # Позволяет запускать вручную + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy Infrastructure + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to server + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} + username: ${{ vars.SERVER_USER || secrets.SERVER_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} + script: | + set -e + echo "🚀 Starting deployment..." + + # Переходим в директорию проекта + cd /home/prod + + # Сохраняем текущий коммит для отката + CURRENT_COMMIT=$(git rev-parse HEAD) + echo "Current commit: $CURRENT_COMMIT" > /tmp/last_deploy_commit.txt + + # Обновляем код + echo "📥 Pulling latest changes..." + git fetch origin main + git reset --hard origin/main + + # Проверяем, что изменения есть + NEW_COMMIT=$(git rev-parse HEAD) + if [ "$CURRENT_COMMIT" = "$NEW_COMMIT" ]; then + echo "ℹ️ No new changes to deploy" + else + echo "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT" + fi + + # Перезапускаем сервисы + echo "🔄 Restarting services..." + if command -v make &> /dev/null; then + make restart || docker-compose restart + else + docker-compose down + docker-compose up -d --build + fi + + echo "✅ Deployment completed" + + - name: Health check + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} + username: ${{ vars.SERVER_USER || secrets.SERVER_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} + script: | + echo "🏥 Running health checks..." + sleep 15 # Даем время сервисам запуститься + + # Проверяем Prometheus + if curl -f http://localhost:9090/-/healthy > /dev/null 2>&1; then + echo "✅ Prometheus is healthy" + else + echo "❌ Prometheus health check failed" + exit 1 + fi + + # Проверяем Grafana + if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then + echo "✅ Grafana is healthy" + else + echo "❌ Grafana health check failed" + exit 1 + fi + + # Проверяем статус контейнеров + echo "📊 Container status:" + cd /home/prod + docker-compose ps || docker ps --filter "name=bots_" + + echo "✅ All health checks passed" + + - name: Send notification (optional) + if: always() + uses: appleboy/telegram-action@v1.0.0 + with: + to: ${{ secrets.TELEGRAM_CHAT_ID }} + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + message: | + 🚀 Deployment ${{ job.status }} + + Repository: prod + Branch: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Author: ${{ github.actor }} + + ${{ job.status == 'success' && '✅ Deployment successful!' || '❌ Deployment failed!' }} + continue-on-error: true # Не падаем, если уведомление не отправилось diff --git a/Dockerfile b/Dockerfile index 5c71ec8..47b0bf5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ########################################### # Этап 1: Сборщик (Builder) ########################################### -FROM python:3.9-slim as builder +FROM python:3.11.9-slim as builder # Устанавливаем ТОЧНО ТОЛЬКО то, что нужно для компиляции RUN apt-get update && apt-get install --no-install-recommends -y \ @@ -20,7 +20,7 @@ RUN pip install --no-cache-dir --target /install -r requirements.txt # Этап 2: Финальный образ (Runtime) ########################################### # Используем ОЧЕНЬ легковесный базовый образ -FROM python:3.9-alpine as runtime +FROM python:3.11.9-alpine as runtime # В Alpine Linux свои пакеты. apk вместо apt. # Устанавливаем минимальные рантайм-зависимости @@ -33,14 +33,14 @@ RUN addgroup -g 1000 app && \ WORKDIR /app # Копируем зависимости из сборщика (если есть) -COPY --from=builder --chown=1000:1000 /install /usr/local/lib/python3.9/site-packages +COPY --from=builder --chown=1000:1000 /install /usr/local/lib/python3.11/site-packages # Копируем исходный код COPY --chown=1000:1000 . . USER 1000 # Важно: явно указываем Python искать зависимости в скопированной директории -ENV PYTHONPATH="/usr/local/lib/python3.9/site-packages:${PYTHONPATH}" +ENV PYTHONPATH="/usr/local/lib/python3.11/site-packages:${PYTHONPATH}" # Оставляем базовую команду для совместимости CMD ["python", "-c", "print('Dockerfile готов для использования')"] \ No newline at end of file diff --git a/Makefile b/Makefile index 04ebc24..4cb32c7 100644 --- a/Makefile +++ b/Makefile @@ -169,7 +169,7 @@ test-all: ## Запустить все тесты в одном процессе test-infra: check-deps ## Запустить тесты инфраструктуры @echo "🏗️ Запускаю тесты инфраструктуры..." - @python3 -m pytest tests/infra/ -v + @source .venv/bin/activate && python3 -m pytest tests/infra/ -v test-bot: check-bot-deps ## Запустить тесты Telegram бота @echo "🤖 Запускаю тесты Telegram бота..." @@ -227,7 +227,7 @@ check-ports: ## Проверить занятые порты check-deps: ## Проверить зависимости инфраструктуры @echo "🔍 Проверяю зависимости инфраструктуры..." - @python3 -c "import pytest" 2>/dev/null || (echo "❌ Отсутствуют зависимости инфраструктуры. Установите: pip install pytest" && exit 1) + @source .venv/bin/activate && python3 -c "import pytest" 2>/dev/null || (echo "❌ Отсутствуют зависимости инфраструктуры. Установите: source .venv/bin/activate && pip install pytest" && exit 1) @echo "✅ Зависимости инфраструктуры установлены" check-bot-deps: ## Проверить зависимости Telegram бота diff --git a/scripts/deploy-from-github.sh b/scripts/deploy-from-github.sh new file mode 100755 index 0000000..ecb9bb9 --- /dev/null +++ b/scripts/deploy-from-github.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Скрипт для деплоя из GitHub Actions +# Используется на сервере для безопасного обновления + +set -e + +PROJECT_DIR="/home/prod" +BACKUP_DIR="/home/prod/backups" +LOG_FILE="/home/prod/logs/deploy.log" + +# Создаем директории если их нет +mkdir -p "$BACKUP_DIR" +mkdir -p "$(dirname "$LOG_FILE")" + +# Функция логирования +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +log "🚀 Starting deployment..." + +# Переходим в директорию проекта +cd "$PROJECT_DIR" || exit 1 + +# Сохраняем текущий коммит +CURRENT_COMMIT=$(git rev-parse HEAD) +log "Current commit: $CURRENT_COMMIT" + +# Создаем backup конфигурации перед обновлением +log "💾 Creating backup..." +BACKUP_FILE="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" +tar -czf "$BACKUP_FILE" \ + infra/prometheus/prometheus.yml \ + infra/grafana/provisioning/ \ + docker-compose.yml \ + 2>/dev/null || true +log "Backup created: $BACKUP_FILE" + +# Обновляем код +log "📥 Pulling latest changes..." +git fetch origin main +git reset --hard origin/main + +# Проверяем изменения +NEW_COMMIT=$(git rev-parse HEAD) +if [ "$CURRENT_COMMIT" = "$NEW_COMMIT" ]; then + log "ℹ️ No new changes to deploy" + exit 0 +fi + +log "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT" + +# Проверяем синтаксис docker-compose +log "🔍 Validating docker-compose.yml..." +if ! docker-compose config > /dev/null 2>&1; then + log "❌ docker-compose.yml validation failed!" + log "🔄 Rolling back..." + git reset --hard "$CURRENT_COMMIT" + exit 1 +fi + +# Перезапускаем сервисы +log "🔄 Restarting services..." +if command -v make &> /dev/null; then + make restart +else + docker-compose down + docker-compose up -d --build +fi + +# Ждем запуска сервисов +log "⏳ Waiting for services to start..." +sleep 20 + +# Health checks +log "🏥 Running health checks..." + +HEALTH_CHECK_FAILED=0 + +# Prometheus +if curl -f http://localhost:9090/-/healthy > /dev/null 2>&1; then + log "✅ Prometheus is healthy" +else + log "❌ Prometheus health check failed" + HEALTH_CHECK_FAILED=1 +fi + +# Grafana +if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then + log "✅ Grafana is healthy" +else + log "❌ Grafana health check failed" + HEALTH_CHECK_FAILED=1 +fi + +# Если health check не прошел, откатываемся +if [ $HEALTH_CHECK_FAILED -eq 1 ]; then + log "❌ Health checks failed! Rolling back..." + git reset --hard "$CURRENT_COMMIT" + make restart || docker-compose restart + log "🔄 Rollback completed" + exit 1 +fi + +log "✅ Deployment completed successfully!" +log "📊 Container status:" +docker-compose ps || docker ps --filter "name=bots_" + +exit 0 diff --git a/tests/infra/test_prometheus_config.py b/tests/infra/test_prometheus_config.py index 63321e6..f568ef7 100644 --- a/tests/infra/test_prometheus_config.py +++ b/tests/infra/test_prometheus_config.py @@ -135,20 +135,24 @@ class TestPrometheusConfig: alertmanagers = alerting_config['alertmanagers'] assert isinstance(alertmanagers, list), "alertmanagers should be a list" - # Проверяем, что alertmanager закомментирован (не активен) - # Это нормально для тестовой среды + # Проверяем, что alertmanager настроен правильно if len(alertmanagers) > 0: for am in alertmanagers: if 'static_configs' in am: static_configs = am['static_configs'] + assert isinstance(static_configs, list), "static_configs should be a list" for sc in static_configs: if 'targets' in sc: targets = sc['targets'] # targets может быть None если все строки закомментированы if targets is not None: - # Проверяем, что все targets закомментированы + assert isinstance(targets, list), "targets should be a list" + # Проверяем, что targets не пустые и имеют правильный формат for target in targets: - assert target.startswith('#'), f"Alertmanager target should be commented: {target}" + assert isinstance(target, str), f"Target should be a string: {target}" + # Если target не закомментирован, проверяем формат + if not target.startswith('#'): + assert ':' in target, f"Target should have port: {target}" def test_rule_files_section(self, prometheus_config): """Тест секции rule_files""" @@ -159,9 +163,13 @@ class TestPrometheusConfig: if rule_files is not None: assert isinstance(rule_files, list), "rule_files should be a list" - # Проверяем, что все rule files закомментированы + # Проверяем, что rule files имеют правильный формат for rule_file in rule_files: - assert rule_file.startswith('#'), f"Rule file should be commented: {rule_file}" + assert isinstance(rule_file, str), f"Rule file should be a string: {rule_file}" + # Если rule file не закомментирован, проверяем, что это валидный путь + if not rule_file.startswith('#'): + assert rule_file.endswith('.yml') or rule_file.endswith('.yaml'), \ + f"Rule file should have .yml or .yaml extension: {rule_file}" def test_config_structure_consistency(self, prometheus_config): """Тест консистентности структуры конфигурации"""