Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions cmd/spire-agent/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/mitchellh/cli"
"github.com/spiffe/spire/cmd/spire-agent/cli/api"
"github.com/spiffe/spire/cmd/spire-agent/cli/debug"
"github.com/spiffe/spire/cmd/spire-agent/cli/healthcheck"
"github.com/spiffe/spire/cmd/spire-agent/cli/logger"
"github.com/spiffe/spire/cmd/spire-agent/cli/run"
Expand All @@ -23,6 +24,9 @@ func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-agent", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
"debug getinfo": func() (cli.Command, error) {
return debug.NewGetInfoCommand(), nil
},
"api fetch": func() (cli.Command, error) {
return api.NewFetchX509Command(), nil
},
Expand Down
117 changes: 117 additions & 0 deletions cmd/spire-agent/cli/debug/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package debug

import (
"context"
"errors"
"flag"
"fmt"
"time"

"github.com/mitchellh/cli"
debugv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/agent/debug/v1"
commoncli "github.com/spiffe/spire/pkg/common/cli"
"github.com/spiffe/spire/pkg/common/cliprinter"
"github.com/spiffe/spire/pkg/common/util"
)

// NewGetInfoCommand creates a new "debug getinfo" subcommand for "debug" command.
func NewGetInfoCommand() cli.Command {
return newGetInfoCommand(commoncli.DefaultEnv)
}

func newGetInfoCommand(env *commoncli.Env) *getInfoCommand {
return &getInfoCommand{
env: env,
}
}

type getInfoCommand struct {
getInfoCommandOS

env *commoncli.Env
printer cliprinter.Printer
}

func (c *getInfoCommand) Help() string {
_ = c.parseFlags([]string{"-h"})
return ""
}

func (c *getInfoCommand) Synopsis() string {
return "Prints debug information about the agent"
}

func (c *getInfoCommand) Run(args []string) int {
if err := c.parseFlags(args); err != nil {
return 1
}
if err := c.run(); err != nil {
_ = c.env.ErrPrintf("Error: %v\n", err)
return 1
}
return 0
}

func (c *getInfoCommand) parseFlags(args []string) error {
fs := flag.NewFlagSet("debug getinfo", flag.ContinueOnError)
fs.SetOutput(c.env.Stderr)
c.addOSFlags(fs)
cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, prettyPrintGetInfo)
return fs.Parse(args)
}

func (c *getInfoCommand) run() error {
addr, err := c.getAddr()
if err != nil {
return err
}
target, err := util.GetTargetName(addr)
if err != nil {
return err
}
conn, err := util.NewGRPCClient(target)
if err != nil {
return err
}
defer conn.Close()

debugClient := debugv1.NewDebugClient(conn)
resp, err := debugClient.GetInfo(context.Background(), &debugv1.GetInfoRequest{})
if err != nil {
return err
}

return c.printer.PrintProto(resp)
}

func prettyPrintGetInfo(env *commoncli.Env, results ...any) error {
resp, ok := results[0].(*debugv1.GetInfoResponse)
if !ok {
return errors.New("internal error: cli printer; please report this bug")
}

env.Printf("Agent Debug Info:\n")
env.Printf(" Uptime: %s\n", (time.Duration(resp.Uptime) * time.Second).String())
env.Printf(" Last Sync Success: %s\n", time.Unix(resp.LastSyncSuccess, 0).UTC().Format(time.RFC3339))
env.Printf(" Cached X.509 SVIDs: %d\n", resp.CachedX509SvidsCount)
Comment on lines +93 to +96
env.Printf(" Cached JWT SVIDs: %d\n", resp.CachedJwtSvidsCount)
env.Printf(" Cached SVID Store X.509 SVIDs: %d\n", resp.CachedSvidstoreX509SvidsCount)

if len(resp.SvidChain) > 0 {
env.Printf(" SVID Chain:\n")
for i, cert := range resp.SvidChain {
env.Printf(" [%d] SPIFFE ID: %s\n", i, spiffeIDString(cert))
env.Printf(" Subject: %s\n", cert.Subject)
env.Printf(" Expires At: %s\n", time.Unix(cert.ExpiresAt, 0).UTC().Format(time.RFC3339))
}
}

return nil
}

