Перейти к содержимому

render config

dwe render config [service] рендерит конфигурационные файлы сервиса (например, .env, env.php) из пакета шаблонов в hub-каталог каждого сервиса, воспроизводя любые выпущенные сервисом секреты, собранные в долговечное хранилище сгенерированных значений. Конфиги — чистые выходы рендера, выведенные из объединённой конфигурации плюс хранилища.

Рендер конфигов пишет прямо в уже примонтированное дерево src/ (без пофайлового bind-mount / машинерии mountpoint). Модель состоит из двух частей:

  1. Render — разрешить пакет шаблонов конфигов, отрендерить каждую запись manifest и записать результат в hub-каталог сервиса (svc.Dir), режим replace (перезапись).
  2. Generated-once — opt-in механизм для выпущенных сервисом секретов (Laravel APP_KEY, Magento crypt.key, …). Сервис генерирует значение, записывая его в свой собственный файл; DWE считывает его обратно («harvest») в долговечное пер-сервисное хранилище (.dwe/generated.yml) и воспроизводит его при каждом последующем рендере через пространство имён ${generated.<name>}.

Рендер конфигов — opt-in: сервис без разрешимого пакета конфигов — это молчаливый no-op. Никакой обвязки scaffold-а dwe init нет — вы сами авторите пакет и шаги конвейера.

В отличие от render ide / ai / git — которые используют сырую подложку Go text/template {{ }} (packcommon.TemplateData) — шаблоны конфигов используют сокращение ${...}, ту же форму, что уже применяется в правилах экспорта ${APP_*} / ${DB_*}. Это намеренное расхождение, оправданное эргономикой конфигурационных файлов: авторы конфигов ожидают паритета ${...} со значениями, на которые они ссылаются.

${X} компилируется в {{ resolve .Raw "X" }}, поэтому dot-путь ищется в объединённой конфигурации (cfg.Raw) без префикса raw. — но только когда голова X относится к известному пространству имён (корневой ключ объединённой конфигурации либо одно из специальных пространств имён ниже); нераспознанная голова остаётся литеральным ${...} вместо молчаливого рендера в "":

workspace/templates/config/laravel/env.tmpl
APP_URL=${services.main.hosts.web}
DB_HOST=${vars.databases.main.host}
DB_DATABASE=${vars.databases.main.name}
APP_KEY=${generated.app_key}
  • Пер-сервисные поля используют ${services.<name>...} (например, ${services.main.ports.http}). Это раскрывает только курируемое подмножество, внедрённое в cfg.Raw["services"]type / container / каталоги / configs / ports / hosts / … — а не render / generated / произвольные объединённые поля. Опущенное или не внедрённое поле рендерится как "" (все резолверы ${...} снисходительны — отсутствующий путь — это пустая строка, никогда не ошибка).
  • Произвольные значения живут под vars: — ссылайтесь на них как ${vars.<путь>} (например, ${vars.databases.main}). Голый dot-путь верхнего уровня без префикса vars. не резолвится: корень объединённой конфигурации — это строгий allowlist (project, services, vars, …), поэтому произвольный ключ вроде databases не может появиться там напрямую.
  • Сгенерированные значения используют ${generated.<name>} (см. ниже).

Единичного связывания текущего сервиса ${service....} нет — ссылайтесь на сервис по имени через ${services.<name>...}.

${generated.<name>} разрешается в собранное значение для поля <name> текущего сервиса из хранилища сгенерированных значений. При первом деплое хранилище пусто, поэтому ${generated.app_key} рендерится как "" — сервис затем выпускает настоящее значение, и DWE его собирает. При последующих рендерах сохранённое значение воспроизводится дословно, поэтому секрет переживает run / редеплой, оставаясь при этом вне git.

Отсутствующий ключ рендерится как "" (снисходительно, как и у любого другого резолвера ${...}). Пространство имён ограничено рендерящимся сервисом: оно читает store[<current-service>], поэтому ${generated.app_key} в пакете main никогда не видит crypt_key из magento.

Хранилище лежит в .dwe/generated.yml (под gitignored runtime-каталогом .dwe/ — оно никогда не коммитится):

services:
main:
app_key: "base64:Xa3…=="
magento:
crypt_key: "241f4fa60be8f69638343cacc5a1a192"
  • Значения — строки (block scalars для многострочных секретов).
  • Записи атомарны (temp-файл + rename), зеркаля журнал деплоя.
  • Отсутствующий файл — это пустое хранилище (первый деплой). Повреждённый файл — это всплывающая ошибка — никогда не проглатывается молча, поэтому некорректное хранилище нельзя принять за «секретов пока нет».
  • У хранилища нет schema_version.
  • Создание/восстановление снапшота намеренно оставляет .dwe/generated.yml нетронутым (это отдельный файл от .dwe/deploy/state.yml).

Сгенерированные поля декларируются в service.yml (пер-сервисное lifecycle-состояние), а не в manifest — воспроизводимое значение может целиться в аргумент команды, а не только в шаблон:

