230 lines
12 KiB
Markdown
230 lines
12 KiB
Markdown
# Ответ Claude Sonnet 4.6 — архитектура и план polygon
|
||
|
||
Дата: 2026-07-31
|
||
|
||
---
|
||
|
||
## Ответы на Q1–Q10
|
||
|
||
**Q1: map-fixed → dataDescriptor**
|
||
Конвертер ГЕНЕРИРУЕТ `dataDescriptor` из `sub_params`. Формат каждой записи:
|
||
```
|
||
{sub_param_code: {dataType, defaultValue, valueList, isRequired}}
|
||
```
|
||
Это обязательно: `normalize_value` в app-autotest использует `dataDescriptor` для генерации JSON из defaults при пустом значении map-параметра.
|
||
|
||
**Q2: subresources**
|
||
Правило простое и точное: `kind == "subresource"` (это явное поле в STANDS YAML).
|
||
`apply_effect` смотрит на `kind`:
|
||
- `kind == "instance"` → меняем статус + `state.params`
|
||
- `kind == "subresource"` → меняем `state.out[plural]`, статус НЕ меняем
|
||
|
||
**Q3: stateOut auto-generation**
|
||
Да, конвертер выводит автоматически. Сканирует операции с `kind=subresource, action=create`, строит `state_out_template`. Пример для postgres: `{users: {}, databases: {}, backups: {}}`. При create инстанса мок копирует этот шаблон в `instance.state.out`.
|
||
|
||
**Q4: "лишние" операции**
|
||
Включить все (restart, recovery, etc.) — иначе `GET /instanceOperations/default/{id}` вернёт 404 и app-autotest упадёт. Реализация: no-op transition (статус остаётся `running`, dtFinish ставится, isSuccessful=True). Если operation_id вообще не найден → 404 с `{"error": "operation not found"}`.
|
||
|
||
**Q5: valueList**
|
||
Хранить и отдавать обязательно. `get_params_with_current_values` в app-autotest заполняет из него дропдауны для `delete_user`/`delete_database`.
|
||
|
||
**Q6: Подход A (generate YAML → services/)**
|
||
Выбрать A. `from_stands.py` генерирует `site/services/N_name.yaml`, `config_loader.py` читает при старте Flask. Плюс: ручная правка для сложных случаев, прозрачность. `from_stands.py` запускается один раз при setup.
|
||
|
||
**Q7: Mock-эндпоинты**
|
||
Все 4:
|
||
- `POST /_mock/reset` — обязателен для тестовой изоляции
|
||
- `GET /_mock/state` — текущее состояние (отладка)
|
||
- `GET /_mock/services` — список загруженных сервисов
|
||
- `POST /_mock/delay/<float>` — изменить MOCK_OP_DELAY на лету
|
||
|
||
**Q8: UUID** → `uuid.uuid4()`. Детерминированные не нужны.
|
||
|
||
**Q9: Пагинация** → stop when `len(batch) < pageSize`, cap at 200, page=1 by default. Response: `{"results": [...], "pageSize": N, "page": P, "total": len(all)}`.
|
||
|
||
**Q10: Тесты**
|
||
К 5 сценариям Опуса добавить: (a) тест пагинации (3 инстанса, pageSize=2), (b) тест subresource create→verify state.out→delete→verify removed, (c) тест modify→verify params merged, (d) тест /_mock/state, (e) тест 404 на невалидный op_id. Каждый тест начинается с `POST /_mock/reset` (через `autouse` fixture).
|
||
|
||
---
|
||
|
||
## Структура файлов
|
||
|
||
```
|
||
polygon/
|
||
├── requirements.txt
|
||
└── site/
|
||
├── app.py # Flask app + все 15 маршрутов (один файл)
|
||
├── mock_state.py # MockState: instances, operations, op_params
|
||
├── state_machine.py # apply_effect()
|
||
├── config_loader.py # load_services() → services_dict + ops_index
|
||
├── from_stands.py # CLI-конвертер STANDS YAML → services/*.yaml
|
||
├── services/ # 37 YAML-файлов (generated)
|
||
└── templates/
|
||
└── index.html
|
||
```
|
||
|
||
**Ключевое ограничение Nubes:**
|
||
- `site/__init__.py` — ЗАПРЕЩЁН
|
||
- `from site.xxx import` — ЗАПРЕЩЁН
|
||
- В app.py: только `import mock_state`, `import config_loader`, `import state_machine` (работает т.к. Python добавляет директорию скрипта в `sys.path[0]`)
|
||
|
||
---
|
||
|
||
## Схема данных в памяти
|
||
|
||
```python
|
||
# mock_state.py — синглтон state = MockState()
|
||
class MockState:
|
||
instances = {} # instanceUid → instance_dict
|
||
operations = {} # opUid → operation_dict
|
||
op_params = {} # opUid → {paramId(int): paramValue(str)}
|
||
```
|
||
|
||
**instance_dict:** `{instanceUid, serviceId, displayName, descr, status, explainedStatus, svc, dtCreate, state: {params: {code: val}, out: {users: {}, databases: {}}}}`
|
||
|
||
**operation_dict:** `{instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, subresource|None, dtStart, dtFinish|None, isSuccessful|None, errorLog, svc, stages}`
|
||
|
||
---
|
||
|
||
## Конвертер from_stands.py
|
||
|
||
**HTML entities:** STANDS YAML содержит `integer >= 0`, `"` и т.д. `from_stands.py` **обязан** применять `html.unescape()` ко всем строковым полям (`data_type`, `value_list` элементы, `default`).
|
||
|
||
**Маппинг STANDS → cfsParam формат:**
|
||
|
||
| STANDS | polygon/cfsParam |
|
||
|---|---|
|
||
| `id` | `svcOperationCfsParamId` |
|
||
| `code` | `svcOperationCfsParam` |
|
||
| `data_type` (unescape) | `dataType` |
|
||
| `required` | `isRequired` |
|
||
| `default` | `defaultValue` |
|
||
| `value_list` | `valueList` |
|
||
| `sub_params` (map-fixed) | → `dataDescriptor: {code: {dataType,defaultValue,valueList,isRequired}}` |
|
||
| `sub_params` (map) | → `sub_params` (хранить как есть для normalize_value) |
|
||
|
||
**stateOut auto-generation:**
|
||
```python
|
||
for op in operations:
|
||
if op.kind == "subresource" and op.action == "create":
|
||
plural = op.subresource + "s" # user→users, database→databases
|
||
state_out_template[plural] = {}
|
||
```
|
||
|
||
---
|
||
|
||
## Полный список API эндпоинтов (15 шт.)
|
||
|
||
| # | Метод | Путь | Назначение |
|
||
|---|---|---|---|
|
||
| 1 | GET | `/health` | `"OK"` |
|
||
| 2 | GET | `/` | HTML-страница |
|
||
| 3 | GET | `/api/v1/svc/instances` | Список инстансов (paged) |
|
||
| 4 | GET | `/api/v1/svc/instances/<uid>` | Детали инстанса |
|
||
| 5 | POST | `/api/v1/svc/instances` | Создать shell → 201 + Location |
|
||
| 6 | GET | `/api/v1/svc/instanceOperations/default/<int:op_id>` | Шаблон операции |
|
||
| 7 | POST | `/api/v1/svc/instanceOperations` | Создать операцию → 201 + Location |
|
||
| 8 | GET | `/api/v1/svc/instanceOperations/<uid>` | Детали операции + cfsParams |
|
||
| 9 | POST | `/api/v1/svc/instanceOperationCfsParams` | Задать значение параметра |
|
||
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | Валидация → 200 **пустое тело** |
|
||
| 11 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | Выполнить операцию |
|
||
| 12 | POST | `/api/v1/svc/_mock/reset` | Сброс состояния |
|
||
| 13 | GET | `/api/v1/svc/_mock/state` | Debug: текущее состояние |
|
||
| 14 | GET | `/api/v1/svc/_mock/services` | Debug: загруженные сервисы |
|
||
| 15 | POST | `/api/v1/svc/_mock/delay/<float:s>` | Изменить MOCK_OP_DELAY |
|
||
|
||
**Критические детали:**
|
||
|
||
- **Эндпоинт 5** (`POST /instances`): возвращает `201` + заголовок `Location: ./{uid}`. `HttpClient.post()` берёт uid из последнего сегмента Location, проверяет `len >= 32`. Body: `{"instanceUid": uid}` (двойная защита).
|
||
|
||
- **Эндпоинт 7** (`POST /instanceOperations`): при `operation=="create"` тело НЕ содержит `svcOperationId` — polygon находит его сам из `svc_def.operations["create"]["id"]`.
|
||
|
||
- **Эндпоинт 10** (validate-cfs): `return "", 200` (НЕ `jsonify`). app-autotest ловит `JSONDecodeError` и считает это успехом.
|
||
|
||
- **Эндпоинт 11** (run): синхронный — `time.sleep(MOCK_OP_DELAY)`, затем `apply_effect`, затем `dtFinish = datetime.utcnow().isoformat() + "Z"`. Первый poll после run увидит dtFinish.
|
||
|
||
- **Маршруты Flask:** `/instanceOperations/default/<int:op_id>` должен стоять **выше** `/instanceOperations/<uid>` в app.py.
|
||
|
||
---
|
||
|
||
## Поток запроса (полный цикл create)
|
||
|
||
```
|
||
executor.py polygon
|
||
───────── ───────
|
||
POST /instances {serviceId,displayName}
|
||
→ создать instance shell (status="creating")
|
||
→ response 201 + Location: ./uid1
|
||
← instanceUid = uid1
|
||
|
||
POST /instanceOperations {instanceUid, operation:"create"}
|
||
→ найти create op_id из service_def
|
||
→ создать operation_dict (dtFinish=None)
|
||
→ response 201 + Location: ./uid2
|
||
← opUid = uid2
|
||
|
||
GET /instanceOperations/uid2?fields=cfsParams
|
||
→ response {"instanceOperation": {"cfsParams": [...]}}
|
||
cfsParams из service_def + пустые paramValue
|
||
|
||
POST /instanceOperationCfsParams × N → state.op_params[uid2][paramId] = val
|
||
GET /instanceOperations/uid2/validate-cfs → response "" 200
|
||
POST /instanceOperations/uid2/run
|
||
→ sleep(0.1s)
|
||
→ apply_effect: state.params←codes, status="running"
|
||
→ dtFinish = now
|
||
→ response {"ok": true}
|
||
|
||
GET /instanceOperations/uid2?fields=dtFinish,...
|
||
→ response {"instanceOperation": {"dtFinish": "2026-...", "isSuccessful": true, ...}}
|
||
← poll done: status="OK"
|
||
```
|
||
|
||
---
|
||
|
||
## Фазы реализации
|
||
|
||
**Этап 1 — Конвертер** (`from_stands.py`): html.unescape, convert_param рекурсивный, build_state_out_template, pluralize, CLI. Запустить: `cd polygon/site && python from_stands.py`. Критерий: 37 YAML в `site/services/`, у postgres `state_out_template: {users:{}, databases:{}, backups:{}}`.
|
||
|
||
**Этап 2 — MockState + config_loader + state_machine**: три отдельных модуля. Критерий: `python -c "import config_loader; s,i=config_loader.load_services('services'); print(len(s))"` → 37.
|
||
|
||
**Этап 3 — Flask API** в app.py: реализовать все 15 эндпоинтов в порядке от простых к сложным (см. таблицу). Критерий: `python -m py_compile app.py` без ошибок, `curl localhost:5000/health` → OK.
|
||
|
||
**Этап 4 — Тесты**:
|
||
- В polygon: `tests/test_converter.py`, `tests/test_state_machine.py` (юнит)
|
||
- В app-autotest: `tests/conftest.py` (+fixture `polygon_server` subprocess), `tests/test_polygon_integration.py` (интеграционные)
|
||
|
||
|
||
## Уточнения (второй раунд)
|
||
|
||
### Уточнение 1: `map` с `sub_params` → тоже `dataDescriptor`
|
||
|
||
Правило: **если у параметра есть `sub_params` — генерировать `dataDescriptor`**, независимо от того `map` это или `map-fixed`.
|
||
|
||
| `data_type` | `sub_params` | Действие |
|
||
|---|---|---|
|
||
| `map-fixed` | есть | генерировать `dataDescriptor` |
|
||
| `map` | есть | генерировать `dataDescriptor` |
|
||
| `array-map-fixed` | есть | генерировать `dataDescriptor` |
|
||
| любой | нет | `dataDescriptor: null` |
|
||
|
||
### Уточнение 2: `state.out` после create — пустые `{}`
|
||
|
||
Достаточно пустых `{}` из `state_out_template`. STANDS YAML не описывает runtime-дефолты (`pgadmin`, `mydb`) — это эффект реального Terraform, не мок.
|
||
|
||
### Уточнение 3: расположение тестов
|
||
|
||
```
|
||
app-autotest/
|
||
tests/
|
||
conftest.py # + fixture polygon_server (subprocess)
|
||
test_polygon_integration.py # интеграционные тесты
|
||
|
||
polygon/
|
||
tests/
|
||
test_converter.py # юнит: from_stands.py
|
||
test_state_machine.py # юнит: apply_effect
|
||
```
|
||
|
||
`polygon_server` fixture: subprocess → poll `/health` (timeout 5s) → после тестов `terminate()`.
|