Окружения¶
Окружение — это именованная копия управляемой модели организации. Физически копия представляет собой
отдельную схему PostgreSQL — не колонку-дискриминатор, не префикс, а настоящую схему, — поэтому
любой существующий запрос репозитория корректен внутри окружения без единой переписанной строки, а
строки одного окружения не могут попасть в чтение другого из-за забытого предиката (REQ-1487,
REQ-1488). [tool-verified: environments.py module docstring; org_schema() at environments.py
lines 86-96]
Каждая организация начинается с одного окружения по имени prod. Его нельзя удалить или
переименовать. Запрос, не называющий окружения, обслуживается prod; запрос, называющий
несуществующее окружение, отклоняется. [tool-verified: PROD = "prod" at environments.py line 44;
select_environment() at env_routing.py lines 93-129]
Окружения доступны организациям на платном тарифе. [inferred: REQ-1507]
Имена окружений¶
Имя должно соответствовать [a-z][a-z0-9_]{1,31} — от двух до тридцати двух символов из строчных
букв, цифр и подчёркиваний, начиная с буквы. prod и имена, начинающиеся с pg_, отклоняются.
Максимальная длина для конкретной организации зависит от идентификатора самой организации:
PostgreSQL молча обрезает идентификатор длиннее 63 байт, и самое длинное имя схемы, выводимое
окружением, — это то, от чего защищает ограничение. [tool-verified: ENV_NAME_PATTERN at
environments.py line 59; validate_env_name() at environments.py lines 119-142;
max_env_name_length() at environments.py lines 108-116]
Что несёт копия¶
Каждая таблица в схеме организации попадает ровно в один класс (REQ-1489). Классификация — это
разрешительный список, а не список исключений: добавленная позже таблица не путешествует, пока
кто-нибудь не назовёт здесь её класс, так что режим отказа для забытой таблицы — красный тест.
[tool-verified: CLASSIFIED constant and module docstring, env_classes.py lines 19-22]
| Класс | Таблицы | Что происходит при копировании |
|---|---|---|
| CARRIED | domains, naming_rules, registered_tables, table_columns, relationships, metrics, roles, rls_rules, tags, tag_param_values, tag_assignments, glossary terms, materialized_views, calendars, api_endpoints, tracked_functions, tracked_webhooks, table_meta_links | Копируется целиком |
| IDENTITY_ONLY | sources, api_sources, kafka_sources, kafka_sinks | Поля идентичности и управления путешествуют; значения подключения остаются на месте (см. «Привязки») |
| SEEDED_AT_CREATION | roles, user_role_assignments | Копируется только при первом создании окружения; последующие слияния их не трогают |
| PARTIAL | org_settings | Копируется по ключам: настройки управления путешествуют, ключи, называющие внешнюю цель или среду выполнения конкретного окружения, остаются на месте |
| NEVER_SENSITIVE | org_secrets, user_directory | Не копируется никогда |
| NEVER_RUNTIME | mv_refresh_log, relationship_candidates, admin_audit_log и другие | Не копируется никогда |
[tool-verified: CARRIED, IDENTITY_ONLY, SEEDED_AT_CREATION, PARTIAL, NEVER_SENSITIVE,
NEVER_RUNTIME frozensets, env_classes.py lines 29-113]
SEEDED_AT_CREATION существует, чтобы решить одну конкретную проблему. Новому окружению нужны роли
и назначения, иначе оно откроется без единого, кто мог бы действовать. Но последующее слияние,
принёсшее строку developer из prod, перезаписало бы ограниченную версию, которая может быть
нужна ограниченной ветке, — и тогда путь ревью стал бы маршрутом эскалации привилегий. Поэтому роли
и назначения путешествуют один раз, при создании, а дальше остаются собственным ответом каждого
окружения. [tool-verified: env_classes.py lines 65-71; env_copy.py lines 41-44]
Привязки¶
Привязки — это колонки, которые говорят, куда источник на самом деле указывает: host, port,
database, username и остальные. Они не путешествуют ни в одной копии. Окружение, которое ещё не
привязано, помечается как unbound, а не оставляется пустым: пустой хост — это не отсутствующий
хост, и построитель подключения прочитал бы его как localhost:5432. [tool-verified:
BOUND_COLUMN = "bound" at env_classes.py line 143; BINDING_COLUMNS dict at env_classes.py
lines 155-172]
Источники окружения разрешаются одним из двух способов.
База — окружение несёт собственные учётные данные. org_admin создаёт базу и затем привязывает
каждый источник явно. [tool-verified: CreateEnvBody.inherit_connections = False (default) at
environments_router.py line 227; "binding a base is an org_admin's act" comment at line 358]
Ветка — окружение наследует учётные данные базы по ссылке. Ничего не копируется. Когда запросу
нужно подключение, разрешение поднимается по цепочке branched_from и останавливается на первом
окружении, чья строка привязана. Ротация учётных данных на базе распространяется на каждую её ветку
без каких-либо действий. Отзыв отзывает их у всех сразу. Ни один секрет нигде не материализуется
так, чтобы ветка, экспорт или репозиторий могли его унести.
[tool-verified: resolve() at env_bindings.py lines 114-151; lineage() at env_bindings.py
lines 74-102; env_bindings.py module docstring lines 11-33]
Чтобы создать ветку, включите Inherit connections в панели Environments. По умолчанию выключено.
[tool-verified: environmentsTab.json key inheritConnections; inheritHelp2 string]
Git-проекция¶
Каждая запись в модель фиксирует результат в git-ветке окружения. Репозиторий — проекция модели, но никогда не её авторитет: Provisa читает и пишет плоскость управления; репозиторий — это запись, а не источник. Развёртывание дерева требует явного вызова — слитый pull request на git-хосте сам себя не развёртывает (REQ-1524, REQ-1526). [tool-verified: deploy endpoint docstring at environments_router.py lines 777-791]
Каждая сущность получает один файл. Путь — это URI из REQ-1385 со снятыми схемой и организацией:
provisa://acme/sales/tables/Order становится sales/tables/Order.yaml. Источники попадают в
sources/, команды — в commands/, метрики — в metrics/. Дочерние строки, каскадирующие от
родителя — колонки, связи, правила RLS, — пишутся внутри родительского файла, а не отдельными
файлами. [tool-verified: table_path() at env_files.py line 109-115; kind_path() at env_files.py
lines 118-120; COMMANDS_DIR = "commands" at env_project.py line 71; env_files.py module
docstring lines 17-24]
Команды и назначения их тегов переживают полный круг. Тег на команде направляется в собственный файл
команды (commands/<name>.yaml); тег, не принадлежащий никакому файлу, исчезает из проекции и был бы
удалён при следующем развёртывании этого дерева. [tool-verified: env_project.py lines 346-364;
owner_command_name routing in _assignments_for() at env_project.py lines 137-164]
Ни один суррогатный ключ не доходит до файла. registered_tables.id — автоинкрементное целое: одна
и та же модель в двух окружениях получает разные целые, поэтому наивный дамп даёт различия сам с
собой. Каждый суррогат отбрасывается, а каждая ссылка на него записывается как путь цели.
[tool-verified: STORAGE_COLUMNS and _model_columns() at env_files.py lines 62-128;
env_project.py docstring lines 26-27]
Сериализация детерминирована. Ключи выводятся по алфавиту, дочерние коллекции упорядочены по своему
адресу, стиль YAML зафиксирован. Два окружения, содержащие одну и ту же модель, дают побайтово
одинаковые деревья. [tool-verified: dump() at env_files.py lines 131-143]
Слияние¶
Слияние модели одного окружения в другое обновляет по идентичности: каждый объект, который есть у
источника, создаётся или обновляется в цели. Объекты, которых у источника больше нет, удаляются
только тогда, когда вызывающая сторона явно запросила удаления. Слияние, оборвавшееся на полпути,
оставляет цель такой, какой она была, — одна транзакция. [tool-verified: copy_model() at
env_copy.py lines 216-234; REQ-1490 description]
Перед применением вызовите эндпоинт предпросмотра (GET /{name}/merge-preview) или передайте
dry_run: true. Предпросмотр проходит тем же путём по коду, что и слияние; это эндпоинт GET,
поэтому CI-скрипт, перепутавший флаг, не может случайно применить слияние, которое собирался лишь
осмотреть. [tool-verified: preview_merge() docstring at environments_router.py lines 1086-1095]
Слияние оставляет привязки, роли и секреты цели ровно такими, какими они были. Окружение разработки не теряет собственных подключений к базам данных, взяв более новую модель из prod. Prod не приобретает грантов dev. [tool-verified: env_copy.py lines 269-287; REQ-1490 scenario]
Что называет отчёт¶
Отчёт о слиянии перечисляет по путям, что было добавлено, изменено, удалено и оставлено без
изменений. Он также называет любые конфликты — объекты, изменённые обеими сторонами с момента
последнего общего коммита. Конфликт сообщается, но не разрешается: побеждает источник, а именно это
и означает слияние в цель. Provisa не предлагает ни разрешения конфликтов, ни маркеров слияния, ни
выбора по объектам. Ценность списка конфликтов — в сигнале: двое правили один объект, не зная об
этом (REQ-1555). [tool-verified: CopyReport.conflicts at env_copy.py lines 151-165;
detect_conflicts() called at env_copy.py lines 261-263; REQ-1555 description]
Объект, который обе стороны изменили к одному и тому же значению, — это согласие, а не конфликт.
Когда два окружения вовсе не имеют общего предка, база в отчёте равна None, и пустой список
конфликтов означает, что ничего не сравнивалось, а не что ничего не столкнулось. [tool-verified:
CopyReport.compared property at env_copy.py lines 164-166; env_copy.py lines 255-264]
Слияние ложится одним сжатым коммитом в ветку цели. Сообщение коммита обязательно и не должно быть
пустым — это единственный отчёт о том объёме работы, за который стоит этот squash. Коммиты источника
остаются на месте и после этого по-прежнему развёртываемы по SHA.
[tool-verified: _squash() docstring at environments_router.py lines 663-680;
MergeBody.message comment at environments_router.py lines 258-260]
Pull¶
Pull берёт то, что удалённый репозиторий содержит для окружения, и делает это моделью. Он не
перематывает локальную ветку напрямую; он применяет полученное дерево обычным путём развёртывания,
поэтому те же валидация и аудит, что управляют ручным развёртыванием, управляют и pull.
[tool-verified: pull_environment() docstring at environments_router.py lines 1450-1462]
Как и слияние, pull сообщает, что он перезаписал, — объекты, изменённые входящим деревом, которые
локальное окружение тоже изменило с момента последнего общего коммита двух линий. Незакоммиченное
локальное изменение — это дрейфующее окружение (см. «История» ниже); pull называет его в отчёте
обычным изменением. [tool-verified: REQ-1556 description; pull_environment() at
environments_router.py lines 1485-1519]
Pull отклоняется, когда две линии разошлись — у каждой есть коммиты, которых нет у другой. Отказ
несёт список объектов, затронутых обеими сторонами, чтобы тот, кому теперь предстоит решить, чья
работа выживет, знал, на какие объекты смотреть. [tool-verified: state["diverged"] check at
environments_router.py lines 1491-1503; _collisions() at environments_router.py lines 1581-1602]
История¶
Каждое развёртывание двигает курсор окружения вперёд по его собственной линии коммитов. Undo делает
шаг назад на один коммит; redo снова шагает вперёд к позиции, откуда undo ушёл. Ни одна из операций
не удаляет коммит — шаг назад добавляет позицию, а не переписывает историю.
[tool-verified: _move() docstring at environments_router.py lines 854-868]
Ветка засевается на вершине окружения, из которого она создана, поэтому undo останавливается в этой
точке засева и не уходит на коммиты родительского окружения. [tool-verified: origin_sha comment at
environments_router.py lines 428-448; _move() at environments_router.py lines 907-916]
Флаги can_undo и can_redo едут вместе с ответом на список окружений. Оба сообщают false, когда
проекция не содержит коммита, который называет плоскость управления, — состояние, допускаемое
дизайном и называемое дрейфующим. Узел, чьё хранилище репозитория так и не получило конкретный
коммит, всё равно перечисляет свои окружения; меняются только ответы об истории (REQ-1561).
[tool-verified: _with_history() at environments_router.py lines 316-344; REQ-1561 description]
Авторизация¶
Окружениями управляют два права. Ни одно из них по умолчанию не принадлежит аналитику (REQ-1573).
[tool-verified: REQ-1573 description; MANAGE_CAPABILITY = "environment_management" and
SWITCH_CAPABILITY = "environment_switch" at environments_router.py line 110 and
env_routing.py line 53]
| Право | Кто им обладает (при засеве) | Чем оно управляет |
|---|---|---|
environment_management |
org_admin, developer | Создание и удаление окружений |
environment_switch |
org_admin, developer | Обслуживание любым окружением, кроме prod |
prod не требует права — им обслуживается запрос, не называющий ничего, и отказ в нём означал бы
отказ каждому запросу.
Применение происходит в точке выбора, до того как будет достигнут любой маршрут. Участнику, у
которого нет environment_switch, отказывают сразу на всех поверхностях — HTTP, GraphQL, SQL и
проводных протоколах, — потому что окружение привязывается в middleware, а не в отдельных
обработчиках. [tool-verified: select_environment() at env_routing.py lines 93-129; env_routing.py
module docstring lines 28-34]
Аналитик, не имеющий никакого права на окружения, может запрашивать prod и не видит переключателя
окружений. Подрядчик, которому выдана роль аналитика, не видит поверхности окружений и не может
создать окружение или переключиться в какое-либо, кроме production. [tool-verified: REQ-1573
use_case and scenario]
Полномочия владельца окружения¶
Создание окружения — единственный путь, которым участник с доступом только на чтение приобретает
права на редактирование модели (REQ-1528). Внутри созданного им окружения создатель обладает
возможностями роли developer — минус права на данные (write, full_results, usage). Права на
построение модели, а не права на данные. [tool-verified: ENVIRONMENT_OWNER_CAPABILITIES at
env_authority.py lines 75-77; _DATA_RIGHTS at env_authority.py lines 74-77; env_authority.py
module docstring lines 14-38]
Этот грант выводится из environments.created_by во время авторизации и никогда не пишется в
таблицу грантов. Удаление окружения снимает его тем же действием.
[tool-verified: env_authority.py module docstring lines 39-42; environment_owner() at
env_authority.py lines 84-98]
Членство в домене по-прежнему ограничивает то, что владелец может менять. Ветвление меняет, что
участник может делать; оно никогда не меняет, к каким доменам он может это делать (REQ-1530).
[tool-verified: domains_within() at env_authority.py lines 121-145]
Защищённые окружения (REQ-1504)¶
Окружение можно сделать защищённым. Слияние или развёртывание в защищённое окружение не применяется по запросу; оно предлагается, и кто-то, кроме запросившего, должен его одобрить.
prod становится защищённым автоматически, как только в организации больше одного участника.
Организация из одного участника не может удовлетворить условию «кто-то, кроме запросившего», поэтому
там правило не применяется — иначе prod стал бы неслиянным. Любое окружение может быть помечено
защищённым администратором организации. [tool-verified: is_protected() at env_approvals.py
lines 79-96; protectedHelp2 UI string in environmentsTab.json line 28]
Запрос на слияние — это строка, а не диалог подтверждения. Одобряющий по определению другой человек, нежели запросивший, и в момент запроса не присутствует; эфемерное подтверждение вынудило бы одобрять внутри сессии запросившего, а это ровно то устройство, которое требование запрещает. [tool-verified: env_approvals.py module docstring lines 11-17]
Строка запроса несёт отчёт о слиянии рядом с сообщением запросившего. Устаревание выводится во время
чтения и никогда не хранится: перепланирование во время чтения и сравнение с сохранённым отчётом —
единственный вариант, который не может ошибиться. Устаревший запрос нужно запросить заново.
Запросивший не может одобрить собственный запрос. [tool-verified: STALE constant and
effective_state() at env_approvals.py lines 53, 215-243; decide() lines 265-268]
Состояния жизненного цикла запроса: requested → approved/rejected → applied. stale
выводится. [tool-verified: REQUESTED, APPROVED, REJECTED, APPLIED, STALE at
env_approvals.py lines 47-53]
Та же дверь обслуживает развёртывания из ref репозитория: запрос закрепляет SHA в момент
предложения. Если ref сдвинулся между предложением и решением, одобряющий читает отчёт по
закреплённому коммиту, а не по новому. [tool-verified: request_deploy() at env_approvals.py lines
150-189; env_approvals.py docstring lines 26-27]
Note
UI запросов на слияние находится во вкладке Merge requests панели Environments.
Колонка Report показывает, что изменилось бы, по количеству; строка разворачивается,
показывая подробности по объектам. [tool-verified: environmentsTab.json keys requestsTitle,
colReport, approve, reject]
Команды CLI env¶
provisa env deploy отправляет модель по ref в окружение. Она завершается с кодом 0, когда
развёртывание применено или было пробным, и с кодом 2, когда окружение защищено и развёртывание было
только предложено: конвейер, принявший ожидающее одобрение за выпущенное развёртывание, был бы
неправ, и код выхода об этом говорит. [tool-verified: _cmd_env_deploy() at cli.py lines 389-411]
provisa env fetch приносит удалённые ветки организации в локальный репозиторий. После этого
развёртывание может назвать origin/<branch>. [tool-verified: _cmd_env_fetch() at cli.py
lines 414-426]
Обе команды принимают --api (URL API Provisa) и --token (bearer-токен). Задайте
PROVISA_API_URL и PROVISA_API_TOKEN в окружении, чтобы не передавать их при каждом вызове.
[inferred: shared _api_call() helper]
Типичный CI-конвейер для рабочего процесса с репозиторием:
provisa env fetch --org acme --api "$PROVISA_API_URL" --token "$PROVISA_API_TOKEN"
provisa env deploy --org acme --env prod --ref "origin/main" \
--message "release: $GIT_COMMIT_MSG" \
--api "$PROVISA_API_URL" --token "$PROVISA_API_TOKEN"
Смотрите также¶
- Развёртывание — как поднять плоскость управления, к которой подключаются окружения
- Команды — отслеживаемые функции и вебхуки, появляющиеся в дереве каждого окружения