workspace/services/main/service.yml
type: app
dir: ./services/main
render:
config:
template: laravel # optional pack pin; else convention + .local
generated:
app_key:
file: src/.env # output file, relative to the service hub (svc.Dir)
pattern: '^APP_KEY=(.*)$' # regex; capture group 1 = value
ПолеТипОбязательноОписание
filestringдаВыходной файл, в который сервис записывает значение, относительно hub-каталога сервиса (svc.Dir). Должен быть содержащимся относительным путём (без ..).
patternstringдаRegex, применяемый построчно; capture group 1 — это собранное значение. Должен компилироваться и объявлять ≥1 capture group (валидируется dwe validate).

Ключ карты (app_key) — это идентификатор ${generated.<name>}. dwe validate отвергает невалидный regex, отсутствующую capture group, выходящий за пределы пути file или имя поля, которое не является валидным идентификатором ${generated.<name>}.

DWE никогда не генерирует секрет сам — движок герметичен (без crypto / случайности), а переиспользование собственного генератора сервиса формат-агностично со стороны DWE (DWE лишь считывает строку обратно). Сбор:

  1. Читает <svc.Dir>/<file>.
  2. Применяет pattern построчно, беря capture group 1 первого совпадения.
  3. Write-if-absent в хранилище, затем сохраняет атомарно, если что-то изменилось.

Ошибки всплывают точно, никогда не пропускаются молча: отсутствующий файл, паттерн, не совпадающий ни с одной строкой, паттерн без capture group и паттерн, который захватывает пустое значение — всё это жёсткие ошибки — поэтому полу-выпущенный секрет не может загрязнить хранилище. Write-if-absent означает, что редеплой — это no-op, как только значение сохранено.

pattern (regex, захватывающий одну строку) используется вместо enum type формата, потому что DWE никогда не перечисляет форматы конфигов и никогда не пишет чужой формат — он лишь читает одну строку из одного файла. Один и тот же pattern извлекает строку dotenv APP_KEY= или значение PHP-массива 'crypt' => ['key' => '…'].

Пакеты конфигов живут под workspace/templates/config/<pack>/ с тем же соглашением оверрайда shadow-пакета <pack>.local/, что у ide/ai/git (см. Локальные оверрайды). Порядок разрешения — используется первое совпадение:

  1. workspace/templates/config/<template>/, когда задано render.config.templateстрого: запинённый пакет, которого не существует, — жёсткая ошибка (ловит опечатки).
  2. workspace/templates/config/<service-name>/.
  3. Каждый предок в цепочке extends сервиса: workspace/templates/config/<ancestor>/.
  4. workspace/templates/config/default/.
  5. Если ни одного нет, рендер конфигов пропускается (opt-in — без ошибки).

Симлинкнутые пакеты отвергаются; каталог пакета должен оставаться содержащимся внутри корня проекта.

Пакеты конфигов управляются manifest-ом, используя общую схему manifest.yml (ту же, что читают ide/ai/git, см. Общая схема manifest), с двумя ограничениями config-kind:

workspace/templates/config/laravel/manifest.yml
render:
- from: env.tmpl
to: src/.env
АспектПакет конфигов
Корень назначенияhub-каталог сервиса (svc.Dir)
Форма toлюбой содержащийся относительный путь
symlinksотвергается — отрендеренные конфигурационные файлы пишутся на месте, никогда не симлинкуются

to: src/... — это соглашение использования, а не захардкоженное соединение: to интерпретируется относительно hub-каталога сервиса. Авторы целятся в дерево приложения (уже dir-mounted в контейнер), записывая to: src/.... Назначения защищены безопасностью путей — to, который выходит за hub-каталог или разрешается вне него через симлинк, отвергается.

Окно терминала
dwe render config # render every enabled app service that resolves a pack (DeployOrder)
dwe render config main # render only the `main` service
dwe render config main --harvest # harvest-only pass: read on-disk values into the store, NO render
  • Без аргумента каждый включённый app-сервис обрабатывается в DeployOrder (детерминированно); сервис без пакета конфигов молча пропускается. Рендер конфигов работает только для app-сервисов — лишь app-сервисы могут объявлять dir / render / generated, поэтому tool- / infra-сервисы не перебираются.
  • С явным [service] аргумент валидируется (должен существовать, быть включённым и иметь hub-каталог); отсутствующий пакет всплывает предупреждением.
  • --harvest переключает на harvest-only проход (HarvestGenerated, без рендера) — для бутстрапа уже закоммиченных секретов существующего проекта в хранилище, прежде чем они перестанут коммититься.

Путь рендера по умолчанию read-only в отношении проектных локов: он не запускает preflight и не захватывает локи, как и рендереры ide/ai/git. --harvest изменяет общее хранилище сгенерированных значений, поэтому он сначала захватывает проектные локи (как builtin harvest при деплое и reset --clear-generated), чтобы не затереть конкурентного писателя в хранилище.

Три builtin-а движка управляют рендером конфигов внутри конвейеров deploy / reset (см. deploy builtins):

BuiltinНазначение
service_configs_renderОтрендерить пакет конфигов сервиса в его hub-каталог (режим replace), воспроизводя сохранённые сгенерированные значения
service_configs_render_checkПроверить, что отрендеренные цели существуют; спаривание его как check: заставляет шаг рендера перезапускаться при каждом деплое
service_generated_harvestСобрать объявленные поля generated: сервиса в хранилище (write-if-absent)

