From 15c3d86fc1b0004918293f162cf57b2b3d00b4d7 Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Thu, 22 Feb 2024 10:54:09 +0800 Subject: [PATCH 1/3] add dashboard feature Signed-off-by: xiaopeng --- internal/cmd/egctl/config_test.go | 2 + internal/cmd/egctl/dashboard.go | 23 +++++ internal/cmd/egctl/envoy_dashboard.go | 133 ++++++++++++++++++++++++++ internal/cmd/egctl/experimental.go | 1 + internal/kubernetes/port-forwarder.go | 6 ++ 5 files changed, 165 insertions(+) create mode 100644 internal/cmd/egctl/dashboard.go create mode 100644 internal/cmd/egctl/envoy_dashboard.go diff --git a/internal/cmd/egctl/config_test.go b/internal/cmd/egctl/config_test.go index 47a70c19fde..bfeba9fa155 100644 --- a/internal/cmd/egctl/config_test.go +++ b/internal/cmd/egctl/config_test.go @@ -79,6 +79,8 @@ func (fw *fakePortForwarder) Address() string { return fmt.Sprintf("localhost:%d", fw.localPort) } +func (fw *fakePortForwarder) WaitForStop() {} + func TestExtractAllConfigDump(t *testing.T) { input, err := readInputConfig("in.all.json") require.NoError(t, err) diff --git a/internal/cmd/egctl/dashboard.go b/internal/cmd/egctl/dashboard.go new file mode 100644 index 00000000000..349662dbeda --- /dev/null +++ b/internal/cmd/egctl/dashboard.go @@ -0,0 +1,23 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package egctl + +import ( + "github.com/spf13/cobra" +) + +func newDashboardCommand() *cobra.Command { + c := &cobra.Command{ + Use: "dashboard", + Aliases: []string{"d"}, + Long: "Retrieve the dashboard.", + Short: "Retrieve the dashboard.", + } + + c.AddCommand(newEnvoyDashboardCmd()) + + return c +} diff --git a/internal/cmd/egctl/envoy_dashboard.go b/internal/cmd/egctl/envoy_dashboard.go new file mode 100644 index 00000000000..795b01a3858 --- /dev/null +++ b/internal/cmd/egctl/envoy_dashboard.go @@ -0,0 +1,133 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package egctl + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "runtime" + + kube "github.com/envoyproxy/gateway/internal/kubernetes" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" +) + +func newEnvoyDashboardCmd() *cobra.Command { + var podName, podNamespace string + var listenPort int + + dashboardCmd := &cobra.Command{ + Use: "envoy-proxy -n ", + Short: "Retrieves Envoy admin dashboard for the specified pod", + Long: `Retrieve Envoy admin dashboard for the specified pod.`, + Example: ` # Retrieve Envoy admin dashboard for the specified pod. + egctl experimental dashboard envoy-proxy -n + + # short syntax + egctl experimental d envoy-proxy -n +`, + Aliases: []string{"d"}, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 && len(labelSelectors) == 0 { + cmd.Println(cmd.UsageString()) + return fmt.Errorf("dashboard requires pod name or label selector") + } + if len(args) > 0 && len(labelSelectors) > 0 { + cmd.Println(cmd.UsageString()) + return fmt.Errorf("name cannot be provided when a selector is specified") + } + return nil + }, + RunE: func(c *cobra.Command, args []string) error { + kubeClient, err := getCLIClient() + if err != nil { + return err + } + if len(args) != 0 { + podName = args[0] + } + if len(labelSelectors) > 0 { + pl, err := kubeClient.PodsForSelector(podNamespace, labelSelectors...) + if err != nil { + return fmt.Errorf("not able to locate pod with selector %s: %v", labelSelectors, err) + } + if len(pl.Items) < 1 { + return errors.New("no pods found") + } + podName = pl.Items[0].Name + podNamespace = pl.Items[0].Namespace + } + + return portForward(podName, podNamespace, "http://%s", adminPort, kubeClient, c.OutOrStdout()) + }, + } + dashboardCmd.PersistentFlags().StringArrayVarP(&labelSelectors, "labels", "l", nil, "Labels to select the envoy proxy pod.") + dashboardCmd.PersistentFlags().StringVarP(&podNamespace, "namespace", "n", "envoy-gateway-system", "Namespace where envoy proxy pod are installed.") + dashboardCmd.PersistentFlags().IntVarP(&listenPort, "port", "p", 0, "Local port to listen to.") + + return dashboardCmd +} + +// portForward first tries to forward localhost:remotePort to podName:remotePort, falls back to dynamic local port +func portForward(podName, namespace, urlFormat string, listenPort int, client kube.CLIClient, writer io.Writer) error { + var fw kube.PortForwarder + meta := types.NamespacedName{ + Namespace: namespace, + Name: podName, + } + fw, err := kube.NewLocalPortForwarder(client, meta, listenPort, adminPort) + if err != nil { + return fmt.Errorf("could not build port forwarder for envoy proxy: %v", err) + } + + if err = fw.Start(); err != nil { + fw.Stop() + return fmt.Errorf("could not start port forwarder for envoy proxy: %v", err) + } + + ClosePortForwarderOnInterrupt(fw) + + openBrowser(fmt.Sprintf(urlFormat, fw.Address()), writer) + + fw.WaitForStop() + + return nil +} + +func ClosePortForwarderOnInterrupt(fw kube.PortForwarder) { + go func() { + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt) + defer signal.Stop(signals) + <-signals + fw.Stop() + }() +} + +func openBrowser(url string, writer io.Writer) { + var err error + + fmt.Fprintf(writer, "%s\n", url) + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + fmt.Fprintf(writer, "Unsupported platform %q; open %s in your browser.\n", runtime.GOOS, url) + } + + if err != nil { + fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\n", url) + } +} diff --git a/internal/cmd/egctl/experimental.go b/internal/cmd/egctl/experimental.go index e22dc3e6ca6..ad307cc7cb3 100644 --- a/internal/cmd/egctl/experimental.go +++ b/internal/cmd/egctl/experimental.go @@ -25,6 +25,7 @@ func newExperimentalCommand() *cobra.Command { experimentalCommand.AddCommand(newTranslateCommand()) experimentalCommand.AddCommand(newStatsCommand()) experimentalCommand.AddCommand(newStatusCommand()) + experimentalCommand.AddCommand(newDashboardCommand()) return experimentalCommand } diff --git a/internal/kubernetes/port-forwarder.go b/internal/kubernetes/port-forwarder.go index a58e88ea5cd..ceb439d7721 100644 --- a/internal/kubernetes/port-forwarder.go +++ b/internal/kubernetes/port-forwarder.go @@ -24,6 +24,8 @@ type PortForwarder interface { Stop() + WaitForStop() + // Address returns the address of the local forwarded address. Address() string } @@ -128,6 +130,10 @@ func (f *localForwarder) Stop() { close(f.stopCh) } +func (f *localForwarder) WaitForStop() { + <-f.stopCh +} + func (f *localForwarder) Address() string { return fmt.Sprintf("%s:%d", netutil.DefaultLocalAddress, f.localPort) } From 94b701b81632710e25e690e363714b6de5fc97e3 Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Thu, 22 Feb 2024 11:06:56 +0800 Subject: [PATCH 2/3] fix lint Signed-off-by: xiaopeng --- internal/cmd/egctl/envoy_dashboard.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/cmd/egctl/envoy_dashboard.go b/internal/cmd/egctl/envoy_dashboard.go index 795b01a3858..4569e64fc85 100644 --- a/internal/cmd/egctl/envoy_dashboard.go +++ b/internal/cmd/egctl/envoy_dashboard.go @@ -14,9 +14,10 @@ import ( "os/signal" "runtime" - kube "github.com/envoyproxy/gateway/internal/kubernetes" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" + + kube "github.com/envoyproxy/gateway/internal/kubernetes" ) func newEnvoyDashboardCmd() *cobra.Command { @@ -56,7 +57,7 @@ func newEnvoyDashboardCmd() *cobra.Command { if len(labelSelectors) > 0 { pl, err := kubeClient.PodsForSelector(podNamespace, labelSelectors...) if err != nil { - return fmt.Errorf("not able to locate pod with selector %s: %v", labelSelectors, err) + return fmt.Errorf("not able to locate pod with selector %s: %w", labelSelectors, err) } if len(pl.Items) < 1 { return errors.New("no pods found") @@ -84,12 +85,12 @@ func portForward(podName, namespace, urlFormat string, listenPort int, client ku } fw, err := kube.NewLocalPortForwarder(client, meta, listenPort, adminPort) if err != nil { - return fmt.Errorf("could not build port forwarder for envoy proxy: %v", err) + return fmt.Errorf("could not build port forwarder for envoy proxy: %w", err) } if err = fw.Start(); err != nil { fw.Stop() - return fmt.Errorf("could not start port forwarder for envoy proxy: %v", err) + return fmt.Errorf("could not start port forwarder for envoy proxy: %w", err) } ClosePortForwarderOnInterrupt(fw) From 8bc249fd4b8b564453f4d022dcb54a1abfeea032 Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Fri, 23 Feb 2024 10:26:15 +0800 Subject: [PATCH 3/3] revise based on comments Signed-off-by: xiaopeng --- internal/cmd/egctl/envoy_dashboard.go | 8 ++++++-- site/content/en/latest/user/egctl.md | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/cmd/egctl/envoy_dashboard.go b/internal/cmd/egctl/envoy_dashboard.go index 4569e64fc85..9b8f8bdf327 100644 --- a/internal/cmd/egctl/envoy_dashboard.go +++ b/internal/cmd/egctl/envoy_dashboard.go @@ -26,7 +26,7 @@ func newEnvoyDashboardCmd() *cobra.Command { dashboardCmd := &cobra.Command{ Use: "envoy-proxy -n ", - Short: "Retrieves Envoy admin dashboard for the specified pod", + Short: "Retrieve Envoy admin dashboard for the specified pod", Long: `Retrieve Envoy admin dashboard for the specified pod.`, Example: ` # Retrieve Envoy admin dashboard for the specified pod. egctl experimental dashboard envoy-proxy -n @@ -47,6 +47,10 @@ func newEnvoyDashboardCmd() *cobra.Command { return nil }, RunE: func(c *cobra.Command, args []string) error { + if listenPort > 65535 || listenPort < 0 { + return fmt.Errorf("invalid port number range") + } + kubeClient, err := getCLIClient() if err != nil { return err @@ -66,7 +70,7 @@ func newEnvoyDashboardCmd() *cobra.Command { podNamespace = pl.Items[0].Namespace } - return portForward(podName, podNamespace, "http://%s", adminPort, kubeClient, c.OutOrStdout()) + return portForward(podName, podNamespace, "http://%s", listenPort, kubeClient, c.OutOrStdout()) }, } dashboardCmd.PersistentFlags().StringArrayVarP(&labelSelectors, "labels", "l", nil, "Labels to select the envoy proxy pod.") diff --git a/site/content/en/latest/user/egctl.md b/site/content/en/latest/user/egctl.md index dba019e1cf5..8fc3e391758 100644 --- a/site/content/en/latest/user/egctl.md +++ b/site/content/en/latest/user/egctl.md @@ -795,3 +795,21 @@ product backend ResolvedRefs True ResolvedRefs [Multi-tenancy]: ../deployment-mode#multi-tenancy [EnvoyProxy]: ../../api/extension_types#envoyproxy + + +## egctl experimental dashboard + +This subcommand streamlines the process for users to access the Envoy admin dashboard. By executing the following command: + +```bash +egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4 +``` + +You will see the following output: + +```bash +egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4 +http://localhost:19000 +``` + +the Envoy admin dashboard will automatically open in your default web browser. This eliminates the need to manually locate and expose the admin port.