- 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
417 lines
13 KiB
Go
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
|
|
}
|