Files
panel/pkg/module/render.go
T
2026-07-14 19:19:43 -07:00

113 lines
3.7 KiB
Go

package module
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"text/template"
)
// Render writes out each ConfigFile declared by the manifest into dataPath,
// applying `values` (which typically combines user config_values + generated
// secrets) via Go text/template. Existing files are overwritten.
//
// Templates are resolved relative to the manifest's Dir. If a ConfigFile
// declares no Template, it is skipped — operators can also manage those
// files by direct edit via the (future) SFTP/file manager.
//
// Render uses each ConfigFile's default Template. Callers that know the
// instance's Steam branch should use RenderForBranch so version-specific
// templates (7DTD 3.0's serverconfig.v3.xml.tmpl) are selected.
func Render(manifest *Manifest, dataPath string, values map[string]string) error {
return RenderForBranch(manifest, dataPath, values, "")
}
// RenderForBranch is Render with branch-aware template selection: each
// ConfigFile renders cf.TemplateForBranch(branch) instead of cf.Template, so
// an instance on a 3.0 branch picks serverconfig.v3.xml.tmpl while ≤2.6
// instances (branch "") keep the default template. branch is the normalized
// Steam branch (see resolveCreateBranch); "" means the module default.
func RenderForBranch(manifest *Manifest, dataPath string, values map[string]string, branch string) error {
if manifest == nil {
return fmt.Errorf("render: manifest is nil")
}
if manifest.Dir == "" {
return fmt.Errorf("render: manifest.Dir is empty (loader must set it)")
}
for _, cf := range manifest.ConfigFiles {
tmpl := cf.TemplateForBranch(branch)
if tmpl == "" {
continue
}
if err := renderOne(manifest.Dir, cf, tmpl, dataPath, values); err != nil {
return err
}
}
return nil
}
func renderOne(moduleDir string, cf ConfigFile, templateRel string, dataPath string, values map[string]string) error {
templatePath := filepath.Join(moduleDir, templateRel)
tmplData, err := os.ReadFile(templatePath)
if err != nil {
return fmt.Errorf("read template %s: %w", templatePath, err)
}
tmpl, err := template.New(cf.Path).Option("missingkey=zero").Parse(string(tmplData))
if err != nil {
return fmt.Errorf("parse template %s: %w", templateRel, err)
}
var buf bytes.Buffer
ctx := struct {
Values map[string]string
}{Values: values}
if err := tmpl.Execute(&buf, ctx); err != nil {
return fmt.Errorf("execute template %s: %w", templateRel, err)
}
outPath := filepath.Join(dataPath, cf.Path)
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", filepath.Dir(outPath), err)
}
if err := os.WriteFile(outPath, buf.Bytes(), 0o644); err != nil {
return fmt.Errorf("write %s: %w", outPath, err)
}
return nil
}
// GenerateSecrets emits a random value for each manifest Secret flagged
// Generated. Values are URL-safe base64 of 24 bytes — safe in XML, shell,
// URLs, and long enough to resist brute force on a local RCON surface.
//
// Callers typically merge this with user-supplied config_values before
// rendering templates and resolving RCON passwords.
func GenerateSecrets(manifest *Manifest) (map[string]string, error) {
if manifest == nil {
return nil, nil
}
out := map[string]string{}
for _, s := range manifest.Secrets {
if !s.Generated {
continue
}
val, err := randomToken(24)
if err != nil {
return nil, fmt.Errorf("generate secret %q: %w", s.Name, err)
}
out[s.Name] = val
}
return out, nil
}
// randomToken returns a URL-safe base64 string of n random bytes.
func randomToken(nBytes int) (string, error) {
b := make([]byte, nBytes)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}