From 5e9098d9d42d2d09bddf6b2d74d40b815aa355ef Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Thu, 22 Feb 2024 10:43:42 +0800 Subject: [PATCH] add dashboard --- internal/cmd/egctl/config_test.go | 2 + internal/cmd/egctl/dashboard.go | 23 +++++ internal/cmd/egctl/envoy_dashboard.go | 128 ++++++++++++++++++++++++++ internal/cmd/egctl/experimental.go | 1 + internal/kubernetes/port-forwarder.go | 6 ++ 5 files changed, 160 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 47a70c19fdee..bfeba9fa1557 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 000000000000..349662dbedab --- /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 000000000000..c41b76339efa --- /dev/null +++ b/internal/cmd/egctl/envoy_dashboard.go @@ -0,0 +1,128 @@ +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 && labelSelectors != nil { + 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 e22dc3e6ca60..ad307cc7cb37 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 a58e88ea5cd0..ceb439d7721b 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) }