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  }