Merge pull request #10321 from hashicorp/dnephin/backport-debug-cli-fix

[1.9.x] debug: remove the CLI check for debug_enabled
This commit is contained in:
Daniel Nephin 2021-05-31 16:10:09 -04:00 committed by GitHub
commit 4afa17ba45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 65 additions and 43 deletions

5
.changelog/10273.txt Normal file
View File

@ -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.
```

View File

@ -237,20 +237,18 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
var token string
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)
if err != nil {
resp.WriteHeader(http.StatusForbidden)
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,
// write a forbidden response
if rule != nil && rule.OperatorRead(nil) != acl.Allow {

View File

@ -29,6 +29,10 @@ func (d *Debug) Heap() ([]byte, error) {
}
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
// from the pprof handlers
body, err := ioutil.ReadAll(resp.Body)
@ -52,6 +56,10 @@ func (d *Debug) Profile(seconds int) ([]byte, error) {
}
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
// from the pprof handlers
body, err := ioutil.ReadAll(resp.Body)
@ -75,6 +83,10 @@ func (d *Debug) Trace(seconds int) ([]byte, error) {
}
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
// from the pprof handlers
body, err := ioutil.ReadAll(resp.Body)
@ -95,6 +107,10 @@ func (d *Debug) Goroutine() ([]byte, error) {
}
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
// from the pprof handlers
body, err := ioutil.ReadAll(resp.Body)

View File

@ -15,10 +15,11 @@ import (
"sync"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
multierror "github.com/hashicorp/go-multierror"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
)
const (
@ -254,28 +255,12 @@ func (c *cmd) prepare() (version string, err error) {
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
// all by default
if len(c.capture) == 0 {
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 {
if !c.allowedTarget(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()
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)
@ -432,7 +417,7 @@ func (c *cmd) captureDynamic() error {
go func() {
prof, err := c.client.Debug().Profile(int(s))
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)
@ -447,7 +432,7 @@ func (c *cmd) captureDynamic() error {
go func() {
trace, err := c.client.Debug().Trace(int(s))
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)
@ -460,7 +445,7 @@ func (c *cmd) captureDynamic() error {
gr, err := c.client.Debug().Goroutine()
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)
@ -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))
go capture()
case e := <-errCh:
c.UI.Error(fmt.Sprintf("Capture failure %s", e))
c.UI.Error(fmt.Sprintf("Capture failure: %s", e))
case <-durationChn:
return nil
case <-c.shutdownCh:

View File

@ -5,15 +5,18 @@ import (
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
)
func TestDebugCommand_noTabs(t *testing.T) {
@ -25,7 +28,9 @@ func TestDebugCommand_noTabs(t *testing.T) {
}
func TestDebugCommand(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
testDir := testutil.TempDir(t, "debug")
@ -61,7 +66,9 @@ func TestDebugCommand(t *testing.T) {
}
func TestDebugCommand_Archive(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
testDir := testutil.TempDir(t, "debug")
@ -171,7 +178,9 @@ func TestDebugCommand_OutputPathBad(t *testing.T) {
}
func TestDebugCommand_OutputPathExists(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
testDir := testutil.TempDir(t, "debug")
@ -208,7 +217,9 @@ func TestDebugCommand_OutputPathExists(t *testing.T) {
}
func TestDebugCommand_CaptureTargets(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
cases := map[string]struct {
// used in -target param
@ -316,7 +327,9 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
}
func TestDebugCommand_ProfilesExist(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
testDir := testutil.TempDir(t, "debug")
@ -362,7 +375,9 @@ func TestDebugCommand_ProfilesExist(t *testing.T) {
}
func TestDebugCommand_ValidateTiming(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("too slow for testing.Short")
}
cases := map[string]struct {
duration string
@ -454,13 +469,16 @@ func TestDebugCommand_DebugDisabled(t *testing.T) {
// Glob ignores file system errors
for _, v := range profiles {
fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v))
if len(fs) > 0 {
t.Errorf("output data should not exist for %s", v)
}
// TODO: make this always one
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()
if !strings.Contains(errOutput, "Unable to capture pprof") {
t.Errorf("expected warn output, got %s", errOutput)
for _, prof := range []string{"heap", "cpu", "goroutine", "trace"} {
expected := fmt.Sprintf("failed to collect %v", prof)
require.Contains(t, errOutput, expected)
}
}