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 )