Boilerplates 模板机制深度指南:template.json 清单、files/ 渲染管线与自定义分隔符
【免费下载链接】boilerplatesCreate reusable templates and turn them into configurable workloads for homelabs and self-hosted infrastructure. Free and Open-Source.项目地址: https://gitcode.com/GitHub_Trending/bo/boilerplates
导读
本文是 Boilerplates 模板系统的核心技术指南。模板(Template)是 Boilerplates 的最小组成单元,一篇可用的模板由template.json清单和files/可渲染目录构成。读完本文,你将掌握模板的必需目录布局、清单字段语义(slug / kind / metadata / variables)、自定义分隔符(<< >>、<% %>、<# #>)的渲染规则、模板发现与验证机制,以及完整的生成工作流,能够独立编写、校验并产出可复用的基础设施配置文件。
什么是 Template:模板的核心单元
在 Boilerplates 中,模板(Template)是核心单元。一个受支持的模板是一个目录,目录中包含:
template.json:模板清单(manifest),声明元数据与变量;files/目录:存放所有可渲染的输出文件。
Boilerplates CLI 发现模板库后,通过读取template.json了解模板的元信息与变量结构,再把files/下每一个文件用自定义分隔符渲染为最终可用的基础设施配置。官方文档对当前运行时的能力界定是“只支持template.json清单 +files/可渲染文件 + 自定义分隔符 + 可选的metadata.version对象”,这一约束在源码中也被严格固化(见下文“清单解析的源码实现”)。
必需的目录布局
一个合法模板的最小结构如下:
my-template/ ├── template.json └── files/ ├── compose.yaml ├── .env └── config/ └── app.yaml布局规则:
template.json是唯一受支持的清单格式;- 所有渲染内容必须位于
files/之下; - 旧版布局(
template.yaml、template.yml、顶层.j2文件)与当前运行时不兼容。
在源码中,这些规则被硬编码为常量,见 cli/core/template/template.py:
TEMPLATE_MANIFEST_FILENAME = "template.json" LEGACY_TEMPLATE_FILENAMES = ("template.yaml", "template.yml") TEMPLATE_FILES_DIRNAME = "files"当目录下找不到template.json时,Template._find_manifest_file()会先检查是否存在旧版清单,若存在则直接抛出兼容性错误,提示“Legacy template manifests are incompatible with boilerplates 0.2.0”,并指导用户将文件迁移到template.json+files/结构(template.py)。这意味着旧版模板不会静默失效,而是会在加载阶段被明确拒绝并给出迁移指引。
值得说明的是:当前仓库的 library/ 目录中仍保留着大量
template.yaml格式的旧版模板(例如 library/compose/nginx/template.yaml、library/compose/portainer/template.yaml)。它们非常适合用来参考真实世界中的变量分组设计(如general/ports/traefik分组),但作为旧格式示例,与本文所述的template.json新运行时并不直接兼容,编写新模板时请以本文为准。
顶层清单(Manifest)结构
template.json顶层包含四个核心字段:
slug:模板的唯一 ID(详见下文);kind:模板类型(如compose、terraform、ansible);metadata:展示与溯源元数据;variables:变量声明数组(分组结构)。
一个完整的最小示例:
{ "slug": "my-template", "kind": "compose", "metadata": { "name": "My Template", "description": "Short human description", "author": "Your Name", "date": "2026-04-22", "tags": ["infra", "dev"], "icon": { "provider": "mdi", "id": "docker", "color": "blue" }, "draft": false, "version": { "name": "v1.1", "source_dep_name": "ghcr.io/example/my-image", "source_dep_version": "1.1.0", "source_dep_digest": "sha256:abc123def456", "upstream_ref": "release-2026-04-22", "notes": "Tracks the tested upstream dependency snapshot" } }, "variables": [ { "name": "general", "title": "General", "items": [ { "name": "service_name", "type": "str", "title": "Service name", "default": "my-service" } ] } ] }清单解析的源码实现
从源码看,Template类在初始化时会依次执行以下步骤(template.py):
- 定位并解析
template.json(必须是合法 JSON 对象,否则报“must contain a JSON object”); - 构造
TemplateMetadata,强制要求存在metadata对象; - 校验
kind字段必须存在(Template._validate_kind); - 依据
slug/ 目录名解析模板 ID; - 校验
files/目录必须存在(缺失时报missing required 'files/' directory); - 校验
variables必须是数组。
其中kind缺失、metadata缺失、variables非数组都会抛出TemplateValidationError,并最终包装为TemplateLoadError呈现在 CLI 中。也就是说,清单的格式错误不会在渲染时才暴露,而是在加载阶段就被拦截。
slug 与模板 ID 的解析规则
slug是 CLI 对外暴露的规范化模板 ID。其解析行为优先级如下:
- 若
slug存在,以slug为准(覆盖目录名); - 若
slug以-<kind>结尾,则该冗余后缀会被归一化去除; - 若
slug缺失,退化为使用目录名。
示例:
- 目录:
portainer/ kind:composeslug:portainer-compose- CLI 中使用 ID:
portainer
这一逻辑由normalize_template_slug()实现(template.py):
def normalize_template_slug(slug: str, kind: str | None = None) -> str: normalized_slug = str(slug).strip() normalized_kind = str(kind or "").strip() if not normalized_slug: return normalized_slug suffix = f"-{normalized_kind}" if normalized_kind else "" if suffix and normalized_slug.endswith(suffix): return normalized_slug[: -len(suffix)] return normalized_slug可以看到,归一化是纯字符串操作:先去掉首尾空白,再检查是否以-<kind>结尾并截断。此外,Template.set_qualified_id()(template.py)支持在多个库存在同名模板时生成original_id.library_name形式的限定 ID(如nginx.local),这是多库场景下消除歧义的机制。
Metadata:展示元数据与版本溯源
metadata常见的字段包括:
name:模板展示名;description:人类可读的简短描述;author:作者;date:创建/更新日期;tags:标签数组;icon:图标声明(provider/id/color);draft:草稿标记,置为true时模板从正常发现中隐藏;version:可选的版本元数据对象。
版本元数据(Version Metadata)
metadata.version是可选的,一旦出现必须是对象。支持字段:
name:面向用户的版本标签,会展示在 list/show 输出中;source_dep_name:上游依赖名称;source_dep_version:上游依赖版本;source_dep_digest:上游依赖镜像摘要(如sha256:...);upstream_ref:上游引用(如release-2026-04-22);notes:备注。
关键行为:
metadata.version.name是用户可见的版本标签;- 其余字段服务于上游依赖追踪(snapshot 溯源);
- 整个
version对象可以省略; - 对象内部的单个字段也可以省略。
源码中的对应实现是TemplateVersionMetadata(template.py):from_metadata()会先检查version是否为字典,非字典直接抛出TemplateValidationError("'metadata.version' must be an object");字段缺失时全部落空字符串。同时__bool__只在name非空时为真,因此只有定义了name的版本对象才会被视为“存在版本信息”并展示。
变量声明是强制的
任何在files/下文件中使用到的变量,都必须先在template.json中声明。
如果某个文件引用了未声明的变量,模板会校验失败,加载和渲染操作都会以模板错误(template error)形式呈现。
这一规则在源码中有完整的强制执行链路(template.py):_validate_variable_definitions()会把files/中所有文件解析成 Jinja AST,用meta.find_undeclared_variables()提取用到的变量集合,再与清单中声明的变量集合做差集;存在未声明变量时,错误信息会列出每个缺失变量出现的具体文件路径,并附带“请把它声明到 variables[].items 下”的示例片段。
有意思的是,未声明的变量名还会触发近似匹配提示:TemplateErrorHandler.get_common_suggestions()(template.py)会尝试在已声明变量中寻找相似候选,给出 “Did you mean: xxx” 的建议,帮助作者快速定位拼写错误。这也解释了为什么模板作者必须保持“清单即事实来源(manifest as source of truth)”的习惯——当前运行时已经不存在独立的 schema 引用层,template.json就是变量定义的唯一权威。
变量组(group)与条目(item)的完整结构、类型、依赖与 toggle 规则,请参考 Variables 文档,此处不再展开。
files/ 目录与渲染管线
files/下的每一个文件都属于输出树的一部分,渲染行为如下:
- Boilerplates 会遍历
files/下所有文件; - 所有文件都使用自定义分隔符集进行渲染;
- 不含模板表达式的文件也会经过渲染管线(原样透传并归一化);
- 输出路径目前与
files/内部的相对路径一一对应(即files/config/app.yaml渲染为<输出目录>/config/app.yaml); - 渲染输出会被清洗(sanitize),规范化空行与行尾空白。
源码层面,Template._collect_template_files()(template.py)用os.walk递归收集files/下所有文件,relative_path与output_path相同;Template.render()(template.py)对每个文件调用jinja_env.get_template(...).render(**variable_values),再执行_sanitize_content()。清洗规则(template.py)包括:
- 每行去除行尾空白;
- 压缩连续空行为单个空行;
- 去除文件开头/结尾的多余空行,并保证文件以单个换行结尾。
此外,渲染结果中内容为空或仅剩---分隔符的文件会被自动丢弃(stripped == "---"时跳过),避免生成无意义的空 YAML 文档。
渲染错误也不会是裸奔的 Jinja 异常:TemplateErrorHandler(template.py)会把未定义变量、语法错误、文件找不到分别整理为友好信息,并附上出错文件的行号上下文(>>>标记出错行)与可操作建议。例如未定义变量会提示“Variable 'xxx' is not defined in template.json”,并建议声明或改用<< var | default('value') >>形式。
自定义分隔符
Boilerplates 使用自定义分隔符而非 Jinja 默认语法:
| 用途 | 分隔符 |
|---|---|
| 变量 | << value >> |
| 块(控制流) | <% if condition %> |
| 注释 | <# comment #> |
示例:
services: << service_name >>: image: nginx:1.27.0 <% if ports_enabled %> ports: - "<< http_port >>:80" <% endif %>旧版 Jinja 默认分隔符{{ }}、{% %}、{# #}会被拒绝(语法解析失败)。
源码中这些分隔符被定义在 template.py,并由_create_jinja_env()(template.py)注入 Jinja 环境:
return SandboxedEnvironment( loader=FileSystemLoader(search_path), autoescape=False, variable_start_string=VARIABLE_START, # "<<" variable_end_string=VARIABLE_END, # ">>" block_start_string=BLOCK_START, # "<%" block_end_string=BLOCK_END, # "%>" comment_start_string=COMMENT_START, # "<#" comment_end_string=COMMENT_END, # "#>" keep_trailing_newline=True, trim_blocks=False, lstrip_blocks=False, )两个细节值得注意:
- 使用
SandboxedEnvironment(Jinja 沙箱环境),渲染在受限环境中执行; - 变量、块、注释分隔符分别配置,这意味着
<< >>只用于变量插值,控制流必须使用<% %>,模板作者需要保持这套分隔符使用的一致性。
包含与导入(Includes / Imports)
include与import均以模板的files/目录为基准解析:
<% include 'partials/header.yaml' %>因为 Jinja 环境的FileSystemLoader(search_path)的搜索路径正是files_dir,所以被包含文件应放在files/内的相对位置(如files/partials/header.yaml)。若引用的文件不存在,渲染时会抛出TemplateNotFound并被TemplateErrorHandler转换为“检查相对 files/ 目录的 include/import 路径”的建议。
模板发现规则
模板从配置好的模板库(library)中发现。一个目录只有在同时满足以下两个条件时才被视为合法模板:
- 存在
template.json; - 存在
files/。
实践中,Boilerplates 按模块目录路径发现模板,例如compose/<template>/或terraform/<template>/。
常用命令:
boilerplates compose list boilerplates compose search nginx boilerplates compose show nginx草稿模板:将metadata.draft置为true,即可把模板从正常发现(list / lookup)中隐藏。多库场景下的优先级与限定 ID 规则详见 Libraries 文档。
生成工作流(Generation Workflow)
典型流程:
boilerplates compose show nginx boilerplates compose generate nginx --output ./my-nginx先show查看模板运行时状态(含变量默认值、依赖、toggle 状态与文件结构),再generate实际产出文件。
常用旗标(flags):
--output:本地输出目录;--remote与--remote-path:SSH 远程上传目标;--var-file:YAML 格式的变量覆盖文件;--var:直接在命令行覆盖变量;--no-interactive:非交互式生成(配合--var使用可完全脚本化);--dry-run:预览生成结果而不写文件;--show-files:在 dry-run 时打印渲染出的文件内容。
变量值的最终生效顺序(从低到高):template.json的default/value→ 配置文件保存的默认值 →--var-file→--var→ 交互式提示的回答。你保存的用户默认值只会在清单值之上叠加、仍可被命令行覆盖,相关管理命令(defaults list / set / get / rm / clear)见 Defaults 文档。
验证(Validation)
校验单个模板:
boilerplates compose validate nginx校验模块下全部模板:
boilerplates compose validate验证覆盖范围:
- 清单结构(manifest structure);
- 变量声明覆盖(variable declaration coverage,即
files/用到的变量都已声明); - 分隔符兼容性(delimiter compatibility);
- 可渲染性(renderability);
- 可选的模块级语义校验器(optional semantic validators for module-specific output)。
从源码看,验证由ValidationRunner执行(validation_runner.py)。它基于依赖矩阵生成多个校验用例(每个用例是一组变量值组合),对每个用例:
- 执行渲染(失败记为
stage="tpl"的失败); - 开启语义校验时,用
get_validator_registry()注册的校验器逐一检查渲染产物(失败记为stage="sem",并带上具体文件与校验器类名); - 存在 kind 级校验器时,对输出做模块专属校验(失败记为
stage="kind";校验器不可用或明确跳过时,相关用例会被放入kind_skipped_cases)。
也就是说,validate不只是“能不能渲染”,而是会针对多组变量取值组合,验证模板在依赖关系(needs)约束下的整体正确性,并进一步检查输出是否符合对应模块(如 Compose、Terraform)的语义规则。这种“矩阵式验证”在 cli/core/validation/dependency_matrix.py 中被驱动。
最佳实践
- 清单统一使用
template.json; - 所有生成文件统一放在
files/下; - 渲染内容中出现过的每个变量都必须在清单中声明;
- 全模板保持一致地使用自定义分隔符;
- 除非确有必要引入变量,否则在渲染文件中硬编码经过测试的上游应用版本;
- 用
metadata.version.name作为面向用户的版本标签; - 其余
metadata.version字段用于记录上游快照(snapshot)上下文,按需填充即可。
这七条实践与源码的校验逻辑一一呼应:前三条保证了“清单即事实来源”的强校验能够通过;硬编码版本则避免为每个镜像 tag 引入不必要的变量声明;版本字段的拆分使用让“用户可见标签”与“上游溯源信息”各司其职,也正对应TemplateVersionMetadata中name与其余字段在展示时的不同角色。
延伸阅读
- 变量系统(Variables):变量组、条目字段、
needs依赖、toggle 与config对象; - 模板库(Libraries):Git/static 库、发现优先级与限定 ID;
- 默认值(Defaults):保存的用户默认值与覆盖顺序;
- 快速上手(Getting Started):安装 CLI、同步库、检查与生成模板;
- 模板核心实现:清单解析、分隔符配置、渲染与清洗的源码级细节;
- 校验运行器:矩阵式校验的三阶段(tpl / sem / kind)实现;
- 仓库内旧版模板示例(供参考变量分组设计):compose/nginx、compose/portainer。
【免费下载链接】boilerplatesCreate reusable templates and turn them into configurable workloads for homelabs and self-hosted infrastructure. Free and Open-Source.项目地址: https://gitcode.com/GitHub_Trending/bo/boilerplates
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考