github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/connection/connection_watcher.go (about) 1 package connection 2 3 import ( 4 "context" 5 "log" 6 7 "github.com/fsnotify/fsnotify" 8 filehelpers "github.com/turbot/go-kit/files" 9 "github.com/turbot/go-kit/filewatcher" 10 "github.com/turbot/go-kit/helpers" 11 "github.com/turbot/steampipe/pkg/cmdconfig" 12 "github.com/turbot/steampipe/pkg/constants" 13 "github.com/turbot/steampipe/pkg/filepaths" 14 "github.com/turbot/steampipe/pkg/steampipeconfig" 15 ) 16 17 type ConnectionWatcher struct { 18 fileWatcherErrorHandler func(error) 19 watcher *filewatcher.FileWatcher 20 // interface exposing the plugin manager functions we need 21 pluginManager pluginManager 22 } 23 24 func NewConnectionWatcher(pluginManager pluginManager) (*ConnectionWatcher, error) { 25 w := &ConnectionWatcher{ 26 pluginManager: pluginManager, 27 } 28 29 watcherOptions := &filewatcher.WatcherOptions{ 30 Directories: []string{filepaths.EnsureConfigDir()}, 31 Include: filehelpers.InclusionsFromExtensions([]string{constants.ConfigExtension}), 32 ListFlag: filehelpers.FilesRecursive, 33 EventMask: fsnotify.Create | fsnotify.Remove | fsnotify.Rename | fsnotify.Write | fsnotify.Chmod, 34 OnChange: func(events []fsnotify.Event) { 35 w.handleFileWatcherEvent(events) 36 }, 37 } 38 watcher, err := filewatcher.NewWatcher(watcherOptions) 39 if err != nil { 40 return nil, err 41 } 42 w.watcher = watcher 43 44 // set the file watcher error handler, which will get called when there are parsing errors 45 // after a file watcher event 46 w.fileWatcherErrorHandler = func(err error) { 47 log.Printf("[WARN] failed to reload connection config: %s", err.Error()) 48 } 49 50 watcher.Start() 51 52 log.Printf("[INFO] created ConnectionWatcher") 53 return w, nil 54 } 55 56 func (w *ConnectionWatcher) handleFileWatcherEvent([]fsnotify.Event) { 57 defer func() { 58 if r := recover(); r != nil { 59 log.Printf("[WARN] ConnectionWatcher caught a panic: %s", helpers.ToError(r).Error()) 60 } 61 }() 62 // this is a file system event handler and not bound to any context 63 ctx := context.Background() 64 65 log.Printf("[INFO] ConnectionWatcher handleFileWatcherEvent") 66 config, errorsAndWarnings := steampipeconfig.LoadConnectionConfig(context.Background()) 67 // send notification if there were any errors or warnings 68 if !errorsAndWarnings.Empty() { 69 w.pluginManager.SendPostgresErrorsAndWarningsNotification(ctx, errorsAndWarnings) 70 // if there was an error return 71 if errorsAndWarnings.GetError() != nil { 72 log.Printf("[WARN] error loading updated connection config: %v", errorsAndWarnings.GetError()) 73 return 74 } 75 } 76 77 log.Printf("[INFO] loaded updated config") 78 79 // We need to update the viper config and GlobalConfig 80 // as these are both used by RefreshConnectionAndSearchPathsWithLocalClient 81 82 // set the global steampipe config 83 steampipeconfig.GlobalConfig = config 84 85 // call on changed callback - we must call this BEFORE calling refresh connections 86 // convert config to format expected by plugin manager 87 // (plugin manager cannot reference steampipe config to avoid circular deps) 88 configMap := NewConnectionConfigMap(config.Connections) 89 w.pluginManager.OnConnectionConfigChanged(ctx, configMap, config.PluginsInstances) 90 91 // The only configurations from GlobalConfig which have 92 // impact during Refresh are Database options and the Connections 93 // themselves. 94 // 95 // It is safe to ignore the Workspace Profile here since this 96 // code runs in the plugin-manager and has been started with the 97 // install-dir properly set from the active Workspace Profile 98 // 99 // Workspace Profile does not have any setting which can alter 100 // behavior in service mode (namely search path). Therefore, it is safe 101 // to use the GlobalConfig here and ignore Workspace Profile in general 102 cmdconfig.SetDefaultsFromConfig(steampipeconfig.GlobalConfig.ConfigMap()) 103 104 log.Printf("[INFO] calling RefreshConnections asyncronously") 105 106 // call RefreshConnections asyncronously 107 // the RefreshConnections implements its own locking to ensure only a single execution and a single queues execution 108 go RefreshConnections(ctx, w.pluginManager) 109 110 log.Printf("[TRACE] File watch event done") 111 } 112 113 func (w *ConnectionWatcher) Close() { 114 w.watcher.Close() 115 }