Skip to content

Commit

Permalink
Post launch Log level control for the Server (spiffe#4880)
Browse files Browse the repository at this point in the history
Signed-off-by: Edwin Buck <edwbuck@gmail.com>
  • Loading branch information
edwbuck authored Mar 11, 2024
1 parent 91ddf4f commit a8d547c
Show file tree
Hide file tree
Showing 28 changed files with 1,969 additions and 7 deletions.
7 changes: 7 additions & 0 deletions cmd/spire-server/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/spiffe/spire/cmd/spire-server/cli/federation"
"github.com/spiffe/spire/cmd/spire-server/cli/healthcheck"
"github.com/spiffe/spire/cmd/spire-server/cli/jwt"
"github.com/spiffe/spire/cmd/spire-server/cli/logger"
"github.com/spiffe/spire/cmd/spire-server/cli/run"
"github.com/spiffe/spire/cmd/spire-server/cli/token"
"github.com/spiffe/spire/cmd/spire-server/cli/validate"
Expand Down Expand Up @@ -96,6 +97,12 @@ func (cc *CLI) Run(ctx context.Context, args []string) int {
"federation update": func() (cli.Command, error) {
return federation.NewUpdateCommand(), nil
},
"logger get": func() (cli.Command, error) {
return logger.NewGetCommand(), nil
},
"logger set": func() (cli.Command, error) {
return logger.NewSetCommand(), nil
},
"run": func() (cli.Command, error) {
return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil
},
Expand Down
59 changes: 59 additions & 0 deletions cmd/spire-server/cli/logger/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package logger

import (
"context"
"flag"
"fmt"

"github.com/mitchellh/cli"
api "github.com/spiffe/spire-api-sdk/proto/spire/api/server/logger/v1"
"github.com/spiffe/spire/cmd/spire-server/util"
commoncli "github.com/spiffe/spire/pkg/common/cli"
"github.com/spiffe/spire/pkg/common/cliprinter"
)

type getCommand struct {
env *commoncli.Env
printer cliprinter.Printer
}

// Returns a cli.command that gets the logger information using
// the default cli environment.
func NewGetCommand() cli.Command {
return NewGetCommandWithEnv(commoncli.DefaultEnv)
}

// Returns a cli.command that gets the root logger information.
func NewGetCommandWithEnv(env *commoncli.Env) cli.Command {
return util.AdaptCommand(env, &getCommand{env: env})
}

// The name of the command.
func (*getCommand) Name() string {
return "logger get"
}

// The help presented description of the command.
func (*getCommand) Synopsis() string {
return "Gets the logger details"
}

// Adds additional flags specific to the command.
func (c *getCommand) AppendFlags(fs *flag.FlagSet) {
cliprinter.AppendFlagWithCustomPretty(&c.printer, fs, c.env, c.prettyPrintLogger)
}

// The routine that executes the command
func (c *getCommand) Run(ctx context.Context, _ *commoncli.Env, serverClient util.ServerClient) error {
logger, err := serverClient.NewLoggerClient().GetLogger(ctx, &api.GetLoggerRequest{})
if err != nil {
return fmt.Errorf("error fetching logger: %w", err)
}

return c.printer.PrintProto(logger)
}

// Formatting for the logger under pretty printing of output.
func (c *getCommand) prettyPrintLogger(env *commoncli.Env, results ...any) error {
return PrettyPrintLogger(env, results...)
}
12 changes: 12 additions & 0 deletions cmd/spire-server/cli/logger/get_posix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build !windows

package logger_test

var (
getUsage = `Usage of logger get:
-output value
Desired output format (pretty, json); default: pretty.
-socketPath string
Path to the SPIRE Server API socket (default "/tmp/spire-server/private/api.sock")
`
)
178 changes: 178 additions & 0 deletions cmd/spire-server/cli/logger/get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package logger_test

import (
"errors"
"testing"

"github.com/stretchr/testify/require"

"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
"github.com/spiffe/spire/cmd/spire-server/cli/logger"
)

func TestGetHelp(t *testing.T) {
test := setupCliTest(t, nil, logger.NewGetCommandWithEnv)
test.client.Help()
require.Equal(t, "", test.stdout.String())
require.Equal(t, getUsage, test.stderr.String())
}

func TestGetSynopsis(t *testing.T) {
cmd := logger.NewGetCommand()
require.Equal(t, "Gets the logger details", cmd.Synopsis())
}

func TestGet(t *testing.T) {
for _, tt := range []struct {
name string
// server state
server *mockLoggerServer
// input
args []string
// expected items
expectReturnCode int
expectStdout string
expectStderr string
}{
{
name: "configured to info, set to info, using pretty output",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_INFO,
LaunchLevel: types.LogLevel_INFO,
},
},
expectReturnCode: 0,
expectStdout: `Logger Level : info
Launch Level : info
`,
},
{
name: "configured to debug, set to warn, using pretty output",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_WARN,
LaunchLevel: types.LogLevel_DEBUG,
},
},
expectReturnCode: 0,
expectStdout: `Logger Level : warning
Launch Level : debug
`,
},
{
name: "configured to error, set to trace, using pretty output",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_TRACE,
LaunchLevel: types.LogLevel_ERROR,
},
},
expectReturnCode: 0,
expectStdout: `Logger Level : trace
Launch Level : error
`,
},
{
name: "configured to panic, set to fatal, using pretty output",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_FATAL,
LaunchLevel: types.LogLevel_PANIC,
},
},
expectReturnCode: 0,
expectStdout: `Logger Level : fatal
Launch Level : panic
`,
},
{
name: "configured to info, set to info, using json output",
args: []string{"-output", "json"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_INFO,
LaunchLevel: types.LogLevel_INFO,
},
},
expectReturnCode: 0,
expectStdout: `{"current_level":"INFO","launch_level":"INFO"}
`,
},
{
name: "configured to debug, set to warn, using json output",
args: []string{"-output", "json"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_WARN,
LaunchLevel: types.LogLevel_DEBUG,
},
},
expectReturnCode: 0,
expectStdout: `{"current_level":"WARN","launch_level":"DEBUG"}
`,
},
{
name: "configured to error, set to trace, using json output",
args: []string{"-output", "json"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_TRACE,
LaunchLevel: types.LogLevel_ERROR,
},
},
expectReturnCode: 0,
expectStdout: `{"current_level":"TRACE","launch_level":"ERROR"}
`,
},
{
name: "configured to panic, set to fatal, using json output",
args: []string{"-output", "json"},
server: &mockLoggerServer{
returnLogger: &types.Logger{
CurrentLevel: types.LogLevel_FATAL,
LaunchLevel: types.LogLevel_PANIC,
},
},
expectReturnCode: 0,
expectStdout: `{"current_level":"FATAL","launch_level":"PANIC"}
`,
},
{
name: "configured to info, set to info, server will error",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnErr: errors.New("server is unavailable"),
},
expectReturnCode: 1,
expectStderr: `Error: error fetching logger: rpc error: code = Unknown desc = server is unavailable
`,
},
{
name: "bizzarro world, returns neither logger nor error",
args: []string{"-output", "pretty"},
server: &mockLoggerServer{
returnLogger: nil,
},
expectReturnCode: 1,
expectStderr: `Error: internal error: returned current log level is undefined; please report this as a bug
`,
},
} {
t.Run(tt.name, func(t *testing.T) {
test := setupCliTest(t, tt.server, logger.NewGetCommandWithEnv)
returnCode := test.client.Run(append(test.args, tt.args...))
require.Equal(t, tt.expectStdout, test.stdout.String())
require.Equal(t, tt.expectStderr, test.stderr.String())
require.Equal(t, tt.expectReturnCode, returnCode)
})
}
}
12 changes: 12 additions & 0 deletions cmd/spire-server/cli/logger/get_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build windows

