12 KiB
Ответ 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.paramskind == "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])
Схема данных в памяти
# 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:
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(+fixturepolygon_serversubprocess),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().