From 1c84924153787f5392cc587b0b2febe1bfa7a332 Mon Sep 17 00:00:00 2001 From: Rene Kaufmann Date: Sat, 5 Nov 2016 22:31:14 +0100 Subject: [PATCH 1/5] initial include_dir support --- config.go | 47 ++++++++++++++---- configWatcher.go | 121 +++++++++++++++++++++++++++++++---------------- main.go | 15 ++---- 3 files changed, 123 insertions(+), 60 deletions(-) diff --git a/config.go b/config.go index eb59ccbf..9e4145ba 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,7 @@ import ( "context" "io/ioutil" "os" + "path/filepath" "sync" "github.com/HeavyHorst/remco/backends" @@ -29,9 +30,20 @@ type resource struct { // configuration is the representation of an config file type configuration struct { - LogLevel string `toml:"log_level"` - LogFormat string `toml:"log_format"` - Resource []resource + LogLevel string `toml:"log_level"` + LogFormat string `toml:"log_format"` + IncludeDir string `toml:"include_dir"` + Resource []resource +} + +func readFileAndExpandEnv(path string) ([]byte, error) { + buf, err := ioutil.ReadFile(path) + if err != nil { + return buf, err + } + // expand the environment variables + buf = []byte(os.ExpandEnv(string(buf))) + return buf, nil } // newConfiguration reads the file at `path`, expand the environment variables @@ -39,23 +51,42 @@ type configuration struct { // it returns an error if any. func newConfiguration(path string) (configuration, error) { var c configuration - buf, err := ioutil.ReadFile(path) + + buf, err := readFileAndExpandEnv(path) if err != nil { return c, err } - buf = []byte(os.ExpandEnv(string(buf))) + if err := toml.Unmarshal(buf, &c); err != nil { return c, err } - c.loadGlobals() + c.configureLogger() + + if c.IncludeDir != "" { + files, err := ioutil.ReadDir(c.IncludeDir) + if err != nil { + return c, err + } + for _, file := range files { + buf, err := readFileAndExpandEnv(filepath.Join(c.IncludeDir, file.Name())) + if err != nil { + return c, err + } + var r resource + if err := toml.Unmarshal(buf, &r); err != nil { + return c, err + } + c.Resource = append(c.Resource, r) + } + } return c, nil } -// loadGlobals configures remco with the global configuration options +// configureLogger configures the global logger // for example it sets the log level and log formatting -func (c *configuration) loadGlobals() { +func (c *configuration) configureLogger() { if c.LogLevel != "" { err := log.SetLevel(c.LogLevel) if err != nil { diff --git a/configWatcher.go b/configWatcher.go index fa36e1e6..4392b8f4 100644 --- a/configWatcher.go +++ b/configWatcher.go @@ -9,31 +9,37 @@ package main import ( - "context" "fmt" + "strings" + "sync" "time" - "github.com/HeavyHorst/easyKV" "github.com/HeavyHorst/remco/log" + "github.com/fsnotify/fsnotify" ) // the configWatcher watches the config file for changes type configWatcher struct { stoppedW chan struct{} stopWatch chan struct{} - filePath string - cancel context.CancelFunc + + stopWatchConf chan struct{} + stoppedWatchConf chan struct{} + + reloadChan chan struct{} + + wg sync.WaitGroup + + configPath string } // call c.run in its own goroutine // and write to the w.stoppedW chan if its done func (w *configWatcher) runConfig(c configuration) { - go func() { - defer func() { - w.stoppedW <- struct{}{} - }() - c.run(w.stopWatch) + defer func() { + w.stoppedW <- struct{}{} }() + c.run(w.stopWatch) } // reload stops the old config and starts a new one @@ -49,62 +55,91 @@ func (w *configWatcher) reload() { } }() - newConf, err := newConfiguration(w.filePath) + newConf, err := newConfiguration(w.configPath) if err != nil { log.Error(err) return } - // stop the old watcher and wait until it has stopped + // stop the old config and wait until it has stopped w.stopWatch <- struct{}{} <-w.stoppedW - // start a new watcher - w.runConfig(newConf) + go w.runConfig(newConf) + + // restart the fsnotify config watcher (the include_dir folder may have changed) + // startWatchConfig returns when calling reload + go w.startWatchConfig(newConf) } -func newConfigWatcher(filepath string, watcher easyKV.ReadWatcher, config configuration, done chan struct{}) *configWatcher { - w := &configWatcher{ - stoppedW: make(chan struct{}), - stopWatch: make(chan struct{}), - filePath: filepath, +func (w *configWatcher) startWatchConfig(config configuration) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Error(err) } + defer func() { + watcher.Close() + w.stoppedWatchConf <- struct{}{} + }() - w.runConfig(config) + // add theconfigfile to the watcher + err = watcher.Add(w.configPath) + if err != nil { + log.Error(err) + } - ctx, cancel := context.WithCancel(context.Background()) - w.cancel = cancel + // add the include_dir to the watcher + if config.IncludeDir != "" { + err = watcher.Add(config.IncludeDir) + if err != nil { + log.Error(err) + } + } - reload := make(chan struct{}) - go func() { - // watch the config for changes - for { - select { - case <-ctx.Done(): - return - default: - _, err := watcher.WatchPrefix("", ctx, easyKV.WithKeys([]string{""})) - if err != nil { - if err != easyKV.ErrWatchCanceled { - log.Error(err) - time.Sleep(2 * time.Second) - } - continue + // watch the config for changes + for { + select { + case <-w.stopWatchConf: + return + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write || + event.Op&fsnotify.Remove == fsnotify.Remove || + event.Op&fsnotify.Create == fsnotify.Create { + // only watch .toml files + if strings.HasSuffix(event.Name, ".toml") { + time.Sleep(500 * time.Millisecond) + w.reloadChan <- struct{}{} + return } - time.Sleep(1 * time.Second) - reload <- struct{}{} } } - }() + } +} +func newConfigWatcher(configPath string, config configuration, done chan struct{}) *configWatcher { + w := &configWatcher{ + stoppedW: make(chan struct{}), + stopWatch: make(chan struct{}), + stopWatchConf: make(chan struct{}), + stoppedWatchConf: make(chan struct{}), + reloadChan: make(chan struct{}), + configPath: configPath, + } + + go w.runConfig(config) + go w.startWatchConfig(config) + w.wg.Add(1) go func() { + defer w.wg.Done() for { select { - case <-reload: + case <-w.reloadChan: w.reload() case <-w.stoppedW: close(done) - // there is no runnign runConfig function which can answer to the stop method + + // there is no running runConfig function which can answer to the stop method // we need to send to the w.stoppedW channel so that we don't block w.stoppedW <- struct{}{} + return } } }() @@ -113,7 +148,9 @@ func newConfigWatcher(filepath string, watcher easyKV.ReadWatcher, config config } func (w *configWatcher) stop() { - w.cancel() close(w.stopWatch) <-w.stoppedW + close(w.stopWatchConf) + <-w.stoppedWatchConf + w.wg.Wait() } diff --git a/main.go b/main.go index cbc84a06..e11023fd 100644 --- a/main.go +++ b/main.go @@ -15,18 +15,17 @@ import ( "os/signal" "syscall" - "github.com/HeavyHorst/remco/backends/file" "github.com/HeavyHorst/remco/log" ) var ( - fileConfig = &file.Config{} + configPath string printVersionAndExit bool ) func init() { const defaultConfig = "/etc/remco/config" - flag.StringVar(&fileConfig.Filepath, "config", defaultConfig, "path to the configuration file") + flag.StringVar(&configPath, "config", defaultConfig, "path to the configuration file") flag.BoolVar(&printVersionAndExit, "version", false, "print version and exit") } @@ -35,18 +34,14 @@ func run() { signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) // we need a working config here - exit on error - c, err := newConfiguration(fileConfig.Filepath) - if err != nil { - log.Fatal(err.Error()) - } - - s, err := fileConfig.Connect() + c, err := newConfiguration(configPath) if err != nil { log.Fatal(err.Error()) } done := make(chan struct{}) - cfgWatcher := newConfigWatcher(fileConfig.Filepath, s.ReadWatcher, c, done) + // watch the configuration file and all files under include_dir for changes + cfgWatcher := newConfigWatcher(configPath, c, done) defer cfgWatcher.stop() for { From 45be8d992fc137238cef9e617fbb030d5f7c1247 Mon Sep 17 00:00:00 2001 From: Rene Kaufmann Date: Sun, 6 Nov 2016 16:46:52 +0100 Subject: [PATCH 2/5] include_dir: only read .toml files --- config.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 9e4145ba..55a8516a 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "sync" "github.com/HeavyHorst/remco/backends" @@ -69,15 +70,24 @@ func newConfiguration(path string) (configuration, error) { return c, err } for _, file := range files { - buf, err := readFileAndExpandEnv(filepath.Join(c.IncludeDir, file.Name())) - if err != nil { - return c, err - } - var r resource - if err := toml.Unmarshal(buf, &r); err != nil { - return c, err + if strings.HasSuffix(file.Name(), ".toml") { + fp := filepath.Join(c.IncludeDir, file.Name()) + log.WithFields(logrus.Fields{ + "path": fp, + }).Info("Loading resource configuration") + buf, err := readFileAndExpandEnv(fp) + if err != nil { + return c, err + } + var r resource + if err := toml.Unmarshal(buf, &r); err != nil { + return c, err + } + // don't add empty resources + if len(r.Template) > 0 { + c.Resource = append(c.Resource, r) + } } - c.Resource = append(c.Resource, r) } } From 66629be60021a54517e75213da900af540926fad Mon Sep 17 00:00:00 2001 From: Rene Kaufmann Date: Sun, 6 Nov 2016 16:47:23 +0100 Subject: [PATCH 3/5] log.go: sorted output --- log/log.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/log.go b/log/log.go index 2864561c..b20c8a4e 100644 --- a/log/log.go +++ b/log/log.go @@ -38,7 +38,7 @@ func SetFormatter(format string) { case "json": log.SetFormatter(&log.JSONFormatter{}) case "text": - log.SetFormatter(&prefixed.TextFormatter{DisableSorting: true}) + log.SetFormatter(&prefixed.TextFormatter{DisableSorting: false}) } } From 358d92514898d8875409ac739a487fd90afa03ef Mon Sep 17 00:00:00 2001 From: Rene Kaufmann Date: Sun, 6 Nov 2016 16:47:44 +0100 Subject: [PATCH 4/5] configWatcher: better sync --- configWatcher.go | 116 +++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/configWatcher.go b/configWatcher.go index 4392b8f4..5f25bde7 100644 --- a/configWatcher.go +++ b/configWatcher.go @@ -9,7 +9,6 @@ package main import ( - "fmt" "strings" "sync" "time" @@ -20,17 +19,15 @@ import ( // the configWatcher watches the config file for changes type configWatcher struct { - stoppedW chan struct{} - stopWatch chan struct{} - + stoppedW chan struct{} + stopWatch chan struct{} stopWatchConf chan struct{} stoppedWatchConf chan struct{} - - reloadChan chan struct{} - - wg sync.WaitGroup - - configPath string + reloadChan chan struct{} + wg sync.WaitGroup + mu sync.Mutex + canceled bool + configPath string } // call c.run in its own goroutine @@ -42,45 +39,25 @@ func (w *configWatcher) runConfig(c configuration) { c.run(w.stopWatch) } -// reload stops the old config and starts a new one -// this function blocks forever if runConfig was never called before -func (w *configWatcher) reload() { - defer func() { - // we may try to send on the closed channel w.stopWatch - // we need to recover from this panic - if r := recover(); r != nil { - if fmt.Sprintf("%v", r) != "send on closed channel" { - panic(r) - } - } - }() - - newConf, err := newConfiguration(w.configPath) - if err != nil { - log.Error(err) - return - } - // stop the old config and wait until it has stopped - w.stopWatch <- struct{}{} - <-w.stoppedW - go w.runConfig(newConf) - - // restart the fsnotify config watcher (the include_dir folder may have changed) - // startWatchConfig returns when calling reload - go w.startWatchConfig(newConf) +func (w *configWatcher) getCanceled() bool { + w.mu.Lock() + defer w.mu.Unlock() + return w.canceled } +// startWatchConfig starts to watch the configuration file and all files under include_dir which ends with .toml +// if there is an event (write, remove, create) we write to w.reloadChan to trigger an relaod and return. func (w *configWatcher) startWatchConfig(config configuration) { + w.wg.Add(1) + defer w.wg.Done() + watcher, err := fsnotify.NewWatcher() if err != nil { log.Error(err) } - defer func() { - watcher.Close() - w.stoppedWatchConf <- struct{}{} - }() + defer watcher.Close() - // add theconfigfile to the watcher + // add the configfile to the watcher err = watcher.Add(w.configPath) if err != nil { log.Error(err) @@ -104,8 +81,14 @@ func (w *configWatcher) startWatchConfig(config configuration) { event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Create == fsnotify.Create { // only watch .toml files - if strings.HasSuffix(event.Name, ".toml") { + if strings.HasSuffix(event.Name, ".toml") || event.Name == w.configPath { time.Sleep(500 * time.Millisecond) + + // don't try to reload if w is already canceled + if w.getCanceled() { + return + } + w.reloadChan <- struct{}{} return } @@ -116,12 +99,11 @@ func (w *configWatcher) startWatchConfig(config configuration) { func newConfigWatcher(configPath string, config configuration, done chan struct{}) *configWatcher { w := &configWatcher{ - stoppedW: make(chan struct{}), - stopWatch: make(chan struct{}), - stopWatchConf: make(chan struct{}), - stoppedWatchConf: make(chan struct{}), - reloadChan: make(chan struct{}), - configPath: configPath, + stoppedW: make(chan struct{}), + stopWatch: make(chan struct{}), + stopWatchConf: make(chan struct{}), + reloadChan: make(chan struct{}), + configPath: configPath, } go w.runConfig(config) @@ -132,13 +114,34 @@ func newConfigWatcher(configPath string, config configuration, done chan struct{ for { select { case <-w.reloadChan: - w.reload() + newConf, err := newConfiguration(w.configPath) + if err != nil { + log.Error(err) + continue + } + + // don't try to relaod anything if w is already canceled + if w.getCanceled() { + continue + } + + // stop the old config and wait until it has stopped + w.stopWatch <- struct{}{} + <-w.stoppedW + go w.runConfig(newConf) + + // restart the fsnotify config watcher (the include_dir folder may have changed) + // startWatchConfig returns when calling reload (we don't need to stop it) + go w.startWatchConfig(newConf) case <-w.stoppedW: - close(done) + // close the reloadChan + // every attempt to write to reloadChan would block forever otherwise + close(w.reloadChan) - // there is no running runConfig function which can answer to the stop method - // we need to send to the w.stoppedW channel so that we don't block - w.stoppedW <- struct{}{} + // close the done channel + // this signals the main function that the configWatcher has completed all work + // for example all backends are configured with onetime=true + close(done) return } } @@ -148,9 +151,12 @@ func newConfigWatcher(configPath string, config configuration, done chan struct{ } func (w *configWatcher) stop() { + w.mu.Lock() + w.canceled = true + w.mu.Unlock() close(w.stopWatch) - <-w.stoppedW close(w.stopWatchConf) - <-w.stoppedWatchConf + + // wait for the main routine and startWatchConfig to exit w.wg.Wait() } From f34ee6e55f2a06202e93b95a8b21925766b89df1 Mon Sep 17 00:00:00 2001 From: Rene Kaufmann Date: Sun, 6 Nov 2016 18:34:53 +0100 Subject: [PATCH 5/5] some docs for include_dir --- docs/config.toml | 9 +++-- docs/content/config/index.md | 2 ++ docs/content/config/sample.toml.md | 42 ++++++++-------------- docs/content/config/sampleresource.toml.md | 20 +++++++++++ 4 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 docs/content/config/sampleresource.toml.md diff --git a/docs/config.toml b/docs/config.toml index d10b0031..5073f5e8 100755 --- a/docs/config.toml +++ b/docs/config.toml @@ -52,15 +52,20 @@ canonifyurls = true url = "config/sample" weight = 20 +[[menu.main]] + name = "Sample resource configuration" + url = "config/sampleresource" + weight = 30 + [[menu.main]] name = "Template" url = "template/" - weight = 30 + weight = 40 [[menu.main]] name = "License" url = "license/" - weight = 40 + weight = 50 [blackfriday] diff --git a/docs/content/config/index.md b/docs/content/config/index.md index 8d082740..6a5cceb4 100644 --- a/docs/content/config/index.md +++ b/docs/content/config/index.md @@ -16,6 +16,8 @@ to configure values, you can simply use $VARIABLE_NAME or ${VARIABLE_NAME} and t - Valid levels are panic, fatal, error, warn, info and debug. Default is info. - **log_format(string):** - The format of the log messages. Valid formats are *text* and *json*. + - **include_dir(string):** + - Specify an entire directory of resource configuration files to include. ## Template configuration options - **src(string):** diff --git a/docs/content/config/sample.toml.md b/docs/content/config/sample.toml.md index 1fab47b7..0cb32412 100644 --- a/docs/content/config/sample.toml.md +++ b/docs/content/config/sample.toml.md @@ -11,6 +11,7 @@ title: Sample configuration file ################################################################ log_level = "debug" log_format = "text" +include_dir = "/etc/remco/resource.d/" ################################################################ @@ -18,39 +19,13 @@ log_format = "text" ################################################################ [[resource]] [[resource.template]] - src = "/etc/confd/templates/test.cfg" - dst = "/home/rkaufmann/haproxy.cfg" + src = "/etc/remco/templates/haproxy.cfg" + dst = "/etc/haproxy/haproxy.cfg" checkCmd = "" reloadCmd = "" mode = "0644" [resource.backend] - [resource.backend.etcd] - nodes = ["127.0.0.1:2379"] - client_cert = "/path/to/client_cert" - client_key = "/path/to/client_key" - client_ca_keys = "/path/to/client_ca_keys" - username = "admin" - password = "p@SsWord" - version = 3 - - # These values are valid in every backend - watch = true - prefix = "/" - onetime = true - interval = 1 - keys = ["/"] - - [resource.backend.file] - filepath = "/etc/remco/test.yml" - - [resource.backend.consul] - nodes = ["127.0.0.1:8500"] - scheme = "http" #{http/https} - client_cert = "/path/to/client_cert" - client_key = "/path/to/client_key" - client_ca_keys = "/path/to/client_ca_keys" - [resource.backend.vault] node = "http://127.0.0.1:8200" ## Token based auth backend @@ -67,5 +42,16 @@ log_format = "text" client_cert = "/path/to/client_cert" client_key = "/path/to/client_key" client_ca_keys = "/path/to/client_ca_keys" + + # These values are valid in every backend + watch = true + prefix = "/" + onetime = true + interval = 1 + keys = ["/"] + + [resource.backend.file] + filepath = "/etc/remco/test.yml" + watch = true ``` diff --git a/docs/content/config/sampleresource.toml.md b/docs/content/config/sampleresource.toml.md new file mode 100644 index 00000000..86eedc10 --- /dev/null +++ b/docs/content/config/sampleresource.toml.md @@ -0,0 +1,20 @@ +--- +date: 2016-11-06T17:24:57+02:00 +title: Sample resource file +--- + +``` +[[template]] + src = "/etc/remco/templates/haproxy.cfg" + dst = "/etc/haproxy/haproxy.cfg" + mode = "0644" + +[backend] + [backend.etcd] + nodes = ["http://localhost:2379"] + keys = ["/"] + watch = true + interval = 1 + version = 3 + +```