diff --git a/cli/cmd/proxy/loglevel/command.go b/cli/cmd/proxy/loglevel/command.go index f80eff08cc..c622955bb4 100644 --- a/cli/cmd/proxy/loglevel/command.go +++ b/cli/cmd/proxy/loglevel/command.go @@ -13,13 +13,20 @@ import ( "github.com/hashicorp/consul-k8s/cli/common" "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/posener/complete" helmCLI "helm.sh/helm/v3/pkg/cli" + "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) -const defaultAdminPort int = 19000 +const ( + defaultAdminPort int = 19000 + flagNameNamespace = "namespace" + flagNameKubeConfig = "kubeconfig" + flagNameKubeContext = "context" +) type LoggerConfig map[string]string @@ -35,14 +42,17 @@ var levelToColor = map[string]string{ "off": "", } -type LogCommand struct { +type LogLevelCommand struct { *common.BaseCommand kubernetes kubernetes.Interface set *flag.Sets // Command Flags - podName string + podName string + namespace string + kubeConfig string + kubeContext string once sync.Once help string @@ -50,19 +60,48 @@ type LogCommand struct { logLevelFetcher func(context.Context, common.PortForwarder) (LoggerConfig, error) } -func (l *LogCommand) init() { +func (l *LogLevelCommand) init() { l.set = flag.NewSets() + f := l.set.NewSet("Command Options") + f.StringVar(&flag.StringVar{ + Name: flagNameNamespace, + Target: &l.namespace, + Usage: "The namespace where the target Pod can be found.", + Aliases: []string{"n"}, + }) + + f = l.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: flagNameKubeConfig, + Aliases: []string{"c"}, + Target: &l.kubeConfig, + Usage: "Set the path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNameKubeContext, + Target: &l.kubeContext, + Usage: "Set the Kubernetes context to use.", + }) + l.help = l.set.Help() } -func (l *LogCommand) Run(args []string) int { +func (l *LogLevelCommand) 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) + l.UI.Output(err.Error(), terminal.WithErrorStyle()) + l.UI.Output(fmt.Sprintf("\n%s", l.Help())) + return 1 + } + + err = l.validateFlags() + if err != nil { + l.UI.Output(err.Error(), terminal.WithErrorStyle()) + l.UI.Output(fmt.Sprintf("\n%s", l.Help())) return 1 } @@ -72,26 +111,29 @@ func (l *LogCommand) Run(args []string) int { err = l.initKubernetes() if err != nil { - fmt.Println(err) + l.UI.Output(err.Error(), terminal.WithErrorStyle()) + l.UI.Output(fmt.Sprintf("\n%s", l.Help())) return 1 } adminPorts, err := l.fetchAdminPorts() if err != nil { - fmt.Println(err) + l.UI.Output(err.Error(), terminal.WithErrorStyle()) + l.UI.Output(fmt.Sprintf("\n%s", l.Help())) return 1 } logLevels, err := l.fetchLogLevels(adminPorts) if err != nil { - fmt.Println(err) + l.UI.Output(err.Error(), terminal.WithErrorStyle()) + l.UI.Output(fmt.Sprintf("\n%s", l.Help())) return 1 } l.outputLevels(logLevels) return 0 } -func (l *LogCommand) parseFlags(args []string) error { +func (l *LogLevelCommand) parseFlags(args []string) error { positional := []string{} // Separate positional args from keyed args for _, arg := range args { @@ -115,10 +157,25 @@ func (l *LogCommand) parseFlags(args []string) error { return nil } -func (l *LogCommand) initKubernetes() error { +func (l *LogLevelCommand) validateFlags() error { + if errs := validation.ValidateNamespaceName(l.namespace, false); l.namespace != "" && len(errs) > 0 { + return fmt.Errorf("invalid namespace name passed for -namespace/-n: %v", strings.Join(errs, "; ")) + } + return nil +} + +func (l *LogLevelCommand) initKubernetes() error { settings := helmCLI.New() var err error + if l.kubeConfig != "" { + settings.KubeConfig = l.kubeConfig + } + + if l.kubeContext != "" { + settings.KubeContext = l.kubeContext + } + if l.restConfig == nil { l.restConfig, err = settings.RESTClientGetter().ToRESTConfig() if err != nil { @@ -133,13 +190,17 @@ func (l *LogCommand) initKubernetes() error { return fmt.Errorf("error creating Kubernetes client %v", err) } } + if l.namespace == "" { + l.namespace = settings.Namespace() + } + return nil } -func (l *LogCommand) fetchAdminPorts() (map[string]int, error) { +// fetchAdminPorts retrieves all admin ports for Envoy Proxies running in a pod given namespace +func (l *LogLevelCommand) 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{}) + pod, err := l.kubernetes.CoreV1().Pods(l.namespace).Get(l.Ctx, l.podName, metav1.GetOptions{}) if err != nil { return adminPorts, err } @@ -159,12 +220,12 @@ func (l *LogCommand) fetchAdminPorts() (map[string]int, error) { return adminPorts, nil } -func (l *LogCommand) fetchLogLevels(adminPorts map[string]int) (map[string]LoggerConfig, error) { +func (l *LogLevelCommand) 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 + Namespace: l.namespace, PodName: l.podName, RemotePort: port, KubeClient: l.kubernetes, @@ -180,6 +241,8 @@ func (l *LogCommand) fetchLogLevels(adminPorts map[string]int) (map[string]Logge return loggers, nil } +// FetchLogLevel requests the logging endpoint from Envoy Admin Interface for a given port +// more can be read about that endpoint https://www.envoyproxy.io/docs/envoy/latest/operations/admin#post--logging func FetchLogLevel(ctx context.Context, portForward common.PortForwarder) (LoggerConfig, error) { endpoint, err := portForward.Open(ctx) if err != nil { @@ -188,7 +251,8 @@ func FetchLogLevel(ctx context.Context, portForward common.PortForwarder) (Logge defer portForward.Close() - response, err := http.Post(fmt.Sprintf("http://%s/logging", endpoint), "application/json", bytes.NewBuffer([]byte{})) + // this endpoint does not support returning json, so we've gotta parse the plain text + response, err := http.Post(fmt.Sprintf("http://%s/logging", endpoint), "text/plain", bytes.NewBuffer([]byte{})) if err != nil { return nil, err } @@ -210,16 +274,7 @@ func FetchLogLevel(ctx context.Context, portForward common.PortForwarder) (Logge 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) { +func (l *LogLevelCommand) 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()) @@ -231,3 +286,30 @@ func (l *LogCommand) outputLevels(logLevels map[string]LoggerConfig) { l.UI.Output("") } } + +func (l *LogLevelCommand) 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 *LogLevelCommand) Synopsis() string { + return "Inspect and Modify the Envoy Log configuration for a given Pod." +} + +// AutocompleteFlags returns a mapping of supported flags and autocomplete +// options for this command. The map key for the Flags map should be the +// complete flag such as "-foo" or "--foo". +func (l *LogLevelCommand) AutocompleteFlags() complete.Flags { + return complete.Flags{ + fmt.Sprintf("-%s", flagNameNamespace): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameKubeConfig): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameKubeContext): complete.PredictNothing, + } +} + +// AutocompleteArgs returns the argument predictor for this command. +// Since argument completion is not supported, this will return +// complete.PredictNothing. +func (l *LogLevelCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} diff --git a/cli/cmd/proxy/loglevel/command_test.go b/cli/cmd/proxy/loglevel/command_test.go index f35ec6b715..9206ea1ba6 100644 --- a/cli/cmd/proxy/loglevel/command_test.go +++ b/cli/cmd/proxy/loglevel/command_test.go @@ -21,7 +21,7 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -func TestFlagParsing(t *testing.T) { +func TestFlagParsingFails(t *testing.T) { t.Parallel() testCases := map[string]struct { args []string @@ -31,9 +31,17 @@ func TestFlagParsing(t *testing.T) { args: []string{}, out: 1, }, - "With pod name": { - args: []string{"now-this-is-pod-racing"}, - out: 0, + "Multiple podnames passed": { + args: []string{"podname", "podname2"}, + out: 1, + }, + "Nonexistent flag passed, -foo bar": { + args: []string{"podName", "-foo", "bar"}, + out: 1, + }, + "Invalid argument passed, -namespace YOLO": { + args: []string{"podName", "-namespace", "YOLO"}, + out: 1, }, } podName := "now-this-is-pod-racing" @@ -58,6 +66,47 @@ func TestFlagParsing(t *testing.T) { } } +func TestFlagParsingSucceeds(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + args []string + podNamespace string + out int + }{ + "With single pod name": { + args: []string{"now-this-is-pod-racing"}, + podNamespace: "default", + out: 0, + }, + "With single pod name and namespace": { + args: []string{"now-this-is-pod-racing", "-n", "another"}, + podNamespace: "another", + out: 0, + }, + } + podName := "now-this-is-pod-racing" + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + fakePod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: tc.podNamespace, + }, + } + + 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" diff --git a/cli/commands.go b/cli/commands.go index dc132eef9a..2946873e51 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -69,7 +69,7 @@ func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseComm }, nil }, "proxy log": func() (cli.Command, error) { - return &loglevel.LogCommand{ + return &loglevel.LogLevelCommand{ BaseCommand: baseCommand, }, nil },