github.com/cilium/cilium@v1.16.2/pkg/hubble/exporter/config_watcher.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package exporter 5 6 import ( 7 "context" 8 "crypto/md5" 9 "encoding/binary" 10 "errors" 11 "fmt" 12 "os" 13 14 "github.com/sirupsen/logrus" 15 "sigs.k8s.io/yaml" 16 17 "github.com/cilium/cilium/pkg/logging/logfields" 18 "github.com/cilium/cilium/pkg/time" 19 ) 20 21 var reloadInterval = 5 * time.Second 22 23 type configWatcher struct { 24 logger logrus.FieldLogger 25 configFilePath string 26 callback func(ctx context.Context, hash uint64, config DynamicExportersConfig) 27 ticker *time.Ticker 28 stop chan bool 29 } 30 31 // NewConfigWatcher creates a config watcher instance. Config watcher notifies 32 // dynamic exporter when config file changes and dynamic exporter should be 33 // reconciled. 34 func NewConfigWatcher( 35 configFilePath string, 36 callback func(ctx context.Context, hash uint64, config DynamicExportersConfig), 37 ) *configWatcher { 38 watcher := &configWatcher{ 39 logger: logrus.New().WithField(logfields.LogSubsys, "hubble").WithField("configFilePath", configFilePath), 40 configFilePath: configFilePath, 41 callback: callback, 42 } 43 44 // initial configuration load 45 watcher.reload() 46 47 // TODO replace ticker reloads with inotify watchers 48 watcher.ticker = time.NewTicker(reloadInterval) 49 watcher.stop = make(chan bool) 50 51 go func() { 52 for { 53 select { 54 case <-watcher.stop: 55 return 56 case <-watcher.ticker.C: 57 watcher.reload() 58 } 59 } 60 }() 61 62 return watcher 63 } 64 65 func (c *configWatcher) reload() { 66 c.logger.Debug("Attempting reload") 67 config, hash, err := c.readConfig() 68 if err != nil { 69 DynamicExporterReconfigurations.WithLabelValues("failure").Inc() 70 c.logger.Warnf("failed reading dynamic exporter config") 71 } else { 72 c.callback(context.TODO(), hash, *config) 73 } 74 } 75 76 // Stop stops watcher. 77 func (c *configWatcher) Stop() { 78 if c.ticker != nil { 79 c.ticker.Stop() 80 } 81 c.stop <- true 82 } 83 84 func (c *configWatcher) readConfig() (*DynamicExportersConfig, uint64, error) { 85 config := &DynamicExportersConfig{} 86 yamlFile, err := os.ReadFile(c.configFilePath) 87 if err != nil { 88 return nil, 0, fmt.Errorf("cannot read file '%s': %w", c.configFilePath, err) 89 } 90 if err := yaml.Unmarshal(yamlFile, config); err != nil { 91 return nil, 0, fmt.Errorf("cannot parse yaml: %w", err) 92 } 93 94 if err := validateConfig(config); err != nil { 95 return nil, 0, fmt.Errorf("invalid yaml config file: %w", err) 96 } 97 98 return config, calculateHash(yamlFile), nil 99 } 100 101 func calculateHash(file []byte) uint64 { 102 sum := md5.Sum(file) 103 return binary.LittleEndian.Uint64(sum[0:16]) 104 } 105 106 func validateConfig(config *DynamicExportersConfig) error { 107 flowlogNames := make(map[string]interface{}) 108 flowlogPaths := make(map[string]interface{}) 109 110 var errs error 111 112 for i := range config.FlowLogs { 113 if config.FlowLogs[i] == nil { 114 errs = errors.Join(errs, fmt.Errorf("invalid flowlog at index %d", i)) 115 continue 116 } 117 name := config.FlowLogs[i].Name 118 if name == "" { 119 errs = errors.Join(errs, fmt.Errorf("name is required")) 120 } else { 121 if _, ok := flowlogNames[name]; ok { 122 errs = errors.Join(errs, fmt.Errorf("duplicated flowlog name %s", name)) 123 } 124 flowlogNames[name] = struct{}{} 125 } 126 127 filePath := config.FlowLogs[i].FilePath 128 if filePath == "" { 129 errs = errors.Join(errs, fmt.Errorf("filePath is required")) 130 } else { 131 if _, ok := flowlogPaths[filePath]; ok { 132 errs = errors.Join(errs, fmt.Errorf("duplicated flowlog path %s", filePath)) 133 } 134 flowlogPaths[filePath] = struct{}{} 135 } 136 } 137 138 return errs 139 }