func spiffeIDString(cert *debugv1.GetInfoResponse_Cert) string {
if cert.Id == nil {
return "(none)"
}
return fmt.Sprintf("spiffe://%s%s", cert.Id.TrustDomain, cert.Id.Path)
}
23 changes: 23 additions & 0 deletions cmd/spire-agent/cli/debug/debug_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build !windows

package debug

import (
"flag"
"net"

"github.com/spiffe/spire/cmd/spire-agent/cli/common"
"github.com/spiffe/spire/pkg/common/util"
)

type getInfoCommandOS struct {
socketPath string
}

func (c *getInfoCommandOS) addOSFlags(flags *flag.FlagSet) {
flags.StringVar(&c.socketPath, "socketPath", common.DefaultAdminSocketPath, "Path to the SPIRE Agent admin API Unix domain socket")
}

func (c *getInfoCommandOS) getAddr() (net.Addr, error) {
return util.GetUnixAddrWithAbsPath(c.socketPath)
}
25 changes: 25 additions & 0 deletions cmd/spire-agent/cli/debug/debug_posix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build !windows

package debug

import (
"testing"

"github.com/spiffe/spire/test/spiretest"
"google.golang.org/grpc"
)

var (
usage = `Usage of debug getinfo:
-output value
Desired output format (pretty, json); default: pretty.
-socketPath string
Path to the SPIRE Agent admin API Unix domain socket (default "/tmp/spire-agent/private/admin.sock")
`
socketAddrArg = "-socketPath"
socketAddrUnavailable = "/tmp/doesnotexist.sock"
)

func startGRPCSocketServer(t *testing.T, registerFn func(srv *grpc.Server)) string {
return spiretest.StartGRPCServer(t, registerFn).String()
}
137 changes: 137 additions & 0 deletions cmd/spire-agent/cli/debug/debug_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package debug

import (
"bytes"
"context"
"testing"
"time"

"github.com/mitchellh/cli"
debugv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/agent/debug/v1"
"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
commoncli "github.com/spiffe/spire/pkg/common/cli"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

type debugTest struct {
stdin *bytes.Buffer
stdout *bytes.Buffer
stderr *bytes.Buffer

cmd cli.Command
}

func setupTest() *debugTest {
stdin := new(bytes.Buffer)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)

cmd := newGetInfoCommand(&commoncli.Env{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
})

return &debugTest{
stdin: stdin,
stdout: stdout,
stderr: stderr,
cmd: cmd,
}
}

func TestSynopsis(t *testing.T) {
test := setupTest()
require.Equal(t, "Prints debug information about the agent", test.cmd.Synopsis())
}

func TestHelp(t *testing.T) {
test := setupTest()
require.Empty(t, test.cmd.Help())
require.Equal(t, usage, test.stderr.String())
}

func TestBadFlags(t *testing.T) {
test := setupTest()

code := test.cmd.Run([]string{"-badflag"})
require.NotEqual(t, 0, code)
require.Empty(t, test.stdout.String(), "stdout")
require.Equal(t, "flag provided but not defined: -badflag\n"+usage, test.stderr.String(), "stderr")
}

func TestFailsOnUnavailable(t *testing.T) {
test := setupTest()

code := test.cmd.Run([]string{socketAddrArg, socketAddrUnavailable})
require.NotEqual(t, 0, code)
require.Empty(t, test.stdout.String(), "stdout")
require.Contains(t, test.stderr.String(), "Error:")
}

func TestGetInfo(t *testing.T) {
now := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
lastSync := now.Add(-30 * time.Second)

fakeResp := &debugv1.GetInfoResponse{
Uptime: 42,
LastSyncSuccess: lastSync.Unix(),
CachedX509SvidsCount: 3,
CachedJwtSvidsCount: 1,
CachedSvidstoreX509SvidsCount: 0,
SvidChain: []*debugv1.GetInfoResponse_Cert{
{
Id: &types.SPIFFEID{TrustDomain: "example.org", Path: "/spire/agent/foo"},
ExpiresAt: now.Add(24 * time.Hour).Unix(),
Subject: "CN=agent",
},
},
}

socketAddr := startGRPCSocketServer(t, func(srv *grpc.Server) {
debugv1.RegisterDebugServer(srv, &fakeDebugServer{resp: fakeResp})
})

test := setupTest()
code := test.cmd.Run([]string{socketAddrArg, socketAddr})
require.Equal(t, 0, code, "exit code; stderr: %s", test.stderr.String())
require.Empty(t, test.stderr.String(), "stderr")

out := test.stdout.String()
require.Contains(t, out, "Agent Debug Info:")
require.Contains(t, out, "42s")
require.Contains(t, out, "Cached X.509 SVIDs:")
require.Contains(t, out, "3")
require.Contains(t, out, "spiffe://example.org/spire/agent/foo")
}

