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  }