dwe render ide
Сгенерировать IDE-специфичные файлы конфигурации для каждого включённого сервиса из пакета шаблонов. Вывод идёт в hub-каталог сервиса (например, services/main/.vscode/settings.json).
Manifest обязателен. Каждый IDE-пакет обязан содержать
manifest.ymlв корне, перечисляющий каждый рендерящийся файл. Отсутствие manifest — жёсткая ошибка. Схема разделяется сrender aiиrender git— см. Общая схема manifest. Пофайловые локальные оверрайды через соседний<pack>.local/shadow-tree применяются ко всем трём рендерерам одинаково.
Содержание
Заголовок раздела «Содержание»- Конвейер
- Выборка сервисов
- Разрешение пакета шаблонов
- Схема manifest
- Пофайловый рендер
- Проработанный пример
- Сообщения вывода
- Частые ловушки
- Связанные справочники
Конвейер
Заголовок раздела «Конвейер»flowchart TD
CFG["Загрузить объединённую конфигурацию"] --> SEL{"Аргумент задан?"}
SEL -- нет --> SP["Выбрать сервисы<br/>фильтр политики + коллизия"]
SEL -- да --> EXV["Валидировать аргумент"]
EXV --> RHA["Разрешить hub-якорь<br/>глубочайший выигрывает"]
SP --> LIST["Отсортированный список сервисов"]
RHA --> LIST
LIST --> EACH{"Для каждого сервиса"}
EACH --> RP["Разрешить пакет шаблонов"]
RP --> LM["Загрузить manifest.yml<br/>строгий декод"]
LM --> VM["Валидировать manifest"]
VM --> RNDR["Рендерить каждый manifest.render<br/>parse, execute, write"]
RNDR --> EACH
EACH -- "конец" --> DONE["готово"]
Выборка сервисов
Заголовок раздела «Выборка сервисов»Гейт активации
Заголовок раздела «Гейт активации»Сервис участвует в IDE-рендере, только если оба флага истинны:
| Гейт | Источник | По умолчанию |
|---|---|---|
| Уровень проекта | services.<name>.enabled (3-слойный merged + обязательный service override) | зависит от сервиса |
| Политика IDE | services.<name>.render.ide.enabled | true для type: app; false иначе |
Если хоть один гейт ложный — сервис пропускается. Пропуски делятся на две группы:
| Группа | Когда | Сообщается как warning? |
|---|---|---|
| Политические пропуски | сервис отключён на уровне проекта, или render.ide.enabled равен false (явно или по дефолту типа) | нет — это документированное opt-in/opt-out поведение |
| Actionable-пропуски | у сервиса нет hub-каталога, или другой сервис выиграл коллизию каталога | да — обычно это указывает на неверную настройку |
Нормализация каталога
Заголовок раздела «Нормализация каталога»После гейта активации сервисы без hub-каталога отбрасываются — либо dir пуст, либо он разрешается в корень проекта. У сервиса без hub нет куда писать, а hub, равный корню проекта, дал бы рендереру царапать workspace.yml поверх самого себя.
Разрешение коллизий: выигрывает глубочайший
Заголовок раздела «Разрешение коллизий: выигрывает глубочайший»Когда более одного выживающего сервиса указывает на один и тот же dir, ровно один выигрывает:
- Пройти цепочку
extendsкаждого сервиса и вычислить глубину. - Выигрывает сервис с самой глубокой цепочкой. Глубина ограничена (сейчас 32 хопа) как cycle-guard.
- Ничьи на одной глубине ломаются лексикографически по имени сервиса, поэтому результат детерминирован.
Проигравшие сервисы выдают предупреждение, называющее победителя и спорный каталог.
flowchart LR
M["main<br/>extends: ничего<br/>глубина 0"] --> D
MD["main-debug<br/>extends: main<br/>глубина 1"] --> D
D["dir: services/main"] --> W{"глубочайший выигрывает"}
W --> WIN["рендерится main-debug"]
W -. "skip warning" .-> M
Обоснование: IDE-конфигурации — это про per-variant оверрайды (разные настройки отладчика для main-debug, разные launch-профили для stage-варианта), поэтому самый специализированный сервис в цепочке владеет отрендеренными файлами.
Явный аргумент [service]
Заголовок раздела «Явный аргумент [service]»dwe render ide <name> трактует <name> как hub-якорь: это должен быть реальный, eligible-сервис, но дальше применяется политика «глубочайший выигрывает», чтобы понять, какой сиблинг действительно рендерится.
Порядок валидации (первая ошибка побеждает):
- Сервис не в конфигурации.
- Сервис отключён на уровне проекта.
- У сервиса нет hub-каталога, или его hub — корень проекта.
render.ide.enabledвычисляется вfalse— либо явно, либо по дефолту типа (не-appтипы выключены по умолчанию; сообщение об ошибке говорит, какой случай и как opt-in).
После валидации применяется то же «глубочайший выигрывает», ограниченное сиблингами, делящими тот же dir. Если победитель отличается от аргумента, info-строка объявляет подмену.
Так dwe render ide main из пер-сервисного deploy-пайплайна всё равно делает правильное, когда активный вариант — main-debug.
Разрешение пакета шаблонов
Заголовок раздела «Разрешение пакета шаблонов»Для каждого выбранного сервиса рендерер выбирает один каталог-пакет под workspace/templates/ide/.
flowchart TD
S{"render.ide.template задан?"}
S -- да --> EX["workspace/templates/ide/{template}/"]
EX -- существует --> USE["использовать этот пакет"]
EX -- отсутствует --> ERR["ошибка<br/>явный — это строго"]
S -- нет --> SN["workspace/templates/ide/{service-name}/"]
SN -- существует --> USE
SN -- отсутствует --> DEF["workspace/templates/ide/default/"]
DEF -- существует --> USE
DEF -- отсутствует --> WARN["warning + skip<br/>implicit не найден"]
Правила:
- Явный — это строго. Если
render.ide.templateзадан, пробуется только этот пакет. Отсутствующий пакет — жёсткая ошибка, никакого тихого fallback. Это защищает от опечаток вродеtemplete:, случайно разрешающихся вdefault/и рендерящих сюрпризы. - Implicit-цепочка (когда
render.ide.templateне задан):<service-name>→ проход по цепочкеextends:предок-за-предком →default. Побеждает первый существующий пакет. Провал-дальше происходит только если кандидатный каталог отсутствует; любая другая ошибка файловой системы — жёсткая. Если implicit-цепочка исчерпана без пакета, рендер пропускается с предупреждением. Невалидные имена пакетов в цепочке (точка в начале, разделитель пути) молча пропускаются, обход продолжается. - Почему предки.
extends:-потомок вродеmain-debugобычно не несёт собственного пакета — он наследует IDE-конфигурацию родителя. Падение сразу наdefault/отрендерило бы не тот контент, потому что default-пакет настроен под другие сервисы. - Пакет должен быть реальным каталогом. Симлинкнутые пакеты отвергаются.
- Выбранный пакет должен лежать внутри корня проекта без симлинкнутых компонентов родителя.
Валидация ключа шаблона отвергает:
- Разделители путей (
/,\). - Точку в начале (включает
..и hidden-ключи). - Пусто (трактуется как «не задано», что разрешено и запускает implicit-цепочку).
Имена сервисов, используемые как implicit-ключ пакета, проходят через тот же строгий валидатор имён пакетов (manifest.ValidatePackName): имя сервиса с точкой в начале, дефисом в начале или разделителем пути молча пропускается как кандидат, и обход продолжается.
Схема manifest
Заголовок раздела «Схема manifest»Каждый IDE-пакет обязан содержать manifest.yml в корне по общей схеме manifest:
render: - from: .vscode/settings.json.tmpl to: .vscode/settings.json - from: .devcontainer/devcontainer.json.tmpl to: .devcontainer/devcontainer.json
# symlinks: опционально — та же семантика, что у render aiОтсутствие manifest.yml — жёсткая ошибка: каждый пакет обязан содержать manifest, перечисляющий каждый файл. Файлы внутри пакета, не упомянутые в render, игнорируются (рендерер сам пакет никогда не обходит).
Валидация идёт в два прохода:
| Проход | Что проверяет |
|---|---|
| Shape (чистый) | хотя бы одна запись render или symlinks; to-пути содержатся под hub; нет дублей to; каждый to симлинка ссылается на известный render-назначение |
| Sources (resolver-aware) | каждый from существует либо в каноническом workspace/templates/ide/<pack>/<from>, либо в overrid-е workspace/templates/ide/<pack>.local/<from> |
Строгий YAML-декод (yaml.Decoder.KnownFields(true)) отвергает опечатанные ключи вроде renders: на загрузке.
Пофайловый рендер
Заголовок раздела «Пофайловый рендер»Для каждой записи назначение строится конкатенацией hub-каталога сервиса с явным путём to записи (путь from/источник независим и не используется для вывода назначения). Рендерер:
- Читает файл шаблона из пакета.
- Парсит его как Go text/template в строгом режиме — любая ссылка на отсутствующее поле прерывает рендер вместо записи плейсхолдера
<no value>. - Выполняет шаблон по переменным шаблона.
- Разрешает назначение и прогоняет гарды безопасности путей.
- Создаёт отсутствующие родительские каталоги.
- Отказывается перезаписывать назначение, если оно уже существует как симлинк.
- Пишет отрендеренные байты.
Существующие обычные файлы по назначению перезаписываются без подтверждения — это и есть смысл команды.
Переменные шаблона
Заголовок раздела «Переменные шаблона»Шаблоны получают один объект с такими полями верхнего уровня:
| Переменная | Источник | Замечания |
|---|---|---|
.Project | блок project: из workspace.yml | например, .Project.Name, .Project.Prefix |
.Service | каноническая конфигурационная идентичность — корень цепочки extends: рендерящегося сервиса | используйте для raw-config поисков по имени сервиса ((index .Cfg.Raw.cs .Service).standard). Равно .Resolved без цепочки extends. |
.Resolved | render-идентичность — сервис, чей hub реально рендерится (победитель коллизии по глубочайшему extends). | Равно .Service без коллизии. |
.ServiceCfg | эффективная конфигурация сервиса .Resolved, после разрешения extends | например, .ServiceCfg.Container, .ServiceCfg.Dir, .ServiceCfg.DirInternal, .ServiceCfg.WorkDirInternal, .ServiceCfg.CLI.* отражают overlay рендерящегося сервиса |
.Runtime | объединённый блок runtime | .Runtime.UseHTTPS, .Runtime.SPX.Path. Порты/хосты на сервис находятся в записи каждого сервиса — используйте ((index .Services "<name>").Port "<port-name>") / ((index .Services "<name>").Host "<host-name>"). |
.Services | map[string]ServiceConfig, ключёванная по имени сервиса | только индексный доступ (требование Go-шаблона): (index .Services "main"). Фильтруйте по типу через .AppServices / .ToolServices / .InfraServices (0-аргументные методы, возвращающие типизированные подмножества). |
.Cfg | объединённый DweConfig (продвинутое) | .Cfg.Raw — мапа после слияния и нормализации DWE (services.* подставляется из per-service service.yml) — см. Шаблоны. Для обычных случаев предпочитайте выделенные поля выше. |
Совет. IDE-выход ложится в
<svc.Dir>/<entry.To>— обычно в отслеживаемые проектные файлы (.vscode/settings.json,.devcontainer/devcontainer.json, …). Избегайте использования developer-local или секретных ключей через.Cfg.Rawв IDE-шаблонах: любое значение, попавшее изworkspace/local.yml, всплывёт в отрендеренном файле и даст разные диффы у разных разработчиков в отслеживаемых артефактах. Используйте.Cfg.Rawтолько для общих для всего проекта соглашений.
Строгий режим означает, что опечатка {{.Servic.Name}} прерывает рендер вместо записи <no value>. Используйте {{if ...}}-гарды для полей, которые могут быть законно пустыми.
Доступ к .Cfg.Raw
Заголовок раздела «Доступ к .Cfg.Raw»Go-овский text/template разрешает dot-сегменты, только если каждый совпадает с [A-Za-z_][A-Za-z0-9_]*. До ключей с дефисами, точками, ведущими цифрами или любыми другими не-идентификаторными символами нельзя добраться dot-синтаксисом — используйте index.
{{ .Cfg.Raw.ide.workspace_name }} {{- /* точка — identifier-safe ключи */ -}}{{ index .Cfg.Raw "my-tool" "api-key" }} {{- /* index — ключи с дефисами */ -}}Замечание: порты/хосты на сервис находятся в значении ServiceConfig этого сервиса — ((index .Services "main").Port "http"), ((index .Services "adminer").Host "web"). Отдельных namespace-ов .Runtime.Ports / .Runtime.Hosts / .Tools нет.
Полный набор хелперов, доступных внутри *.tmpl (appURL, sprout-реестры, встроенные text/template), описан в Шаблоны.
Гарды безопасности путей
Заголовок раздела «Гарды безопасности путей»Рендерер применяет множественные граничные проверки, потому что вредоносный или невнимательный путь шаблона иначе мог бы использоваться для записи файлов вне hub сервиса или корня проекта. Полная цепочка по порядку:
- Очистка пути. Относительный путь каждой записи пакета нормализуется; абсолютные пути и
..-побеги отвергаются при обходе пакета. - Containment service-dir. Разрешённое назначение должно быть внутри hub-каталога сервиса.
- Никаких симлинков в пути назначения. Существующие компоненты пути проверяются до создания любого каталога. Это ловит предсуществующий симлинк
.devcontainer, иначе позволивший бы созданию каталога уйти за пределы hub. - Границы реального пути после создания. После создания родительских каталогов каталог назначения разрешается через любые симлинки и должен по-прежнему быть внутри и реального корня проекта, и реального hub. Это ловит гонку, когда симлинк подбрасывают между проверками.
- Никаких симлинков на месте файла назначения. Если по target-пути уже есть симлинк, запись отвергается (а не следуется по ссылке с перезаписью того, на что она указывает).
Те же гарды применяются к самому пакету: каталог пакета проверяется на симлинкнутые компоненты родителя, а walker отвергает любой симлинк внутри дерева.
Проработанный пример
Заголовок раздела «Проработанный пример»Раскладка:
workspace/services/ main/ service.yml main-debug/ service.ymlworkspace/templates/ide/ default/ .devcontainer/devcontainer.json.tmpl .vscode/settings.json.tmpl main-debug/ .devcontainer/devcontainer.json.tmpl .vscode/settings.json.tmpl .vscode/launch.json.tmplworkspace/services/main/service.yml:
type: appcontainer: app-maindir: ./services/main# render.ide.enabled defaults to true (type: app)workspace/services/main-debug/service.yml:
type: appextends: maincontainer: app-main-debugdir: ./services/main # тот же dir, что у родителя — коллизияrender: ide: template: main-debug # использовать пакет main-debugШаблон workspace/templates/ide/main-debug/.vscode/settings.json.tmpl:
{ "container.name": "{{.ServiceCfg.Container}}", "workspace.root": "{{.ServiceCfg.DirInternal}}", "service": "{{.Resolved}}"}dwe render ide (без аргумента):
- Выборка: и
main, иmain-debugпроходят гейт активации. Они делятdir: ./services/main. Уmain-debugцепочкаextendsглубже (1 против 0), поэтомуmain-debugвыигрывает.mainсообщается как пропуск из-за коллизии, выводится предупреждение. - Разрешение пакета для
main-debug:render.ide.template: main-debugявный;workspace/templates/ide/main-debug/существует — он и используется. - Записи
renderиз manifest (в порядке объявления) дают три вывода:.devcontainer/devcontainer.json,.vscode/launch.json,.vscode/settings.json. - Каждая рендерится с
.Service = "main"(корень цепочки — по нему ключёваны user-config мапы),.Resolved = "main-debug"(рендерящийся сервис),.ServiceCfg.Container = "app-main-debug"и т. д.
Результат:
services/main/ .devcontainer/ devcontainer.json .vscode/ launch.json settings.jsondwe render ide main даёт тот же результат — main валидируется, но hub-anchor разрешение выбирает main-debug и печатает ide [main] — resolved to main-debug (hub services/main).
Сообщения вывода
Заголовок раздела «Сообщения вывода»| Поток | Триггер |
|---|---|
| info | Явный аргумент разрешился в другого сиблинга — называется выбранный победитель и общий hub-каталог. |
| warning | Выбранный сервис пропущен, потому что у него нет hub-каталога (или его hub — корень проекта). |
| warning | Выбранный сервис пропущен, потому что другой сервис выиграл коллизию каталога — победитель назван. |
| info | Использован <pack>.local/<rel>-override вместо канонического файла. |
| success | По одной строке на каждый отрендеренный файл, с относительным путём внутри проекта. |
| info | Ничего не выбрано после применения политики и правил коллизии. |
Ошибки возвращаются как падение команды и называют проблемный сервис, чтобы источник проблемы был очевиден.
Частые ловушки
Заголовок раздела «Частые ловушки»- Сервисы не-
appне рендерятся по умолчанию. Задайтеrender.ide.enabled: trueвworkspace/services/<name>/service.yml, чтобы opt-in. - Опечатки в
render.ide.template— жёсткие ошибки. Явные пакеты строгие; отсутствующийworkspace/templates/ide/<name>/не проваливается тихо вdefault/. Либо исправьте имя, либо уберитеrender.ide.template. - Шаблоны, ссылающиеся на отсутствующие поля, падают. Строгий режим рендера означает, что
{{.ServiceCfg.NoSuchField}}прерывает рендер. Заворачивайте опциональные поля в{{if ...}}. - Симлинки на назначении отвергаются. Если
.devcontainer/илиsettings.json— симлинк, рендерер не перезапишет его. Уберите симлинк и перезапустите. - Файлы, не перечисленные в
manifest.yml, молча игнорируются. Рендерер не обходит пакет — добавьте запись подrender:, чтобы включить шаблон. dir: "."отвергается. Сервис, чей hub — корень проекта, дал бы шаблонам царапатьworkspace.ymlи другие корневые файлы. Дайте каждому IDE-рендерящемуся сервису реальный подкаталог.
Связанные справочники
Заголовок раздела «Связанные справочники»- блок
services.<name>.render.ide—enabled,template, наследование черезextends render ai— родственная команда с противоположной политикой коллизий- Запустите
dwe render ide --help, чтобы увидеть актуальный CLI-интерфейс