orama/pkg/cli/sandbox/config.go
anonpenguin23 a0468461ab feat(sandbox): add reset command and interactive setup
- new `orama sandbox reset` deletes Hetzner resources (IPs, firewall, SSH key) and local config
- interactive location/server type selection during `setup`
- add Hetzner API methods for listing locations/types, deleting resources
- update defaults to nbg1/cx23
2026-02-28 10:14:02 +02:00

154 lines
3.9 KiB
Go

package sandbox
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Config holds sandbox configuration, stored at ~/.orama/sandbox.yaml.
type Config struct {
HetznerAPIToken string `yaml:"hetzner_api_token"`
Domain string `yaml:"domain"`
Location string `yaml:"location"` // Hetzner datacenter (default: fsn1)
ServerType string `yaml:"server_type"` // Hetzner server type (default: cx22)
FloatingIPs []FloatIP `yaml:"floating_ips"`
SSHKey SSHKeyConfig `yaml:"ssh_key"`
FirewallID int64 `yaml:"firewall_id,omitempty"` // Hetzner firewall resource ID
}
// FloatIP holds a Hetzner floating IP reference.
type FloatIP struct {
ID int64 `yaml:"id"`
IP string `yaml:"ip"`
}
// SSHKeyConfig holds SSH key paths and the Hetzner resource ID.
type SSHKeyConfig struct {
HetznerID int64 `yaml:"hetzner_id"`
PrivateKeyPath string `yaml:"private_key_path"`
PublicKeyPath string `yaml:"public_key_path"`
}
// configDir returns ~/.orama/, creating it if needed.
func configDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home directory: %w", err)
}
dir := filepath.Join(home, ".orama")
if err := os.MkdirAll(dir, 0700); err != nil {
return "", fmt.Errorf("create config directory: %w", err)
}
return dir, nil
}
// configPath returns the full path to ~/.orama/sandbox.yaml.
func configPath() (string, error) {
dir, err := configDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "sandbox.yaml"), nil
}
// LoadConfig reads the sandbox config from ~/.orama/sandbox.yaml.
// Returns an error if the file doesn't exist (user must run setup first).
func LoadConfig() (*Config, error) {
path, err := configPath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("sandbox not configured, run: orama sandbox setup")
}
return nil, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
cfg.Defaults()
return &cfg, nil
}
// SaveConfig writes the sandbox config to ~/.orama/sandbox.yaml.
func SaveConfig(cfg *Config) error {
path, err := configPath()
if err != nil {
return err
}
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := os.WriteFile(path, data, 0600); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}
// validate checks that required fields are present.
func (c *Config) validate() error {
if c.HetznerAPIToken == "" {
return fmt.Errorf("hetzner_api_token is required")
}
if c.Domain == "" {
return fmt.Errorf("domain is required")
}
if len(c.FloatingIPs) < 2 {
return fmt.Errorf("2 floating IPs required, got %d", len(c.FloatingIPs))
}
if c.SSHKey.PrivateKeyPath == "" {
return fmt.Errorf("ssh_key.private_key_path is required")
}
return nil
}
// Defaults fills in default values for optional fields.
func (c *Config) Defaults() {
if c.Location == "" {
c.Location = "nbg1"
}
if c.ServerType == "" {
c.ServerType = "cx23"
}
}
// ExpandedPrivateKeyPath returns the absolute path to the SSH private key.
func (c *Config) ExpandedPrivateKeyPath() string {
return expandHome(c.SSHKey.PrivateKeyPath)
}
// ExpandedPublicKeyPath returns the absolute path to the SSH public key.
func (c *Config) ExpandedPublicKeyPath() string {
return expandHome(c.SSHKey.PublicKeyPath)
}
// expandHome replaces a leading ~ with the user's home directory.
func expandHome(path string) string {
if len(path) < 2 || path[:2] != "~/" {
return path
}
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[2:])
}