wire_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. //go:build !windows
  2. package localapi
  3. import (
  4. "context"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. "github.com/slackhq/nebula/config"
  11. "github.com/slackhq/nebula/test"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. // TestWireRecoversFromBadPathAtStartup covers a socket path that is unusable when nebula
  16. // starts. The operator's fix is a config edit and a reload, so the reload has to be able
  17. // to bring up a socket that never existed, not only move one that does.
  18. func TestWireRecoversFromBadPathAtStartup(t *testing.T) {
  19. ctx, cancel := context.WithCancel(context.Background())
  20. t.Cleanup(cancel)
  21. c := config.NewC(test.NewLogger())
  22. require.NoError(t, c.LoadString(fmt.Sprintf("local_api:\n control:\n socket: /%s\n", strings.Repeat("x", sunPathMax))))
  23. srv := Wire(ctx, test.NewLogger(), c, basicRegistry())
  24. t.Cleanup(srv.Stop)
  25. assert.Empty(t, srv.Path(), "an over-long path must not produce a socket")
  26. good := testSocket(t)
  27. require.NoError(t, c.ReloadConfigString(fmt.Sprintf("local_api:\n control:\n socket: %s\n", good)).Err)
  28. assert.Equal(t, good, srv.Path())
  29. out, errText := run(t, good, "echo", "hi")
  30. assert.Empty(t, errText)
  31. assert.Equal(t, "[hi]", out)
  32. }
  33. // TestRebindAfterReloadBoundTheSocket covers a HUP that lands inside Wire, after the
  34. // reload callback is registered and before the first attempt runs. The callback does the
  35. // first bind; the attempt must then find the socket it was about to create already up
  36. // and leave it, not fail or rebind it, which would have Wire warn that commands are
  37. // unavailable while they are.
  38. func TestRebindAfterReloadBoundTheSocket(t *testing.T) {
  39. ctx, cancel := context.WithCancel(context.Background())
  40. t.Cleanup(cancel)
  41. path := testSocket(t)
  42. cfg := fmt.Sprintf("local_api:\n control:\n socket: %s\n", path)
  43. c := config.NewC(test.NewLogger())
  44. require.NoError(t, c.LoadString(cfg))
  45. srv := NewServer(test.NewLogger(), basicRegistry())
  46. t.Cleanup(srv.Stop)
  47. // Wire's first half, then the HUP.
  48. followControlConfig(ctx, test.NewLogger(), c, srv)
  49. require.NoError(t, c.ReloadConfigString(cfg).Err)
  50. require.Equal(t, path, srv.Path(), "the reload callback does the first bind")
  51. // Wire's second half.
  52. require.NoError(t, srv.Rebind(ctx, path, ownerOnly()))
  53. assert.Equal(t, path, srv.Path())
  54. out, errText := run(t, path, "echo", "hi")
  55. assert.Empty(t, errText)
  56. assert.Equal(t, "[hi]", out)
  57. }
  58. // TestWireRetriesAFailedMoveOnPlainReload covers a move whose bind fails for a reason
  59. // outside the config: the key names a path under a runtime directory that is not there
  60. // yet. The daemon keeps the old socket, but the client reads the new path from the same
  61. // file and cannot reach it, so the operator creates the directory and reloads. The key
  62. // has not changed since the move was asked for, and the reload still has to make it.
  63. func TestWireRetriesAFailedMoveOnPlainReload(t *testing.T) {
  64. ctx, cancel := context.WithCancel(context.Background())
  65. t.Cleanup(cancel)
  66. section := func(path string) string {
  67. return fmt.Sprintf("local_api:\n control:\n socket: %s\n", path)
  68. }
  69. first := testSocket(t)
  70. c := config.NewC(test.NewLogger())
  71. require.NoError(t, c.LoadString(section(first)))
  72. srv := Wire(ctx, test.NewLogger(), c, basicRegistry())
  73. t.Cleanup(srv.Stop)
  74. require.Equal(t, first, srv.Path())
  75. runtimeDir := filepath.Join(privateBase(t), "run")
  76. moved := filepath.Join(runtimeDir, DirName, "s.sock")
  77. require.NoError(t, c.ReloadConfigString(section(moved)).Err)
  78. assert.Equal(t, first, srv.Path(), "a move that cannot bind keeps the socket it had")
  79. require.NoError(t, os.Mkdir(runtimeDir, 0700))
  80. require.NoError(t, c.ReloadConfigString(section(moved)).Err)
  81. require.False(t, c.HasChanged(CtlSocketKey), "the config is the same; the fix was on disk")
  82. assert.Equal(t, moved, srv.Path())
  83. _, err := os.Stat(first)
  84. assert.True(t, os.IsNotExist(err), "the old socket is given up once the move binds")
  85. out, errText := run(t, moved, "echo", "hi")
  86. assert.Empty(t, errText)
  87. assert.Equal(t, "[hi]", out)
  88. }
  89. // TestWireRecoversOnPlainReloadAfterBadBind covers a path that is fine but a bind that
  90. // is not: the runtime directory is missing when nebula starts. The fix is made outside
  91. // the config, so the config does not change; a plain reload still has to bring the
  92. // socket up.
  93. func TestWireRecoversOnPlainReloadAfterBadBind(t *testing.T) {
  94. ctx, cancel := context.WithCancel(context.Background())
  95. t.Cleanup(cancel)
  96. base := privateBase(t)
  97. runtimeDir := filepath.Join(base, "run")
  98. path := filepath.Join(runtimeDir, DirName, "s.sock")
  99. cfg := fmt.Sprintf("local_api:\n control:\n socket: %s\n", path)
  100. c := config.NewC(test.NewLogger())
  101. require.NoError(t, c.LoadString(cfg))
  102. srv := Wire(ctx, test.NewLogger(), c, basicRegistry())
  103. t.Cleanup(srv.Stop)
  104. assert.Empty(t, srv.Path(), "listen creates one level only, so a missing runtime dir fails the bind")
  105. require.NoError(t, os.Mkdir(runtimeDir, 0700))
  106. require.NoError(t, c.ReloadConfigString(cfg).Err)
  107. require.False(t, c.HasChanged(CtlSocketKey), "the config is the same; the fix was on disk")
  108. assert.Equal(t, path, srv.Path())
  109. out, errText := run(t, path, "echo", "hi")
  110. assert.Empty(t, errText)
  111. assert.Equal(t, "[hi]", out)
  112. // And a reload with a socket up and no change in the key is still a no-op.
  113. require.NoError(t, c.ReloadConfigString(cfg).Err)
  114. assert.Equal(t, path, srv.Path())
  115. out, errText = run(t, path, "echo", "still")
  116. assert.Empty(t, errText)
  117. assert.Equal(t, "[still]", out)
  118. }
  119. // TestWireControlCanBeDisabled: local_api.control.enabled false serves no socket, and
  120. // the key follows reloads both ways.
  121. func TestWireControlCanBeDisabled(t *testing.T) {
  122. ctx, cancel := context.WithCancel(context.Background())
  123. t.Cleanup(cancel)
  124. path := testSocket(t)
  125. section := func(enabled bool) string {
  126. return fmt.Sprintf("local_api:\n control:\n enabled: %t\n socket: %s\n", enabled, path)
  127. }
  128. c := config.NewC(test.NewLogger())
  129. require.NoError(t, c.LoadString(section(false)))
  130. srv := Wire(ctx, test.NewLogger(), c, basicRegistry())
  131. t.Cleanup(srv.Stop)
  132. assert.Empty(t, srv.Path())
  133. _, err := os.Lstat(path)
  134. assert.True(t, os.IsNotExist(err), "no socket is bound")
  135. require.NoError(t, c.ReloadConfigString(section(true)).Err)
  136. assert.Equal(t, path, srv.Path())
  137. out, errText := run(t, path, "echo", "on")
  138. assert.Empty(t, errText)
  139. assert.Equal(t, "[on]", out)
  140. require.NoError(t, c.ReloadConfigString(section(false)).Err)
  141. assert.Empty(t, srv.Path())
  142. _, err = os.Lstat(path)
  143. assert.True(t, os.IsNotExist(err), "the socket is removed")
  144. }