github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/dashboard/dashboard.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package dashboard
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"net/url"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/spf13/cobra"
    32  
    33  	corev1 "k8s.io/api/core/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  	"k8s.io/client-go/kubernetes"
    37  	"k8s.io/client-go/tools/portforward"
    38  	"k8s.io/client-go/transport/spdy"
    39  	cmdpf "k8s.io/kubectl/pkg/cmd/portforward"
    40  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    41  	"k8s.io/kubectl/pkg/util/templates"
    42  	"k8s.io/utils/pointer"
    43  
    44  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    45  	"github.com/1aal/kubeblocks/pkg/cli/util"
    46  )
    47  
    48  // kb support dashboard name
    49  const (
    50  	grafanaAddonName       = "kubeblocks-grafana"
    51  	bytebaseAddonName      = "bytebase"
    52  	nyancatAddonName       = "kubeblocks-nyancat"
    53  	prometheusAlertManager = "kubeblocks-prometheus-alertmanager"
    54  	prometheusServer       = "kubeblocks-prometheus-server"
    55  	pyroscopeServer        = "kubeblocks-pyroscope-server"
    56  	jupyterHubAddon        = "jupyter-hub"
    57  	jupyterNoteBookAddon   = "jupyter-notebook"
    58  	minio                  = "minio"
    59  )
    60  
    61  const (
    62  	podRunningTimeoutFlag = "pod-running-timeout"
    63  	defaultPodExecTimeout = 60 * time.Second
    64  
    65  	lokiAddonName     = "kubeblocks-logs"
    66  	lokiGrafanaDirect = "container-logs"
    67  	localAdd          = "127.0.0.1"
    68  )
    69  
    70  type dashboard struct {
    71  	Name         string
    72  	AddonName    string
    73  	Port         string
    74  	TargetPort   string
    75  	Namespace    string
    76  	CreationTime string
    77  
    78  	// Label used to get the service
    79  	Label string
    80  }
    81  
    82  var (
    83  	listExample = templates.Examples(`
    84  		# List all dashboards
    85  		kbcli dashboard list
    86  	`)
    87  
    88  	openExample = templates.Examples(`
    89  		# Open a dashboard, such as kube-grafana
    90  		kbcli dashboard open kubeblocks-grafana
    91  
    92  		# Open a dashboard with a specific local port
    93  		kbcli dashboard open kubeblocks-grafana --port 8080
    94  
    95  		# for dashboard kubeblocks-grafana, support to direct the specified dashboard type
    96  		# now we support mysql,mongodb,postgresql,redis,weaviate,kafka,cadvisor,jmx and node
    97  		kbcli dashboard open kubeblocks-grafana mysql
    98  	`)
    99  
   100  	// we do not use the default port to port-forward to avoid conflict with other services
   101  	dashboards = [...]*dashboard{
   102  		{
   103  			Name:       grafanaAddonName,
   104  			AddonName:  "kb-addon-grafana",
   105  			Label:      "app.kubernetes.io/instance=kb-addon-grafana,app.kubernetes.io/name=grafana",
   106  			TargetPort: "13000",
   107  		},
   108  		{
   109  			Name:       prometheusAlertManager,
   110  			AddonName:  "kb-addon-prometheus-alertmanager",
   111  			Label:      "app=prometheus,component=alertmanager,release=kb-addon-prometheus",
   112  			TargetPort: "19093",
   113  		},
   114  		{
   115  			Name:       prometheusServer,
   116  			AddonName:  "kb-addon-prometheus-server",
   117  			Label:      "app=prometheus,component=server,release=kb-addon-prometheus",
   118  			TargetPort: "19090",
   119  		},
   120  		{
   121  			Name:       nyancatAddonName,
   122  			AddonName:  "kb-addon-nyancat",
   123  			Label:      "app.kubernetes.io/instance=kb-addon-nyancat",
   124  			TargetPort: "8087",
   125  		},
   126  		{
   127  			Name:       lokiAddonName,
   128  			AddonName:  "kb-addon-loki",
   129  			Label:      "app.kubernetes.io/instance=kb-addon-loki",
   130  			TargetPort: "13100",
   131  		},
   132  		{
   133  			Name:       pyroscopeServer,
   134  			AddonName:  "kb-addon-pyroscope-server",
   135  			Label:      "app.kubernetes.io/instance=kb-addon-pyroscope-server,app.kubernetes.io/name=pyroscope",
   136  			TargetPort: "14040",
   137  		}, {
   138  			Name:       bytebaseAddonName,
   139  			AddonName:  "bytebase-entrypoint",
   140  			Label:      "app=bytebase",
   141  			TargetPort: "18080",
   142  		},
   143  		{
   144  			Name:       jupyterHubAddon,
   145  			AddonName:  "proxy-public",
   146  			Label:      "app=jupyterhub",
   147  			TargetPort: "18081",
   148  		},
   149  		{
   150  			Name:       jupyterNoteBookAddon,
   151  			AddonName:  "jupyter-notebook",
   152  			Label:      " app.kubernetes.io/instance=kb-addon-jupyter-notebook",
   153  			TargetPort: "18888",
   154  		},
   155  		{
   156  			Name:       minio,
   157  			AddonName:  "kb-addon-minio",
   158  			Label:      "app.kubernetes.io/instance=kb-addon-minio",
   159  			TargetPort: "9001",
   160  			Port:       "9001",
   161  		},
   162  	}
   163  )
   164  
   165  type listOptions struct {
   166  	genericiooptions.IOStreams
   167  	factory cmdutil.Factory
   168  	client  *kubernetes.Clientset
   169  }
   170  
   171  func newListOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *listOptions {
   172  	return &listOptions{
   173  		factory:   f,
   174  		IOStreams: streams,
   175  	}
   176  }
   177  
   178  // NewDashboardCmd creates the dashboard command
   179  func NewDashboardCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   180  	cmd := &cobra.Command{
   181  		Use:   "dashboard",
   182  		Short: "List and open the KubeBlocks dashboards.",
   183  	}
   184  
   185  	// add subcommands
   186  	cmd.AddCommand(
   187  		newListCmd(f, streams),
   188  		newOpenCmd(f, streams),
   189  	)
   190  
   191  	return cmd
   192  }
   193  
   194  func newListCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   195  	o := newListOptions(f, streams)
   196  	cmd := &cobra.Command{
   197  		Use:     "list",
   198  		Short:   "List all dashboards.",
   199  		Example: listExample,
   200  		Args:    cobra.NoArgs,
   201  		Run: func(cmd *cobra.Command, args []string) {
   202  			util.CheckErr(o.complete())
   203  			util.CheckErr(o.run())
   204  		},
   205  	}
   206  	return cmd
   207  }
   208  
   209  func (o *listOptions) complete() error {
   210  	var err error
   211  	o.client, err = o.factory.KubernetesClientSet()
   212  	return err
   213  }
   214  
   215  // get all dashboard service and print
   216  func (o *listOptions) run() error {
   217  	if err := getDashboardInfo(o.client); err != nil {
   218  		return err
   219  	}
   220  
   221  	return printTable(o.Out)
   222  }
   223  
   224  func printTable(out io.Writer) error {
   225  	tbl := printer.NewTablePrinter(out)
   226  	tbl.SetHeader("NAME", "NAMESPACE", "PORT", "CREATED-TIME")
   227  	for _, d := range dashboards {
   228  		if d.Namespace == "" {
   229  			continue
   230  		}
   231  		tbl.AddRow(d.Name, d.Namespace, d.TargetPort, d.CreationTime)
   232  	}
   233  	tbl.Print()
   234  	return nil
   235  }
   236  
   237  type openOptions struct {
   238  	factory cmdutil.Factory
   239  	genericiooptions.IOStreams
   240  	portForwardOptions *cmdpf.PortForwardOptions
   241  
   242  	name      string
   243  	localPort string
   244  }
   245  
   246  func newOpenOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *openOptions {
   247  	return &openOptions{
   248  		factory:   f,
   249  		IOStreams: streams,
   250  		portForwardOptions: &cmdpf.PortForwardOptions{
   251  			PortForwarder: &defaultPortForwarder{streams},
   252  		},
   253  	}
   254  }
   255  
   256  func newOpenCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   257  	o := newOpenOptions(f, streams)
   258  	cmd := &cobra.Command{
   259  		Use:     "open NAME [DASHBOARD-TYPE] [--port PORT]",
   260  		Short:   "Open one dashboard.",
   261  		Example: openExample,
   262  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   263  			if len(args) == 1 && args[0] == supportDirectDashboard {
   264  				var name []string
   265  				for i := range availableTypes {
   266  					if strings.HasPrefix(availableTypes[i], toComplete) {
   267  						name = append(name, availableTypes[i])
   268  					}
   269  				}
   270  				return name, cobra.ShellCompDirectiveNoFileComp
   271  			}
   272  			var names []string
   273  			for _, d := range dashboards {
   274  				names = append(names, d.Name)
   275  			}
   276  			return names, cobra.ShellCompDirectiveNoFileComp
   277  		},
   278  		Run: func(cmd *cobra.Command, args []string) {
   279  			util.CheckErr(o.complete(cmd, args))
   280  			util.CheckErr(o.run())
   281  		},
   282  	}
   283  
   284  	cmd.Flags().StringVar(&o.localPort, "port", "", "dashboard local port")
   285  	cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout,
   286  		"The time (like 5s, 2m, or 3h, higher than zero) to wait for at least one pod is running")
   287  	return cmd
   288  }
   289  
   290  func (o *openOptions) complete(cmd *cobra.Command, args []string) error {
   291  	if len(args) == 0 {
   292  		return fmt.Errorf("missing dashborad name")
   293  	}
   294  
   295  	o.name = args[0]
   296  	client, err := o.factory.KubernetesClientSet()
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	if err = getDashboardInfo(client); err != nil {
   302  		return err
   303  	}
   304  	dashName := o.name
   305  	// opening loki dashboard redirects to grafana dashboard
   306  	if o.name == lokiAddonName {
   307  		dashName = grafanaAddonName
   308  	}
   309  	dash := getDashboardByName(dashName)
   310  	if dash == nil {
   311  		return fmt.Errorf("failed to find dashboard \"%s\", run \"kbcli dashboard list\" to list all dashboards", o.name)
   312  	}
   313  	if dash.Name == supportDirectDashboard && len(args) > 1 {
   314  		clusterType = args[1]
   315  	}
   316  	if o.localPort == "" {
   317  		if o.name == lokiAddonName {
   318  			// revert the target port for loki dashboard
   319  			o.localPort = getDashboardByName(lokiAddonName).TargetPort
   320  		} else {
   321  			o.localPort = dash.TargetPort
   322  		}
   323  	}
   324  	pfArgs := []string{fmt.Sprintf("svc/%s", dash.AddonName), fmt.Sprintf("%s:%s", o.localPort, dash.Port)}
   325  	o.portForwardOptions.Namespace = dash.Namespace
   326  	o.portForwardOptions.Address = []string{localAdd}
   327  	return o.portForwardOptions.Complete(newFactory(dash.Namespace), cmd, pfArgs)
   328  }
   329  
   330  func (o *openOptions) run() error {
   331  	url := fmt.Sprintf("http://%s:%s", localAdd, o.localPort)
   332  	if o.name == "kubeblocks-grafana" {
   333  		err := buildGrafanaDirectURL(&url, clusterType)
   334  		if err != nil {
   335  			return err
   336  		}
   337  	}
   338  	// customized by loki
   339  	if o.name == lokiAddonName {
   340  		err := buildGrafanaDirectURL(&url, lokiGrafanaDirect)
   341  		if err != nil {
   342  			return err
   343  		}
   344  	}
   345  	go func() {
   346  		<-o.portForwardOptions.ReadyChannel
   347  		fmt.Fprintf(o.Out, "Forward successfully! Opening browser ...\n")
   348  		if err := util.OpenBrowser(url); err != nil {
   349  			fmt.Fprintf(o.ErrOut, "Failed to open browser: %v", err)
   350  		}
   351  	}()
   352  	return o.portForwardOptions.RunPortForward()
   353  }
   354  
   355  func getDashboardByName(name string) *dashboard {
   356  	for i, d := range dashboards {
   357  		if d.Name == name {
   358  			return dashboards[i]
   359  		}
   360  	}
   361  	return nil
   362  }
   363  
   364  func getDashboardInfo(client *kubernetes.Clientset) error {
   365  	getSvcs := func(client *kubernetes.Clientset, label string) (*corev1.ServiceList, error) {
   366  		return client.CoreV1().Services(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{
   367  			LabelSelector: label,
   368  		})
   369  	}
   370  
   371  	for _, d := range dashboards {
   372  		var svc *corev1.Service
   373  
   374  		// get all services that match the label
   375  		svcs, err := getSvcs(client, d.Label)
   376  		if err != nil {
   377  			return err
   378  		}
   379  
   380  		// find the dashboard service
   381  		for i, s := range svcs.Items {
   382  			if s.Name == d.AddonName {
   383  				svc = &svcs.Items[i]
   384  				break
   385  			}
   386  		}
   387  
   388  		if svc == nil {
   389  			continue
   390  		}
   391  
   392  		// fill dashboard information
   393  		d.Namespace = svc.Namespace
   394  		d.CreationTime = util.TimeFormat(&svc.CreationTimestamp)
   395  		// if port is not specified, use the first port of the service
   396  		if len(svc.Spec.Ports) > 0 && d.Port == "" {
   397  			d.Port = fmt.Sprintf("%d", svc.Spec.Ports[0].Port)
   398  			if d.TargetPort == "" {
   399  				d.TargetPort = svc.Spec.Ports[0].TargetPort.String()
   400  			}
   401  		}
   402  	}
   403  	return nil
   404  }
   405  
   406  func newFactory(namespace string) cmdutil.Factory {
   407  	cf := util.NewConfigFlagNoWarnings()
   408  	cf.Namespace = pointer.String(namespace)
   409  	return cmdutil.NewFactory(cf)
   410  }
   411  
   412  type defaultPortForwarder struct {
   413  	genericiooptions.IOStreams
   414  }
   415  
   416  func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts cmdpf.PortForwardOptions) error {
   417  	transport, upgrader, err := spdy.RoundTripperFor(opts.Config)
   418  	if err != nil {
   419  		return err
   420  	}
   421  	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
   422  	pf, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	return pf.ForwardPorts()
   427  }