Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

debug: Improve namespace and region support #11269

Merged
merged 24 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6b13b90
Rename argNodes to generic utility function
davemay99 Oct 6, 2021
3c47f92
Add region and prefix matching for server members
davemay99 Oct 6, 2021
8897ed6
Include region and namespace in CLI output
davemay99 Oct 6, 2021
25ed8ff
Add region awareness to WaitForClient helper
davemay99 Oct 6, 2021
2db750d
Align variable names with underlying type
davemay99 Oct 6, 2021
4c8fa58
Add test for region
davemay99 Oct 6, 2021
33132cf
Add namespaces and regions to cluster meta info
davemay99 Oct 6, 2021
c43b697
Add changelog
davemay99 Oct 6, 2021
d45cfa0
Refactor WaitForClient helper function
davemay99 Oct 7, 2021
74867b1
Simplify test agent configuration functions
davemay99 Oct 7, 2021
8274faf
Tighten StringToSlice test coverage
davemay99 Oct 7, 2021
23bd22e
Clarify test names
davemay99 Oct 7, 2021
38bb29b
Rename server filter function for clarity
davemay99 Oct 7, 2021
a99ab0e
Move leader check outside loop to prevent duplicates
davemay99 Oct 7, 2021
1db4315
Adjust comment for clarity
davemay99 Oct 7, 2021
1ee480e
Fix region regression
davemay99 Oct 12, 2021
c9b0393
Refactor test client agent generation
davemay99 Oct 12, 2021
6d3c8ec
testServer already handles agent shutdown
davemay99 Oct 12, 2021
668e3bd
Use region var for expected outputs
davemay99 Oct 12, 2021
48ff716
Add test for SliceStringContainsPrefix
davemay99 Oct 12, 2021
4c9f655
Fix logic/tests for slice prefix helper functions
davemay99 Oct 12, 2021
037e801
revert testutil.WaitForClient addition
davemay99 Oct 12, 2021
37e5e22
eliminate import cycle caused by nomad/client
davemay99 Oct 12, 2021
4059695
Clarify slice HasPrefix tests
davemay99 Oct 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/11269.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cli: Improve debug namespace and region support
```
67 changes: 56 additions & 11 deletions command/operator_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func (c *OperatorDebugCommand) Run(args []string) int {
nodeLookupFailCount := 0
nodeCaptureCount := 0

for _, id := range argNodes(nodeIDs) {
for _, id := range stringToSlice(nodeIDs) {
if id == "all" {
// Capture from all nodes using empty prefix filter
id = ""
Expand Down Expand Up @@ -382,15 +382,15 @@ func (c *OperatorDebugCommand) Run(args []string) int {
c.Ui.Error(fmt.Sprintf("Failed to retrieve server list; err: %v", err))
return 1
}

// Write complete list of server members to file
c.writeJSON("version", "members.json", members, err)
// We always write the error to the file, but don't range if no members found
if serverIDs == "all" && members != nil {
// Special case to capture from all servers
for _, member := range members.Members {
c.serverIDs = append(c.serverIDs, member.Name)
}
} else {
c.serverIDs = append(c.serverIDs, argNodes(serverIDs)...)

// Filter for servers matching criteria
c.serverIDs, err = parseMembers(members, serverIDs, c.region)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse server list; err: %v", err))
return 1
}

serversFound := 0
Expand All @@ -412,6 +412,8 @@ func (c *OperatorDebugCommand) Run(args []string) int {
// Display general info about the capture
c.Ui.Output("Starting debugger...")
c.Ui.Output("")
c.Ui.Output(fmt.Sprintf(" Region: %s", c.region))
c.Ui.Output(fmt.Sprintf(" Namespace: %s", c.namespace))
c.Ui.Output(fmt.Sprintf(" Servers: (%d/%d) %v", serverCaptureCount, serversFound, c.serverIDs))
c.Ui.Output(fmt.Sprintf(" Clients: (%d/%d) %v", nodeCaptureCount, nodesFound, c.nodeIDs))
if nodeCaptureCount > 0 && nodeCaptureCount == c.maxNodes {
Expand Down Expand Up @@ -468,6 +470,13 @@ func (c *OperatorDebugCommand) collect(client *api.Client) error {
self, err := client.Agent().Self()
c.writeJSON(dir, "agent-self.json", self, err)

var qo *api.QueryOptions
namespaces, _, err := client.Namespaces().List(qo)
c.writeJSON(dir, "namespaces.json", namespaces, err)

regions, err := client.Regions().List()
c.writeJSON(dir, "regions.json", regions, err)

// Fetch data directly from consul and vault. Ignore errors
var consul, vault string

Expand Down Expand Up @@ -1055,8 +1064,44 @@ func TarCZF(archive string, src, target string) error {
})
}

// argNodes splits node ids from the command line by ","
func argNodes(input string) []string {
// parseMembers returns a slice of server member names matching the search criteria
func parseMembers(serverMembers *api.ServerMembers, serverIDs string, region string) (membersFound []string, err error) {
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
prefixes := stringToSlice(serverIDs)

if serverMembers.Members == nil {
return nil, fmt.Errorf("Failed to parse server members, members==nil")
}
davemay99 marked this conversation as resolved.
Show resolved Hide resolved

for _, member := range serverMembers.Members {
// If region is provided it must match exactly
if region != "" && member.Tags["region"] != region {
continue
}

// Always include "all"
if serverIDs == "all" {
membersFound = append(membersFound, member.Name)
continue
}

// Special case passthrough as literally "leader"
if serverIDs == "leader" {
membersFound = append(membersFound, "leader")
continue
}
davemay99 marked this conversation as resolved.
Show resolved Hide resolved

// Include member if name matches any prefix
if helper.SliceStringContainsPrefix(prefixes, member.Name) {
membersFound = append(membersFound, member.Name)
}
}

return membersFound, nil
}

// stringToSlice splits CSV string into slice, trims whitespace, and prunes
// empty values
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
func stringToSlice(input string) []string {
ns := strings.Split(input, ",")
var out []string
for _, n := range ns {
Expand Down
155 changes: 128 additions & 27 deletions command/operator_debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ func TestDebug_NodeClass(t *testing.T) {
}

// Start client 1
client1 := agent.NewTestAgent(t, "client1", agentConfFunc1)
defer client1.Shutdown()
agent1 := agent.NewTestAgent(t, "client1", agentConfFunc1)
defer agent1.Shutdown()

// Wait for client1 to connect
client1NodeID := client1.Agent.Client().NodeID()
testutil.WaitForClient(t, srv.Agent.RPC, client1NodeID)
t.Logf("[TEST] Client1 ready, id: %s", client1NodeID)
client1 := agent1.Agent.Client()
client1NodeID := client1.NodeID()
client1Region := client1.Region()
testutil.WaitForClient(t, srv.Agent.RPC, client1NodeID, client1Region)
t.Logf("[TEST] Client1 ready, id: %s, region: %s", client1NodeID, client1Region)
davemay99 marked this conversation as resolved.
Show resolved Hide resolved

// Setup client 2 (nodeclass = clientb)
agentConfFunc2 := func(c *agent.Config) {
Expand All @@ -91,13 +93,15 @@ func TestDebug_NodeClass(t *testing.T) {
}

// Start client 2
client2 := agent.NewTestAgent(t, "client2", agentConfFunc2)
defer client2.Shutdown()
agent2 := agent.NewTestAgent(t, "client2", agentConfFunc2)
defer agent2.Shutdown()

// Wait for client2 to connect
client2NodeID := client2.Agent.Client().NodeID()
testutil.WaitForClient(t, srv.Agent.RPC, client2NodeID)
t.Logf("[TEST] Client2 ready, id: %s", client2NodeID)
client2 := agent2.Agent.Client()
client2NodeID := client2.NodeID()
client2Region := client2.Region()
testutil.WaitForClient(t, srv.Agent.RPC, client2NodeID, client2Region)
t.Logf("[TEST] Client2 ready, id: %s, region: %s", client2NodeID, client2Region)

// Setup client 3 (nodeclass = clienta)
agentConfFunc3 := func(c *agent.Config) {
Expand All @@ -107,13 +111,15 @@ func TestDebug_NodeClass(t *testing.T) {
}

// Start client 3
client3 := agent.NewTestAgent(t, "client3", agentConfFunc3)
defer client3.Shutdown()
agent3 := agent.NewTestAgent(t, "client3", agentConfFunc3)
defer agent3.Shutdown()

// Wait for client3 to connect
client3NodeID := client3.Agent.Client().NodeID()
testutil.WaitForClient(t, srv.Agent.RPC, client3NodeID)
t.Logf("[TEST] Client3 ready, id: %s", client3NodeID)
client3 := agent3.Agent.Client()
client3NodeID := client3.NodeID()
client3Region := client3.Region()
testutil.WaitForClient(t, srv.Agent.RPC, client3NodeID, client3Region)
t.Logf("[TEST] Client3 ready, id: %s, region: %s", client3NodeID, client3Region)

// Setup test cases
cases := testCases{
Expand Down Expand Up @@ -169,17 +175,19 @@ func TestDebug_ClientToServer(t *testing.T) {
}

// Start client 1
client1 := agent.NewTestAgent(t, "client1", agentConfFunc1)
defer client1.Shutdown()
agent1 := agent.NewTestAgent(t, "client1", agentConfFunc1)
defer agent1.Shutdown()

// Wait for client 1 to connect
client1NodeID := client1.Agent.Client().NodeID()
testutil.WaitForClient(t, srv.Agent.RPC, client1NodeID)
t.Logf("[TEST] Client1 ready, id: %s", client1NodeID)
client1 := agent1.Agent.Client()
client1NodeID := client1.NodeID()
client1Region := client1.Region()
testutil.WaitForClient(t, srv.Agent.RPC, client1NodeID, client1Region)
t.Logf("[TEST] Client1 ready, id: %s, region: %s", client1NodeID, client1Region)

// Get API addresses
addrServer := srv.HTTPAddr()
addrClient1 := client1.HTTPAddr()
addrClient1 := agent1.HTTPAddr()

t.Logf("[TEST] testAgent api address: %s", url)
t.Logf("[TEST] Server api address: %s", addrServer)
Expand Down Expand Up @@ -210,6 +218,88 @@ func TestDebug_ClientToServer(t *testing.T) {
runTestCases(t, cases)
}

func TestDebug_ClientToServer_Region(t *testing.T) {
agentConfFunc := func(c *agent.Config) {
c.Region = "testregion"
}

// Start test server and API client
srv, _, url := testServer(t, false, agentConfFunc)
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
defer srv.Shutdown()

// Wait for leadership to establish
testutil.WaitForLeader(t, srv.Agent.RPC)

// Retrieve server RPC address to join client
srvRPCAddr := srv.GetConfig().AdvertiseAddrs.RPC
t.Logf("[TEST] Leader started, srv.GetConfig().AdvertiseAddrs.RPC: %s", srvRPCAddr)

// Setup client 1 (nodeclass = clienta)
agentConfFunc1 := func(c *agent.Config) {
c.Region = "testregion"
c.Server.Enabled = false
c.Client.NodeClass = "clienta"
c.Client.Enabled = true
c.Client.Servers = []string{srvRPCAddr}
}

// Start client 1
agent1 := agent.NewTestAgent(t, "client1", agentConfFunc1)
defer agent1.Shutdown()

// Wait for client 1 to connect
client1NodeID := agent1.Agent.Client().NodeID()
client1Region := agent1.Agent.Client().Region()
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
testutil.WaitForClient(t, srv.Agent.RPC, client1NodeID, client1Region)
t.Logf("[TEST] Client1 ready, id: %s, region: %s", client1NodeID, client1Region)

// Get API addresses
addrServer := srv.HTTPAddr()
addrClient1 := agent1.HTTPAddr()

t.Logf("[TEST] testAgent api address: %s", url)
t.Logf("[TEST] Server api address: %s", addrServer)
t.Logf("[TEST] Client1 api address: %s", addrClient1)

// Setup test cases
var cases = testCases{
// Good
{
name: "testAgent api server",
args: []string{"-address", url, "-region", "testregion", "-duration", "250ms", "-interval", "250ms", "-server-id", "all", "-node-id", "all"},
expectedCode: 0,
expectedOutputs: []string{
"Region: testregion\n",
"Servers: (1/1)",
"Clients: (1/1)",
"Created debug archive",
},
},
{
name: "server address",
args: []string{"-address", addrServer, "-region", "testregion", "-duration", "250ms", "-interval", "250ms", "-server-id", "all", "-node-id", "all"},
expectedCode: 0,
expectedOutputs: []string{"Created debug archive"},
},
{
name: "client1 address - verify no SIGSEGV panic",
args: []string{"-address", addrClient1, "-region", "testregion", "-duration", "250ms", "-interval", "250ms", "-server-id", "all", "-node-id", "all"},
expectedCode: 0,
expectedOutputs: []string{"Created debug archive"},
},

// Bad
{
name: "invalid region - all servers, all clients",
args: []string{"-address", url, "-region", "never", "-duration", "250ms", "-interval", "250ms", "-server-id", "all", "-node-id", "all"},
expectedCode: 1,
expectedError: "500 (No path to region)",
},
}

runTestCases(t, cases)
}

func TestDebug_SingleServer(t *testing.T) {
srv, _, url := testServer(t, false, nil)
defer srv.Shutdown()
Expand Down Expand Up @@ -432,15 +522,26 @@ func TestDebug_Fail_Pprof(t *testing.T) {
require.Contains(t, ui.OutputWriter.String(), "Created debug archive") // Archive should be generated anyway
}

func TestDebug_Utils(t *testing.T) {
func TestDebug_StringToSlice(t *testing.T) {
t.Parallel()

xs := argNodes("foo, bar")
require.Equal(t, []string{"foo", "bar"}, xs)
cases := []struct {
input string
expected []string
}{
{input: ",,", expected: []string(nil)},
{input: "", expected: []string(nil)},
{input: "foo, bar", expected: []string{"foo", "bar"}},
}
for _, tc := range cases {
out := stringToSlice(tc.input)
require.Equal(t, tc.expected, out)
require.Equal(t, true, helper.CompareSliceSetString(tc.expected, out))
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
}
}

xs = argNodes("")
require.Len(t, xs, 0)
require.Empty(t, xs)
func TestDebug_External(t *testing.T) {
t.Parallel()

// address calculation honors CONSUL_HTTP_SSL
// ssl: true - Correct alignment
Expand Down
10 changes: 10 additions & 0 deletions helper/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ func SliceStringContains(list []string, item string) bool {
return false
}

// SliceStringContainsPrefix returns true if any string in list matches prefix
func SliceStringContainsPrefix(list []string, prefix string) bool {
davemay99 marked this conversation as resolved.
Show resolved Hide resolved
for _, s := range list {
if strings.HasPrefix(s, prefix) {
return true
}
}
return false
}

func SliceSetDisjoint(first, second []string) (bool, []string) {
contained := make(map[string]struct{}, len(first))
for _, k := range first {
Expand Down
2 changes: 1 addition & 1 deletion nomad/client_agent_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ func TestAgentProfile_RemoteClient(t *testing.T) {
})
defer cleanupC()

testutil.WaitForClient(t, s2.RPC, c.NodeID())
testutil.WaitForClient(t, s2.RPC, c.NodeID(), c.Region())
testutil.WaitForResult(func() (bool, error) {
nodes := s2.connectedNodes()
return len(nodes) == 1, nil
Expand Down
7 changes: 5 additions & 2 deletions testutil/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,15 @@ func WaitForLeader(t testing.TB, rpc rpcFn) {
}

// WaitForClient blocks until the client can be found
func WaitForClient(t testing.TB, rpc rpcFn, nodeID string) {
func WaitForClient(t testing.TB, rpc rpcFn, nodeID string, region string) {
t.Helper()
if region == "" {
region = "global"
}
WaitForResult(func() (bool, error) {
req := structs.NodeSpecificRequest{
NodeID: nodeID,
QueryOptions: structs.QueryOptions{Region: "global"},
QueryOptions: structs.QueryOptions{Region: region},
}
var out structs.SingleNodeResponse

Expand Down