| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- package nebula
- import (
- "context"
- "io"
- "log/slog"
- "net"
- "strconv"
- "testing"
- "time"
- "github.com/slackhq/nebula/config"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func newTestStatsServer(t *testing.T) (*statsServer, *config.C) {
- t.Helper()
- l := slog.New(slog.DiscardHandler)
- ctx, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
- return &statsServer{
- l: l,
- ctx: ctx,
- }, config.NewC(l)
- }
- func setStatsConfig(c *config.C, m map[string]any) {
- c.Settings["stats"] = m
- }
- func currentRuntime(s *statsServer) *statsRuntime {
- s.runMu.Lock()
- defer s.runMu.Unlock()
- return s.run
- }
- func TestStatsServer_reload_initial_disabled(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{"type": "none"})
- require.NoError(t, s.reload(c, true))
- assert.False(t, s.enabled.Load())
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_reload_initial_invalidInterval(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "graphite",
- "host": "127.0.0.1:0",
- "prefix": "test",
- })
- err := s.reload(c, true)
- require.Error(t, err)
- assert.False(t, s.enabled.Load())
- }
- func TestStatsServer_reload_initial_unknownType(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "carbon",
- "interval": "1s",
- })
- err := s.reload(c, true)
- require.Error(t, err)
- assert.False(t, s.enabled.Load())
- }
- func TestStatsServer_reload_unchanged_noOp(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{"type": "none"})
- require.NoError(t, s.reload(c, true))
- require.NoError(t, s.reload(c, false))
- assert.False(t, s.enabled.Load())
- }
- func TestStatsServer_reload_initial_graphite(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "graphite",
- "interval": "1s",
- "protocol": "tcp",
- "host": "127.0.0.1:2003",
- "prefix": "test",
- })
- require.NoError(t, s.reload(c, true))
- assert.True(t, s.enabled.Load())
- // reload only records config; Start builds the runtime.
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_reload_initial_prometheus(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:0",
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- assert.True(t, s.enabled.Load())
- // reload only records config; Start builds the runtime.
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_Start_graphite_blocksUntilStop(t *testing.T) {
- sink := newGraphiteSink(t)
- defer sink.Close()
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "graphite",
- "interval": "1s",
- "protocol": "tcp",
- "host": sink.Addr(),
- "prefix": "test",
- })
- require.NoError(t, s.reload(c, true))
- done := make(chan struct{})
- go func() {
- s.Start()
- close(done)
- }()
- // Wait for Start to publish runtime state.
- waitFor(t, func() bool { return currentRuntime(s) != nil })
- rt := currentRuntime(s)
- require.NotNil(t, rt)
- assert.Nil(t, rt.listener, "graphite has no listener")
- s.Stop()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("graphite Start did not return after Stop")
- }
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_StartStop_lifecycle(t *testing.T) {
- port := freeTCPPort(t)
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- done := make(chan struct{})
- go func() {
- s.Start()
- close(done)
- }()
- waitForListening(t, "127.0.0.1:"+port)
- rt := currentRuntime(s)
- require.NotNil(t, rt)
- require.NotNil(t, rt.listener)
- s.Stop()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("Start did not return after Stop")
- }
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_reload_disable_stopsRunningRuntime(t *testing.T) {
- port := freeTCPPort(t)
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- done := make(chan struct{})
- go func() {
- s.Start()
- close(done)
- }()
- waitForListening(t, "127.0.0.1:"+port)
- setStatsConfig(c, map[string]any{"type": "none"})
- require.NoError(t, s.reload(c, false))
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("Start did not return after reload disabled stats")
- }
- assert.False(t, s.enabled.Load())
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_reload_changeListener_restartsListener(t *testing.T) {
- port1 := freeTCPPort(t)
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port1,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- firstDone := make(chan struct{})
- go func() {
- s.Start()
- close(firstDone)
- }()
- waitForListening(t, "127.0.0.1:"+port1)
- first := currentRuntime(s)
- require.NotNil(t, first)
- port2 := freeTCPPort(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port2,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, false))
- select {
- case <-firstDone:
- case <-time.After(5 * time.Second):
- t.Fatal("old Start did not return after reload")
- }
- waitForListening(t, "127.0.0.1:"+port2)
- second := currentRuntime(s)
- require.NotNil(t, second)
- assert.NotSame(t, first, second, "expected a new runtime after listen address change")
- s.Stop()
- }
- func TestStatsServer_Stop_beforeStart_doesNotBlock(t *testing.T) {
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:0",
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- stopped := make(chan struct{})
- go func() {
- s.Stop()
- close(stopped)
- }()
- select {
- case <-stopped:
- case <-time.After(time.Second):
- t.Fatal("Stop hung with no runtime started")
- }
- }
- func TestStatsServer_configTest_validatesWithoutSpawning(t *testing.T) {
- s, c := newTestStatsServer(t)
- s.configTest = true
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:0",
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- s.Start()
- assert.Nil(t, currentRuntime(s))
- }
- func TestStatsServer_ctxCancel_unblocksStart(t *testing.T) {
- // Ensures ctx cancellation alone (no explicit Stop) tears down both
- // graphite and prom Start invocations.
- port := freeTCPPort(t)
- l := slog.New(slog.DiscardHandler)
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- s := &statsServer{l: l, ctx: ctx}
- c := config.NewC(l)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- done := make(chan struct{})
- go func() {
- s.Start()
- close(done)
- }()
- waitForListening(t, "127.0.0.1:"+port)
- cancel()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("Start did not return after ctx cancel")
- }
- }
- func TestStatsServer_listenerBindFailure_sameCfgReloadRetries(t *testing.T) {
- // Hold the port so ListenAndServe will fail on first Start.
- blocker, err := net.Listen("tcp", "127.0.0.1:0")
- require.NoError(t, err)
- port := strconv.Itoa(blocker.Addr().(*net.TCPAddr).Port)
- s, c := newTestStatsServer(t)
- setStatsConfig(c, map[string]any{
- "type": "prometheus",
- "interval": "1s",
- "listen": "127.0.0.1:" + port,
- "path": "/metrics",
- })
- require.NoError(t, s.reload(c, true))
- done := make(chan struct{})
- go func() {
- s.Start()
- close(done)
- }()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- t.Fatal("Start did not return after bind failure")
- }
- // Bind failure should have dropped the cached config so a same-cfg
- // SIGHUP can retry.
- s.runMu.Lock()
- cfgAfterFailure := s.runCfg
- s.runMu.Unlock()
- assert.Nil(t, cfgAfterFailure)
- // Free the port and reload with the same config; Start should fire again.
- require.NoError(t, blocker.Close())
- require.NoError(t, s.reload(c, false))
- waitForListening(t, "127.0.0.1:"+port)
- require.NotNil(t, currentRuntime(s))
- s.Stop()
- }
- func waitForListening(t *testing.T, addr string) {
- t.Helper()
- waitFor(t, func() bool {
- conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
- if err != nil {
- return false
- }
- _ = conn.Close()
- return true
- })
- }
- // graphiteSink is a minimal TCP accept-and-discard server so graphiteOnce
- // calls in tests don't spam error logs or wedge on connection refused.
- type graphiteSink struct {
- ln net.Listener
- }
- func newGraphiteSink(t *testing.T) *graphiteSink {
- t.Helper()
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- require.NoError(t, err)
- g := &graphiteSink{ln: ln}
- go func() {
- for {
- conn, err := ln.Accept()
- if err != nil {
- return
- }
- go func(c net.Conn) {
- _, _ = io.Copy(io.Discard, c)
- _ = c.Close()
- }(conn)
- }
- }()
- return g
- }
- func (g *graphiteSink) Addr() string { return g.ln.Addr().String() }
- func (g *graphiteSink) Close() { _ = g.ln.Close() }
- func freeTCPPort(t *testing.T) string {
- t.Helper()
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- require.NoError(t, err)
- port := ln.Addr().(*net.TCPAddr).Port
- require.NoError(t, ln.Close())
- return strconv.Itoa(port)
- }
|