Boilerplates 模板机制深度指南:template.json 清单、files/ 渲染管线与自定义分隔符
2026/9/16 12:56:13 网站建设 项目流程

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.yamltemplate.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:模板类型(如composeterraformansible);
  • 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):

  1. 定位并解析template.json(必须是合法 JSON 对象,否则报“must contain a JSON object”);
  2. 构造TemplateMetadata,强制要求存在metadata对象;
  3. 校验kind字段必须存在(Template._validate_kind);
  4. 依据slug/ 目录名解析模板 ID;
  5. 校验files/目录必须存在(缺失时报missing required 'files/' directory);
  6. 校验variables必须是数组。

其中kind缺失、metadata缺失、variables非数组都会抛出TemplateValidationError,并最终包装为TemplateLoadError呈现在 CLI 中。也就是说,清单的格式错误不会在渲染时才暴露,而是在加载阶段就被拦截。

slug 与模板 ID 的解析规则

slug是 CLI 对外暴露的规范化模板 ID。其解析行为优先级如下:

  • slug存在,以slug为准(覆盖目录名);
  • slug-<kind>结尾,则该冗余后缀会被归一化去除;
  • slug缺失,退化为使用目录名。

示例:

  • 目录:portainer/
  • kindcompose
  • slugportainer-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_pathoutput_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, )

两个细节值得注意:

  1. 使用SandboxedEnvironment(Jinja 沙箱环境),渲染在受限环境中执行;
  2. 变量、块、注释分隔符分别配置,这意味着<< >>只用于变量插值,控制流必须使用<% %>,模板作者需要保持这套分隔符使用的一致性。

包含与导入(Includes / Imports)

includeimport均以模板的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.jsondefault/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)。它基于依赖矩阵生成多个校验用例(每个用例是一组变量值组合),对每个用例:

  1. 执行渲染(失败记为stage="tpl"的失败);
  2. 开启语义校验时,用get_validator_registry()注册的校验器逐一检查渲染产物(失败记为stage="sem",并带上具体文件与校验器类名);
  3. 存在 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 引入不必要的变量声明;版本字段的拆分使用让“用户可见标签”与“上游溯源信息”各司其职,也正对应TemplateVersionMetadataname与其余字段在展示时的不同角色。

延伸阅读

  • 变量系统(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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询