重构架构
This commit is contained in:
53
pkg/templating/embedded.go
Normal file
53
pkg/templating/embedded.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// internal/templating/embedded.go
|
||||
package templating
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sunhpc/data/tmpls"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
|
||||
func ListEmbeddedTemplates() ([]string, error) {
|
||||
entries, err := fs.ReadDir(tmpls.ConfigFS, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".yaml" {
|
||||
continue
|
||||
}
|
||||
names = append(names, entry.Name()[:len(entry.Name())-5]) // 去掉 .yaml
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// LoadEmbeddedTemplate 从二进制加载内置模板
|
||||
func LoadEmbeddedTemplate(name string) (*Template, error) {
|
||||
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var tmpl Template
|
||||
if err := yaml.Unmarshal(data, &tmpl); err != nil {
|
||||
return nil, fmt.Errorf("解析内置模板失败: %w", err)
|
||||
}
|
||||
return &tmpl, nil
|
||||
}
|
||||
|
||||
// DumpEmbeddedTemplateToFile 将内置模板写入文件
|
||||
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
|
||||
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
|
||||
}
|
||||
return os.WriteFile(outputPath, data, 0644)
|
||||
}
|
||||
104
pkg/templating/engine.go
Normal file
104
pkg/templating/engine.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// internal/templating/engine.go
|
||||
package templating
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// LoadTemplate 从文件加载 YAML 模板
|
||||
func LoadTemplate(path string) (*Template, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法读取模板文件 %s: %w", path, err)
|
||||
}
|
||||
var tmpl Template
|
||||
if err := yaml.Unmarshal(data, &tmpl); err != nil {
|
||||
return nil, fmt.Errorf("YAML 解析失败: %w", err)
|
||||
}
|
||||
return &tmpl, nil
|
||||
}
|
||||
|
||||
// Render 渲染模板为具体操作
|
||||
func (t *Template) Render(ctx Context) (map[string][]RenderedStep, error) {
|
||||
result := make(map[string][]RenderedStep)
|
||||
|
||||
for stageName, steps := range t.Stages {
|
||||
var renderedSteps []RenderedStep
|
||||
for _, step := range steps {
|
||||
// 处理 condition
|
||||
if step.Condition != "" {
|
||||
condTmpl, err := template.New("condition").Parse(step.Condition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("条件模板语法错误: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := condTmpl.Execute(&buf, ctx); err != nil {
|
||||
return nil, fmt.Errorf("执行条件模板失败: %w", err)
|
||||
}
|
||||
if buf.String() == "" {
|
||||
continue // 条件不满足,跳过
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染 content
|
||||
contentTmpl, err := template.New("content").Parse(step.Content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("内容模板语法错误: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := contentTmpl.Execute(&buf, ctx); err != nil {
|
||||
return nil, fmt.Errorf("执行内容模板失败: %w", err)
|
||||
}
|
||||
|
||||
renderedSteps = append(renderedSteps, RenderedStep{
|
||||
Type: step.Type,
|
||||
Path: step.Path,
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
result[stageName] = renderedSteps
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RenderedStep 是渲染后的步骤
|
||||
type RenderedStep struct {
|
||||
Type string
|
||||
Path string
|
||||
Content string
|
||||
}
|
||||
|
||||
// WriteFiles 将 file 类型步骤写入磁盘
|
||||
func WriteFiles(steps []RenderedStep, rootDir string) error {
|
||||
for _, s := range steps {
|
||||
if s.Type != "file" {
|
||||
continue
|
||||
}
|
||||
fullPath := s.Path
|
||||
if !filepath.IsAbs(s.Path) {
|
||||
fullPath = filepath.Join(rootDir, s.Path)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(fullPath, []byte(s.Content), 0644); err != nil {
|
||||
return fmt.Errorf("写入文件 %s 失败: %w", fullPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintScripts 打印 script 内容(安全起见,先不自动执行)
|
||||
func PrintScripts(steps []RenderedStep) {
|
||||
for _, s := range steps {
|
||||
if s.Type == "script" {
|
||||
fmt.Printf("# --- 脚本开始 ---\n%s\n# --- 脚本结束 ---\n", s.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
pkg/templating/types.go
Normal file
38
pkg/templating/types.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package templating
|
||||
|
||||
// Template 是 YAML 模板的顶层结构
|
||||
type Template struct {
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Copyright string `yaml:"copyright,omitempty"`
|
||||
Stages map[string][]Step `yaml:"stages"`
|
||||
}
|
||||
|
||||
// Step 表示一个操作步骤
|
||||
type Step struct {
|
||||
Type string `yaml:"type"` // "file" 或 "script"
|
||||
Path string `yaml:"path,omitempty"` // 文件路径(仅 type=file)
|
||||
Content string `yaml:"content"` // 多行内容
|
||||
Condition string `yaml:"condition,omitempty"` // 条件表达式(Go template)
|
||||
}
|
||||
|
||||
// Context 是渲染模板时的上下文数据
|
||||
type Context struct {
|
||||
Node NodeInfo `json:"node"`
|
||||
Cluster ClusterInfo `json:"cluster"`
|
||||
}
|
||||
|
||||
// NodeInfo 节点信息
|
||||
type NodeInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OldHostname string `json:"old_hostname,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// ClusterInfo 集群信息
|
||||
type ClusterInfo struct {
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
}
|
||||
Reference in New Issue
Block a user