|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + "os/user" |
| 9 | + "path/filepath" |
| 10 | + "regexp" |
| 11 | + "strconv" |
| 12 | + "strings" |
| 13 | + "time" |
| 14 | + |
| 15 | + "github.com/superfly/litefs" |
| 16 | + "github.com/superfly/litefs/http" |
| 17 | + "gopkg.in/yaml.v3" |
| 18 | +) |
| 19 | + |
| 20 | +// NOTE: Update etc/litefs.yml configuration file after changing the structure below. |
| 21 | + |
| 22 | +// Config represents a configuration for the binary process. |
| 23 | +type Config struct { |
| 24 | + Exec string `yaml:"exec"` |
| 25 | + ExitOnError bool `yaml:"exit-on-error"` |
| 26 | + SkipSync bool `yaml:"skip-sync"` |
| 27 | + StrictVerify bool `yaml:"strict-verify"` |
| 28 | + |
| 29 | + Data DataConfig `yaml:"data"` |
| 30 | + FUSE FUSEConfig `yaml:"fuse"` |
| 31 | + HTTP HTTPConfig `yaml:"http"` |
| 32 | + Proxy ProxyConfig `yaml:"proxy"` |
| 33 | + Lease LeaseConfig `yaml:"lease"` |
| 34 | + Tracing TracingConfig `yaml:"tracing"` |
| 35 | +} |
| 36 | + |
| 37 | +// NewConfig returns a new instance of Config with defaults set. |
| 38 | +func NewConfig() Config { |
| 39 | + var config Config |
| 40 | + config.ExitOnError = true |
| 41 | + |
| 42 | + config.Data.Compress = true |
| 43 | + config.Data.Retention = litefs.DefaultRetention |
| 44 | + config.Data.RetentionMonitorInterval = litefs.DefaultRetentionMonitorInterval |
| 45 | + |
| 46 | + config.HTTP.Addr = http.DefaultAddr |
| 47 | + |
| 48 | + config.Lease.Candidate = true |
| 49 | + config.Lease.ReconnectDelay = litefs.DefaultReconnectDelay |
| 50 | + config.Lease.DemoteDelay = litefs.DefaultDemoteDelay |
| 51 | + |
| 52 | + config.Tracing.MaxSize = DefaultTracingMaxSize |
| 53 | + config.Tracing.MaxCount = DefaultTracingMaxCount |
| 54 | + config.Tracing.Compress = DefaultTracingCompress |
| 55 | + |
| 56 | + return config |
| 57 | +} |
| 58 | + |
| 59 | +// DataConfig represents the configuration for internal LiteFS data. This |
| 60 | +// includes database files as well as LTX transaction files. |
| 61 | +type DataConfig struct { |
| 62 | + Dir string `yaml:"dir"` |
| 63 | + Compress bool `yaml:"compress"` |
| 64 | + |
| 65 | + Retention time.Duration `yaml:"retention"` |
| 66 | + RetentionMonitorInterval time.Duration `yaml:"retention-monitor-interval"` |
| 67 | +} |
| 68 | + |
| 69 | +// FUSEConfig represents the configuration for the FUSE file system. |
| 70 | +type FUSEConfig struct { |
| 71 | + Dir string `yaml:"dir"` |
| 72 | + AllowOther bool `yaml:"allow-other"` |
| 73 | + Debug bool `yaml:"debug"` |
| 74 | +} |
| 75 | + |
| 76 | +// HTTPConfig represents the configuration for the HTTP server. |
| 77 | +type HTTPConfig struct { |
| 78 | + Addr string `yaml:"addr"` |
| 79 | +} |
| 80 | + |
| 81 | +// ProxyConfig represents the configuration for the HTTP proxy server. |
| 82 | +type ProxyConfig struct { |
| 83 | + Addr string `yaml:"addr"` |
| 84 | + Target string `yaml:"target"` |
| 85 | + DB string `yaml:"db"` |
| 86 | + Debug bool `yaml:"debug"` |
| 87 | + Passthrough []string `yaml:"passthrough"` |
| 88 | +} |
| 89 | + |
| 90 | +// LeaseConfig represents a generic configuration for all lease types. |
| 91 | +type LeaseConfig struct { |
| 92 | + // Specifies the type of leasing to use: "consul" or "static" |
| 93 | + Type string `yaml:"type"` |
| 94 | + |
| 95 | + // The hostname of this node. Used by the application to forward requests. |
| 96 | + Hostname string `yaml:"hostname"` |
| 97 | + |
| 98 | + // URL for other nodes to access this node's API. |
| 99 | + AdvertiseURL string `yaml:"advertise-url"` |
| 100 | + |
| 101 | + // Specifies if this node can become primary. Defaults to true. |
| 102 | + // |
| 103 | + // If using a "static" lease, setting this to true makes it the primary. |
| 104 | + // Replicas in a state lease should set this to false. |
| 105 | + Candidate bool `yaml:"candidate"` |
| 106 | + |
| 107 | + // After disconnect, time before node tries to reconnect to primary or |
| 108 | + // becomes primary itself. |
| 109 | + ReconnectDelay time.Duration `yaml:"reconnect-delay"` |
| 110 | + |
| 111 | + // Amount of time to wait after a forced demotion before attempting to |
| 112 | + // become primary again. |
| 113 | + DemoteDelay time.Duration `yaml:"demote-delay"` |
| 114 | + |
| 115 | + // Consul lease settings. |
| 116 | + Consul struct { |
| 117 | + URL string `yaml:"url"` |
| 118 | + Key string `yaml:"key"` |
| 119 | + TTL time.Duration `yaml:"ttl"` |
| 120 | + LockDelay time.Duration `yaml:"lock-delay"` |
| 121 | + } `yaml:"consul"` |
| 122 | +} |
| 123 | + |
| 124 | +// Tracing configuration defaults. |
| 125 | +const ( |
| 126 | + DefaultTracingMaxSize = 64 // MB |
| 127 | + DefaultTracingMaxCount = 8 |
| 128 | + DefaultTracingCompress = true |
| 129 | +) |
| 130 | + |
| 131 | +// TracingConfig represents the configuration the on-disk trace log. |
| 132 | +type TracingConfig struct { |
| 133 | + Path string `yaml:"path"` |
| 134 | + MaxSize int `yaml:"max-size"` |
| 135 | + MaxCount int `yaml:"max-count"` |
| 136 | + Compress bool `yaml:"compress"` |
| 137 | +} |
| 138 | + |
| 139 | +// UnmarshalConfig unmarshals config from data. |
| 140 | +// If expandEnv is true then environment variables are expanded in the config. |
| 141 | +func UnmarshalConfig(config *Config, data []byte, expandEnv bool) error { |
| 142 | + // Expand environment variables, if enabled. |
| 143 | + if expandEnv { |
| 144 | + data = []byte(ExpandEnv(string(data))) |
| 145 | + } |
| 146 | + |
| 147 | + dec := yaml.NewDecoder(bytes.NewReader(data)) |
| 148 | + dec.KnownFields(true) // strict checking |
| 149 | + if err := dec.Decode(&config); err != nil { |
| 150 | + return err |
| 151 | + } |
| 152 | + return nil |
| 153 | +} |
| 154 | + |
| 155 | +// ExpandEnv replaces environment variables just like os.ExpandEnv() but also |
| 156 | +// allows for equality/inequality binary expressions within the ${} form. |
| 157 | +func ExpandEnv(s string) string { |
| 158 | + return os.Expand(s, func(v string) string { |
| 159 | + v = strings.TrimSpace(v) |
| 160 | + |
| 161 | + if a := expandExprSingleQuote.FindStringSubmatch(v); a != nil { |
| 162 | + if a[2] == "==" { |
| 163 | + return strconv.FormatBool(os.Getenv(a[1]) == a[3]) |
| 164 | + } |
| 165 | + return strconv.FormatBool(os.Getenv(a[1]) != a[3]) |
| 166 | + } |
| 167 | + |
| 168 | + if a := expandExprDoubleQuote.FindStringSubmatch(v); a != nil { |
| 169 | + if a[2] == "==" { |
| 170 | + return strconv.FormatBool(os.Getenv(a[1]) == a[3]) |
| 171 | + } |
| 172 | + return strconv.FormatBool(os.Getenv(a[1]) != a[3]) |
| 173 | + } |
| 174 | + |
| 175 | + if a := expandExprVar.FindStringSubmatch(v); a != nil { |
| 176 | + if a[2] == "==" { |
| 177 | + return strconv.FormatBool(os.Getenv(a[1]) == os.Getenv(a[3])) |
| 178 | + } |
| 179 | + return strconv.FormatBool(os.Getenv(a[1]) != os.Getenv(a[3])) |
| 180 | + } |
| 181 | + |
| 182 | + return os.Getenv(v) |
| 183 | + }) |
| 184 | +} |
| 185 | + |
| 186 | +var ( |
| 187 | + expandExprSingleQuote = regexp.MustCompile(`^(\w+)\s*(==|!=)\s*'(.*)'$`) |
| 188 | + expandExprDoubleQuote = regexp.MustCompile(`^(\w+)\s*(==|!=)\s*"(.*)"$`) |
| 189 | + expandExprVar = regexp.MustCompile(`^(\w+)\s*(==|!=)\s*(\w+)$`) |
| 190 | +) |
| 191 | + |
| 192 | +// splitArgs returns the list of args before and after a "--" arg. If the double |
| 193 | +// dash is not specified, then args0 is args and args1 is empty. |
| 194 | +func splitArgs(args []string) (args0, args1 []string) { |
| 195 | + for i, v := range args { |
| 196 | + if v == "--" { |
| 197 | + return args[:i], args[i+1:] |
| 198 | + } |
| 199 | + } |
| 200 | + return args, nil |
| 201 | +} |
| 202 | + |
| 203 | +// ParseConfigPath parses the configuration file from configPath, if specified. |
| 204 | +// Returns ok true if a configuration file was successfully found. |
| 205 | +// |
| 206 | +// Otherwise searches the standard list of search paths. Returns an error if |
| 207 | +// no configuration files could be found. |
| 208 | +func ParseConfigPath(ctx context.Context, configPath string, expandEnv bool, config *Config) (err error) { |
| 209 | + // Only read from explicit path, if specified. Report any error. |
| 210 | + if configPath != "" { |
| 211 | + // Read configuration. |
| 212 | + buf, err := os.ReadFile(configPath) |
| 213 | + if err != nil { |
| 214 | + return err |
| 215 | + } |
| 216 | + return UnmarshalConfig(config, buf, expandEnv) |
| 217 | + } |
| 218 | + |
| 219 | + // Otherwise attempt to read each config path until we succeed. |
| 220 | + for _, path := range configSearchPaths() { |
| 221 | + if path, err = filepath.Abs(path); err != nil { |
| 222 | + return err |
| 223 | + } |
| 224 | + |
| 225 | + buf, err := os.ReadFile(path) |
| 226 | + if os.IsNotExist(err) { |
| 227 | + continue |
| 228 | + } else if err != nil { |
| 229 | + return fmt.Errorf("cannot read config file at %s: %s", path, err) |
| 230 | + } |
| 231 | + |
| 232 | + if err := UnmarshalConfig(config, buf, expandEnv); err != nil { |
| 233 | + return fmt.Errorf("cannot unmarshal config file at %s: %s", path, err) |
| 234 | + } |
| 235 | + |
| 236 | + fmt.Printf("config file read from %s\n", path) |
| 237 | + return nil |
| 238 | + } |
| 239 | + |
| 240 | + return fmt.Errorf("config file not found") |
| 241 | +} |
| 242 | + |
| 243 | +// configSearchPaths returns paths to search for the config file. It starts with |
| 244 | +// the current directory, then home directory, if available. And finally it tries |
| 245 | +// to read from the /etc directory. |
| 246 | +func configSearchPaths() []string { |
| 247 | + a := []string{"litefs.yml"} |
| 248 | + if u, _ := user.Current(); u != nil && u.HomeDir != "" { |
| 249 | + a = append(a, filepath.Join(u.HomeDir, "litefs.yml")) |
| 250 | + } |
| 251 | + a = append(a, "/etc/litefs.yml") |
| 252 | + return a |
| 253 | +} |
0 commit comments