package config import ( "os" "path/filepath" "sync" "testing" "time" "dario.cat/mergo" "github.com/slackhq/nebula/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v3" ) func TestConfig_Load(t *testing.T) { l := test.NewLogger() dir, err := os.MkdirTemp("", "config-test") // invalid yaml c := NewC(l) os.WriteFile(filepath.Join(dir, "01.yaml"), []byte(" invalid yaml"), 0644) 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 {}") // simple multi config merge c = NewC(l) os.RemoveAll(dir) os.Mkdir(dir, 0755) require.NoError(t, err) os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644) os.WriteFile(filepath.Join(dir, "02.yml"), []byte("outer:\n inner: override\nnew: hi"), 0644) require.NoError(t, c.Load(dir)) expected := map[string]any{ "outer": map[string]any{ "inner": "override", }, "new": "hi", } assert.Equal(t, expected, c.Settings) } func TestConfig_Get(t *testing.T) { l := test.NewLogger() // test simple type c := NewC(l) c.Settings["firewall"] = map[string]any{"outbound": "hi"} assert.Equal(t, "hi", c.Get("firewall.outbound")) // test complex type inner := []map[string]any{{"port": "1", "code": "2"}} c.Settings["firewall"] = map[string]any{"outbound": inner} assert.EqualValues(t, inner, c.Get("firewall.outbound")) // test missing assert.Nil(t, c.Get("firewall.nope")) } func TestConfig_GetStringSlice(t *testing.T) { l := test.NewLogger() c := NewC(l) c.Settings["slice"] = []any{"one", "two"} assert.Equal(t, []string{"one", "two"}, c.GetStringSlice("slice", []string{})) } func TestConfig_GetBool(t *testing.T) { l := test.NewLogger() c := NewC(l) c.Settings["bool"] = true assert.True(t, c.GetBool("bool", false)) c.Settings["bool"] = "true" assert.True(t, c.GetBool("bool", false)) c.Settings["bool"] = false assert.False(t, c.GetBool("bool", true)) c.Settings["bool"] = "false" assert.False(t, c.GetBool("bool", true)) c.Settings["bool"] = "Y" assert.True(t, c.GetBool("bool", false)) c.Settings["bool"] = "yEs" assert.True(t, c.GetBool("bool", false)) c.Settings["bool"] = "N" assert.False(t, c.GetBool("bool", true)) c.Settings["bool"] = "nO" assert.False(t, c.GetBool("bool", true)) } func TestConfig_HasChanged(t *testing.T) { l := test.NewLogger() // No reload has occurred, return false c := NewC(l) c.Settings["test"] = "hi" assert.False(t, c.HasChanged("")) // Test key change c = NewC(l) c.Settings["test"] = "hi" c.oldSettings = map[string]any{"test": "no"} assert.True(t, c.HasChanged("test")) assert.True(t, c.HasChanged("")) // No key change c = NewC(l) c.Settings["test"] = "hi" c.oldSettings = map[string]any{"test": "hi"} assert.False(t, c.HasChanged("test")) assert.False(t, c.HasChanged("")) } func TestConfig_ReloadConfig(t *testing.T) { l := test.NewLogger() done := make(chan bool, 1) dir, err := os.MkdirTemp("", "config-test") require.NoError(t, err) os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644) c := NewC(l) require.NoError(t, c.Load(dir)) assert.False(t, c.HasChanged("outer.inner")) assert.False(t, c.HasChanged("outer")) assert.False(t, c.HasChanged("")) os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: ho"), 0644) c.RegisterReloadCallback(func(c *C, _ *Reload) { done <- true }) c.ReloadConfig() assert.True(t, c.HasChanged("outer.inner")) assert.True(t, c.HasChanged("outer")) assert.True(t, c.HasChanged("")) // Make sure we call the callbacks select { case <-done: case <-time.After(1 * time.Second): panic("timeout") } } // Ensure mergo merges are done the way we expect. // This is needed to test for potential regressions, like: // - https://github.com/imdario/mergo/issues/187 func TestConfig_MergoMerge(t *testing.T) { configs := [][]byte{ []byte(` listen: port: 1234 `), []byte(` firewall: inbound: - port: 443 proto: tcp groups: - server - port: 443 proto: tcp groups: - webapp `), []byte(` listen: host: 0.0.0.0 port: 4242 firewall: outbound: - port: any proto: any host: any inbound: - port: any proto: icmp host: any `), } var m map[string]any // merge the same way config.parse() merges for _, b := range configs { var nm map[string]any err := yaml.Unmarshal(b, &nm) require.NoError(t, err) // We need to use WithAppendSlice so that firewall rules in separate // files are appended together err = mergo.Merge(&nm, m, mergo.WithAppendSlice) m = nm require.NoError(t, err) } t.Logf("Merged Config: %#v", m) mYaml, err := yaml.Marshal(m) require.NoError(t, err) t.Logf("Merged Config as YAML:\n%s", mYaml) // If a bug is present, some items might be replaced instead of merged like we expect expected := map[string]any{ "firewall": map[string]any{ "inbound": []any{ map[string]any{"host": "any", "port": "any", "proto": "icmp"}, map[string]any{"groups": []any{"server"}, "port": 443, "proto": "tcp"}, map[string]any{"groups": []any{"webapp"}, "port": 443, "proto": "tcp"}}, "outbound": []any{ map[string]any{"host": "any", "port": "any", "proto": "any"}}}, "listen": map[string]any{ "host": "0.0.0.0", "port": 4242, }, } assert.Equal(t, expected, m) } // TestRegisterReloadCallbackDuringReload: registration is not confined to startup. The // HUP handler runs from inside nebula.Main, and anything wired after Main returns // registers while a reload may already be iterating the callbacks. Under -race this is // the data race on the slice; without it, it checks a callback registered mid-reload is // picked up by the next one. func TestRegisterReloadCallbackDuringReload(t *testing.T) { l := test.NewLogger() c := NewC(l) require.NoError(t, c.LoadString("a: 1")) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i := 0; i < 200; i++ { c.RegisterReloadCallback(func(*C, *Reload) {}) } }() go func() { defer wg.Done() for i := 0; i < 200; i++ { assert.NoError(t, c.ReloadConfigString("a: 1").Err) } }() wg.Wait() // A callback that registers another callback must not deadlock, and the new one // runs from the following reload on. var ran int c.RegisterReloadCallback(func(c *C, _ *Reload) { c.RegisterReloadCallback(func(*C, *Reload) { ran++ }) }) require.NoError(t, c.ReloadConfigString("a: 1").Err) assert.Equal(t, 0, ran, "registered during this reload, so not run by it") require.NoError(t, c.ReloadConfigString("a: 1").Err) assert.Equal(t, 1, ran, "and run by the next") }