config_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package config
  2. import (
  3. "os"
  4. "path/filepath"
  5. "sync"
  6. "testing"
  7. "time"
  8. "dario.cat/mergo"
  9. "github.com/slackhq/nebula/test"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "go.yaml.in/yaml/v3"
  13. )
  14. func TestConfig_Load(t *testing.T) {
  15. l := test.NewLogger()
  16. dir, err := os.MkdirTemp("", "config-test")
  17. // invalid yaml
  18. c := NewC(l)
  19. os.WriteFile(filepath.Join(dir, "01.yaml"), []byte(" invalid yaml"), 0644)
  20. require.EqualError(t, c.Load(dir), filepath.Join(dir, "01.yaml")+": yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `invalid...` into map[string]interface {}")
  21. // simple multi config merge
  22. c = NewC(l)
  23. os.RemoveAll(dir)
  24. os.Mkdir(dir, 0755)
  25. require.NoError(t, err)
  26. os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
  27. os.WriteFile(filepath.Join(dir, "02.yml"), []byte("outer:\n inner: override\nnew: hi"), 0644)
  28. require.NoError(t, c.Load(dir))
  29. expected := map[string]any{
  30. "outer": map[string]any{
  31. "inner": "override",
  32. },
  33. "new": "hi",
  34. }
  35. assert.Equal(t, expected, c.Settings)
  36. }
  37. func TestConfig_Get(t *testing.T) {
  38. l := test.NewLogger()
  39. // test simple type
  40. c := NewC(l)
  41. c.Settings["firewall"] = map[string]any{"outbound": "hi"}
  42. assert.Equal(t, "hi", c.Get("firewall.outbound"))
  43. // test complex type
  44. inner := []map[string]any{{"port": "1", "code": "2"}}
  45. c.Settings["firewall"] = map[string]any{"outbound": inner}
  46. assert.EqualValues(t, inner, c.Get("firewall.outbound"))
  47. // test missing
  48. assert.Nil(t, c.Get("firewall.nope"))
  49. }
  50. func TestConfig_GetStringSlice(t *testing.T) {
  51. l := test.NewLogger()
  52. c := NewC(l)
  53. c.Settings["slice"] = []any{"one", "two"}
  54. assert.Equal(t, []string{"one", "two"}, c.GetStringSlice("slice", []string{}))
  55. }
  56. func TestConfig_GetBool(t *testing.T) {
  57. l := test.NewLogger()
  58. c := NewC(l)
  59. c.Settings["bool"] = true
  60. assert.True(t, c.GetBool("bool", false))
  61. c.Settings["bool"] = "true"
  62. assert.True(t, c.GetBool("bool", false))
  63. c.Settings["bool"] = false
  64. assert.False(t, c.GetBool("bool", true))
  65. c.Settings["bool"] = "false"
  66. assert.False(t, c.GetBool("bool", true))
  67. c.Settings["bool"] = "Y"
  68. assert.True(t, c.GetBool("bool", false))
  69. c.Settings["bool"] = "yEs"
  70. assert.True(t, c.GetBool("bool", false))
  71. c.Settings["bool"] = "N"
  72. assert.False(t, c.GetBool("bool", true))
  73. c.Settings["bool"] = "nO"
  74. assert.False(t, c.GetBool("bool", true))
  75. }
  76. func TestConfig_HasChanged(t *testing.T) {
  77. l := test.NewLogger()
  78. // No reload has occurred, return false
  79. c := NewC(l)
  80. c.Settings["test"] = "hi"
  81. assert.False(t, c.HasChanged(""))
  82. // Test key change
  83. c = NewC(l)
  84. c.Settings["test"] = "hi"
  85. c.oldSettings = map[string]any{"test": "no"}
  86. assert.True(t, c.HasChanged("test"))
  87. assert.True(t, c.HasChanged(""))
  88. // No key change
  89. c = NewC(l)
  90. c.Settings["test"] = "hi"
  91. c.oldSettings = map[string]any{"test": "hi"}
  92. assert.False(t, c.HasChanged("test"))
  93. assert.False(t, c.HasChanged(""))
  94. }
  95. func TestConfig_ReloadConfig(t *testing.T) {
  96. l := test.NewLogger()
  97. done := make(chan bool, 1)
  98. dir, err := os.MkdirTemp("", "config-test")
  99. require.NoError(t, err)
  100. os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
  101. c := NewC(l)
  102. require.NoError(t, c.Load(dir))
  103. assert.False(t, c.HasChanged("outer.inner"))
  104. assert.False(t, c.HasChanged("outer"))
  105. assert.False(t, c.HasChanged(""))
  106. os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: ho"), 0644)
  107. c.RegisterReloadCallback(func(c *C, _ *Reload) {
  108. done <- true
  109. })
  110. c.ReloadConfig()
  111. assert.True(t, c.HasChanged("outer.inner"))
  112. assert.True(t, c.HasChanged("outer"))
  113. assert.True(t, c.HasChanged(""))
  114. // Make sure we call the callbacks
  115. select {
  116. case <-done:
  117. case <-time.After(1 * time.Second):
  118. panic("timeout")
  119. }
  120. }
  121. // Ensure mergo merges are done the way we expect.
  122. // This is needed to test for potential regressions, like:
  123. // - https://github.com/imdario/mergo/issues/187
  124. func TestConfig_MergoMerge(t *testing.T) {
  125. configs := [][]byte{
  126. []byte(`
  127. listen:
  128. port: 1234
  129. `),
  130. []byte(`
  131. firewall:
  132. inbound:
  133. - port: 443
  134. proto: tcp
  135. groups:
  136. - server
  137. - port: 443
  138. proto: tcp
  139. groups:
  140. - webapp
  141. `),
  142. []byte(`
  143. listen:
  144. host: 0.0.0.0
  145. port: 4242
  146. firewall:
  147. outbound:
  148. - port: any
  149. proto: any
  150. host: any
  151. inbound:
  152. - port: any
  153. proto: icmp
  154. host: any
  155. `),
  156. }
  157. var m map[string]any
  158. // merge the same way config.parse() merges
  159. for _, b := range configs {
  160. var nm map[string]any
  161. err := yaml.Unmarshal(b, &nm)
  162. require.NoError(t, err)
  163. // We need to use WithAppendSlice so that firewall rules in separate
  164. // files are appended together
  165. err = mergo.Merge(&nm, m, mergo.WithAppendSlice)
  166. m = nm
  167. require.NoError(t, err)
  168. }
  169. t.Logf("Merged Config: %#v", m)
  170. mYaml, err := yaml.Marshal(m)
  171. require.NoError(t, err)
  172. t.Logf("Merged Config as YAML:\n%s", mYaml)
  173. // If a bug is present, some items might be replaced instead of merged like we expect
  174. expected := map[string]any{
  175. "firewall": map[string]any{
  176. "inbound": []any{
  177. map[string]any{"host": "any", "port": "any", "proto": "icmp"},
  178. map[string]any{"groups": []any{"server"}, "port": 443, "proto": "tcp"},
  179. map[string]any{"groups": []any{"webapp"}, "port": 443, "proto": "tcp"}},
  180. "outbound": []any{
  181. map[string]any{"host": "any", "port": "any", "proto": "any"}}},
  182. "listen": map[string]any{
  183. "host": "0.0.0.0",
  184. "port": 4242,
  185. },
  186. }
  187. assert.Equal(t, expected, m)
  188. }
  189. // TestRegisterReloadCallbackDuringReload: registration is not confined to startup. The
  190. // HUP handler runs from inside nebula.Main, and anything wired after Main returns
  191. // registers while a reload may already be iterating the callbacks. Under -race this is
  192. // the data race on the slice; without it, it checks a callback registered mid-reload is
  193. // picked up by the next one.
  194. func TestRegisterReloadCallbackDuringReload(t *testing.T) {
  195. l := test.NewLogger()
  196. c := NewC(l)
  197. require.NoError(t, c.LoadString("a: 1"))
  198. var wg sync.WaitGroup
  199. wg.Add(2)
  200. go func() {
  201. defer wg.Done()
  202. for i := 0; i < 200; i++ {
  203. c.RegisterReloadCallback(func(*C, *Reload) {})
  204. }
  205. }()
  206. go func() {
  207. defer wg.Done()
  208. for i := 0; i < 200; i++ {
  209. assert.NoError(t, c.ReloadConfigString("a: 1").Err)
  210. }
  211. }()
  212. wg.Wait()
  213. // A callback that registers another callback must not deadlock, and the new one
  214. // runs from the following reload on.
  215. var ran int
  216. c.RegisterReloadCallback(func(c *C, _ *Reload) {
  217. c.RegisterReloadCallback(func(*C, *Reload) { ran++ })
  218. })
  219. require.NoError(t, c.ReloadConfigString("a: 1").Err)
  220. assert.Equal(t, 0, ran, "registered during this reload, so not run by it")
  221. require.NoError(t, c.ReloadConfigString("a: 1").Err)
  222. assert.Equal(t, 1, ran, "and run by the next")
  223. }