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

dwe render ide

Сгенерировать IDE-специфичные файлы конфигурации для каждого включённого сервиса из пакета шаблонов. Вывод идёт в hub-каталог сервиса (например, services/main/.vscode/settings.json).

Manifest обязателен. Каждый IDE-пакет обязан содержать manifest.yml в корне, перечисляющий каждый рендерящийся файл. Отсутствие manifest — жёсткая ошибка. Схема разделяется с render ai и render git — см. Общая схема manifest. Пофайловые локальные оверрайды через соседний <pack>.local/ shadow-tree применяются ко всем трём рендерерам одинаково.

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)зависит от сервиса
Политика IDEservices.<name>.render.ide.enabledtrue для type: app; false иначе

Если хоть один гейт ложный — сервис пропускается. Пропуски делятся на две группы:

ГруппаКогдаСообщается как warning?
Политические пропускисервис отключён на уровне проекта, или render.ide.enabled равен false (явно или по дефолту типа)нет — это документированное opt-in/opt-out поведение
Actionable-пропускиу сервиса нет hub-каталога, или другой сервис выиграл коллизию каталогада — обычно это указывает на неверную настройку

После гейта активации сервисы без hub-каталога отбрасываются — либо dir пуст, либо он разрешается в корень проекта. У сервиса без hub нет куда писать, а hub, равный корню проекта, дал бы рендереру царапать workspace.yml поверх самого себя.

Разрешение коллизий: выигрывает глубочайший

Заголовок раздела «Разрешение коллизий: выигрывает глубочайший»

Когда более одного выживающего сервиса указывает на один и тот же dir, ровно один выигрывает:

  1. Пройти цепочку extends каждого сервиса и вычислить глубину.
  2. Выигрывает сервис с самой глубокой цепочкой. Глубина ограничена (сейчас 32 хопа) как cycle-guard.
  3. Ничьи на одной глубине ломаются лексикографически по имени сервиса, поэтому результат детерминирован.

Проигравшие сервисы выдают предупреждение, называющее победителя и спорный каталог.

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-варианта), поэтому самый специализированный сервис в цепочке владеет отрендеренными файлами.

dwe render ide <name> трактует <name> как hub-якорь: это должен быть реальный, eligible-сервис, но дальше применяется политика «глубочайший выигрывает», чтобы понять, какой сиблинг действительно рендерится.

Порядок валидации (первая ошибка побеждает):

  1. Сервис не в конфигурации.
  2. Сервис отключён на уровне проекта.
  3. У сервиса нет hub-каталога, или его hub — корень проекта.
  4. 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): имя сервиса с точкой в начале, дефисом в начале или разделителем пути молча пропускается как кандидат, и обход продолжается.

Каждый 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/источник независим и не используется для вывода назначения). Рендерер:

  1. Читает файл шаблона из пакета.
  2. Парсит его как Go text/template в строгом режиме — любая ссылка на отсутствующее поле прерывает рендер вместо записи плейсхолдера <no value>.
  3. Выполняет шаблон по переменным шаблона.
  4. Разрешает назначение и прогоняет гарды безопасности путей.
  5. Создаёт отсутствующие родительские каталоги.
  6. Отказывается перезаписывать назначение, если оно уже существует как симлинк.
  7. Пишет отрендеренные байты.

Существующие обычные файлы по назначению перезаписываются без подтверждения — это и есть смысл команды.

Шаблоны получают один объект с такими полями верхнего уровня:

ПеременнаяИсточникЗамечания
.Projectблок project: из workspace.ymlнапример, .Project.Name, .Project.Prefix
.Serviceканоническая конфигурационная идентичность — корень цепочки extends: рендерящегося сервисаиспользуйте для raw-config поисков по имени сервиса ((index .Cfg.Raw.cs .Service).standard). Равно .Resolved без цепочки extends.
.Resolvedrender-идентичность — сервис, чей 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>").
.Servicesmap[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 ...}}-гарды для полей, которые могут быть законно пустыми.

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 сервиса или корня проекта. Полная цепочка по порядку:

  1. Очистка пути. Относительный путь каждой записи пакета нормализуется; абсолютные пути и ..-побеги отвергаются при обходе пакета.
  2. Containment service-dir. Разрешённое назначение должно быть внутри hub-каталога сервиса.
  3. Никаких симлинков в пути назначения. Существующие компоненты пути проверяются до создания любого каталога. Это ловит предсуществующий симлинк .devcontainer, иначе позволивший бы созданию каталога уйти за пределы hub.
  4. Границы реального пути после создания. После создания родительских каталогов каталог назначения разрешается через любые симлинки и должен по-прежнему быть внутри и реального корня проекта, и реального hub. Это ловит гонку, когда симлинк подбрасывают между проверками.
  5. Никаких симлинков на месте файла назначения. Если по target-пути уже есть симлинк, запись отвергается (а не следуется по ссылке с перезаписью того, на что она указывает).

Те же гарды применяются к самому пакету: каталог пакета проверяется на симлинкнутые компоненты родителя, а walker отвергает любой симлинк внутри дерева.

Раскладка:

workspace/services/
main/
service.yml
main-debug/
service.yml
workspace/templates/ide/
default/
.devcontainer/devcontainer.json.tmpl
.vscode/settings.json.tmpl
main-debug/
.devcontainer/devcontainer.json.tmpl
.vscode/settings.json.tmpl
.vscode/launch.json.tmpl

workspace/services/main/service.yml:

type: app
container: app-main
dir: ./services/main
# render.ide.enabled defaults to true (type: app)

workspace/services/main-debug/service.yml:

type: app
extends: main
container: app-main-debug
dir: ./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 (без аргумента):

  1. Выборка: и main, и main-debug проходят гейт активации. Они делят dir: ./services/main. У main-debug цепочка extends глубже (1 против 0), поэтому main-debug выигрывает. main сообщается как пропуск из-за коллизии, выводится предупреждение.
  2. Разрешение пакета для main-debug: render.ide.template: main-debug явный; workspace/templates/ide/main-debug/ существует — он и используется.
  3. Записи render из manifest (в порядке объявления) дают три вывода: .devcontainer/devcontainer.json, .vscode/launch.json, .vscode/settings.json.
  4. Каждая рендерится с .Service = "main" (корень цепочки — по нему ключёваны user-config мапы), .Resolved = "main-debug" (рендерящийся сервис), .ServiceCfg.Container = "app-main-debug" и т. д.

Результат:

services/main/
.devcontainer/
devcontainer.json
.vscode/
launch.json
settings.json

dwe 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.ideenabled, template, наследование через extends
  • render ai — родственная команда с противоположной политикой коллизий
  • Запустите dwe render ide --help, чтобы увидеть актуальный CLI-интерфейс