From df2f132c0556e60f771e6adc2dcad7e9a74654ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D0=B8=D1=81=20=D0=AE=D1=81=D1=83=D0=BF=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=B2?= Date: Sun, 8 Mar 2026 15:18:46 +0300 Subject: [PATCH] 009 add docker compose --- Dockerfile | 16 ++++++ docker-compose.yml | 26 +++++++++- infra/README.md | 42 ++++++++++++++++ spec/index.md | 9 ++-- spec/tasks/009_add_local_infra_stack.md | 2 +- .../010_migrate_to_yaml_only_configuration.md | 39 +++++++++++++++ tests/smoke/test_local_infra_stack.py | 50 +++++++++++++++++++ 7 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 Dockerfile create mode 100644 infra/README.md create mode 100644 spec/tasks/010_migrate_to_yaml_only_configuration.md create mode 100644 tests/smoke/test_local_infra_stack.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f111240 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.14-slim + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN pip install --no-cache-dir poetry + +COPY pyproject.toml poetry.lock ./ +RUN poetry config virtualenvs.create false \ + && poetry install --no-interaction --no-root + +COPY app ./app +COPY config.yaml ./config.yaml + +CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml index f5dbb9d..d17643a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,25 @@ services: + app: + image: yusupal1ev/g2s-aggregator:0.0.0 + environment: + G2S_CONFIG_FILE: /app/config.yaml + G2S_REPOSITORY__REDIS_DSN: redis://redis:6379/0 + G2S_OBSERVABILITY__OTLP_ENDPOINT: http://signoz:4317 + PYTHONUNBUFFERED: "1" + ports: + - "8000:8000" + depends_on: + - redis + - signoz + + redis: + image: redis:7-alpine + command: ["redis-server", "--save", "", "--appendonly", "no"] + ports: + - "6379:6379" + signoz: - image: signoz/signoz:0.82.0 + image: signoz/signoz:latest container_name: signoz environment: G2S_ALERTS__TELEGRAM_BOT_TOKEN: ${G2S_ALERTS__TELEGRAM_BOT_TOKEN:-} @@ -9,5 +28,10 @@ services: SIGNOZ_ALERTS_RULES_FILE: /etc/signoz/alerts/signoz_alert_rules.yaml ports: - "3301:3301" + - "4317:4317" volumes: + - signoz_data:/var/lib/signoz - ./infra/alerts:/etc/signoz/alerts:ro + +volumes: + signoz_data: diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..32826e9 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,42 @@ +# Local Infrastructure Stack + +## Services + +- `app`: FastAPI service (`uvicorn app.main:app`) built from root `Dockerfile` +- `redis`: price cache backend +- `signoz`: observability backend (UI + OTLP receiver) + +## Environment wiring + +### `app` service + +- `G2S_CONFIG_FILE=/app/config.yaml` +- `G2S_REPOSITORY__REDIS_DSN=redis://redis:6379/0` +- `G2S_OBSERVABILITY__OTLP_ENDPOINT=http://signoz:4317` +- `PYTHONUNBUFFERED=1` + +### `signoz` service + +- `G2S_ALERTS__TELEGRAM_BOT_TOKEN` (optional for Telegram alerts) +- `G2S_ALERTS__TELEGRAM_CHAT_ID` (optional for Telegram alerts) + +## Startup instructions + +1. Validate compose syntax: + - `docker compose config` +2. Start dependencies: + - `docker compose up -d redis signoz` +3. Verify running containers: + - `docker compose ps` +4. Start application: + - `docker compose up -d app` +5. Stop local stack: + - `docker compose down` + +## Smoke command sequence + +```bash +docker compose config +docker compose up -d redis signoz +docker compose ps +``` diff --git a/spec/index.md b/spec/index.md index ac7aa47..f2b1525 100644 --- a/spec/index.md +++ b/spec/index.md @@ -1,7 +1,7 @@ # Spec Tasks Index > ⚠️ This file is generated. Do not edit manually. -> Generated at (UTC): `2026-03-08T09:04:29+00:00` +> Generated at (UTC): `2026-03-08T12:18:20+00:00` ## Tasks @@ -16,10 +16,11 @@ | 006 | DONE | 2026-03-07 | Add delivery price controller endpoint | `spec/tasks/006_add_delivery_price_controller.md` | | 007 | DONE | 2026-03-07 | Add observability correlation and telemetry | `spec/tasks/007_add_observability_correlation.md` | | 008 | DONE | 2026-03-07 | Add SigNoz to Telegram alerting configuration | `spec/tasks/008_add_signoz_telegram_alerting.md` | -| 009 | TODO | 2026-03-07 | Add local infrastructure stack | `spec/tasks/009_add_local_infra_stack.md` | +| 009 | DONE | 2026-03-07 | Add local infrastructure stack | `spec/tasks/009_add_local_infra_stack.md` | +| 010 | TODO | 2026-03-08 | Migrate to YAML-only configuration loading | `spec/tasks/010_migrate_to_yaml_only_configuration.md` | ## Summary -- Total: **10** +- Total: **11** - TODO: **1** -- DONE: **9** +- DONE: **10** diff --git a/spec/tasks/009_add_local_infra_stack.md b/spec/tasks/009_add_local_infra_stack.md index 858fb28..75ccc73 100644 --- a/spec/tasks/009_add_local_infra_stack.md +++ b/spec/tasks/009_add_local_infra_stack.md @@ -1,7 +1,7 @@ --- id: 009 title: Add local infrastructure stack -status: TODO +status: DONE created: 2026-03-07 --- diff --git a/spec/tasks/010_migrate_to_yaml_only_configuration.md b/spec/tasks/010_migrate_to_yaml_only_configuration.md new file mode 100644 index 0000000..aa74a38 --- /dev/null +++ b/spec/tasks/010_migrate_to_yaml_only_configuration.md @@ -0,0 +1,39 @@ +--- +id: 010 +title: Migrate to YAML-only configuration loading +status: TODO +created: 2026-03-08 +--- + +## Context +В проекте используется смешанный подход конфигурации с переменными окружения. Требуется унифицировать источник конфигурации и оставить только `config.yaml`. + +## Goal +Убрать использование переменных окружения из runtime-конфигурации приложения и перевести загрузку всех параметров на чтение из `config.yaml`. + +## Constraints +- Соблюдать layered architecture из `AGENTS.md`. +- Scope задачи: только механизм загрузки конфигурации и места её потребления. +- Запрещено менять бизнес-правила и поведение use-case, не связанное с конфигурацией. +- Не изменять файлы в `spec/`. + +## Acceptance criteria +- Runtime-конфигурация приложения загружается только из `config.yaml`. +- Переменные окружения не используются для чтения параметров Controller, Service, Business Logic, Repository, Adapter, Observability и Alerts. +- При отсутствии обязательных полей в `config.yaml` приложение завершает запуск с детерминированной ошибкой валидации конфигурации. +- Тесты конфигурации больше не опираются на `monkeypatch` переменных окружения и проверяют чтение из YAML. + +## Definition of Done +- [ ] Удалено чтение переменных окружения из конфигурационного слоя. +- [ ] Все компонентные секции конфигурации продолжают читаться из `config.yaml`. +- [ ] Обновлены тесты конфигурации под YAML-only поведение. +- [ ] Smoke test старта приложения проходит с валидным `config.yaml`. + +## Tests +- Обновить `tests/config/test_config_sections.py` под проверку YAML-only загрузки. +- Добавить/обновить негативные тесты отсутствующих обязательных YAML-полей. +- Проверить app startup smoke test с валидным `config.yaml`. + +## Commands +- `poetry run pytest tests/config/test_config_sections.py -q` +- `poetry run pytest tests/smoke/test_app_import.py -q` diff --git a/tests/smoke/test_local_infra_stack.py b/tests/smoke/test_local_infra_stack.py new file mode 100644 index 0000000..ed072b5 --- /dev/null +++ b/tests/smoke/test_local_infra_stack.py @@ -0,0 +1,50 @@ +from pathlib import Path + +import yaml + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +COMPOSE_FILE = PROJECT_ROOT / "docker-compose.yml" +INFRA_README = PROJECT_ROOT / "infra" / "README.md" + + +def _load_compose() -> dict: + content = yaml.safe_load(COMPOSE_FILE.read_text(encoding="utf-8")) + assert isinstance(content, dict) + return content + + +def test_compose_defines_required_services() -> None: + compose = _load_compose() + services = compose.get("services") + assert isinstance(services, dict) + assert {"app", "redis", "signoz"}.issubset(services.keys()) + + +def test_app_service_contains_cache_and_observability_wiring() -> None: + compose = _load_compose() + app_service = compose["services"]["app"] + + build_config = app_service.get("build") + assert isinstance(build_config, dict) + assert build_config["context"] == "." + assert build_config["dockerfile"] == "Dockerfile" + + environment = app_service.get("environment") + assert isinstance(environment, dict) + assert environment["G2S_REPOSITORY__REDIS_DSN"] == "redis://redis:6379/0" + assert environment["G2S_OBSERVABILITY__OTLP_ENDPOINT"] == "http://signoz:4317" + assert environment["G2S_CONFIG_FILE"] == "/app/config.yaml" + + depends_on = app_service.get("depends_on") + assert isinstance(depends_on, list) + assert "redis" in depends_on + assert "signoz" in depends_on + + +def test_smoke_command_sequence_is_documented() -> None: + readme = INFRA_README.read_text(encoding="utf-8") + + assert "docker compose config" in readme + assert "docker compose up -d redis signoz" in readme + assert "docker compose ps" in readme