Added new CLI command dapr dashboard (#383)

* Added files for new command, added cobra command line code for dapr dashboard

* Janky first implementation

* WIP: added default host, port for dashboard

* implemented port-forwarding for dashboard

* fixed lint errors

* removed dashboard deployment, fixed PR comments
This commit is contained in:
Gurpreet Singh 2020-06-29 10:16:14 -07:00 committed by GitHub
parent 15726263f2
commit 0a1a0bdf2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 265 additions and 0 deletions

104
cmd/dashboard.go Normal file
View File

@ -0,0 +1,104 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package cmd
import (
"fmt"
"os"
"os/signal"
"github.com/dapr/cli/pkg/kubernetes"
"github.com/dapr/cli/pkg/print"
"github.com/pkg/browser"
"github.com/spf13/cobra"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// dashboardSvc is the name of the dashboard service running in cluster
dashboardSvc = "dapr-dashboard"
// defaultHost is the default host used for port forwarding for `dapr dashboard`
defaultHost = "localhost"
// defaultLocalPort is the default local port used for port forwarding for `dapr dashboard`
defaultLocalPort = 8080
// remotePort is the port dapr dashboard pod is listening on
remotePort = 8080
)
var localPort int
var DashboardCmd = &cobra.Command{
Use: "dashboard",
Short: "Runs the Dapr dashboard on a Kubernetes cluster",
Run: func(cmd *cobra.Command, args []string) {
if port < 0 {
localPort = defaultLocalPort
} else {
localPort = port
}
config, err := kubernetes.GetKubeConfig()
if err != nil {
print.FailureStatusEvent(os.Stdout, "Failed to initialize kubernetes client")
return
}
// manage termination of port forwarding connection on interrupt
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
defer signal.Stop(signals)
portForward, err := kubernetes.NewPortForward(
config,
meta_v1.NamespaceDefault,
dashboardSvc,
defaultHost,
localPort,
remotePort,
false,
)
if err != nil {
print.FailureStatusEvent(os.Stdout, "%s\n", err)
os.Exit(1)
}
// initialize port forwarding
if err = portForward.Init(); err != nil {
print.FailureStatusEvent(os.Stdout, "Error in port forwarding: %s\nCheck for `dapr dashboard` running in other terminal sessions, or use the `--port` flag to use a different port.\n", err)
os.Exit(1)
}
// block until interrupt signal is received
go func() {
<-signals
portForward.Stop()
}()
// url for dashboard after port forwarding
var webURL string = fmt.Sprintf("http://%s:%d", defaultHost, localPort)
print.InfoStatusEvent(os.Stdout, fmt.Sprintf("Dapr dashboard available at:\t%s\n", webURL))
err = browser.OpenURL(webURL)
if err != nil {
print.FailureStatusEvent(os.Stdout, "Failed to start Dapr dashboard in browser automatically")
print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Visit %s in your browser to view the dashboard", webURL))
}
<-portForward.GetStop()
},
}
func init() {
DashboardCmd.Flags().BoolVarP(&kubernetesMode, "kubernetes", "k", false, "Start Dapr dashboard in local browser")
DashboardCmd.Flags().IntVarP(&port, "port", "p", defaultLocalPort, "The local port on which to serve dashboard")
DashboardCmd.MarkFlagRequired("kubernetes")
RootCmd.AddCommand(DashboardCmd)
}

1
go.mod
View File

@ -24,6 +24,7 @@ require (
github.com/olekukonko/tablewriter v0.0.1
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/shirou/gopsutil v2.20.2+incompatible
github.com/spf13/cobra v0.0.6
github.com/spf13/viper v1.5.0

5
go.sum
View File

@ -43,6 +43,7 @@ github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+B
github.com/Azure/go-autorest/autorest v0.9.2 h1:6AWuh3uWrsZJcNoCHrCF/+g4aKPCU39kaMO6/qrnK/4=
github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY=
github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.6.0 h1:UCTq22yE3RPgbU/8u4scfnnzuCW6pwQ9n+uBtV78ouo=
@ -51,6 +52,7 @@ github.com/Azure/go-autorest/autorest/adal v0.8.0 h1:CxTzQrySOxDnKpLjFJeZAS5Qrv/
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.3 h1:O1AGG9Xig71FxdX9HO5pGNyZ7TbSyHaVg+5eJO/jSGw=
github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/azure/auth v0.3.0/go.mod h1:CI4BQYBct8NS7BXNBBX+RchsFsUu5+oz+OSyR/ZIi7U=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
@ -175,6 +177,7 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
@ -506,6 +509,8 @@ github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5 h1:rZQtoozkfsiNs36
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -51,6 +51,11 @@ func getConfig() (*rest.Config, error) {
return config, nil
}
// GetKubeConfig returns kubeconfig
func GetKubeConfig() (*rest.Config, error) {
return getConfig()
}
// Client returns a new Kubernetes client.
func Client() (*k8s.Clientset, error) {
config, err := getConfig()

View File

@ -0,0 +1,150 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package kubernetes
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
core_v1 "k8s.io/api/core/v1"
k8s "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
)
// PortForward provides a port-forward connection in a kubernetes cluster.
type PortForward struct {
Config *rest.Config
Method string
URL *url.URL
Host string
LocalPort int
RemotePort int
EmitLogs bool
StopCh chan struct{}
ReadyCh chan struct{}
}
// NewPortForward returns an instance of PortForward struct that can be used
// for establishing port-forwarding connection to a pod in kubernetes cluster,
// specified by namespace and deployName.
func NewPortForward(
config *rest.Config,
namespace, deployName string,
host string, localPort, remotePort int,
emitLogs bool,
) (*PortForward, error) {
client, err := k8s.NewForConfig(config)
if err != nil {
return nil, err
}
podList, err := ListPods(client, namespace, nil)
if err != nil {
return nil, err
}
podName := ""
for _, pod := range podList.Items {
if pod.Status.Phase == core_v1.PodRunning {
if strings.HasPrefix(pod.Name, deployName) {
podName = pod.Name
break
}
}
}
if podName == "" {
return nil, fmt.Errorf("no running pods found for %s", deployName)
}
req := client.CoreV1().RESTClient().Post().
Resource("pods").
Namespace(namespace).
Name(podName).
SubResource("portforward")
return &PortForward{
Config: config,
Method: "POST",
URL: req.URL(),
Host: host,
LocalPort: localPort,
RemotePort: remotePort,
EmitLogs: emitLogs,
StopCh: make(chan struct{}, 1),
ReadyCh: make(chan struct{}),
}, nil
}
// run creates port-forward connection and blocks
// until Stop() is called.
func (pf *PortForward) run() error {
transport, upgrader, err := spdy.RoundTripperFor(pf.Config)
if err != nil {
return err
}
out := ioutil.Discard
errOut := ioutil.Discard
if pf.EmitLogs {
out = os.Stdout
errOut = os.Stderr
}
ports := []string{fmt.Sprintf("%d:%d", pf.LocalPort, pf.RemotePort)}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, pf.Method, pf.URL)
fw, err := portforward.NewOnAddresses(dialer, []string{pf.Host}, ports, pf.StopCh, pf.ReadyCh, out, errOut)
if err != nil {
return err
}
return fw.ForwardPorts()
}
// Init creates and runs a port-forward connection.
// This function blocks until connection is established.
// Note: Caller should call Stop() to finish the connection.
func (pf *PortForward) Init() error {
failure := make(chan error)
go func() {
if err := pf.run(); err != nil {
failure <- err
}
}()
select {
// if `pf.run()` succeeds, block until terminated
case <-pf.ReadyCh:
// if failure, causing a receive `<-failure` and returns the error
case err := <-failure:
return err
}
return nil
}
// Stop terminates port-forwarding connection.
func (pf *PortForward) Stop() {
close(pf.StopCh)
}
// GetStop returns StopCh for a PortForward instance.
// Receiving on StopCh will block until the port forwarding stops.
func (pf *PortForward) GetStop() <-chan struct{} {
return pf.StopCh
}