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  }