config_reloader.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package config
  2. import (
  3. "os"
  4. "path/filepath"
  5. "reflect"
  6. "regexp"
  7. "sort"
  8. "strings"
  9. "github.com/go-viper/mapstructure/v2"
  10. "github.com/knadh/koanf/parsers/yaml"
  11. "github.com/knadh/koanf/providers/file"
  12. "github.com/knadh/koanf/v2"
  13. "github.com/prometheus/client_golang/prometheus"
  14. "github.com/prometheus/client_golang/prometheus/promauto"
  15. log "github.com/sirupsen/logrus"
  16. )
  17. var (
  18. metricConfigActionCount = promauto.NewGauge(prometheus.GaugeOpts{
  19. Name: "olivetin_config_action_count",
  20. Help: "The number of actions in the config file",
  21. })
  22. metricConfigReloadedCount = promauto.NewCounter(prometheus.CounterOpts{
  23. Name: "olivetin_config_reloaded_count",
  24. Help: "The number of times the config has been reloaded",
  25. })
  26. listeners []func()
  27. )
  28. func AddListener(l func()) {
  29. listeners = append(listeners, l)
  30. }
  31. func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
  32. log.WithFields(log.Fields{
  33. "configPath": configPath,
  34. }).Info("Appending cfg source")
  35. loadIncludedConfigsFromDir(k, configPath)
  36. if !unmarshalRoot(k, cfg) {
  37. return
  38. }
  39. afterLoadFinalize(cfg, configPath)
  40. }
  41. func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
  42. err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{
  43. Tag: "koanf",
  44. DecoderConfig: &mapstructure.DecoderConfig{
  45. DecodeHook: mapstructure.ComposeDecodeHookFunc(
  46. envDecodeHookFunc,
  47. mapstructure.StringToTimeDurationHookFunc(),
  48. mapstructure.TextUnmarshallerHookFunc(),
  49. ),
  50. WeaklyTypedInput: true,
  51. },
  52. })
  53. if err != nil {
  54. log.Errorf("Error unmarshalling config: %v", err)
  55. return false
  56. }
  57. return true
  58. }
  59. func afterLoadFinalize(cfg *Config, configPath string) {
  60. metricConfigReloadedCount.Inc()
  61. metricConfigActionCount.Set(float64(len(cfg.Actions)))
  62. cfg.SetDir(filepath.Dir(configPath))
  63. cfg.Sanitize()
  64. for _, l := range listeners {
  65. l()
  66. }
  67. }
  68. // buildIncludePath constructs the full path to the include directory.
  69. func buildIncludePath(k *koanf.Koanf, baseConfigPath string) string {
  70. relativeIncludePath := k.String("include")
  71. return filepath.Join(filepath.Dir(baseConfigPath), relativeIncludePath)
  72. }
  73. // loadAndMergeYamlFiles loads and merges all YAML files from the include directory.
  74. func loadAndMergeYamlFiles(k *koanf.Koanf, includePath string, yamlFiles []string) {
  75. sort.Strings(yamlFiles)
  76. for _, filename := range yamlFiles {
  77. loadAndMergeIncludedFile(k, includePath, filename)
  78. }
  79. log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
  80. }
  81. // loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
  82. func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
  83. relativeIncludePath := k.String("include")
  84. if relativeIncludePath == "" {
  85. return
  86. }
  87. includePath := buildIncludePath(k, baseConfigPath)
  88. log.WithFields(log.Fields{
  89. "includePath": includePath,
  90. }).Infof("Loading included configs from dir")
  91. yamlFiles, ok := listYamlFiles(includePath)
  92. if !ok || len(yamlFiles) == 0 {
  93. return
  94. }
  95. loadAndMergeYamlFiles(k, includePath, yamlFiles)
  96. }
  97. // validateIncludeDirectory checks if the given path exists and is a directory.
  98. func validateIncludeDirectory(includePath string) bool {
  99. dirInfo, err := os.Stat(includePath)
  100. if err != nil {
  101. log.Warnf("Include directory not found: %s", includePath)
  102. return false
  103. }
  104. if !dirInfo.IsDir() {
  105. log.Warnf("Include path is not a directory: %s", includePath)
  106. return false
  107. }
  108. return true
  109. }
  110. // isYamlFile checks if a filename has a YAML extension.
  111. func isYamlFile(name string) bool {
  112. return strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml")
  113. }
  114. // filterYamlFilesFromEntries extracts YAML file names from directory entries.
  115. func filterYamlFilesFromEntries(entries []os.DirEntry) []string {
  116. var yamlFiles []string
  117. for _, entry := range entries {
  118. if entry.IsDir() {
  119. continue
  120. }
  121. if isYamlFile(entry.Name()) {
  122. yamlFiles = append(yamlFiles, entry.Name())
  123. }
  124. }
  125. return yamlFiles
  126. }
  127. func listYamlFiles(includePath string) ([]string, bool) {
  128. if !validateIncludeDirectory(includePath) {
  129. return nil, false
  130. }
  131. entries, err := os.ReadDir(includePath)
  132. if err != nil {
  133. log.Errorf("Error reading include directory: %v", err)
  134. return nil, false
  135. }
  136. yamlFiles := filterYamlFilesFromEntries(entries)
  137. if len(yamlFiles) == 0 {
  138. log.Infof("No YAML files found in include directory: %s", includePath)
  139. }
  140. return yamlFiles, true
  141. }
  142. func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
  143. filePath := filepath.Join(includePath, filename)
  144. if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
  145. log.Errorf("Error loading included config file %s: %v", filePath, err)
  146. return
  147. }
  148. log.WithFields(log.Fields{
  149. "filePath": filePath,
  150. }).Info("Successfully loaded included config file")
  151. }
  152. // mergeActionsWhenBothExist merges actions when both src and dest have actions.
  153. func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}) {
  154. srcSlice, ok1 := srcActions.([]interface{})
  155. destSlice, ok2 := destActions.([]interface{})
  156. if ok1 && ok2 {
  157. dest["actions"] = append(destSlice, srcSlice...)
  158. } else {
  159. dest["actions"] = srcActions
  160. }
  161. }
  162. // mergeActionsFromSource merges actions from source into destination.
  163. func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}) {
  164. if destActions, ok := dest["actions"]; ok {
  165. mergeActionsWhenBothExist(srcActions, destActions, dest)
  166. } else {
  167. dest["actions"] = srcActions
  168. }
  169. }
  170. // mergeDashboardsWhenBothExist merges dashboards when both src and dest have dashboards.
  171. func mergeDashboardsWhenBothExist(srcDashboards interface{}, destDashboards interface{}, dest map[string]interface{}) {
  172. srcSlice, ok1 := srcDashboards.([]interface{})
  173. destSlice, ok2 := destDashboards.([]interface{})
  174. if ok1 && ok2 {
  175. dest["dashboards"] = append(destSlice, srcSlice...)
  176. } else {
  177. dest["dashboards"] = srcDashboards
  178. }
  179. }
  180. // mergeDashboardsFromSource merges dashboards from source into destination.
  181. func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interface{}) {
  182. if destDashboards, ok := dest["dashboards"]; ok {
  183. mergeDashboardsWhenBothExist(srcDashboards, destDashboards, dest)
  184. } else {
  185. dest["dashboards"] = srcDashboards
  186. }
  187. }
  188. // mergeEntitiesWhenBothExist merges entities when both src and dest have entities.
  189. func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) {
  190. srcSlice, ok1 := srcEntities.([]interface{})
  191. destSlice, ok2 := destEntities.([]interface{})
  192. if ok1 && ok2 {
  193. dest["entities"] = append(destSlice, srcSlice...)
  194. } else {
  195. dest["entities"] = srcEntities
  196. }
  197. }
  198. // mergeEntitiesFromSource merges entities from source into destination.
  199. func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) {
  200. if destEntities, ok := dest["entities"]; ok {
  201. mergeEntitiesWhenBothExist(srcEntities, destEntities, dest)
  202. } else {
  203. dest["entities"] = srcEntities
  204. }
  205. }
  206. func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
  207. if srcActions, ok := src["actions"]; ok {
  208. mergeActionsFromSource(srcActions, dest)
  209. }
  210. if srcDashboards, ok := src["dashboards"]; ok {
  211. mergeDashboardsFromSource(srcDashboards, dest)
  212. }
  213. if srcEntities, ok := src["entities"]; ok {
  214. mergeEntitiesFromSource(srcEntities, dest)
  215. }
  216. return nil
  217. }
  218. var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
  219. func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) {
  220. log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data)
  221. if from.Kind() != reflect.String {
  222. return data, nil
  223. }
  224. input := data.(string)
  225. log.Debugf("Processing string input: %q", input)
  226. output := envRegex.ReplaceAllStringFunc(input, func(match string) string {
  227. submatches := envRegex.FindStringSubmatch(match)
  228. key := submatches[1]
  229. val, set := os.LookupEnv(key)
  230. log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val)
  231. if !set {
  232. log.Warnf("Config file references unset environment variable: \"%s\"", key)
  233. }
  234. return val
  235. })
  236. log.Debugf("Environment variable interpolation result: %q -> %q", input, output)
  237. return output, nil
  238. }