github.com/imyousuf/webhook-broker@v0.1.2/config/cliconfig.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/rs/zerolog/log"
    15  
    16  	"github.com/fsnotify/fsnotify"
    17  )
    18  
    19  var (
    20  	errNoFileToWatch       = errors.New("no file to watch")
    21  	errTruncatedConfigFile = errors.New("truncated config file")
    22  )
    23  
    24  // CLIConfig represents the Command Line Args config
    25  type CLIConfig struct {
    26  	ConfigPath             string
    27  	MigrationSource        string
    28  	StopOnConfigChange     bool
    29  	DoNotWatchConfigChange bool
    30  	callbacks              []func()
    31  	watcherStarted         bool
    32  	watcherStarterMutex    sync.Mutex
    33  	watcher                *fsnotify.Watcher
    34  }
    35  
    36  // IsMigrationEnabled returns whether migration is enabled
    37  func (conf *CLIConfig) IsMigrationEnabled() bool {
    38  	return len(conf.MigrationSource) > 0
    39  }
    40  
    41  // NotifyOnConfigFileChange registers a callback function for changes to ConfigPath; it calls the `callback` when a change is detected
    42  func (conf *CLIConfig) NotifyOnConfigFileChange(callback func()) {
    43  	if conf.DoNotWatchConfigChange {
    44  		return
    45  	}
    46  	conf.callbacks = append(conf.callbacks, callback)
    47  	if !conf.watcherStarted {
    48  		conf.startConfigWatcher()
    49  	}
    50  }
    51  
    52  // IsConfigWatcherStarted returns whether config watcher is running
    53  func (conf *CLIConfig) IsConfigWatcherStarted() bool {
    54  	return conf.watcherStarted
    55  }
    56  
    57  func (conf *CLIConfig) startConfigWatcher() {
    58  	conf.watcherStarterMutex.Lock()
    59  	defer conf.watcherStarterMutex.Unlock()
    60  	conf.watchFileIfExists()
    61  	conf.watcherStarted = true
    62  }
    63  
    64  // StopWatcher stops any watcher if started for CLI ConfigPath file change
    65  func (conf *CLIConfig) StopWatcher() {
    66  	if conf.watcherStarted {
    67  		log.Print("closing watcher")
    68  		conf.watcher.Close()
    69  	}
    70  }
    71  
    72  type watcherWorkerConfig struct {
    73  	configFile     string
    74  	filename       string
    75  	realConfigFile string
    76  	filehash       string
    77  	callbacks      []func()
    78  }
    79  
    80  func (conf *CLIConfig) watchFileIfExists() {
    81  	watcher, err := createNewWatcher()
    82  	if err != nil {
    83  		log.Error().Err(err).Msg("could not setup watcher")
    84  		return
    85  	}
    86  	conf.watcher = watcher
    87  	// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
    88  	filename, err := getFileToWatch(conf.ConfigPath)
    89  	if err != nil {
    90  		log.Error().Err(err).Msg("could not get file to watch")
    91  		return
    92  	}
    93  	configFile := filepath.Clean(filename)
    94  	configDir, _ := filepath.Split(configFile)
    95  	realConfigFile, _ := filepath.EvalSymlinks(filename)
    96  	filehash, err := getFileHash(realConfigFile)
    97  	if err != nil {
    98  		log.Error().Err(err).Msg("could not generate original config file hash")
    99  		return
   100  	}
   101  	watcherConfig := &watcherWorkerConfig{filename: filename, configFile: configFile, realConfigFile: realConfigFile, filehash: filehash, callbacks: conf.callbacks}
   102  	watcher.Add(configDir)
   103  	go watchWorker(watcher, watcherConfig)
   104  }
   105  
   106  func watchWorker(watcher *fsnotify.Watcher, workerConf *watcherWorkerConfig) {
   107  	// Heavily inspired from - https://github.com/spf13/viper/blob/8c894384998e656900b125e674b8c20dbf87cc06/viper.go
   108  	for {
   109  		select {
   110  		case event, ok := <-watcher.Events:
   111  			if ok {
   112  				if processFileChangeEvent(&event, workerConf) {
   113  					return
   114  				}
   115  			}
   116  		case err, ok := <-watcher.Errors:
   117  			if ok {
   118  				log.Warn().Err(err).Msg("watcher error")
   119  			}
   120  			return
   121  		}
   122  	}
   123  }
   124  
   125  var (
   126  	processFileChangeEvent = func(event *fsnotify.Event, workerConf *watcherWorkerConfig) bool {
   127  		currentConfigFile, _ := filepath.EvalSymlinks(workerConf.filename)
   128  		const writeOrCreateMask = fsnotify.Write | fsnotify.Create
   129  		log.Debug().Uint32("writeOrCreateMask", uint32(event.Op)).Str("eventName", event.Name).Msg("File change event")
   130  		if (filepath.Clean(event.Name) == workerConf.configFile &&
   131  			event.Op&writeOrCreateMask != 0) ||
   132  			(currentConfigFile != "" && currentConfigFile != workerConf.realConfigFile) {
   133  			workerConf.realConfigFile = currentConfigFile
   134  			workerConf.filehash = callCallbacksIfChanged(workerConf.realConfigFile, workerConf.filehash, workerConf.callbacks)
   135  
   136  		} else if filepath.Clean(event.Name) == workerConf.configFile &&
   137  			event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
   138  			return true
   139  		}
   140  		return false
   141  	}
   142  
   143  	callCallbacksIfChanged = func(realConfigFile, oldHash string, callbacks []func()) string {
   144  		newhash, err := getFileHash(realConfigFile)
   145  		if err != nil {
   146  			if err == errTruncatedConfigFile {
   147  				log.Warn().Err(err).Msg("truncation of config file not expected")
   148  			} else {
   149  				log.Error().Err(err).Msg("could not generate file hash on change")
   150  			}
   151  			return oldHash
   152  		}
   153  		log.Debug().Str("oldHash", oldHash).Str("newHash", newhash).Msg("Old and new hash")
   154  		if newhash != oldHash {
   155  			for _, callback := range callbacks {
   156  				go callback()
   157  			}
   158  		}
   159  		return newhash
   160  	}
   161  
   162  	createNewWatcher = func() (*fsnotify.Watcher, error) {
   163  		return fsnotify.NewWatcher()
   164  	}
   165  
   166  	getFileToWatch = func(configPath string) (filename string, err error) {
   167  		filename = configPath
   168  		fileInfo, err := os.Stat(filename)
   169  		if err != nil || !fileInfo.Mode().IsRegular() {
   170  			filename = ConfigFilename
   171  			fileInfo, err = os.Stat(filename)
   172  			if err != nil || !fileInfo.Mode().IsRegular() {
   173  				log.Warn().Err(errNoFileToWatch).Msg("could not find any file to watch")
   174  				return "", errNoFileToWatch
   175  			}
   176  		}
   177  		return filename, nil
   178  	}
   179  
   180  	getFileHash = func(filePath string) (hashHex string, err error) {
   181  		file, err := os.Open(filePath)
   182  		if err != nil {
   183  			return "", err
   184  		}
   185  		defer file.Close()
   186  
   187  		var buf bytes.Buffer
   188  		if _, err = io.Copy(&buf, file); err == nil {
   189  			log.Debug().Str("Content", buf.String()).Msg("Content generating hash for")
   190  			if buf.Len() == 0 {
   191  				return "", errTruncatedConfigFile
   192  			}
   193  			hasher := sha256.New()
   194  			if _, err = io.Copy(hasher, strings.NewReader(buf.String())); err == nil {
   195  				hashHex = hex.EncodeToString(hasher.Sum(nil))
   196  			}
   197  		}
   198  		return hashHex, err
   199  	}
   200  )