You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
369 lines
12 KiB
369 lines
12 KiB
package main |
|
|
|
import ( |
|
"encoding/json" |
|
"os" |
|
"path/filepath" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/adrg/xdg" |
|
) |
|
|
|
// ConfigFile is the JSON structure for persistent configuration. |
|
type ConfigFile struct { |
|
DBBackend string `json:"db_backend,omitempty"` |
|
DBBinary string `json:"db_binary,omitempty"` |
|
RelayBinary string `json:"relay_binary,omitempty"` |
|
ACLBinary string `json:"acl_binary,omitempty"` |
|
DBListen string `json:"db_listen,omitempty"` |
|
ACLListen string `json:"acl_listen,omitempty"` |
|
ACLEnabled *bool `json:"acl_enabled,omitempty"` |
|
ACLMode string `json:"acl_mode,omitempty"` |
|
DataDir string `json:"data_dir,omitempty"` |
|
LogLevel string `json:"log_level,omitempty"` |
|
AdminPort *int `json:"admin_port,omitempty"` |
|
AdminOwners []string `json:"admin_owners,omitempty"` |
|
BinDir string `json:"bin_dir,omitempty"` |
|
RelayPort *int `json:"relay_port,omitempty"` |
|
RelayHost string `json:"relay_host,omitempty"` |
|
TLSDomains string `json:"tls_domains,omitempty"` |
|
AuthToWrite *bool `json:"auth_to_write,omitempty"` |
|
AuthRequired *bool `json:"auth_required,omitempty"` |
|
|
|
// Sync services |
|
DistributedSyncEnabled *bool `json:"distributed_sync_enabled,omitempty"` |
|
ClusterSyncEnabled *bool `json:"cluster_sync_enabled,omitempty"` |
|
RelayGroupEnabled *bool `json:"relay_group_enabled,omitempty"` |
|
NegentropyEnabled *bool `json:"negentropy_enabled,omitempty"` |
|
NegentropyBinary string `json:"negentropy_binary,omitempty"` |
|
NegentropyListen string `json:"negentropy_listen,omitempty"` |
|
|
|
// Certificate service |
|
CertsEnabled *bool `json:"certs_enabled,omitempty"` |
|
CertsBinary string `json:"certs_binary,omitempty"` |
|
} |
|
|
|
// configFilePath returns the path to the config file. |
|
func configFilePath() string { |
|
return filepath.Join(xdg.ConfigHome, "orly", "launcher.json") |
|
} |
|
|
|
// loadConfigFile loads configuration from the JSON file if it exists. |
|
func loadConfigFile() (*ConfigFile, error) { |
|
path := configFilePath() |
|
data, err := os.ReadFile(path) |
|
if err != nil { |
|
if os.IsNotExist(err) { |
|
return &ConfigFile{}, nil |
|
} |
|
return nil, err |
|
} |
|
|
|
var cf ConfigFile |
|
if err := json.Unmarshal(data, &cf); err != nil { |
|
return nil, err |
|
} |
|
return &cf, nil |
|
} |
|
|
|
// SaveConfigFile saves the configuration to the JSON file. |
|
func SaveConfigFile(cf *ConfigFile) error { |
|
path := configFilePath() |
|
|
|
// Ensure directory exists |
|
dir := filepath.Dir(path) |
|
if err := os.MkdirAll(dir, 0755); err != nil { |
|
return err |
|
} |
|
|
|
data, err := json.MarshalIndent(cf, "", " ") |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return os.WriteFile(path, data, 0644) |
|
} |
|
|
|
// ConfigToFile converts a Config to a ConfigFile for persistence. |
|
func ConfigToFile(cfg *Config) *ConfigFile { |
|
return &ConfigFile{ |
|
DBBackend: cfg.DBBackend, |
|
DBBinary: cfg.DBBinary, |
|
RelayBinary: cfg.RelayBinary, |
|
ACLBinary: cfg.ACLBinary, |
|
DBListen: cfg.DBListen, |
|
ACLListen: cfg.ACLListen, |
|
ACLEnabled: &cfg.ACLEnabled, |
|
ACLMode: cfg.ACLMode, |
|
DataDir: cfg.DataDir, |
|
LogLevel: cfg.LogLevel, |
|
AdminPort: &cfg.AdminPort, |
|
AdminOwners: cfg.AdminOwners, |
|
BinDir: cfg.BinDir, |
|
DistributedSyncEnabled: &cfg.DistributedSyncEnabled, |
|
ClusterSyncEnabled: &cfg.ClusterSyncEnabled, |
|
RelayGroupEnabled: &cfg.RelayGroupEnabled, |
|
NegentropyEnabled: &cfg.NegentropyEnabled, |
|
NegentropyBinary: cfg.NegentropyBinary, |
|
NegentropyListen: cfg.NegentropyListen, |
|
CertsEnabled: &cfg.CertsEnabled, |
|
CertsBinary: cfg.CertsBinary, |
|
} |
|
} |
|
|
|
// Config holds the launcher configuration. |
|
type Config struct { |
|
// DBBackend is the database backend: badger or neo4j |
|
DBBackend string |
|
|
|
// DBBinary is the path to the database server binary (computed from DBBackend if not set) |
|
DBBinary string |
|
|
|
// RelayBinary is the path to the orly binary |
|
RelayBinary string |
|
|
|
// ACLBinary is the path to the ACL server binary (computed from ACLMode if not set) |
|
ACLBinary string |
|
|
|
// DBListen is the address the database server listens on |
|
DBListen string |
|
|
|
// ACLListen is the address the ACL server listens on |
|
ACLListen string |
|
|
|
// ACLEnabled controls whether to run the ACL server as a separate process |
|
// When false, the relay runs in open mode (no ACL restrictions) |
|
ACLEnabled bool |
|
|
|
// ACLMode is the ACL mode: follows, managed, curation |
|
// Determines which ACL binary to use when ACLEnabled is true |
|
ACLMode string |
|
|
|
// DBReadyTimeout is how long to wait for the database to be ready |
|
DBReadyTimeout time.Duration |
|
|
|
// ACLReadyTimeout is how long to wait for the ACL server to be ready |
|
ACLReadyTimeout time.Duration |
|
|
|
// StopTimeout is how long to wait for processes to stop gracefully |
|
StopTimeout time.Duration |
|
|
|
// DataDir is the data directory to pass to orly-db |
|
DataDir string |
|
|
|
// LogLevel is the log level to use for all processes |
|
LogLevel string |
|
|
|
// Sync service configuration |
|
// DistributedSyncEnabled enables the distributed sync service |
|
DistributedSyncEnabled bool |
|
// DistributedSyncBinary is the path to the distributed sync binary |
|
DistributedSyncBinary string |
|
// DistributedSyncListen is the gRPC listen address for distributed sync |
|
DistributedSyncListen string |
|
|
|
// ClusterSyncEnabled enables the cluster sync service |
|
ClusterSyncEnabled bool |
|
// ClusterSyncBinary is the path to the cluster sync binary |
|
ClusterSyncBinary string |
|
// ClusterSyncListen is the gRPC listen address for cluster sync |
|
ClusterSyncListen string |
|
|
|
// RelayGroupEnabled enables the relay group service |
|
RelayGroupEnabled bool |
|
// RelayGroupBinary is the path to the relay group binary |
|
RelayGroupBinary string |
|
// RelayGroupListen is the gRPC listen address for relay group |
|
RelayGroupListen string |
|
|
|
// NegentropyEnabled enables the negentropy sync service |
|
NegentropyEnabled bool |
|
// NegentropyBinary is the path to the negentropy sync binary |
|
NegentropyBinary string |
|
// NegentropyListen is the gRPC listen address for negentropy |
|
NegentropyListen string |
|
|
|
// SyncReadyTimeout is how long to wait for sync services to be ready |
|
SyncReadyTimeout time.Duration |
|
|
|
// Certificate service configuration |
|
// CertsEnabled enables the certificate service |
|
CertsEnabled bool |
|
// CertsBinary is the path to the certificate service binary |
|
CertsBinary string |
|
|
|
// ServicesEnabled controls whether to start the DB, relay, and other services |
|
// When false, only the admin UI runs (useful for initial setup/updates) |
|
ServicesEnabled bool |
|
|
|
// Admin UI configuration |
|
// AdminEnabled controls whether to run the admin HTTP server |
|
AdminEnabled bool |
|
// AdminPort is the port for the admin HTTP server |
|
AdminPort int |
|
// AdminOwners is a list of pubkeys (hex) allowed to access the admin UI |
|
AdminOwners []string |
|
// BinDir is the directory for versioned binary management |
|
BinDir string |
|
} |
|
|
|
func loadConfig() (*Config, error) { |
|
// Load config file first (provides defaults) |
|
cf, err := loadConfigFile() |
|
if err != nil { |
|
// Log but don't fail - env vars are still valid |
|
cf = &ConfigFile{} |
|
} |
|
|
|
// Get backend and mode - file first, then env |
|
dbBackend := stringOr(cf.DBBackend, getEnvOrDefault("ORLY_LAUNCHER_DB_BACKEND", "badger")) |
|
aclMode := stringOr(cf.ACLMode, getEnvOrDefault("ORLY_ACL_MODE", "follows")) |
|
|
|
// Compute default binary names based on backend/mode |
|
defaultDBBinary := "orly-db-" + dbBackend |
|
defaultACLBinary := "orly-acl-" + aclMode |
|
|
|
// Parse admin owners - env takes precedence, then file |
|
envOwners := getEnvOrDefault("ORLY_LAUNCHER_OWNERS", "") |
|
var adminOwners []string |
|
if envOwners != "" { |
|
adminOwners = parseOwnersList(envOwners) |
|
} else if len(cf.AdminOwners) > 0 { |
|
adminOwners = cf.AdminOwners |
|
} |
|
|
|
cfg := &Config{ |
|
DBBackend: dbBackend, |
|
DBBinary: envOrFileOrDefault("ORLY_LAUNCHER_DB_BINARY", cf.DBBinary, defaultDBBinary), |
|
RelayBinary: envOrFileOrDefault("ORLY_LAUNCHER_RELAY_BINARY", cf.RelayBinary, "orly"), |
|
ACLBinary: envOrFileOrDefault("ORLY_LAUNCHER_ACL_BINARY", cf.ACLBinary, defaultACLBinary), |
|
DBListen: envOrFileOrDefault("ORLY_LAUNCHER_DB_LISTEN", cf.DBListen, "127.0.0.1:50051"), |
|
ACLListen: envOrFileOrDefault("ORLY_LAUNCHER_ACL_LISTEN", cf.ACLListen, "127.0.0.1:50052"), |
|
ACLEnabled: boolEnvOrFile("ORLY_LAUNCHER_ACL_ENABLED", cf.ACLEnabled, false), |
|
ACLMode: aclMode, |
|
DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second), |
|
ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 120*time.Second), |
|
StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), |
|
DataDir: envOrFileOrDefault("ORLY_DATA_DIR", cf.DataDir, filepath.Join(xdg.DataHome, "ORLY")), |
|
LogLevel: envOrFileOrDefault("ORLY_LOG_LEVEL", cf.LogLevel, "info"), |
|
|
|
// Sync services configuration |
|
DistributedSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", cf.DistributedSyncEnabled, false), |
|
DistributedSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"), |
|
DistributedSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"), |
|
|
|
ClusterSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", cf.ClusterSyncEnabled, false), |
|
ClusterSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"), |
|
ClusterSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"), |
|
|
|
RelayGroupEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", cf.RelayGroupEnabled, false), |
|
RelayGroupBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"), |
|
RelayGroupListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"), |
|
|
|
NegentropyEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", cf.NegentropyEnabled, false), |
|
NegentropyBinary: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", cf.NegentropyBinary, "orly-sync-negentropy"), |
|
NegentropyListen: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", cf.NegentropyListen, "127.0.0.1:50064"), |
|
|
|
SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second), |
|
|
|
// Certificate service configuration |
|
CertsEnabled: boolEnvOrFile("ORLY_LAUNCHER_CERTS_ENABLED", cf.CertsEnabled, false), |
|
CertsBinary: envOrFileOrDefault("ORLY_LAUNCHER_CERTS_BINARY", cf.CertsBinary, "orly-certs"), |
|
|
|
// Services enabled (default true for backwards compatibility) |
|
ServicesEnabled: getEnvOrDefault("ORLY_LAUNCHER_SERVICES_ENABLED", "true") == "true", |
|
|
|
// Admin UI configuration |
|
AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true", |
|
AdminPort: intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080), |
|
AdminOwners: adminOwners, |
|
BinDir: envOrFileOrDefault("ORLY_LAUNCHER_BIN_DIR", cf.BinDir, filepath.Join(xdg.DataHome, "orly", "bin")), |
|
} |
|
|
|
return cfg, nil |
|
} |
|
|
|
// stringOr returns the first non-empty string. |
|
func stringOr(a, b string) string { |
|
if a != "" { |
|
return a |
|
} |
|
return b |
|
} |
|
|
|
// envOrFileOrDefault returns env var if set, then file value if set, then default. |
|
func envOrFileOrDefault(envKey, fileValue, defaultValue string) string { |
|
if v := os.Getenv(envKey); v != "" { |
|
return v |
|
} |
|
if fileValue != "" { |
|
return fileValue |
|
} |
|
return defaultValue |
|
} |
|
|
|
// boolEnvOrFile returns env var if set, then file value if set, then default. |
|
func boolEnvOrFile(envKey string, fileValue *bool, defaultValue bool) bool { |
|
if v := os.Getenv(envKey); v != "" { |
|
return v == "true" |
|
} |
|
if fileValue != nil { |
|
return *fileValue |
|
} |
|
return defaultValue |
|
} |
|
|
|
// intEnvOrFile returns env var if set, then file value if set, then default. |
|
func intEnvOrFile(envKey string, fileValue *int, defaultValue int) int { |
|
if v := os.Getenv(envKey); v != "" { |
|
if i, err := strconv.Atoi(v); err == nil { |
|
return i |
|
} |
|
} |
|
if fileValue != nil { |
|
return *fileValue |
|
} |
|
return defaultValue |
|
} |
|
|
|
func parseOwnersList(s string) []string { |
|
if s == "" { |
|
return nil |
|
} |
|
parts := strings.Split(s, ",") |
|
var owners []string |
|
for _, p := range parts { |
|
p = strings.TrimSpace(p) |
|
if p != "" { |
|
owners = append(owners, p) |
|
} |
|
} |
|
return owners |
|
} |
|
|
|
func parseInt(key string, defaultValue int) int { |
|
if v := os.Getenv(key); v != "" { |
|
if i, err := strconv.Atoi(v); err == nil { |
|
return i |
|
} |
|
} |
|
return defaultValue |
|
} |
|
|
|
func getEnvOrDefault(key, defaultValue string) string { |
|
if v := os.Getenv(key); v != "" { |
|
return v |
|
} |
|
return defaultValue |
|
} |
|
|
|
func parseDuration(key string, defaultValue time.Duration) time.Duration { |
|
if v := os.Getenv(key); v != "" { |
|
if d, err := time.ParseDuration(v); err == nil { |
|
return d |
|
} |
|
} |
|
return defaultValue |
|
}
|
|
|