ADR 0081 — Жизненный цикл контейнера: таблица состояния, без оркестратора
Статус
Принят, с тем же видом среза, который ADR 0065 уже объявил для этого среза: таблица и машина состояний существуют; того, что командует настоящим контейнером Docker, не существует, и не этот документ решает, как оно будет существовать.
Этот ADR пересматривает ADR 0065, закрывая половину, которую его раздел «Чего этот ADR НЕ делает» объявлял, — «состоянию контейнера нужна таблица… единственный слот миграции этой волны принадлежит другой фазе», — и затрагивает территорию ADR 0055, который остаётся принятым и не редактируется ни одним из них.
Контекст
ADR 0065 поставил половину, которой не нужна была база: решение Arquiteto об
образе (artifact.project_image, в event log) и затвор, который оно
открывает для вкладки Code (RN-105). Он прямо сказал, чего не хватало:
Жизненный цикл контейнера (25b). Provisioning, остановка, пересоздание, очистка; что происходит при смене образа; что переживает перезапуск; worktree агента, переезжающий жить внутрь контейнера.
Причина конкретна и не в нехватке проектирования: состоянию контейнера нужна таблица. Id контейнера, статус, используемый образ, когда поднялся, какой версии артефакта соответствует — ничто из этого не событие, это изменяемое состояние, и запихивание его в event log означало бы использовать не тот инструмент, потому что правильный был занят. Единственный слот миграции этой волны принадлежит другой фазе.
Поставить половину provisioning было бы хуже, чем не поставить ничего: контейнер, который поднимается и не пересоздаётся, хуже, чем никакого. — ADR 0065
ПРОГРАММА 28 подошла к волне со свободным для этой таблицы слотом миграции. Прежде чем написать строку кода, на один вопрос нужно было ответить исследованием, а не предположением: может ли какой-либо сервис продукта уже командовать контейнером Docker?
Ответ, проверенный строка за строкой в docker/docker-compose.yml: нет. Ни
один сервис (api, engine, ни сервисы разработки) не монтирует
/var/run/docker.sock, и ни один не работает с privileged: true. Это не
упущение, которое следует исправить мимоходом, — это отсутствие реального
решения о безопасности, а реальное решение о безопасности не рождается как
побочный эффект таблицы. Монтирование сокета Docker в контейнер — известный
вектор эскалации до root на хосте; предоставить это без явного разрешения
пользователя, с выложенными на стол последствиями, было бы той же ошибкой,
которую ADR 0065 уже зафиксировал как не совершённую в отношении сети и
ресурсов: «решено ОДИН раз, а не команда за командой» — а «решено один раз»
предполагает, что кто-то решает, а не инфраструктурный ADR, выбирающий из
удобства.
Решение
Таблица — это КОНТРАКТ оркестратора, которого ещё нет
project_containers (миграция 0046) записывает изменяемое СОСТОЯНИЕ
контейнера проекта: status (явная машина состояний, ниже),
image_version (версия artifact.project_image, которой соответствует эта
строка, — никогда не копия image/rationale/network, которые
по-прежнему живут только в event log), container_id (реальный id в демоне
Docker, всегда NULL, пока не существует оркестратор), ОБЪЯВЛЕННЫЙ потолок
ресурсов (cpus/memory_mb/pids_limit, отражающий
RecursosDoContainer артефакта, действовавшего в момент рождения строки) и
failure_reason.
Одна строка на ПРОЕКТ (project_id уникален) — тот же дизайн, что у
dev_agent_states в engine (ADR 0045): одновременно существует только один
действующий контейнер, и повторное provisioning после удаления
переиспользует ту же строку, а не накапливает в ней историю. У неизменяемой
истории уже есть место — event log, — и эта таблица не пытается быть и тем и
другим.
Машина состояний
provisioning → running ⇄ stopped, с failed, достижимым из
provisioning/running/stopped, и removed как единственным состоянием,
из которого выходят только повторным provisioning (removed → provisioning).
Ни одно состояние не является по-настоящему терминальным: даже removed
позволяет подняться снова, потому что проект может переподготовиться с
образом, пересмотренным Arquiteto. Проверяется в
apps/api/src/domain/containers/container-lifecycle.ts, в ТОМ ЖЕ формате,
что session-state-machine.ts и pr-gate-state-machine.ts, — таблица
разрешённых переходов, чистая функция, типизированная ошибка
(InvalidContainerTransitionError), которую use case переводит в 409.
ПЕРВЫЙ переход особый: строки не существует до первого вызова с
to: 'provisioning', и он принимается, только если Arquiteto уже решил об
образе (RN-105), — тот же затвор, который уже защищает вкладку Code,
применённый здесь у источника, а не продублированный. Версия и ресурсы
действующего в тот момент решения ЗАМОРАЖИВАЮТСЯ в новой строке; последующий
пересмотр артефакта не меняет задним числом то, что обещает уже
подготовленный экземпляр, — заново артефакт читает повторное provisioning.
Никаких вызовов Docker — ни в одном из двух use cases
RegistrarTransicaoDeContainerUseCase и
ObterCicloDeVidaDoContainerUseCase делают ровно то, что говорят их имена:
записывают и читают. Ни один не вызывает docker run, docker stop, Docker
Engine API или какой-либо клиент Docker — клиента Docker в коде продукта не
существует. Настоящий оркестратор, когда появится, будет тем, кто
ПОТРЕБЛЯЕТ эту таблицу: сначала действует против демона и только затем
вызывает RegistrarTransicaoDeContainerUseCase, чтобы зафиксировать
произошедшее, — никогда наоборот, по той же причине, по которой
TransitionSessionUseCase.activate вызывает engine ДО записи active:
таблица не должна говорить, что что-то работает, когда оно не работает.
Согласованность с режимом workspace (ADR 0072)
Проект в workspace_mode: 'local' не поднимает контейнер — он работает в
контейнере АГЕНТА, как всегда, поменялась только папка (RN-169). Запрос
перехода для проекта local отклоняется с 400 до того, как тронуть таблицу:
согласованность — это не «таблица разрешает любое состояние любому проекту, а
UI потом фильтрует», это use case, отклоняющий у источника.
Никакого нового HTTP-маршрута
Ничто в Волне 4 пока не потребляет эту таблицу по HTTP — интерактивный терминал (25b/Волна 5) — очевидный кандидат, а решать форму маршрута до того, как станет точно известно, что ему нужно читать, значило бы угадывать контракт. Два use cases остаются доступными модулю контейнеров, готовые к маршруту, когда появится реальный потребитель.
Последствия
Что начинает существовать. Единое протестированное место, чтобы ответить «в каком состоянии контейнер этого проекта» и зафиксировать проверенный переход, — предпосылка ЛЮБОГО будущего оркестратора, без которой ему пришлось бы изобретать собственное хранилище состояния или заново открывать это решение.
Что остаётся ровно таким, как было, — без смягчения. Половина политики
терминала «внутри агент свободен», которая, как уже говорил ADR 0065, не
изменилась, ПРОДОЛЖАЕТ не меняться. ADR 0055 (область пути, узкий allowlist)
по-прежнему действует слово в слово. Контейнер running в этой таблице не
меняет того, что терминал разрешает выполнять, потому что ни одна команда
терминала сегодня не маршрутизируется внутрь контейнера, управляемого ею, —
таблица и выполнение команд — две системы, которые ещё не соприкасаются.
Находки Z и AD (allowlist глаголов не сходится) остаются открытыми по
привычной причине: закрытие требует физической стены (настоящего
оркестратора, изолирующего исполнение), а не таблицы, описывающей намерение
стены.
Что объявлено, а не спрятано. Сегодня нет ни одного процесса, который переводил бы эту таблицу сам, — каждый переход пока внешний (тест или ручной вызов). Это ожидаемо: таблица рождается до потребителя, а не после. День, когда будет спроектирован настоящий оркестратор, — sidecar с ограниченными привилегиями, отдельный демон или другой формат, — это отдельное решение о безопасности, с пользователем, проинформированным о том, что предоставляется, ровно как ADR 0065 уже требует для сети и ресурсов. Этот документ его не предвосхищает и не сокращает.
Рассмотренные альтернативы
Смонтировать /var/run/docker.sock в api или engine, чтобы «заработало по-
настоящему» уже в этом срезе. Отвергнуто самым прямым из существующих
доводов: это решение о безопасности (известный вектор эскалации до root на
хосте), которого в этом срезе никто не просил, и принятие его мимоходом,
чтобы таблица «выглядела завершённой», — ровно та ошибка, которую уже назвала
ФАЗА 13: это не ослабление allowlist глаголов, но тот же класс короткого
пути — получить функциональность, обменяв гарантию на удобство.
Не создавать таблицу сейчас, ждать проектирования оркестратора. Отвергнуто по причине, которую уже дал сам ADR 0065: слот миграции дефицитен (один на волну), а таблица — предпосылка, а не аксессуар, любого дизайна оркестратора — проектирование его без места для записи состояния породило бы ту же таблицу позже, под давлением реального ожидающего потребителя.
Хранить состояние как второй тип события в event log
(container.status_changed) вместо таблицы. Отвергнуто тем же доводом,
который отличает её от artifact.project_image: это СОСТОЯНИЕ (одно значение
за раз), а не исторический ФАКТ. Проецирование «текущего состояния» из
событий при каждом чтении переизобрело бы таблицу поверх event log, платя
цену косвенности и ничего не выигрывая, — тот же вывод, который уже
зафиксировали «Рассмотренные альтернативы» ADR 0065, откладывая эту таблицу.
Ссылки
- ADR 0065 — решение об образе и затвор, которые этот документ не повторяет, а лишь ссылается.
- ADR 0055 — политика терминала, которая ПРОДОЛЖАЕТ действовать как есть; здесь не меняется ни одной строки.
- ADR 0072 —
workspace_mode, решающий, есть ли у проекта жизненный цикл контейнера. - ADR 0045 — сохраняемая
машина состояний (
dev_agent_states), чей дизайн (одна строка на агента,status, проверяемый вне колонки) эта таблица отражает для контейнера. - RN-105, RN-169, RN-243–RN-248.
docs/explanation/achados-execucao-real.md— находки Z и AD, которые этот документ явно НЕ закрывает.apps/api/src/domain/containers/container-lifecycle.ts,apps/api/src/application/use-cases/containers/registrar-transicao-de-container.use-case.ts,apps/api/src/application/use-cases/containers/obter-ciclo-de-vida-do-container.use-case.ts,apps/api/src/db/migrations/0046_chilly_forgotten_one.sql.