obm/internal/config/schema.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

417 lines
13 KiB
Go

// Package config handles loading, parsing, and writing obm configuration.
// It supports .env files (TF_VAR_ prefixed) and terraform.tfvars generation.
package config
// ValueType represents the Terraform variable type.
type ValueType string
const (
TypeString ValueType = "string"
TypeNumber ValueType = "number"
TypeBool ValueType = "bool"
TypeList ValueType = "list"
)
// VarGroup categorizes variables for organized output.
type VarGroup string
const (
GroupProvider VarGroup = "PROVIDER"
GroupProviderDO VarGroup = "PROVIDER — DigitalOcean"
GroupProviderHetzner VarGroup = "PROVIDER — Hetzner"
GroupServer VarGroup = "SERVER CONFIGURATION"
GroupSSH VarGroup = "SSH CONFIGURATION"
GroupAPIKeys VarGroup = "API KEYS"
GroupDiscord VarGroup = "DISCORD"
GroupTailscale VarGroup = "TAILSCALE"
GroupHermes VarGroup = "HERMES-SPECIFIC"
GroupOpenClaw VarGroup = "OPENCLAW-SPECIFIC"
GroupSecurity VarGroup = "SECURITY"
GroupProject VarGroup = "PROJECT METADATA"
GroupModel VarGroup = "MODEL CONFIGURATION"
)
// VarDef defines a single Terraform variable with metadata.
type VarDef struct {
Name string // TF variable name (e.g. "cloud_provider")
Type ValueType // string, number, bool, list
Default string // Default value as string (empty = no default)
Required bool // Must be set by user
Sensitive bool // Secret value (redacted in output)
Description string // Human-readable description
Group VarGroup // Section for organized output
EnvComment string // Additional .env comment hint (e.g. "or digitalocean")
}
// schemaCache stores the schema to avoid reallocation. Must be initialized first.
var schemaCache []VarDef
// init initializes the schema cache.
func init() {
schemaCache = buildSchema()
}
// buildSchema constructs the complete variable schema.
// Order matters — this controls the output order in .env and tfvars.
func buildSchema() []VarDef {
return []VarDef{
// --- Provider Selection ---
{
Name: "cloud_provider", Type: TypeString, Default: "hetzner",
Required: true, Sensitive: false,
Description: "Cloud provider to use: 'digitalocean' or 'hetzner'",
Group: GroupProvider,
EnvComment: "or digitalocean",
},
{
Name: "agent_framework", Type: TypeString, Default: "hermes",
Required: false, Sensitive: false,
Description: "Agent framework to deploy: 'openclaw' or 'hermes'",
Group: GroupProvider,
},
// --- Provider Tokens ---
{
Name: "hcloud_token", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Hetzner Cloud API token",
Group: GroupProviderHetzner,
},
{
Name: "do_token", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "DigitalOcean API token",
Group: GroupProviderDO,
},
// --- Server Configuration ---
{
Name: "server_name", Type: TypeString, Default: "agent-gateway",
Required: false, Sensitive: false,
Description: "Hostname for the server",
Group: GroupServer,
},
{
Name: "server_type_hetzner", Type: TypeString, Default: "cpx21",
Required: false, Sensitive: false,
Description: "Hetzner server type (e.g., cx23, cpx21)",
Group: GroupProviderHetzner,
},
{
Name: "server_image", Type: TypeString, Default: "ubuntu-24.04",
Required: false, Sensitive: false,
Description: "Hetzner server image (e.g., ubuntu-24.04)",
Group: GroupProviderHetzner,
},
{
Name: "location_hetzner", Type: TypeString, Default: "ash",
Required: false, Sensitive: false,
Description: "Hetzner location (nbg1, fsn1, hel1, ash)",
Group: GroupProviderHetzner,
},
{
Name: "droplet_size_digitalocean", Type: TypeString, Default: "s-2vcpu-4gb",
Required: false, Sensitive: false,
Description: "DigitalOcean droplet size (e.g., s-2vcpu-4gb)",
Group: GroupProviderDO,
},
{
Name: "region_digitalocean", Type: TypeString, Default: "nyc3",
Required: false, Sensitive: false,
Description: "DigitalOcean region (e.g., nyc3, sfo2, ams3)",
Group: GroupProviderDO,
},
{
Name: "create_network", Type: TypeBool, Default: "false",
Required: false, Sensitive: false,
Description: "Create a private network for multi-server deployments",
Group: GroupServer,
},
{
Name: "network_ip_range", Type: TypeString, Default: "10.10.0.0/16",
Required: false, Sensitive: false,
Description: "IP range for private network",
Group: GroupServer,
},
{
Name: "network_zone", Type: TypeString, Default: "eu-central",
Required: false, Sensitive: false,
Description: "Hetzner network zone",
Group: GroupProviderHetzner,
},
// --- SSH Configuration ---
{
Name: "ssh_key_names", Type: TypeList, Default: "[]",
Required: false, Sensitive: false,
Description: "SSH key names (Hetzner: key name in console)",
Group: GroupSSH,
},
{
Name: "ssh_key_fingerprints", Type: TypeList, Default: "[]",
Required: false, Sensitive: false,
Description: "DigitalOcean SSH key fingerprints",
Group: GroupSSH,
},
{
Name: "ssh_port", Type: TypeNumber, Default: "22",
Required: false, Sensitive: false,
Description: "SSH port (non-standard can be more secure)",
Group: GroupSSH,
},
{
Name: "ssh_allowed_ips", Type: TypeList, Default: `["0.0.0.0/0", "::/0"]`,
Required: false, Sensitive: false,
Description: "IPs allowed to connect via SSH",
Group: GroupSSH,
},
{
Name: "admin_user", Type: TypeString, Default: "",
Required: false, Sensitive: false,
Description: "Admin username (defaults to framework name)",
Group: GroupSSH,
},
{
Name: "admin_ssh_keys", Type: TypeList, Default: "[]",
Required: false, Sensitive: false,
Description: "Additional public SSH keys for admin user",
Group: GroupSSH,
},
// --- API Keys ---
{
Name: "venice_api_key", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Venice AI API key for inference",
Group: GroupAPIKeys,
},
{
Name: "brave_search_api_key", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Brave Search API key",
Group: GroupAPIKeys,
},
// --- Model Configuration ---
{
Name: "primary_model", Type: TypeString, Default: "olafangensan-glm-4.7-flash-heretic",
Required: false, Sensitive: false,
Description: "Primary model for inference",
Group: GroupModel,
},
{
Name: "primary_model_name", Type: TypeString, Default: "GLM 4.7 Flash Heretic",
Required: false, Sensitive: false,
Description: "Human-readable name for the primary model",
Group: GroupModel,
},
{
Name: "fallback_models", Type: TypeList, Default: `["zai-org-glm-5"]`,
Required: false, Sensitive: false,
Description: "Fallback models in priority order",
Group: GroupModel,
},
{
Name: "venice_base_url", Type: TypeString, Default: "https://api.venice.ai/api/v1",
Required: false, Sensitive: false,
Description: "Venice AI base URL",
Group: GroupModel,
},
// --- Discord ---
{
Name: "discord_bot_token", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Discord bot token",
Group: GroupDiscord,
},
{
Name: "discord_server_id", Type: TypeString, Default: "",
Required: false, Sensitive: false,
Description: "Discord server/guild ID",
Group: GroupDiscord,
},
{
Name: "discord_user_id", Type: TypeList, Default: "[]",
Required: false, Sensitive: false,
Description: "Discord user IDs for allowlist",
Group: GroupDiscord,
},
{
Name: "discord_home_channel", Type: TypeString, Default: "",
Required: false, Sensitive: false,
Description: "Discord channel ID for home channel (Hermes)",
Group: GroupDiscord,
},
{
Name: "discord_allowed_users", Type: TypeString, Default: "",
Required: false, Sensitive: false,
Description: "Comma-separated Discord user IDs allowed (Hermes)",
Group: GroupDiscord,
},
{
Name: "discord_auto_thread", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Auto-create threads on @mention (Hermes)",
Group: GroupDiscord,
},
// --- Tailscale ---
{
Name: "enable_tailscale", Type: TypeBool, Default: "false",
Required: false, Sensitive: false,
Description: "Install Tailscale for secure remote access",
Group: GroupTailscale,
},
{
Name: "tailscale_auth_key", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Tailscale auth key",
Group: GroupTailscale,
},
{
Name: "tailscale_tailnet_domain", Type: TypeString, Default: "tailnet",
Required: false, Sensitive: false,
Description: "Tailscale tailnet domain (without .ts.net suffix)",
Group: GroupTailscale,
},
// --- Hermes-specific ---
{
Name: "agent_name", Type: TypeString, Default: "hermes",
Required: false, Sensitive: false,
Description: "Name for the agent (Hermes)",
Group: GroupHermes,
},
{
Name: "docker_enabled", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Deploy in Docker (true) or install directly (false)",
Group: GroupHermes,
},
{
Name: "gateway_token", Type: TypeString, Default: "",
Required: false, Sensitive: true,
Description: "Gateway authentication token (Hermes)",
Group: GroupHermes,
},
{
Name: "gateway_allowed_users", Type: TypeString, Default: "",
Required: false, Sensitive: false,
Description: "Comma-separated list of allowed user IDs (Hermes gateway)",
Group: GroupHermes,
},
{
Name: "gateway_allow_all_users", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Allow all users without allowlist (Hermes gateway)",
Group: GroupHermes,
},
{
Name: "agent_timezone", Type: TypeString, Default: "UTC",
Required: false, Sensitive: false,
Description: "Timezone for the agent",
Group: GroupHermes,
},
// --- OpenClaw-specific ---
{
Name: "openclaw_version", Type: TypeString, Default: "lts",
Required: false, Sensitive: false,
Description: "OpenClaw version: 'latest', 'lts', or specific version",
Group: GroupOpenClaw,
},
{
Name: "node_version", Type: TypeString, Default: "22",
Required: false, Sensitive: false,
Description: "Node.js major version (22 recommended)",
Group: GroupOpenClaw,
},
{
Name: "enable_swap", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Create a swap file on the server",
Group: GroupOpenClaw,
},
{
Name: "swap_size", Type: TypeNumber, Default: "2",
Required: false, Sensitive: false,
Description: "Switch file size in GB",
Group: GroupOpenClaw,
},
// --- Security ---
{
Name: "enable_fail2ban", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Install and configure fail2ban for SSH protection",
Group: GroupSecurity,
},
{
Name: "enable_unattended_upgrades", Type: TypeBool, Default: "true",
Required: false, Sensitive: false,
Description: "Enable automatic security updates",
Group: GroupSecurity,
},
// --- Project Metadata ---
{
Name: "project_name", Type: TypeString, Default: "OpenBoatmobile",
Required: false, Sensitive: false,
Description: "Project name for tagging",
Group: GroupProject,
},
{
Name: "environment", Type: TypeString, Default: "production",
Required: false, Sensitive: false,
Description: "Environment name (e.g., production, staging, development)",
Group: GroupProject,
},
}
}
// Schema returns the complete variable schema for OpenBoatmobile.
// Order matters — this controls the output order in .env and tfvars.
func Schema() []VarDef {
return schemaCache
}
// SchemaMap returns a lookup map of variable name -> VarDef.
func SchemaMap() map[string]VarDef {
m := make(map[string]VarDef, len(schemaCache))
for _, v := range schemaCache {
m[v.Name] = v
}
return m
}
// RequiredVars returns only the required variables.
func RequiredVars() []VarDef {
var out []VarDef
for _, v := range schemaCache {
if v.Required {
out = append(out, v)
}
}
return out
}
// SensitiveVars returns only the sensitive variables.
func SensitiveVars() []VarDef {
var out []VarDef
for _, v := range schemaCache {
if v.Sensitive {
out = append(out, v)
}
}
return out
}
// VarsByGroup returns variables organized by group, preserving order.
func VarsByGroup() map[VarGroup][]VarDef {
out := make(map[VarGroup][]VarDef)
for _, v := range schemaCache {
out[v.Group] = append(out[v.Group], v)
}
return out
}