Skip to content

Commit 3e60f36

Browse files
authored
Implement -promote & -if-candidate flags on run command (#305)
1 parent cc5b3a6 commit 3e60f36

File tree

7 files changed

+546
-248
lines changed

7 files changed

+546
-248
lines changed

cmd/litefs/config.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)