service_configs_render_check зеркалит service_configs_copy + service_configs_check: его присутствие как check: дёргает рычаг hasCheck → Run, обходя пропуск по action-hash, поэтому шаг рендера всегда перезапускается — правки шаблонов и очистки хранилища поэтому всегда вступают в силу.

Конвейер деплоя, который рендерит конфиги и собирает выпущенный сервисом секрет:

phases:
- name: configs
steps:
- name: render-configs
type: builtin
cmd: service_configs_render
with:
service: main
check: # presence forces re-run every deploy
type: builtin
cmd: service_configs_render_check
with:
service: main
- name: generate-app-key
when: # gate: only when the store has no value yet
type: builtin
cmd: "generated-missing main app_key"
type: dwe
cmd: "shell main -- php artisan key:generate"
- name: harvest-app-key
type: builtin
cmd: service_generated_harvest
with:
service: main

Первый деплой: рендер пишет APP_KEY= (хранилище пусто) → гейт открыт → сервис выпускает APP_KEY=base64:… → сбор его захватывает.

Последующие деплои: рендер воспроизводит сохранённое значение → гейт закрыт → генерация пропускается → сбор — no-op. Рендер перезапускается при каждом деплое через свой check:. Инвариант: хранилище пусто для ключа ⟺ значение перевыпускается.

Обратите внимание: шаг сбора намеренно не закрыт гейтом — service_generated_harvest пропускает любое поле, которое уже есть в хранилище, вообще не читая его файл. Именно это делает его no-op выше, и это не оптимизация, а несущее свойство: dwe reset run (без --clear-generated) сохраняет хранилище, но стирает хаб сервиса, так что на следующем деплое выпущенного файла уже нет, а полученное из него значение всё ещё авторитетно. Сбор, настаивающий на перечитывании, уронил бы весь деплой из-за значения, которое у него уже есть. Поэтому строгие ошибки ниже (нет файла, нет совпадения, пустой захват) относятся только к полю, которого в хранилище ещё нет, — то есть ровно к случаю, когда плохое чтение способно испортить хранилище.

Предикат generated-missing <svc> <field> (см. conditions) читает .dwe/generated.yml и истинен, когда поле отсутствует или хранилища нет.

dwe run перерендеривает конфиги сервисов из хранилища после прохождения гейта деплоя (и после post-pull перезагрузки конфигурации), но до lifecycle-фаз — поэтому конфиги отражают текущие шаблоны и воспроизведённые секреты на момент run. Он никогда не запускает generate/harvest на run.

Run-рендер не разрушителен, когда данных для воспроизведения нет: если развёрнутый сервис объявляет ключи generated:, которых нет в хранилище, рендер этого сервиса пропускается с подсказкой dwe deploy run, а не рендерит обнулённый секрет. Поскольку рендер выполняется только после гейта, reset --clear-generated, за которым следует dwe run, проваливает гейт до достижения рендера, поэтому секреты никогда не обнуляются.

reset сохраняет хранилище по умолчанию — секрет переживает reset. Передайте --clear-generated, чтобы очистить его (с областью по --service / всё):

Окно терминала
dwe reset run --clear-generated # clear the whole store on full reset
dwe reset run --service main --clear-generated # clear only main's entries

Хранилище очищается только после успешного полного reset, включая post-pipeline очистку журнала — никогда, если конвейер или мутация журнала провалились (иначе рассогласование deployed-журнал + пустое-хранилище заставило бы run-гейт доверять сервису без секретов). На TTY с непустым хранилищем интерактивный запрос спрашивает, очищать ли также сгенерированные значения (по умолчанию No). Ротация = очистка + редеплой.

Механизм копирования (configs: / mountpoint в service.yml, builtin-ы service_configs_copy / service_configs_check) продолжает работать, но устарелdwe validate выдаёт предупреждение, и одно runtime-уведомление срабатывает на каждый шаг копирования. Чтобы мигрировать:

  1. Переместите каждый запечённый файл из configs/services/<svc>/ в шаблон под workspace/templates/config/<pack>/, заменив литеральные значения ссылками ${...}.
  2. Объявите render.config (optional pin) и любые поля generated: в service.yml; уберите блок configs: / mountpoint.
  3. Замените service_configs_copy (+ service_configs_check) на service_configs_render (+ service_configs_render_check) в конвейере, добавив generate-гейт + шаги service_generated_harvest для любых выпущенных сервисом секретов.
  4. Забутстрапьте уже закоммиченный секрет в хранилище через dwe render config <svc> --harvest, затем перестаньте его коммитить.
  • определения сервисов (service.yml) — справочник полей render.config и generated:
  • deploy builtinsservice_configs_render, service_configs_render_check, service_generated_harvest
  • conditions — предикат generated-missing
  • render index — общая схема manifest, локальные оверрайды, разрешение пакета
  • Шаблоны — синтаксис Go-шаблонов и render-контексты
  • Запустите dwe render config --help, чтобы увидеть актуальный CLI-интерфейс