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 }