obm/internal/config/tfvars.go
MermaidMan 33d9a2cb2e deploy walkthrough, API validation, inference client, Hetzner provider
- Interactive deploy command with 8-step walkthrough:
  framework → provider → token → SSH → server → inference → tailscale → discord
- .env file generation from walkthrough config
- DeploymentConfig struct with framework-aware defaults
- Inference API client with validation for Venice, OpenRouter, OpenAI, Anthropic
- Hetzner Cloud provider: token validation, SSH key listing
- DotEnv parser/writer with schema validation
- Destroy command with confirmation prompt
- Validation subcommand for checking existing .env files
- All tests passing, go vet clean
2026-05-22 15:29:27 +00:00

252 lines
5.6 KiB
Go

package config
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
)
// WriteTfVars generates a terraform.tfvars file from a Config.
// The output is valid HCL syntax for Terraform variable files.
func WriteTfVars(cfg *Config, path string) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating tfvars file %s: %w", path, err)
}
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
// Write header
header := `# OpenBoatmobile Terraform Variables
# Generated by obm CLI
# Values set here can be overridden by environment variables (TF_VAR_<name>)
`
fmt.Fprint(w, header)
// Write variables by group
groups := VarsByGroup()
groupOrder := []VarGroup{
GroupProvider, GroupProviderHetzner, GroupProviderDO,
GroupServer, GroupSSH,
GroupAPIKeys, GroupModel,
GroupDiscord, GroupTailscale,
GroupHermes, GroupOpenClaw,
GroupSecurity, GroupProject,
}
writtenVars := make(map[string]bool)
for _, group := range groupOrder {
vars := groups[group]
if len(vars) == 0 {
continue
}
// Write group header
fmt.Fprintf(w, "\n# ===%s===\n", strings.Repeat("=", 73-len(string(group))-len("===")))
fmt.Fprintf(w, "# %s\n", group)
fmt.Fprintf(w, "# %s\n", strings.Repeat("-", len(group)))
fmt.Fprintln(w)
for _, v := range vars {
if writtenVars[v.Name] {
continue
}
writtenVars[v.Name] = true
writeTfVarsVar(w, v, cfg)
}
}
// Write any custom variables not in schema
customVars := []string{}
for name := range cfg.Variables {
if !writtenVars[name] {
customVars = append(customVars, name)
}
}
if len(customVars) > 0 {
fmt.Fprint(w, "\n# === Custom Variables ===\n")
sort.Strings(customVars)
for _, name := range customVars {
val := cfg.Variables[name]
fmt.Fprintf(w, "%s = %s\n\n", name, formatTfVarsValue(val))
}
}
return nil
}
// writeTfVarsVar writes a single variable to the tfvars file.
func writeTfVarsVar(w *bufio.Writer, v VarDef, cfg *Config) {
val, ok := cfg.GetValue(v.Name)
if !ok {
val = v.Default
}
// Write description comment
if v.Description != "" {
fmt.Fprintf(w, "# %s\n", v.Description)
}
// Mark required/sensitive
requirements := []string{}
if v.Required {
requirements = append(requirements, "required")
}
if v.Sensitive {
requirements = append(requirements, "sensitive: set via TF_VAR_"+v.Name)
}
if len(requirements) > 0 {
fmt.Fprintf(w, "# %s\n", strings.Join(requirements, ", "))
}
// For sensitive empty values, show placeholder
displayVal := val
if v.Sensitive && val == "" {
displayVal = "" // Will output as ""
fmt.Fprintf(w, "%s = \"\"\n", v.Name)
} else {
fmt.Fprintf(w, "%s = %s\n", v.Name, formatTfVarsValue(displayVal))
}
fmt.Fprintln(w) // Blank line after
}
// formatTfVarsValue formats a value for HCL/terraform.tfvars output.
func formatTfVarsValue(val string) string {
if val == "" {
return "\"\""
}
// Lists: keep as-is if already in HCL format
if strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]") {
return val
}
// Booleans: return as-is (true/false)
if val == "true" || val == "false" {
return val
}
// Numbers: return as-is if numeric
if isNumeric(val) {
return val
}
// Strings: quote
return fmt.Sprintf("\"%s\"", escapeHCLString(val))
}
// escapeHCLString escapes special characters for HCL strings.
func escapeHCLString(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "\r", "\\r")
s = strings.ReplaceAll(s, "\t", "\\t")
return s
}
// isNumeric checks if a string represents a number.
func isNumeric(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if (c < '0' || c > '9') && c != '-' && c != '.' {
return false
}
}
return true
}
// ParseTfVars reads a terraform.tfvars file and returns a Config.
// This is a simple parser that handles the common cases:
// - key = "value"
// - key = value (number/bool)
// - key = ["list", "values"]
// - # comments
func ParseTfVars(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening tfvars file %s: %w", path, err)
}
defer f.Close()
cfg := &Config{
Variables: make(map[string]string),
}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Parse key = value
idx := strings.Index(line, "=")
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
// Parse value
val = parseTfVarsValue(val)
cfg.Variables[key] = val
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("reading tfvars file %s: %w", path, err)
}
return cfg, nil
}
// parseTfVarsValue extracts the value from an HCL assignment.
func parseTfVarsValue(val string) string {
val = strings.TrimSpace(val)
// Boolean
if val == "true" || val == "false" {
return val
}
// Number
if isNumeric(val) {
return val
}
// Quoted string
if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' {
return unescapeHCLString(val[1 : len(val)-1])
}
// List
if strings.HasPrefix(val, "[") {
// Return as-is for lists (user needs to handle the format)
return val
}
// Unknown: return as-is
return val
}
// unescapeHCLString reverses HCL string escaping.
func unescapeHCLString(s string) string {
s = strings.ReplaceAll(s, "\\\"", "\"")
s = strings.ReplaceAll(s, "\\\\", "\\")
s = strings.ReplaceAll(s, "\\n", "\n")
s = strings.ReplaceAll(s, "\\r", "\r")
s = strings.ReplaceAll(s, "\\t", "\t")
return s
}