debug: remove the CLI check for debug_enabled (#10273)
* debug: remove the CLI check for debug_enabled The API allows collecting profiles even debug_enabled=false as long as ACLs are enabled. Remove this check from the CLI so that users do not need to set debug_enabled=true for no reason. Also: - fix the API client to return errors on non-200 status codes for debug endpoints - improve the failure messages when pprof data can not be collected Co-Authored-By: Dhia Ayachi <dhia@hashicorp.com> * remove parallel test runs parallel runs create a race condition that fail the debug tests * Add changelog Co-authored-by: Daniel Nephin <dnephin@hashicorp.com>
This commit is contained in:
parent
c2dc8cf56b
commit
00f7e0772a
|
@ -0,0 +1,5 @@
|
||||||
|
```release-note:bug
|
||||||
|
cli: removes the need to set debug_enabled=true to collect debug data from the CLI. Now
|
||||||
|
the CLI behaves the same way as the API and accepts either an ACL token with operator:read, or
|
||||||
|
debug_enabled=true.
|
||||||
|
```
|
|
@ -238,20 +238,18 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
||||||
var token string
|
var token string
|
||||||
s.parseToken(req, &token)
|
s.parseToken(req, &token)
|
||||||
|
|
||||||
|
// If enableDebug is not set, and ACLs are disabled, write
|
||||||
|
// an unauthorized response
|
||||||
|
if !enableDebug && s.checkACLDisabled(resp, req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rule, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
|
rule, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.WriteHeader(http.StatusForbidden)
|
resp.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If enableDebug is not set, and ACLs are disabled, write
|
|
||||||
// an unauthorized response
|
|
||||||
if !enableDebug {
|
|
||||||
if s.checkACLDisabled(resp, req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the token provided does not have the necessary permissions,
|
// If the token provided does not have the necessary permissions,
|
||||||
// write a forbidden response
|
// write a forbidden response
|
||||||
if rule != nil && rule.OperatorRead(nil) != acl.Allow {
|
if rule != nil && rule.OperatorRead(nil) != acl.Allow {
|
||||||
|
|
16
api/debug.go
16
api/debug.go
|
@ -29,6 +29,10 @@ func (d *Debug) Heap() ([]byte, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, generateUnexpectedResponseCodeError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
// We return a raw response because we're just passing through a response
|
// We return a raw response because we're just passing through a response
|
||||||
// from the pprof handlers
|
// from the pprof handlers
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
@ -52,6 +56,10 @@ func (d *Debug) Profile(seconds int) ([]byte, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, generateUnexpectedResponseCodeError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
// We return a raw response because we're just passing through a response
|
// We return a raw response because we're just passing through a response
|
||||||
// from the pprof handlers
|
// from the pprof handlers
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
@ -75,6 +83,10 @@ func (d *Debug) Trace(seconds int) ([]byte, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, generateUnexpectedResponseCodeError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
// We return a raw response because we're just passing through a response
|
// We return a raw response because we're just passing through a response
|
||||||
// from the pprof handlers
|
// from the pprof handlers
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
@ -95,6 +107,10 @@ func (d *Debug) Goroutine() ([]byte, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, generateUnexpectedResponseCodeError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
// We return a raw response because we're just passing through a response
|
// We return a raw response because we're just passing through a response
|
||||||
// from the pprof handlers
|
// from the pprof handlers
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
|
|
@ -15,10 +15,11 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/api"
|
|
||||||
"github.com/hashicorp/consul/command/flags"
|
|
||||||
multierror "github.com/hashicorp/go-multierror"
|
multierror "github.com/hashicorp/go-multierror"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/command/flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -254,28 +255,12 @@ func (c *cmd) prepare() (version string, err error) {
|
||||||
return "", fmt.Errorf("agent response did not contain version key")
|
return "", fmt.Errorf("agent response did not contain version key")
|
||||||
}
|
}
|
||||||
|
|
||||||
debugEnabled, ok := self["DebugConfig"]["EnableDebug"].(bool)
|
|
||||||
if !ok {
|
|
||||||
return version, fmt.Errorf("agent response did not contain debug key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If none are specified we will collect information from
|
// If none are specified we will collect information from
|
||||||
// all by default
|
// all by default
|
||||||
if len(c.capture) == 0 {
|
if len(c.capture) == 0 {
|
||||||
c.capture = c.defaultTargets()
|
c.capture = c.defaultTargets()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled && c.configuredTarget("pprof") {
|
|
||||||
cs := c.capture
|
|
||||||
for i := 0; i < len(cs); i++ {
|
|
||||||
if cs[i] == "pprof" {
|
|
||||||
c.capture = append(cs[:i], cs[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.UI.Warn("[WARN] Unable to capture pprof. Set enable_debug to true on target agent to enable profiling.")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range c.capture {
|
for _, t := range c.capture {
|
||||||
if !c.allowedTarget(t) {
|
if !c.allowedTarget(t) {
|
||||||
return version, fmt.Errorf("target not found: %s", t)
|
return version, fmt.Errorf("target not found: %s", t)
|
||||||
|
@ -414,7 +399,7 @@ func (c *cmd) captureDynamic() error {
|
||||||
|
|
||||||
heap, err := c.client.Debug().Heap()
|
heap, err := c.client.Debug().Heap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- fmt.Errorf("failed to collect heap profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(fmt.Sprintf("%s/heap.prof", timestampDir), heap, 0644)
|
err = ioutil.WriteFile(fmt.Sprintf("%s/heap.prof", timestampDir), heap, 0644)
|
||||||
|
@ -432,7 +417,7 @@ func (c *cmd) captureDynamic() error {
|
||||||
go func() {
|
go func() {
|
||||||
prof, err := c.client.Debug().Profile(int(s))
|
prof, err := c.client.Debug().Profile(int(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- fmt.Errorf("failed to collect cpu profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(fmt.Sprintf("%s/profile.prof", timestampDir), prof, 0644)
|
err = ioutil.WriteFile(fmt.Sprintf("%s/profile.prof", timestampDir), prof, 0644)
|
||||||
|
@ -447,7 +432,7 @@ func (c *cmd) captureDynamic() error {
|
||||||
go func() {
|
go func() {
|
||||||
trace, err := c.client.Debug().Trace(int(s))
|
trace, err := c.client.Debug().Trace(int(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- fmt.Errorf("failed to collect trace: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(fmt.Sprintf("%s/trace.out", timestampDir), trace, 0644)
|
err = ioutil.WriteFile(fmt.Sprintf("%s/trace.out", timestampDir), trace, 0644)
|
||||||
|
@ -460,7 +445,7 @@ func (c *cmd) captureDynamic() error {
|
||||||
|
|
||||||
gr, err := c.client.Debug().Goroutine()
|
gr, err := c.client.Debug().Goroutine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- fmt.Errorf("failed to collect goroutine profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(fmt.Sprintf("%s/goroutine.prof", timestampDir), gr, 0644)
|
err = ioutil.WriteFile(fmt.Sprintf("%s/goroutine.prof", timestampDir), gr, 0644)
|
||||||
|
@ -532,7 +517,7 @@ func (c *cmd) captureDynamic() error {
|
||||||
c.UI.Output(fmt.Sprintf("Capture successful %s (%d)", time.Unix(t, 0).Local().String(), intervalCount))
|
c.UI.Output(fmt.Sprintf("Capture successful %s (%d)", time.Unix(t, 0).Local().String(), intervalCount))
|
||||||
go capture()
|
go capture()
|
||||||
case e := <-errCh:
|
case e := <-errCh:
|
||||||
c.UI.Error(fmt.Sprintf("Capture failure %s", e))
|
c.UI.Error(fmt.Sprintf("Capture failure: %s", e))
|
||||||
case <-durationChn:
|
case <-durationChn:
|
||||||
return nil
|
return nil
|
||||||
case <-c.shutdownCh:
|
case <-c.shutdownCh:
|
||||||
|
|
|
@ -5,15 +5,18 @@ import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent"
|
"github.com/hashicorp/consul/agent"
|
||||||
"github.com/hashicorp/consul/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDebugCommand_noTabs(t *testing.T) {
|
func TestDebugCommand_noTabs(t *testing.T) {
|
||||||
|
@ -29,8 +32,6 @@ func TestDebugCommand(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testDir := testutil.TempDir(t, "debug")
|
testDir := testutil.TempDir(t, "debug")
|
||||||
|
|
||||||
a := agent.NewTestAgent(t, `
|
a := agent.NewTestAgent(t, `
|
||||||
|
@ -69,8 +70,6 @@ func TestDebugCommand_Archive(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testDir := testutil.TempDir(t, "debug")
|
testDir := testutil.TempDir(t, "debug")
|
||||||
|
|
||||||
a := agent.NewTestAgent(t, `
|
a := agent.NewTestAgent(t, `
|
||||||
|
@ -187,8 +186,6 @@ func TestDebugCommand_OutputPathExists(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testDir := testutil.TempDir(t, "debug")
|
testDir := testutil.TempDir(t, "debug")
|
||||||
|
|
||||||
a := agent.NewTestAgent(t, "")
|
a := agent.NewTestAgent(t, "")
|
||||||
|
@ -228,8 +225,6 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
// used in -target param
|
// used in -target param
|
||||||
targets []string
|
targets []string
|
||||||
|
@ -340,8 +335,6 @@ func TestDebugCommand_ProfilesExist(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testDir := testutil.TempDir(t, "debug")
|
testDir := testutil.TempDir(t, "debug")
|
||||||
|
|
||||||
a := agent.NewTestAgent(t, `
|
a := agent.NewTestAgent(t, `
|
||||||
|
@ -390,8 +383,6 @@ func TestDebugCommand_ValidateTiming(t *testing.T) {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
duration string
|
duration string
|
||||||
interval string
|
interval string
|
||||||
|
@ -486,13 +477,16 @@ func TestDebugCommand_DebugDisabled(t *testing.T) {
|
||||||
// Glob ignores file system errors
|
// Glob ignores file system errors
|
||||||
for _, v := range profiles {
|
for _, v := range profiles {
|
||||||
fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v))
|
fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v))
|
||||||
if len(fs) > 0 {
|
// TODO: make this always one
|
||||||
t.Errorf("output data should not exist for %s", v)
|
require.True(t, len(fs) >= 1)
|
||||||
}
|
content, err := ioutil.ReadFile(fs[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, content, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
errOutput := ui.ErrorWriter.String()
|
errOutput := ui.ErrorWriter.String()
|
||||||
if !strings.Contains(errOutput, "Unable to capture pprof") {
|
for _, prof := range []string{"heap", "cpu", "goroutine", "trace"} {
|
||||||
t.Errorf("expected warn output, got %s", errOutput)
|
expected := fmt.Sprintf("failed to collect %v", prof)
|
||||||
|
require.Contains(t, errOutput, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue