diff --git a/cli/cmd/proxy/log/command.go b/cli/cmd/proxy/log/command.go deleted file mode 100644 index 4959c47eb2..0000000000 --- a/cli/cmd/proxy/log/command.go +++ /dev/null @@ -1,63 +0,0 @@ -package log - -import ( - "errors" - "strings" - "sync" - - "github.com/hashicorp/consul-k8s/cli/common" - "github.com/hashicorp/consul-k8s/cli/common/flag" -) - -type LogCommand struct { - *common.BaseCommand - set *flag.Sets - - // Command Flags - podName string - - once sync.Once - help string -} - -var ErrMissingPodName = errors.New("Exactly one positional argument is requied: ") - -func (l *LogCommand) init() { - l.set = flag.NewSets() -} - -func (l *LogCommand) Run(args []string) int { - l.once.Do(l.init) - l.Log.ResetNamed("log") - defer common.CloseWithError(l.BaseCommand) - - err := l.parseFlags(args) - if err != nil { - return 1 - } - return 0 -} - -func (l *LogCommand) parseFlags(args []string) error { - positional := []string{} - // Separate positional args from keyed args - for _, arg := range args { - if strings.HasPrefix(arg, "-") { - break - } - positional = append(positional, arg) - } - keyed := args[len(positional):] - - if len(positional) != 1 { - return ErrMissingPodName - } - - l.podName = positional[0] - - err := l.set.Parse(keyed) - if err != nil { - return err - } - return nil -} diff --git a/cli/cmd/proxy/log/command_test.go b/cli/cmd/proxy/log/command_test.go deleted file mode 100644 index fb6b57a8cb..0000000000 --- a/cli/cmd/proxy/log/command_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package log - -import ( - "bytes" - "context" - "io" - "os" - "testing" - - "github.com/hashicorp/consul-k8s/cli/common" - "github.com/hashicorp/consul-k8s/cli/common/terminal" - "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/require" -) - -func TestFlagParsing(t *testing.T) { - testCases := map[string]struct { - args []string - out int - }{ - "No args": { - args: []string{}, - out: 1, - }, - "With pod name": { - args: []string{"now-this-is-pod-racing"}, - out: 0, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - c := setupCommand(bytes.NewBuffer([]byte{})) - out := c.Run(tc.args) - require.Equal(t, tc.out, out) - }) - } -} - -func setupCommand(buf io.Writer) *LogCommand { - log := hclog.New(&hclog.LoggerOptions{ - Name: "test", - Level: hclog.Debug, - Output: os.Stdout, - }) - - command := &LogCommand{ - BaseCommand: &common.BaseCommand{ - Log: log, - UI: terminal.NewUI(context.Background(), buf), - }, - } - command.init() - return command -} diff --git a/cli/cmd/proxy/loglevel/command.go b/cli/cmd/proxy/loglevel/command.go new file mode 100644 index 0000000000..83504e018c --- /dev/null +++ b/cli/cmd/proxy/loglevel/command.go @@ -0,0 +1,223 @@ +package loglevel + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "sync" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + helmCLI "helm.sh/helm/v3/pkg/cli" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +const defaultAdminPort int = 19000 + +type LoggerConfig map[string]string + +type LogCommand struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + set *flag.Sets + + // Command Flags + podName string + + once sync.Once + help string + restConfig *rest.Config + logLevelFetcher func(context.Context, common.PortForwarder) (LoggerConfig, error) +} + +var ErrMissingPodName = errors.New("Exactly one positional argument is required: ") + +func (l *LogCommand) init() { + l.set = flag.NewSets() + l.help = l.set.Help() +} + +func (l *LogCommand) Run(args []string) int { + l.once.Do(l.init) + l.Log.ResetNamed("loglevel") + defer common.CloseWithError(l.BaseCommand) + + err := l.parseFlags(args) + if err != nil { + fmt.Println(err) + return 1 + } + + if l.logLevelFetcher == nil { + l.logLevelFetcher = FetchLogLevel + } + + err = l.initKubernetes() + if err != nil { + fmt.Println(err) + return 1 + } + + adminPorts, err := l.fetchAdminPorts() + if err != nil { + fmt.Println(err) + return 1 + } + + logLevels, err := l.fetchLogLevels(adminPorts) + if err != nil { + fmt.Println(err) + return 1 + } + l.outputLevels(logLevels) + return 0 +} + +func (l *LogCommand) parseFlags(args []string) error { + positional := []string{} + // Separate positional args from keyed args + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + break + } + positional = append(positional, arg) + } + keyed := args[len(positional):] + + if len(positional) != 1 { + return ErrMissingPodName + } + + l.podName = positional[0] + + err := l.set.Parse(keyed) + if err != nil { + return err + } + return nil +} + +func (l *LogCommand) initKubernetes() error { + settings := helmCLI.New() + var err error + + if l.restConfig == nil { + l.restConfig, err = settings.RESTClientGetter().ToRESTConfig() + if err != nil { + return fmt.Errorf("error creating Kubernetes REST config %v", err) + } + + } + + if l.kubernetes == nil { + l.kubernetes, err = kubernetes.NewForConfig(l.restConfig) + if err != nil { + return fmt.Errorf("error creating Kubernetes client %v", err) + } + } + return nil +} + +func (l *LogCommand) fetchAdminPorts() (map[string]int, error) { + adminPorts := make(map[string]int, 0) + // TODO: support different namespaces + pod, err := l.kubernetes.CoreV1().Pods("default").Get(l.Ctx, l.podName, metav1.GetOptions{}) + if err != nil { + return adminPorts, err + } + + connectService, isMultiport := pod.Annotations["consul.hashicorp.com/connect-service"] + + if !isMultiport { + // Return the default port configuration. + adminPorts[l.podName] = defaultAdminPort + return adminPorts, nil + } + + for idx, svc := range strings.Split(connectService, ",") { + adminPorts[svc] = defaultAdminPort + idx + } + + return adminPorts, nil +} + +func (l *LogCommand) fetchLogLevels(adminPorts map[string]int) (map[string]LoggerConfig, error) { + loggers := make(map[string]LoggerConfig, 0) + + for name, port := range adminPorts { + pf := common.PortForward{ + Namespace: "default", // TODO: change this to use the configurable namespace + PodName: l.podName, + RemotePort: port, + KubeClient: l.kubernetes, + RestConfig: l.restConfig, + } + + logLevels, err := l.logLevelFetcher(l.Ctx, &pf) + if err != nil { + return loggers, err + } + loggers[name] = logLevels + } + return loggers, nil +} + +func FetchLogLevel(ctx context.Context, portForward common.PortForwarder) (LoggerConfig, error) { + endpoint, err := portForward.Open(ctx) + if err != nil { + return nil, err + } + + defer portForward.Close() + + response, err := http.Post(fmt.Sprintf("http://%s/logging", endpoint), "application/json", bytes.NewBuffer([]byte{})) + if err != nil { + return nil, err + } + body, err := io.ReadAll(response.Body) + loggers := strings.Split(string(body), "\n") + logLevels := make(map[string]string) + var name string + var level string + + // the first line here is just a header + for _, logger := range loggers[1:] { + if len(logger) == 0 { + continue + } + fmt.Sscanf(logger, "%s %s", &name, &level) + name = strings.TrimRight(name, ":") + logLevels[name] = level + } + return logLevels, nil +} + +func (l *LogCommand) Help() string { + l.once.Do(l.init) + return fmt.Sprintf("%s\n\nUsage: consul-k8s proxy log [flags]\n\n%s", l.Synopsis(), l.help) +} + +func (l *LogCommand) Synopsis() string { + return "Inspect and Modify the Envoy Log configuration for a given Pod." +} + +func (l *LogCommand) outputLevels(logLevels map[string]LoggerConfig) { + l.UI.Output(fmt.Sprintf("Envoy log configuration for %s in namespace default:", l.podName)) + for n, levels := range logLevels { + l.UI.Output(fmt.Sprintf("Log Levels for %s", n), terminal.WithHeaderStyle()) + table := terminal.NewTable("Name", "Level") + for name, level := range levels { + table.AddRow([]string{name, level}, []string{}) + } + l.UI.Table(table) + l.UI.Output("") + } +} diff --git a/cli/cmd/proxy/loglevel/command_test.go b/cli/cmd/proxy/loglevel/command_test.go new file mode 100644 index 0000000000..1cb575ba7c --- /dev/null +++ b/cli/cmd/proxy/loglevel/command_test.go @@ -0,0 +1,164 @@ +package loglevel + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "regexp" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestFlagParsing(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + args []string + out int + }{ + "No args": { + args: []string{}, + out: 1, + }, + "With pod name": { + args: []string{"now-this-is-pod-racing"}, + out: 0, + }, + } + podName := "now-this-is-pod-racing" + fakePod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + c := setupCommand(bytes.NewBuffer([]byte{})) + c.kubernetes = fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{fakePod}}) + c.logLevelFetcher = func(context.Context, common.PortForwarder) (LoggerConfig, error) { + return testLogConfig, nil + } + + out := c.Run(tc.args) + require.Equal(t, tc.out, out) + }) + } +} + +func TestOutputForGettingLogLevel(t *testing.T) { + t.Parallel() + podName := "now-this-is-pod-racing" + expectedHeader := fmt.Sprintf("Envoy log configuration for %s in namespace default:", podName) + fakePod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + } + + buf := bytes.NewBuffer([]byte{}) + c := setupCommand(buf) + c.logLevelFetcher = func(context.Context, common.PortForwarder) (LoggerConfig, error) { + return testLogConfig, nil + } + c.kubernetes = fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{fakePod}}) + + args := []string{podName} + out := c.Run(args) + require.Equal(t, 0, out) + + actual := buf.String() + + require.Regexp(t, expectedHeader, actual) + require.Regexp(t, "Log Levels for now-this-is-pod-racing", actual) + for logger, level := range testLogConfig { + require.Regexp(t, regexp.MustCompile(logger+`\s*`+level), actual) + } +} + +var testLogConfig = LoggerConfig{ + "admin": "debug", + "alternate_protocols_cache": "debug", + "aws": "debug", + "assert": "debug", + "backtrace": "debug", + "cache_filter": "debug", + "client": "debug", + "config": "debug", + "connection": "debug", + "conn_handler": "debug", + "decompression": "debug", + "dns": "debug", + "dubbo": "debug", + "envoy_bug": "debug", + "ext_authz": "debug", + "ext_proc": "debug", + "rocketmq": "debug", + "file": "debug", + "filter": "debug", + "forward_proxy": "debug", + "grpc": "debug", + "happy_eyeballs": "debug", + "hc": "debug", + "health_checker": "debug", + "http": "debug", + "http2": "debug", + "hystrix": "debug", + "init": "debug", + "io": "debug", + "jwt": "debug", + "kafka": "debug", + "key_value_store": "debug", + "lua": "debug", + "main": "debug", + "matcher": "debug", + "misc": "debug", + "mongo": "debug", + "multi_connection": "debug", + "oauth2": "debug", + "quic": "debug", + "quic_stream": "debug", + "pool": "debug", + "rbac": "debug", + "rds": "debug", + "redis": "debug", + "router": "debug", + "runtime": "debug", + "stats": "debug", + "secret": "debug", + "tap": "debug", + "testing": "debug", + "thrift": "debug", + "tracing": "debug", + "upstream": "debug", + "udp": "debug", + "wasm": "debug", + "websocket": "debug", +} + +func setupCommand(buf io.Writer) *LogCommand { + log := hclog.New(&hclog.LoggerOptions{ + Name: "test", + Level: hclog.Debug, + Output: os.Stdout, + }) + + command := &LogCommand{ + BaseCommand: &common.BaseCommand{ + Log: log, + UI: terminal.NewUI(context.Background(), buf), + }, + } + command.init() + return command +} diff --git a/cli/commands.go b/cli/commands.go index d29b4a8ad7..dc132eef9a 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/cmd/install" "github.com/hashicorp/consul-k8s/cli/cmd/proxy" "github.com/hashicorp/consul-k8s/cli/cmd/proxy/list" + "github.com/hashicorp/consul-k8s/cli/cmd/proxy/loglevel" "github.com/hashicorp/consul-k8s/cli/cmd/proxy/read" "github.com/hashicorp/consul-k8s/cli/cmd/status" "github.com/hashicorp/consul-k8s/cli/cmd/uninstall" @@ -19,7 +20,6 @@ import ( ) func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseCommand, map[string]cli.CommandFactory) { - baseCommand := &common.BaseCommand{ Ctx: ctx, Log: log, @@ -68,6 +68,11 @@ func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseComm BaseCommand: baseCommand, }, nil }, + "proxy log": func() (cli.Command, error) { + return &loglevel.LogCommand{ + BaseCommand: baseCommand, + }, nil + }, } return baseCommand, commands