|
|
|
@ -1,6 +1,7 @@ |
|
|
|
package main |
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
import ( |
|
|
|
|
|
|
|
"encoding/json" |
|
|
|
"os" |
|
|
|
"os" |
|
|
|
"path/filepath" |
|
|
|
"path/filepath" |
|
|
|
"strconv" |
|
|
|
"strconv" |
|
|
|
@ -10,6 +11,102 @@ import ( |
|
|
|
"github.com/adrg/xdg" |
|
|
|
"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"` |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Config holds the launcher configuration.
|
|
|
|
// Config holds the launcher configuration.
|
|
|
|
type Config struct { |
|
|
|
type Config struct { |
|
|
|
// DBBackend is the database backend: badger or neo4j
|
|
|
|
// DBBackend is the database backend: badger or neo4j
|
|
|
|
@ -97,61 +194,117 @@ type Config struct { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func loadConfig() (*Config, error) { |
|
|
|
func loadConfig() (*Config, error) { |
|
|
|
// Get backend and mode first to compute default binary names
|
|
|
|
// Load config file first (provides defaults)
|
|
|
|
dbBackend := getEnvOrDefault("ORLY_LAUNCHER_DB_BACKEND", "badger") |
|
|
|
cf, err := loadConfigFile() |
|
|
|
aclMode := getEnvOrDefault("ORLY_ACL_MODE", "follows") |
|
|
|
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
|
|
|
|
// Compute default binary names based on backend/mode
|
|
|
|
defaultDBBinary := "orly-db-" + dbBackend |
|
|
|
defaultDBBinary := "orly-db-" + dbBackend |
|
|
|
defaultACLBinary := "orly-acl-" + aclMode |
|
|
|
defaultACLBinary := "orly-acl-" + aclMode |
|
|
|
|
|
|
|
|
|
|
|
// Parse admin owners (comma-separated hex pubkeys)
|
|
|
|
// Parse admin owners - env takes precedence, then file
|
|
|
|
adminOwners := parseOwnersList(getEnvOrDefault("ORLY_LAUNCHER_OWNERS", "")) |
|
|
|
envOwners := getEnvOrDefault("ORLY_LAUNCHER_OWNERS", "") |
|
|
|
|
|
|
|
var adminOwners []string |
|
|
|
|
|
|
|
if envOwners != "" { |
|
|
|
|
|
|
|
adminOwners = parseOwnersList(envOwners) |
|
|
|
|
|
|
|
} else if len(cf.AdminOwners) > 0 { |
|
|
|
|
|
|
|
adminOwners = cf.AdminOwners |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
cfg := &Config{ |
|
|
|
cfg := &Config{ |
|
|
|
DBBackend: dbBackend, |
|
|
|
DBBackend: dbBackend, |
|
|
|
DBBinary: getEnvOrDefault("ORLY_LAUNCHER_DB_BINARY", defaultDBBinary), |
|
|
|
DBBinary: envOrFileOrDefault("ORLY_LAUNCHER_DB_BINARY", cf.DBBinary, defaultDBBinary), |
|
|
|
RelayBinary: getEnvOrDefault("ORLY_LAUNCHER_RELAY_BINARY", "orly"), |
|
|
|
RelayBinary: envOrFileOrDefault("ORLY_LAUNCHER_RELAY_BINARY", cf.RelayBinary, "orly"), |
|
|
|
ACLBinary: getEnvOrDefault("ORLY_LAUNCHER_ACL_BINARY", defaultACLBinary), |
|
|
|
ACLBinary: envOrFileOrDefault("ORLY_LAUNCHER_ACL_BINARY", cf.ACLBinary, defaultACLBinary), |
|
|
|
DBListen: getEnvOrDefault("ORLY_LAUNCHER_DB_LISTEN", "127.0.0.1:50051"), |
|
|
|
DBListen: envOrFileOrDefault("ORLY_LAUNCHER_DB_LISTEN", cf.DBListen, "127.0.0.1:50051"), |
|
|
|
ACLListen: getEnvOrDefault("ORLY_LAUNCHER_ACL_LISTEN", "127.0.0.1:50052"), |
|
|
|
ACLListen: envOrFileOrDefault("ORLY_LAUNCHER_ACL_LISTEN", cf.ACLListen, "127.0.0.1:50052"), |
|
|
|
ACLEnabled: getEnvOrDefault("ORLY_LAUNCHER_ACL_ENABLED", "false") == "true", |
|
|
|
ACLEnabled: boolEnvOrFile("ORLY_LAUNCHER_ACL_ENABLED", cf.ACLEnabled, false), |
|
|
|
ACLMode: aclMode, |
|
|
|
ACLMode: aclMode, |
|
|
|
DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second), |
|
|
|
DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second), |
|
|
|
ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 120*time.Second), |
|
|
|
ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 120*time.Second), |
|
|
|
StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), // Increased for DB flush
|
|
|
|
StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), |
|
|
|
DataDir: getEnvOrDefault("ORLY_DATA_DIR", filepath.Join(xdg.DataHome, "ORLY")), |
|
|
|
DataDir: envOrFileOrDefault("ORLY_DATA_DIR", cf.DataDir, filepath.Join(xdg.DataHome, "ORLY")), |
|
|
|
LogLevel: getEnvOrDefault("ORLY_LOG_LEVEL", "info"), |
|
|
|
LogLevel: envOrFileOrDefault("ORLY_LOG_LEVEL", cf.LogLevel, "info"), |
|
|
|
|
|
|
|
|
|
|
|
// Sync services configuration
|
|
|
|
// Sync services configuration
|
|
|
|
DistributedSyncEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", "false") == "true", |
|
|
|
DistributedSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", cf.DistributedSyncEnabled, false), |
|
|
|
DistributedSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"), |
|
|
|
DistributedSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"), |
|
|
|
DistributedSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"), |
|
|
|
DistributedSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"), |
|
|
|
|
|
|
|
|
|
|
|
ClusterSyncEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", "false") == "true", |
|
|
|
ClusterSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", cf.ClusterSyncEnabled, false), |
|
|
|
ClusterSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"), |
|
|
|
ClusterSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"), |
|
|
|
ClusterSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"), |
|
|
|
ClusterSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"), |
|
|
|
|
|
|
|
|
|
|
|
RelayGroupEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", "false") == "true", |
|
|
|
RelayGroupEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", cf.RelayGroupEnabled, false), |
|
|
|
RelayGroupBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"), |
|
|
|
RelayGroupBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"), |
|
|
|
RelayGroupListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"), |
|
|
|
RelayGroupListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"), |
|
|
|
|
|
|
|
|
|
|
|
NegentropyEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", "false") == "true", |
|
|
|
NegentropyEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", cf.NegentropyEnabled, false), |
|
|
|
NegentropyBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", "orly-sync-negentropy"), |
|
|
|
NegentropyBinary: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", cf.NegentropyBinary, "orly-sync-negentropy"), |
|
|
|
NegentropyListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", "127.0.0.1:50064"), |
|
|
|
NegentropyListen: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", cf.NegentropyListen, "127.0.0.1:50064"), |
|
|
|
|
|
|
|
|
|
|
|
SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second), |
|
|
|
SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second), |
|
|
|
|
|
|
|
|
|
|
|
// Admin UI configuration
|
|
|
|
// Admin UI configuration
|
|
|
|
AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true", |
|
|
|
AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true", |
|
|
|
AdminPort: parseInt("ORLY_LAUNCHER_ADMIN_PORT", 8080), |
|
|
|
AdminPort: intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080), |
|
|
|
AdminOwners: adminOwners, |
|
|
|
AdminOwners: adminOwners, |
|
|
|
BinDir: getEnvOrDefault("ORLY_LAUNCHER_BIN_DIR", filepath.Join(xdg.DataHome, "orly", "bin")), |
|
|
|
BinDir: envOrFileOrDefault("ORLY_LAUNCHER_BIN_DIR", cf.BinDir, filepath.Join(xdg.DataHome, "orly", "bin")), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return cfg, nil |
|
|
|
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 { |
|
|
|
func parseOwnersList(s string) []string { |
|
|
|
if s == "" { |
|
|
|
if s == "" { |
|
|
|
return nil |
|
|
|
return nil |
|
|
|
|