istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/monitor/monitor.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package monitor 16 17 import ( 18 "os" 19 "path/filepath" 20 "reflect" 21 "strings" 22 "time" 23 24 "github.com/fsnotify/fsnotify" 25 26 "istio.io/istio/pilot/pkg/model" 27 "istio.io/istio/pkg/config" 28 istiolog "istio.io/istio/pkg/log" 29 ) 30 31 // Monitor will poll a config function in order to update a ConfigStore as 32 // changes are found. 33 type Monitor struct { 34 name string 35 root string 36 store model.ConfigStore 37 configs []*config.Config 38 getSnapshotFunc func() ([]*config.Config, error) 39 // channel to trigger updates on 40 // generally set to a file watch, but used in tests as well 41 updateCh chan struct{} 42 } 43 44 var log = istiolog.RegisterScope("monitor", "file configuration monitor") 45 46 // NewMonitor creates a Monitor and will delegate to a passed in controller. 47 // The controller holds a reference to the actual store. 48 // Any func that returns a []*model.Config can be used with the Monitor 49 func NewMonitor(name string, delegateStore model.ConfigStore, getSnapshotFunc func() ([]*config.Config, error), root string) *Monitor { 50 monitor := &Monitor{ 51 name: name, 52 root: root, 53 store: delegateStore, 54 getSnapshotFunc: getSnapshotFunc, 55 } 56 return monitor 57 } 58 59 const watchDebounceDelay = 50 * time.Millisecond 60 61 // Trigger notifications when a file is mutated 62 func fileTrigger(path string, ch chan struct{}, stop <-chan struct{}) error { 63 if path == "" { 64 return nil 65 } 66 fs, err := fsnotify.NewWatcher() 67 if err != nil { 68 return err 69 } 70 watcher := recursiveWatcher{fs} 71 if err = watcher.watchRecursive(path); err != nil { 72 return err 73 } 74 go func() { 75 defer watcher.Close() 76 var debounceC <-chan time.Time 77 for { 78 select { 79 case <-debounceC: 80 debounceC = nil 81 ch <- struct{}{} 82 case e := <-watcher.Events: 83 s, err := os.Stat(e.Name) 84 if err == nil && s != nil && s.IsDir() { 85 // If it's a directory, add a watch for it so we see nested files. 86 if e.Op&fsnotify.Create != 0 { 87 log.Debugf("add watch for %v: %v", s.Name(), watcher.watchRecursive(e.Name)) 88 } 89 } 90 // Can't stat a deleted directory, so attempt to remove it. If it fails it is not a problem 91 if e.Op&fsnotify.Remove != 0 { 92 _ = watcher.Remove(e.Name) 93 } 94 if debounceC == nil { 95 debounceC = time.After(watchDebounceDelay) 96 } 97 case err := <-watcher.Errors: 98 log.Warnf("Error watching file trigger: %v %v", path, err) 99 return 100 case signal := <-stop: 101 log.Infof("Shutting down file watcher: %v %v", path, signal) 102 return 103 } 104 } 105 }() 106 return nil 107 } 108 109 // recursiveWatcher wraps a fsnotify wrapper to add a best-effort recursive directory watching in user 110 // space. See https://github.com/fsnotify/fsnotify/issues/18. The implementation is inherently racy, 111 // as files added to a directory immediately after creation may not trigger events; as such it is only useful 112 // when an event causes a full reconciliation, rather than acting on an individual event 113 type recursiveWatcher struct { 114 *fsnotify.Watcher 115 } 116 117 // watchRecursive adds all directories under the given one to the watch list. 118 func (m recursiveWatcher) watchRecursive(path string) error { 119 err := filepath.Walk(path, func(walkPath string, fi os.FileInfo, err error) error { 120 if err != nil { 121 return err 122 } 123 if fi.IsDir() { 124 if err = m.Watcher.Add(walkPath); err != nil { 125 return err 126 } 127 } 128 return nil 129 }) 130 return err 131 } 132 133 // Start starts a new Monitor. Immediately checks the Monitor getSnapshotFunc 134 // and updates the controller. It then kicks off an asynchronous event loop that 135 // periodically polls the getSnapshotFunc for changes until a close event is sent. 136 func (m *Monitor) Start(stop <-chan struct{}) { 137 m.checkAndUpdate() 138 139 c := make(chan struct{}, 1) 140 m.updateCh = c 141 if err := fileTrigger(m.root, m.updateCh, stop); err != nil { 142 log.Errorf("Unable to setup FileTrigger for %s: %v", m.root, err) 143 } 144 // Run the close loop asynchronously. 145 go func() { 146 for { 147 select { 148 case <-c: 149 log.Infof("Triggering reload of file configuration") 150 m.checkAndUpdate() 151 case <-stop: 152 return 153 } 154 } 155 }() 156 } 157 158 func (m *Monitor) checkAndUpdate() { 159 newConfigs, err := m.getSnapshotFunc() 160 // If an error exists then log it and return to running the check and update 161 // Do not edit the local []*model.config until the connection has been reestablished 162 // The error will only come from a directory read error or a gRPC connection error 163 if err != nil { 164 log.Warnf("checkAndUpdate Error Caught %s: %v\n", m.name, err) 165 return 166 } 167 168 // make a deep copy of newConfigs to prevent data race 169 copyConfigs := make([]*config.Config, 0) 170 for _, config := range newConfigs { 171 cpy := config.DeepCopy() 172 copyConfigs = append(copyConfigs, &cpy) 173 } 174 175 // Compare the new list to the previous one and detect changes. 176 oldLen := len(m.configs) 177 newLen := len(newConfigs) 178 oldIndex, newIndex := 0, 0 179 for oldIndex < oldLen && newIndex < newLen { 180 oldConfig := m.configs[oldIndex] 181 newConfig := newConfigs[newIndex] 182 if v := compareIDs(oldConfig, newConfig); v < 0 { 183 m.deleteConfig(oldConfig) 184 oldIndex++ 185 } else if v > 0 { 186 m.createConfig(newConfig) 187 newIndex++ 188 } else { 189 // version may change without content changing 190 oldConfig.Meta.ResourceVersion = newConfig.Meta.ResourceVersion 191 if !reflect.DeepEqual(oldConfig, newConfig) { 192 m.updateConfig(newConfig) 193 } 194 oldIndex++ 195 newIndex++ 196 } 197 } 198 199 // Detect remaining deletions 200 for ; oldIndex < oldLen; oldIndex++ { 201 m.deleteConfig(m.configs[oldIndex]) 202 } 203 204 // Detect remaining additions 205 for ; newIndex < newLen; newIndex++ { 206 m.createConfig(newConfigs[newIndex]) 207 } 208 209 // Save the updated list. 210 m.configs = copyConfigs 211 } 212 213 func (m *Monitor) createConfig(c *config.Config) { 214 if _, err := m.store.Create(*c); err != nil { 215 log.Warnf("Failed to create config %s %s/%s: %v (%+v)", c.GroupVersionKind, c.Namespace, c.Name, err, *c) 216 } 217 } 218 219 func (m *Monitor) updateConfig(c *config.Config) { 220 // Set the resource version and create timestamp based on the existing config. 221 if prev := m.store.Get(c.GroupVersionKind, c.Name, c.Namespace); prev != nil { 222 c.ResourceVersion = prev.ResourceVersion 223 c.CreationTimestamp = prev.CreationTimestamp 224 } 225 226 if _, err := m.store.Update(*c); err != nil { 227 log.Warnf("Failed to update config (%+v): %v ", *c, err) 228 } 229 } 230 231 func (m *Monitor) deleteConfig(c *config.Config) { 232 if err := m.store.Delete(c.GroupVersionKind, c.Name, c.Namespace, nil); err != nil { 233 log.Warnf("Failed to delete config (%+v): %v ", *c, err) 234 } 235 } 236 237 // compareIDs compares the IDs (i.e. Namespace, GroupVersionKind, and Name) of the two configs and returns 238 // 0 if a == b, -1 if a < b, and 1 if a > b. Used for sorting config arrays. 239 func compareIDs(a, b *config.Config) int { 240 return strings.Compare(a.Key(), b.Key()) 241 }