config.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. package config
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "maps"
  8. "math"
  9. "os"
  10. "os/signal"
  11. "path/filepath"
  12. "sort"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "syscall"
  17. "time"
  18. "dario.cat/mergo"
  19. "go.yaml.in/yaml/v3"
  20. )
  21. type C struct {
  22. path string
  23. files []string
  24. Settings map[string]any
  25. oldSettings map[string]any
  26. callbacks []func(*C, *Reload)
  27. l *slog.Logger
  28. reloadLock sync.Mutex
  29. // Set by the first reload that needs a restart and never cleared. Guarded by reloadLock
  30. needsRestart bool
  31. }
  32. func NewC(l *slog.Logger) *C {
  33. return &C{
  34. Settings: make(map[string]any),
  35. l: l,
  36. }
  37. }
  38. // Load will find all yaml files within path and load them in lexical order
  39. func (c *C) Load(path string) error {
  40. c.path = path
  41. c.files = make([]string, 0)
  42. err := c.resolve(path, true)
  43. if err != nil {
  44. return err
  45. }
  46. if len(c.files) == 0 {
  47. return fmt.Errorf("no config files found at %s", path)
  48. }
  49. sort.Strings(c.files)
  50. err = c.parse()
  51. if err != nil {
  52. return err
  53. }
  54. return nil
  55. }
  56. func (c *C) LoadString(raw string) error {
  57. if raw == "" {
  58. return errors.New("Empty configuration")
  59. }
  60. return c.parseRaw([]byte(raw))
  61. }
  62. // RegisterReloadCallback stores a function to be called when a config reload is triggered. The functions registered
  63. // here should decide if they need to make a change to the current process before making the change. HasChanged can be
  64. // used to help decide if a change is necessary.
  65. // These functions should return quickly or spawn their own go routine if they will take a while.
  66. // Use r to report anything that failed or needs a restart to apply.
  67. func (c *C) RegisterReloadCallback(f func(c *C, r *Reload)) {
  68. c.callbacks = append(c.callbacks, f)
  69. }
  70. // InitialLoad returns true if this is the first load of the config, and ReloadConfig has not been called yet.
  71. func (c *C) InitialLoad() bool {
  72. return c.oldSettings == nil
  73. }
  74. // Render returns keys and their values as yaml, for a subsystem to remember what it applied. Empty means they couldn't
  75. // be rendered and should never match.
  76. func (c *C) Render(keys ...string) string {
  77. m := make(map[string]any, len(keys))
  78. for _, k := range keys {
  79. m[k] = c.Get(k)
  80. }
  81. b, err := yaml.Marshal(m)
  82. if err != nil {
  83. c.l.Debug("Failed to render config", "keys", keys, "error", err)
  84. return ""
  85. }
  86. return string(b)
  87. }
  88. // HasChanged checks if the underlying structure of the provided key has changed after a config reload. The value of
  89. // k in both the old and new settings will be serialized, the result of the string comparison is returned.
  90. // If k is an empty string the entire config is tested.
  91. // It's important to note that this is very rudimentary and susceptible to configuration ordering issues indicating
  92. // there is change when there actually wasn't any.
  93. func (c *C) HasChanged(k string) bool {
  94. if c.oldSettings == nil {
  95. return false
  96. }
  97. var (
  98. nv any
  99. ov any
  100. )
  101. if k == "" {
  102. nv = c.Settings
  103. ov = c.oldSettings
  104. k = "all settings"
  105. } else {
  106. nv = c.get(k, c.Settings)
  107. ov = c.get(k, c.oldSettings)
  108. }
  109. newVals, err := yaml.Marshal(nv)
  110. if err != nil {
  111. c.l.Error("Error while marshaling new config",
  112. "config_path", k,
  113. "error", err,
  114. )
  115. }
  116. oldVals, err := yaml.Marshal(ov)
  117. if err != nil {
  118. c.l.Error("Error while marshaling old config",
  119. "config_path", k,
  120. "error", err,
  121. )
  122. }
  123. return string(newVals) != string(oldVals)
  124. }
  125. // CatchHUP will listen for the HUP signal in a go routine and reload all configs found in the
  126. // original path provided to Load. The old settings are shallow copied for change detection after the reload.
  127. func (c *C) CatchHUP(ctx context.Context) {
  128. if c.path == "" {
  129. return
  130. }
  131. ch := make(chan os.Signal, 1)
  132. signal.Notify(ch, syscall.SIGHUP)
  133. go func() {
  134. for {
  135. select {
  136. case <-ctx.Done():
  137. signal.Stop(ch)
  138. close(ch)
  139. return
  140. case <-ch:
  141. c.l.Info("Caught HUP, reloading config")
  142. c.ReloadConfig().Log(c.l)
  143. }
  144. }
  145. }()
  146. }
  147. // ReloadConfig reloads the config from the path provided to Load and runs every registered callback. A config that
  148. // fails to load is logged here, like any other failure, and the result carries the error for the summary.
  149. func (c *C) ReloadConfig() ReloadResult {
  150. c.reloadLock.Lock()
  151. defer c.reloadLock.Unlock()
  152. c.oldSettings = make(map[string]any)
  153. maps.Copy(c.oldSettings, c.Settings)
  154. if err := c.Load(c.path); err != nil {
  155. c.l.Error("Error occurred while reloading config", "config_path", c.path, "error", err)
  156. return ReloadResult{Err: err, RestartRequired: c.needsRestart}
  157. }
  158. return c.runCallbacks()
  159. }
  160. // ReloadConfigString is ReloadConfig for a config held in memory rather than on disk.
  161. func (c *C) ReloadConfigString(raw string) ReloadResult {
  162. c.reloadLock.Lock()
  163. defer c.reloadLock.Unlock()
  164. c.oldSettings = make(map[string]any)
  165. maps.Copy(c.oldSettings, c.Settings)
  166. if err := c.LoadString(raw); err != nil {
  167. c.l.Error("Error occurred while reloading config", "error", err)
  168. return ReloadResult{Err: err, RestartRequired: c.needsRestart}
  169. }
  170. return c.runCallbacks()
  171. }
  172. func (c *C) runCallbacks() ReloadResult {
  173. r := &Reload{c: c}
  174. for _, v := range c.callbacks {
  175. v(c, r)
  176. }
  177. if r.restart {
  178. c.needsRestart = true
  179. }
  180. return ReloadResult{Failed: r.failed, RestartRequired: c.needsRestart, RestartKeys: r.keys}
  181. }
  182. // GetString will get the string for k or return the default d if not found or invalid
  183. func (c *C) GetString(k, d string) string {
  184. r := c.Get(k)
  185. if r == nil {
  186. return d
  187. }
  188. return fmt.Sprintf("%v", r)
  189. }
  190. // GetStringSlice will get the slice of strings for k or return the default d if not found or invalid
  191. func (c *C) GetStringSlice(k string, d []string) []string {
  192. r := c.Get(k)
  193. if r == nil {
  194. return d
  195. }
  196. rv, ok := r.([]any)
  197. if !ok {
  198. return d
  199. }
  200. v := make([]string, len(rv))
  201. for i := range v {
  202. v[i] = fmt.Sprintf("%v", rv[i])
  203. }
  204. return v
  205. }
  206. // GetMap will get the map for k or return the default d if not found or invalid
  207. func (c *C) GetMap(k string, d map[string]any) map[string]any {
  208. r := c.Get(k)
  209. if r == nil {
  210. return d
  211. }
  212. v, ok := r.(map[string]any)
  213. if !ok {
  214. return d
  215. }
  216. return v
  217. }
  218. // GetInt will get the int for k or return the default d if not found or invalid
  219. func (c *C) GetInt(k string, d int) int {
  220. r := c.GetString(k, strconv.Itoa(d))
  221. v, err := strconv.Atoi(r)
  222. if err != nil {
  223. return d
  224. }
  225. return v
  226. }
  227. // GetUint32 will get the uint32 for k or return the default d if not found or invalid
  228. func (c *C) GetUint32(k string, d uint32) uint32 {
  229. r := c.GetInt(k, int(d))
  230. if r < 0 || uint64(r) > uint64(math.MaxUint32) {
  231. return d
  232. }
  233. return uint32(r)
  234. }
  235. // GetBool will get the bool for k or return the default d if not found or invalid
  236. func (c *C) GetBool(k string, d bool) bool {
  237. r := strings.ToLower(c.GetString(k, fmt.Sprintf("%v", d)))
  238. v, err := strconv.ParseBool(r)
  239. if err != nil {
  240. switch r {
  241. case "y", "yes":
  242. return true
  243. case "n", "no":
  244. return false
  245. }
  246. return d
  247. }
  248. return v
  249. }
  250. func AsBool(v any) (value bool, ok bool) {
  251. switch x := v.(type) {
  252. case bool:
  253. return x, true
  254. case string:
  255. switch x {
  256. case "y", "yes":
  257. return true, true
  258. case "n", "no":
  259. return false, true
  260. }
  261. }
  262. return false, false
  263. }
  264. // GetDuration will get the duration for k or return the default d if not found or invalid
  265. func (c *C) GetDuration(k string, d time.Duration) time.Duration {
  266. r := c.GetString(k, "")
  267. v, err := time.ParseDuration(r)
  268. if err != nil {
  269. return d
  270. }
  271. return v
  272. }
  273. func (c *C) Get(k string) any {
  274. return c.get(k, c.Settings)
  275. }
  276. func (c *C) IsSet(k string) bool {
  277. return c.get(k, c.Settings) != nil
  278. }
  279. func (c *C) get(k string, v any) any {
  280. parts := strings.SplitSeq(k, ".")
  281. for p := range parts {
  282. m, ok := v.(map[string]any)
  283. if !ok {
  284. return nil
  285. }
  286. v, ok = m[p]
  287. if !ok {
  288. return nil
  289. }
  290. }
  291. return v
  292. }
  293. // direct signifies if this is the config path directly specified by the user,
  294. // versus a file/dir found by recursing into that path
  295. func (c *C) resolve(path string, direct bool) error {
  296. i, err := os.Stat(path)
  297. if err != nil {
  298. return nil
  299. }
  300. if !i.IsDir() {
  301. c.addFile(path, direct)
  302. return nil
  303. }
  304. paths, err := readDirNames(path)
  305. if err != nil {
  306. return fmt.Errorf("problem while reading directory %s: %s", path, err)
  307. }
  308. for _, p := range paths {
  309. err := c.resolve(filepath.Join(path, p), false)
  310. if err != nil {
  311. return err
  312. }
  313. }
  314. return nil
  315. }
  316. func (c *C) addFile(path string, direct bool) error {
  317. ext := filepath.Ext(path)
  318. if !direct && ext != ".yaml" && ext != ".yml" {
  319. return nil
  320. }
  321. ap, err := filepath.Abs(path)
  322. if err != nil {
  323. return err
  324. }
  325. c.files = append(c.files, ap)
  326. return nil
  327. }
  328. func (c *C) parseRaw(b []byte) error {
  329. var m map[string]any
  330. err := yaml.Unmarshal(b, &m)
  331. if err != nil {
  332. return err
  333. }
  334. c.Settings = m
  335. return nil
  336. }
  337. func (c *C) parse() error {
  338. var m map[string]any
  339. for _, path := range c.files {
  340. b, err := os.ReadFile(path)
  341. if err != nil {
  342. return err
  343. }
  344. var nm map[string]any
  345. err = yaml.Unmarshal(b, &nm)
  346. if err != nil {
  347. return fmt.Errorf("%s: %w", path, err)
  348. }
  349. // We need to use WithAppendSlice so that firewall rules in separate
  350. // files are appended together
  351. err = mergo.Merge(&nm, m, mergo.WithAppendSlice)
  352. m = nm
  353. if err != nil {
  354. return err
  355. }
  356. }
  357. c.Settings = m
  358. return nil
  359. }
  360. func readDirNames(path string) ([]string, error) {
  361. f, err := os.Open(path)
  362. if err != nil {
  363. return nil, err
  364. }
  365. paths, err := f.Readdirnames(-1)
  366. f.Close()
  367. if err != nil {
  368. return nil, err
  369. }
  370. sort.Strings(paths)
  371. return paths, nil
  372. }