package logger_test

var (
getUsage = `Usage of logger get:
-namedPipeName string
Pipe name of the SPIRE Server API named pipe (default "\\spire-server\\private\\api")
-output value
Desired output format (pretty, json); default: pretty.
`
)
110 changes: 110 additions & 0 deletions cmd/spire-server/cli/logger/mocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package logger_test

import (
"io"
"testing"

"github.com/spiffe/spire/test/spiretest"

"bytes"
"context"

"github.com/mitchellh/cli"
loggerv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/logger/v1"
"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
"github.com/spiffe/spire/cmd/spire-server/cli/common"
commoncli "github.com/spiffe/spire/pkg/common/cli"
"google.golang.org/grpc"
)

// an input/output capture struct
type loggerTest struct {
stdin *bytes.Buffer
stdout *bytes.Buffer
stderr *bytes.Buffer
args []string
server *mockLoggerServer
client cli.Command
}

// serialization of capture
func (l *loggerTest) afterTest(t *testing.T) {
t.Logf("TEST:%s", t.Name())
t.Logf("STDOUT:\n%s", l.stdout.String())
t.Logf("STDIN:\n%s", l.stdin.String())
t.Logf("STDERR:\n%s", l.stderr.String())
}

// setup of input/output capture
func setupCliTest(t *testing.T, server *mockLoggerServer, newClient func(*commoncli.Env) cli.Command) *loggerTest {
addr := spiretest.StartGRPCServer(t, func(s *grpc.Server) {
loggerv1.RegisterLoggerServer(s, server)
})

stdin := new(bytes.Buffer)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)

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

test := &loggerTest{
stdin: stdin,
stdout: stdout,
stderr: stderr,
args: []string{common.AddrArg, common.GetAddr(addr)},
server: server,
client: client,
}

t.Cleanup(func() {
test.afterTest(t)
})

return test
}

// a mock grpc logger server
type mockLoggerServer struct {
loggerv1.UnimplementedLoggerServer

receivedSetValue *types.LogLevel
returnLogger *types.Logger
returnErr error
}

// mock implementation for GetLogger
func (s *mockLoggerServer) GetLogger(_ context.Context, _ *loggerv1.GetLoggerRequest) (*types.Logger, error) {
return s.returnLogger, s.returnErr
}

func (s *mockLoggerServer) SetLogLevel(_ context.Context, req *loggerv1.SetLogLevelRequest) (*types.Logger, error) {
s.receivedSetValue = &req.NewLevel
return s.returnLogger, s.returnErr
}

func (s *mockLoggerServer) ResetLogLevel(_ context.Context, _ *loggerv1.ResetLogLevelRequest) (*types.Logger, error) {
s.receivedSetValue = nil
return s.returnLogger, s.returnErr
}

var _ io.Writer = &errorWriter{}

type errorWriter struct {
ReturnError error
Buffer bytes.Buffer
}

func (e *errorWriter) Write(p []byte) (n int, err error) {
if e.ReturnError != nil {
return 0, e.ReturnError
}
return e.Buffer.Write(p)
}

func (e *errorWriter) String() string {
return e.Buffer.String()
}
Loading

0 comments on commit a8d547c

Please sign in to comment.