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

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
}