03a281d009
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
3.7 KiB
Go
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
|
|
}
|