func TestGetInfoJSON(t *testing.T) {
fakeResp := &debugv1.GetInfoResponse{
Uptime: 10,
LastSyncSuccess: 1705320000,
CachedX509SvidsCount: 2,
}

socketAddr := startGRPCSocketServer(t, func(srv *grpc.Server) {
debugv1.RegisterDebugServer(srv, &fakeDebugServer{resp: fakeResp})
})

test := setupTest()
code := test.cmd.Run([]string{socketAddrArg, socketAddr, "-output", "json"})
require.Equal(t, 0, code, "exit code; stderr: %s", test.stderr.String())
require.Empty(t, test.stderr.String(), "stderr")

out := test.stdout.String()
require.Contains(t, out, `"uptime"`)
require.Contains(t, out, `"cached_x509_svids_count"`)
}

type fakeDebugServer struct {
debugv1.UnimplementedDebugServer
resp *debugv1.GetInfoResponse
}

func (s *fakeDebugServer) GetInfo(_ context.Context, _ *debugv1.GetInfoRequest) (*debugv1.GetInfoResponse, error) {
return s.resp, nil
}
23 changes: 23 additions & 0 deletions cmd/spire-agent/cli/debug/debug_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build windows

package debug

import (
"flag"
"net"

"github.com/spiffe/spire/cmd/spire-agent/cli/common"
"github.com/spiffe/spire/pkg/common/namedpipe"
)

type getInfoCommandOS struct {
namedPipeName string
}

func (c *getInfoCommandOS) addOSFlags(flags *flag.FlagSet) {
flags.StringVar(&c.namedPipeName, "namedPipeName", common.DefaultAdminNamedPipeName, "Pipe name of the SPIRE Agent admin API named pipe")
}

func (c *getInfoCommandOS) getAddr() (net.Addr, error) {
return namedpipe.AddrFromName(c.namedPipeName), nil
}
26 changes: 26 additions & 0 deletions cmd/spire-agent/cli/debug/debug_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build windows

package debug

import (
"testing"

"github.com/spiffe/spire/pkg/common/namedpipe"
"github.com/spiffe/spire/test/spiretest"
"google.golang.org/grpc"
)

var (
usage = `Usage of debug getinfo:
-namedPipeName string
Pipe name of the SPIRE Agent admin API named pipe (default "\\spire-agent\\private\\admin")
-output value
Desired output format (pretty, json); default: pretty.
`
socketAddrArg = "-namedPipeName"
socketAddrUnavailable = "doesnotexist"
)

func startGRPCSocketServer(t *testing.T, registerFn func(srv *grpc.Server)) string {
return namedpipe.GetPipeName(spiretest.StartGRPCServer(t, registerFn).String())
}
4 changes: 4 additions & 0 deletions cmd/spire-server/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/mitchellh/cli"
"github.com/spiffe/spire/cmd/spire-server/cli/agent"
"github.com/spiffe/spire/cmd/spire-server/cli/bundle"
"github.com/spiffe/spire/cmd/spire-server/cli/debug"
"github.com/spiffe/spire/cmd/spire-server/cli/entry"
"github.com/spiffe/spire/cmd/spire-server/cli/federation"
"github.com/spiffe/spire/cmd/spire-server/cli/healthcheck"
Expand Down Expand Up @@ -39,6 +40,9 @@ func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-server", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
"debug getinfo": func() (cli.Command, error) {
return debug.NewGetInfoCommand(), nil
},
"agent ban": func() (cli.Command, error) {
return agent.NewBanCommand(), nil
},
Expand Down
Loading
Loading