基础设施模板CLI工具:Boilerplates
Boilerplates CLI
Boilerplates 是一个用于管理基础设施模板(boilerplates)的复杂集合,并配备了一个Python CLI工具。它支持Terraform、Docker、Ansible、Kubernetes等多种技术,帮助您快速生成、定制和部署配置模板。
功能特性
- 多技术模板支持:提供Docker Compose、Terraform、Ansible、Kubernetes、Packer等基础设施模板。
- 交互式变量收集:通过交互式CLI提示,引导用户为模板变量输入值。
- 智能默认值管理:支持保存常用变量的默认值,并在多个项目中复用。
- Git仓库集成:模板库基于Git管理,支持添加、更新和移除自定义模板仓库。
- Jinja2模板渲染:使用Jinja2引擎渲染模板,支持条件语句和变量替换。
- 模块化架构:通过模块系统(Module)组织不同技术的模板和命令。
- 配置文件管理:提供配置管理功能,存储用户默认值和偏好设置。
- 安全与验证:包含模板语法验证、变量类型检查和路径安全防护。
安装指南
使用安装脚本(推荐)
通过自动化安装脚本安装Boilerplates CLI:
# 安装最新版本
curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash
# 安装特定版本
curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --version v1.2.3
安装脚本使用pipx为CLI工具创建一个隔离环境。安装完成后,终端中即可使用boilerplates命令。
依赖要求
- Python 3+
- Git(用于模板库管理)
- pipx(用于隔离安装,安装脚本会自动检查)
使用说明
基础命令
# 查看帮助信息
boilerplates --help
# 查看版本
boilerplates --version
# 更新模板库
boilerplates repo update
# 列出所有已配置的库
boilerplates repo list
模板操作
# 列出所有可用的Docker Compose模板
boilerplates compose list
# 查看特定模板的详细信息
boilerplates compose show nginx
# 生成模板(交互式模式)
boilerplates compose generate authentik
# 生成模板到自定义输出目录
boilerplates compose generate nginx my-nginx-server
# 非交互式模式,直接指定变量值
boilerplates compose generate traefik my-proxy \
--var service_name=traefik \
--var traefik_enabled=true \
--var traefik_host=proxy.example.com \
--no-interactive
管理默认值
# 设置变量的默认值
boilerplates compose defaults set container_timezone "America/New_York"
boilerplates compose defaults set restart_policy "unless-stopped"
模板库管理
# 添加自定义模板库
boilerplates repo add my-templates https://github.com/user/templates \
--directory library \
--branch main
# 移除模板库
boilerplates repo remove my-templates
核心代码
CLI主入口 (cli/__main__.py)
#!/usr/bin/env python3
"""
Main entry point for the Boilerplates CLI application.
This file serves as the primary executable when running the CLI.
"""
from __future__ import annotations
import importlib
import logging
import pkgutil
import sys
from pathlib import Path
from typing import Optional
from typer import Typer, Context, Option
from rich.console import Console
import cli.modules
from cli.core.registry import registry
from cli.core import repo
# Using standard Python exceptions instead of custom ones
# NOTE: Placeholder version - will be overwritten by release script (.github/workflows/release.yaml)
__version__ = "0.0.0"
app = Typer(
help="CLI tool for managing infrastructure boilerplates.\n\n[dim]Easily generate, customize, and deploy templates for Docker Compose, Terraform, Kubernetes, and more.\n\n [white]Made with :purple_heart: by [bold]Christian Lempa[/bold]",
add_completion=True,
rich_markup_mode="rich",
)
console = Console()
def setup_logging(log_level: str = "WARNING") -> None:
"""Configure the logging system with the specified log level.
Args:
log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Raises:
ValueError: If the log level is invalid
RuntimeError: If logging configuration fails
"""
numeric_level = getattr(logging, log_level.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError(
f"Invalid log level '{log_level}'. Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL"
)
try:
logging.basicConfig(
level=numeric_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
logger.setLevel(numeric_level)
except Exception as e:
raise RuntimeError(f"Failed to configure logging: {e}")
@app.callback(invoke_without_command=True)
def main(
version: Optional[bool] = Option(
None,
"--version",
"-v",
help="Show the application version and exit",
),
log_level: str = Option(
"WARNING",
"--log-level",
"-l",
help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
),
ctx: Context = None,
) -> None:
"""Main entry point for the boilerplates CLI.
Args:
version: Show version information
log_level: Logging level for the application
ctx: Typer context object
"""
if version:
console.print(f"Boilerplates CLI [bold]v{__version__}[/bold]")
raise SystemExit(0)
try:
setup_logging(log_level)
except (ValueError, RuntimeError) as e:
console_err.print(f"[red]Failed to set up logging: {e}[/red]")
sys.exit(1)
# Discover and register modules
_discover_modules()
# Register repository management commands
app.add_typer(repo.app, name="repo")
if ctx.invoked_subcommand is None:
console.print(app.info.help)
raise SystemExit(0)
def _discover_modules() -> None:
"""Dynamically discover and register all modules in the modules package."""
try:
for _, name, _ in pkgutil.iter_modules(cli.modules.__path__):
module_path = f"cli.modules.{name}"
try:
importlib.import_module(module_path)
logger.debug(f"Successfully imported module: {name}")
except Exception as e:
logger.warning(f"Failed to import module '{name}': {e}")
continue
logger.info(f"Module discovery completed. Total modules: {len(list(registry.iter_module_classes()))}")
except Exception as e:
logger.error(f"Module discovery failed: {e}")
raise
if __name__ == "__main__":
app()
变量集合管理 (cli/core/collection.py)
from __future__ import annotations
from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Union
import logging
from .variable import Variable
from .section import VariableSection
logger = logging.getLogger(__name__)
class VariableCollection:
"""Manages variables grouped by sections and builds Jinja context."""
def __init__(self, spec: dict[str, Any]) -> None:
"""Initialize VariableCollection from a specification dictionary.
Args:
spec: Dictionary containing the complete variable specification structure
Expected format (as used in compose.py):
{
"section_key": {
"title": "Section Title",
"prompt": "Optional prompt text",
"toggle": "optional_toggle_var_name",
"description": "Optional description",
"vars": {
"var_name": {
"description": "Variable description",
"type": "str",
"default": "default_value",
...
}
}
}
}
"""
if not isinstance(spec, dict):
raise ValueError("Spec must be a dictionary")
self._sections: Dict[str, VariableSection] = {}
# NOTE: The _variable_map provides a flat, O(1) lookup for any variable by its name,
# avoiding the need to iterate through sections. It stores references to the same
# Variable objects contained in the _set structure.
self._variable_map: Dict[str, Variable] = {}
self._initialize_sections(spec)
# Validate dependencies after all sections are loaded
self._validate_dependencies()
def _initialize_sections(self, spec: dict[str, Any]) -> None:
"""Initialize sections from the spec."""
for section_key, section_data in spec.items():
if not isinstance(section_data, dict):
continue
# Create VariableSection
section_dict = {
"key": section_key,
"title": section_data.get("title", section_key.title()),
"description": section_data.get("description"),
"toggle": section_data.get("toggle"),
"required": section_data.get("required", section_key == "general"),
}
# Handle dependencies
if needs := section_data.get("needs"):
section_dict["needs"] = needs
section = VariableSection(section_dict)
# Add variables to section
if "vars" in section_data and isinstance(section_data["vars"], dict):
for var_name, var_data in section_data["vars"].items():
if not isinstance(var_data, dict):
continue
# Create Variable
var_dict = var_data.copy()
var_dict["name"] = var_name
var_dict["origin"] = "template"
variable = Variable(var_dict)
# Store in section and flat map
section.variables[var_name] = variable
self._variable_map[var_name] = variable
self._sections[section_key] = section
def _validate_dependencies(self) -> None:
"""Validate that all section dependencies exist."""
for section_key, section in self._sections.items():
for dep in section.needs:
if dep not in self._sections:
logger.warning(
f"Section '{section_key}' depends on non-existent section '{dep}'"
)
def get_section(self, section_key: str) -> Optional[VariableSection]:
"""Get a section by its key."""
return self._sections.get(section_key)
def get_sections(self) -> Dict[str, VariableSection]:
"""Get all sections."""
return self._sections.copy()
def is_section_satisfied(self, section_key: str) -> bool:
"""Check if a section's dependencies are satisfied."""
section = self._sections.get(section_key)
if not section:
return False
# General section is always satisfied
if section_key == "general":
return True
# Check all dependencies
for dep in section.needs:
dep_section = self._sections.get(dep)
if not dep_section or not dep_section.is_enabled():
return False
return True
def get_jinja_context(self) -> Dict[str, Any]:
"""Build Jinja2 template context from all variables.
Returns:
Dictionary mapping variable names to their values for
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码


浙公网安备 33010602011771号