github.com/cilium/cilium@v1.16.2/pkg/clustermesh/common/config.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package common 5 6 import ( 7 "crypto/sha256" 8 "errors" 9 "os" 10 "path/filepath" 11 "slices" 12 "strings" 13 14 "github.com/fsnotify/fsnotify" 15 "github.com/sirupsen/logrus" 16 ) 17 18 // clusterLifecycle is the interface to implement in order to receive cluster 19 // configuration lifecycle events. This is implemented by the ClusterMesh. 20 type clusterLifecycle interface { 21 add(clusterName, clusterConfigPath string) 22 remove(clusterName string) 23 } 24 25 type fhash [sha256.Size]byte 26 27 type configDirectoryWatcher struct { 28 watcher *fsnotify.Watcher 29 lifecycle clusterLifecycle 30 path string 31 tracked map[string]fhash 32 stop chan struct{} 33 } 34 35 func createConfigDirectoryWatcher(path string, lifecycle clusterLifecycle) (*configDirectoryWatcher, error) { 36 watcher, err := fsnotify.NewWatcher() 37 if err != nil { 38 return nil, err 39 } 40 41 if err := watcher.Add(path); err != nil { 42 watcher.Close() 43 return nil, err 44 } 45 46 return &configDirectoryWatcher{ 47 watcher: watcher, 48 path: path, 49 tracked: map[string]fhash{}, 50 lifecycle: lifecycle, 51 stop: make(chan struct{}), 52 }, nil 53 } 54 55 // isEtcdConfigFile returns whether the given path looks like a configuration 56 // file, and in that case it returns the corresponding hash to detect modifications. 57 func isEtcdConfigFile(path string) (bool, fhash) { 58 if info, err := os.Stat(path); err != nil || info.IsDir() { 59 return false, fhash{} 60 } 61 62 b, err := os.ReadFile(path) 63 if err != nil { 64 return false, fhash{} 65 } 66 67 // search for the "endpoints:" string 68 if strings.Contains(string(b), "endpoints:") { 69 return true, sha256.Sum256(b) 70 } 71 72 return false, fhash{} 73 } 74 75 func (cdw *configDirectoryWatcher) handle(abspath string) { 76 filename := filepath.Base(abspath) 77 isConfig, newHash := isEtcdConfigFile(abspath) 78 79 if !isConfig { 80 // If the corresponding cluster was tracked, then trigger the remove 81 // event, since the configuration file is no longer present/readable 82 if _, tracked := cdw.tracked[filename]; tracked { 83 log.WithFields(logrus.Fields{ 84 fieldClusterName: filename, 85 fieldConfig: abspath, 86 }).Debug("Removed cluster configuration") 87 88 // The remove operation returns an error if the file does no longer exists. 89 _ = cdw.watcher.Remove(abspath) 90 delete(cdw.tracked, filename) 91 cdw.lifecycle.remove(filename) 92 } 93 94 return 95 } 96 97 if !slices.Contains(cdw.watcher.WatchList(), abspath) { 98 // Start watching explicitly the file. This allows to receive a notification 99 // when the underlying file gets updated, if path points to a symbolic link. 100 // This is required to correctly detect file modifications when the folder 101 // is mounted from a Kubernetes ConfigMap/Secret. 102 if err := cdw.watcher.Add(abspath); err != nil && !errors.Is(err, os.ErrNotExist) { 103 log.WithError(err).WithField(fieldConfig, abspath). 104 Warning("Failed adding explicit path watch for config") 105 } else { 106 // There is a small chance that the file content changed in the time 107 // window from reading it at the beginning of the function to establishing 108 // the watcher. To avoid missing that possible update, let's re-read the 109 // file, so that we are sure to process the most up-to-date version. 110 // This prevents issues when modifying the same file twice back-to-back. 111 // We don't recurse in case a failure occurred when registering the 112 // watcher (except for NotFound) to prevent an infinite loop if 113 // something wrong happened. 114 cdw.handle(abspath) 115 return 116 } 117 } 118 119 oldHash, tracked := cdw.tracked[filename] 120 121 // Do not emit spurious notifications if the config file did not change. 122 if tracked && oldHash == newHash { 123 return 124 } 125 126 log.WithFields(logrus.Fields{ 127 fieldClusterName: filename, 128 fieldConfig: abspath, 129 }).Debug("Added or updated cluster configuration") 130 131 cdw.tracked[filename] = newHash 132 cdw.lifecycle.add(filename, abspath) 133 } 134 135 func (cdw *configDirectoryWatcher) watch() error { 136 log.WithField(fieldConfigDir, cdw.path).Debug("Starting config directory watcher") 137 138 files, err := os.ReadDir(cdw.path) 139 if err != nil { 140 return err 141 } 142 143 for _, f := range files { 144 if f.IsDir() { 145 continue 146 } 147 148 absolutePath := filepath.Join(cdw.path, f.Name()) 149 cdw.handle(absolutePath) 150 } 151 152 go cdw.loop() 153 return nil 154 } 155 156 func (cdw *configDirectoryWatcher) loop() { 157 for { 158 select { 159 case event := <-cdw.watcher.Events: 160 log.WithFields(logrus.Fields{ 161 fieldConfigDir: cdw.path, 162 fieldEvent: event, 163 }).Debug("Received fsnotify event") 164 cdw.handle(event.Name) 165 166 case err := <-cdw.watcher.Errors: 167 log.WithError(err).WithField(fieldConfigDir, cdw.path). 168 Warning("Error encountered while watching directory with fsnotify") 169 170 case <-cdw.stop: 171 return 172 } 173 } 174 } 175 176 func (cdw *configDirectoryWatcher) close() { 177 log.WithField(fieldConfigDir, cdw.path).Debug("Stopping config directory watcher") 178 close(cdw.stop) 179 cdw.watcher.Close() 180 } 181 182 // ConfigFiles returns the list of configuration files in the given path. It 183 // shall be used by CLI tools only, as it doesn't handle subsequent updates. 184 func ConfigFiles(cfgdir string) (configs map[string]string, err error) { 185 files, err := os.ReadDir(cfgdir) 186 if err != nil { 187 return nil, err 188 } 189 190 configs = make(map[string]string) 191 for _, f := range files { 192 cfgfile := filepath.Join(cfgdir, f.Name()) 193 if ok, _ := isEtcdConfigFile(cfgfile); ok { 194 configs[f.Name()] = cfgfile 195 } 196 } 197 198 return configs, nil 199 }