mirror of https://github.com/dapr/cli.git
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:
parent
15726263f2
commit
0a1a0bdf2e
|
@ -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
1
go.mod
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue