186 lines
9.9 KiB
Markdown
186 lines
9.9 KiB
Markdown
# Задача: Code Review polygon v0.2.0
|
||
|
||
> Адресат: Claude Sonnet 4.6 (новый чат, с нуля)
|
||
> Дата: 2026-07-31
|
||
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы. Не создавать файлы.
|
||
|
||
---
|
||
|
||
## 1. Что такое polygon
|
||
|
||
**Polygon** — эмулятор REST API облачной платформы Nubes. Отдельный managed-сервис
|
||
(`polygon.pythonk8s.dev.nubes.ru`), притворяется реальным Nubes API для интеграционных
|
||
тестов приложения **app-autotest**.
|
||
|
||
- Flask 3.0 + gunicorn, деплой на Nubes pythonk8s
|
||
- Все данные в памяти (MockState), без БД
|
||
- Data-driven: конфиги сервисов генерируются из STANDS YAML через `from_stands.py`
|
||
- 37 сервисов (dummy, postgres, redis, kafka, flask, ...)
|
||
- 17 API-эндпоинтов, префикс `/api/v1/svc`
|
||
- 19 юнит-тестов (все PASS)
|
||
|
||
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/polygon.git` (ветка `master`)
|
||
Деплой: `https://polygon.pythonk8s.dev.nubes.ru/`
|
||
Версия: **v0.2.0**
|
||
|
||
## 2. Структура кода
|
||
|
||
```
|
||
polygon/
|
||
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||
├── .gitignore
|
||
├── tests/
|
||
│ ├── test_converter.py # 10 юнит-тестов from_stands.py
|
||
│ └── test_state_machine.py # 9 юнит-тестов state_machine.py + mock_state
|
||
└── site/
|
||
├── app.py # Flask-приложение (17 эндпоинтов)
|
||
├── mock_state.py # MockState: instances, operations, op_params в памяти
|
||
├── state_machine.py # apply_effect() — мутация состояния по kind/action
|
||
├── config_loader.py # загрузка services/*.yaml → {svcId: def} + {opId: def}
|
||
├── from_stands.py # конвертер STANDS YAML → polygon config
|
||
└── services/ # 37 сгенерированных YAML-конфигов
|
||
```
|
||
|
||
## 3. Что делает каждый модуль
|
||
|
||
### app.py — 17 эндпоинтов
|
||
|
||
Полный эмулятор Nubes API. Порядок маршрутов критичен: `default/<int>` ДО `<uid>`.
|
||
|
||
| # | Метод | Путь | Назначение |
|
||
|---|-------|------|------------|
|
||
| 1 | GET | `/health` | `"OK"` (для Nubes healthcheck) |
|
||
| 2 | GET | `/` | HTML с версией, счётчиками |
|
||
| 3 | GET | `/api/v1/svc/services` | список сервисов |
|
||
| 4 | GET | `/api/v1/svc/services/<id>` | операции сервиса |
|
||
| 5 | GET | `/api/v1/svc/instances` | пагинация |
|
||
| 6 | GET | `/api/v1/svc/instances/<uid>` | инстанс + state.params + state.out |
|
||
| 7 | POST | `/api/v1/svc/instances` | создать shell → 201 + `Location: ./{uid}` |
|
||
| 8 | GET | `/api/v1/svc/instanceOperations/default/<id>` | cfsParams с dataDescriptor, valueList |
|
||
| 9 | POST | `/api/v1/svc/instanceOperations` | создать операцию → 201 + Location |
|
||
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>` | статус + cfsParams (?fields=...) |
|
||
| 11 | POST | `/api/v1/svc/instanceOperationCfsParams` | paramId → value |
|
||
| 12 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | **пустое тело**, 200 |
|
||
| 13 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | sleep → apply_effect → dtFinish |
|
||
| 14 | POST | `/api/v1/svc/_mock/reset` | сброс |
|
||
| 15 | GET | `/api/v1/svc/_mock/state` | отладка |
|
||
| 16 | GET | `/api/v1/svc/_mock/services` | отладка |
|
||
| 17 | POST | `/api/v1/svc/_mock/delay/<s>` | MOCK_OP_DELAY |
|
||
|
||
**Критические точки:**
|
||
- `POST /instances` и `POST /instanceOperations` **обязаны** отдавать `Location: ./{uuid}` — app-autotest достаёт UUID из заголовка
|
||
- `POST /instanceOperations` при `operation=="create"` — тело НЕ содержит `svcOperationId`, polygon ищет сам
|
||
- `validate-cfs`: `return "", 200` (НЕ `jsonify`) — app-autotest ждёт пустое тело
|
||
- `run`: синхронный `time.sleep(MOCK_OP_DELAY)` + `apply_effect` + `dtFinish = now`
|
||
- `MOCK_OP_DELAY` из env, по умолчанию 0.1с
|
||
|
||
### mock_state.py — состояние в памяти
|
||
|
||
```python
|
||
class MockState:
|
||
instances = {} # instanceUid → {instanceUid, serviceId, displayName, status, state: {params, out}, ...}
|
||
operations = {} # opUid → {instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, dtStart, dtFinish, isSuccessful, ...}
|
||
op_params = {} # opUid → {paramId(int): paramValue(str)}
|
||
```
|
||
|
||
Методы: `create_instance`, `get_instance`, `list_instances` (пагинация), `create_operation`,
|
||
`get_operation`, `set_param`, `get_params`, `reset`.
|
||
|
||
UUID через `uuid.uuid4()`. Пагинация: pageSize ≤ 200, стоп по `len(batch) < pageSize`.
|
||
|
||
### state_machine.py — apply_effect
|
||
|
||
Мутирует MockState после завершения операции:
|
||
|
||
| kind | action | Эффект |
|
||
|------|--------|--------|
|
||
| instance | create | статус `running`, stateParams из шаблона, stateOut из шаблона |
|
||
| instance | modify | мерж op_params в state.params через cfsParamsByOp |
|
||
| instance | delete | удалить инстанс |
|
||
| instance | suspend | статус `suspended` |
|
||
| instance | resume | статус `running` |
|
||
| instance | redeploy | статус `running` |
|
||
| instance | restart/recovery/... | no-op |
|
||
| subresource | create | `state.out[plural][name] = {}` |
|
||
| subresource | delete | `del state.out[plural][name]` |
|
||
|
||
`_extract_subresource_name`: ищет первое непустое строковое значение в op_params, fallback на subresource_name.
|
||
|
||
### config_loader.py — загрузка конфигов
|
||
|
||
Читает все `.yaml` из `services/`. Возвращает:
|
||
- `services`: `{service_id: service_def}`
|
||
- `ops_index`: `{svcOperationId: service_def}` — для поиска сервиса по ID операции
|
||
|
||
### from_stands.py — конвертер
|
||
|
||
Конвертирует STANDS YAML (из Terraform-провайдера) → polygon-конфиг.
|
||
Запуск: `python from_stands.py <STANDS_DIR> [SERVICES_DIR]`
|
||
|
||
- `html.unescape()` для всех строк (`>` → `>`, `"` → `"`)
|
||
- Маппинг полей: `id`→`svcOperationCfsParamId`, `code`→`svcOperationCfsParam`, `data_type`→`dataType`, `value_list`→`valueList`, `sub_params`→`dataDescriptor`
|
||
- `dataDescriptor` генерируется для всех типов с `sub_params` (map, map-fixed, array-map-fixed)
|
||
- `state_out_template`: авто-генерация из операций с `kind=subresource, action=create`
|
||
- `stateParams`: defaults из create-операции, JSON-генерация для map-fixed
|
||
- `cfsParamsByOp`: связка opId → список paramId
|
||
|
||
## 4. Что проверять (фокус code review)
|
||
|
||
### Безопасность
|
||
- Все ли входные данные валидируются (JSON body, query params)?
|
||
- Есть ли возможность инъекции через `displayName`, `paramValue`?
|
||
- `_mock/*` эндпоинты — не утечка ли это на проде?
|
||
|
||
### Корректность API
|
||
- Совпадают ли форматы ответов с реальным Nubes API?
|
||
- Правильно ли обрабатываются краевые случаи: отсутствующий сервис, невалидный instanceUid, пустой body?
|
||
- Корректны ли статус-коды (201, 400, 404)?
|
||
- `Location`-заголовки правильного формата?
|
||
|
||
### Состояние и стейт-машина
|
||
- Нет ли гонок в MockState (хотя воркер один, но Flask debug mode reloads)?
|
||
- Все ли переходы стейт-машины корректны?
|
||
- Не теряются ли данные при modify/reset?
|
||
- Правильно ли работает пагинация при пустом/частичном списке?
|
||
|
||
### Конвертер
|
||
- Все ли краевые случаи STANDS YAML обрабатываются (пустые поля, отсутствующие sub_params)?
|
||
- Правильно ли `html.unescape` применяется ко всем строковым полям?
|
||
- Не падает ли на нестандартных YAML (template, s3bucket)?
|
||
|
||
### Код и архитектура
|
||
- Нет ли дублирования логики?
|
||
- Понятны ли имена функций/переменных?
|
||
- Нет ли мёртвого кода?
|
||
- Правильно ли обрабатываются ошибки (try/except где нужно)?
|
||
|
||
### Nubes-совместимость
|
||
- `site/__init__.py` отсутствует?
|
||
- `app.run(host="0.0.0.0", port=5000)` на месте?
|
||
- `from site.xxx` нигде нет?
|
||
- `/health` возвращает `"OK"`?
|
||
|
||
## 5. Формат ответа
|
||
|
||
Сгруппируй находки по категориям:
|
||
- 🔴 Критические (сломает работу)
|
||
- 🟡 Средние (потенциальная проблема)
|
||
- 🔵 Минорные (стиль, имена)
|
||
|
||
Для каждой находки: файл, строка (примерная), проблема, предлагаемое исправление.
|
||
|
||
Если код в порядке — скажи что всё ок по каждой категории.
|
||
|
||
## 6. Что можно спрашивать у меня
|
||
|
||
Можешь задавать уточняющие вопросы. Например:
|
||
- «Какой формат у real Nubes API для эндпоинта X?»
|
||
- «Почему сделано так, а не иначе?»
|
||
- «Какие именно поля ждёт app-autotest в ответе Y?»
|
||
|
||
Формат вопросов:
|
||
```
|
||
### Вопрос N: <краткий заголовок>
|
||
<развёрнутый вопрос>
|
||
```
|