| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cmd |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "net" |
| "os" |
| "os/exec" |
| "os/signal" |
| "runtime" |
| "strconv" |
| "strings" |
| ) |
| |
| import ( |
| "github.com/spf13/cobra" |
| |
| "go.uber.org/zap/zapcore" |
| |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| |
| "k8s.io/client-go/kubernetes" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-kubernetes/app/dubboctl/identifier" |
| "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/kube" |
| "github.com/apache/dubbo-kubernetes/pkg/core/logger" |
| ) |
| |
| var ( |
| // TODO: think about a efficient way to change selectors and ports when yaml files change |
| // ports are coming from /deploy/charts and /deploy/kubernetes |
| ComponentPortMap = map[kube.ComponentName]int{ |
| kube.Admin: 8080, |
| kube.Grafana: 3000, |
| kube.Nacos: 8848, |
| kube.Prometheus: 9090, |
| kube.Skywalking: 8080, |
| kube.Zipkin: 9411, |
| } |
| // ComponentSelectorMap selectors are coming from /deploy/charts and /deploy/kubernetes |
| ComponentSelectorMap = map[kube.ComponentName]string{ |
| kube.Admin: "app.kubernetes.io/name=dubbo-admin", |
| kube.Grafana: "app.kubernetes.io/name=grafana", |
| kube.Nacos: "app.kubernetes.io/name=nacos", |
| kube.Prometheus: "app=prometheus", |
| kube.Skywalking: "app=skywalking, component=ui", |
| kube.Zipkin: "app.kubernetes.io/name=zipkin", |
| } |
| ) |
| |
| type DashboardCommonArgs struct { |
| port int |
| host string |
| openBrowser bool |
| namespace string |
| KubeConfigPath string |
| // selected cluster info of kubeconfig |
| Context string |
| } |
| |
| func (dca *DashboardCommonArgs) setDefault() { |
| if dca == nil { |
| return |
| } |
| if dca.host == "" { |
| dca.host = "127.0.0.1" |
| } |
| if dca.namespace == "" { |
| dca.namespace = identifier.DubboSystemNamespace |
| } |
| } |
| |
| func commonDashboardCmd(baseCmd *cobra.Command, compName kube.ComponentName) { |
| nameStr := string(compName) |
| lowerNameStr := strings.ToLower(nameStr) |
| dcArgs := &DashboardCommonArgs{} |
| cmd := &cobra.Command{ |
| Use: lowerNameStr, |
| Short: fmt.Sprintf("create PortForward between local address and target component %s pod. open browser by default", nameStr), |
| Example: fmt.Sprintf(` # create PortForward in 127.0.0.1:%d and open browser directly |
| dubboctl dashboard %s |
| # specify port |
| dubboctl dashboard %s --port %d |
| # do not open browser |
| dubboctl dashboard %s --openBrowser false |
| # specify namespace of Admin |
| dubboctl dashboard %s --namespace ns_user_specified |
| `, ComponentPortMap[compName], lowerNameStr, lowerNameStr, ComponentPortMap[compName], lowerNameStr, lowerNameStr), |
| RunE: func(cmd *cobra.Command, args []string) error { |
| logger.InitCmdSugar(zapcore.AddSync(cmd.OutOrStdout())) |
| dcArgs.setDefault() |
| if err := portForward(dcArgs, compName, cmd.OutOrStdout()); err != nil { |
| return err |
| } |
| return nil |
| }, |
| } |
| cmd.PersistentFlags().IntVarP(&dcArgs.port, "port", "p", 0, |
| fmt.Sprintf("local port to listen on. If not set, it would be same as the default port of component %s", nameStr)) |
| cmd.PersistentFlags().StringVarP(&dcArgs.host, "host", "", "", |
| "local host to bind. If not set, it would be 127.0.0.1") |
| // openBrowser is default behaviour |
| cmd.PersistentFlags().BoolVarP(&dcArgs.openBrowser, "openBrowser", "", true, |
| "whether to open browser automatically") |
| cmd.PersistentFlags().StringVarP(&dcArgs.namespace, "namespace", "n", "", |
| fmt.Sprintf("namespace in which component %s is located", nameStr)) |
| cmd.PersistentFlags().StringVarP(&dcArgs.KubeConfigPath, "kubeConfig", "", "", |
| "Path to kubeconfig") |
| cmd.PersistentFlags().StringVarP(&dcArgs.Context, "context", "", "", |
| "Context in kubeconfig to use") |
| |
| baseCmd.AddCommand(cmd) |
| } |
| |
| func ConfigDashboardAdminCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Admin) |
| } |
| |
| func ConfigDashboardGrafanaCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Grafana) |
| } |
| |
| func ConfigDashboardNacosCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Nacos) |
| } |
| |
| func ConfigDashboardPrometheusCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Prometheus) |
| } |
| |
| func ConfigDashboardSkywalkingCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Skywalking) |
| } |
| |
| func ConfigDashboardZipkinCmd(baseCmd *cobra.Command) { |
| commonDashboardCmd(baseCmd, kube.Zipkin) |
| } |
| |
| func portForward(args *DashboardCommonArgs, compName kube.ComponentName, writer io.Writer) error { |
| // process args |
| var podPort int |
| podPort = ComponentPortMap[compName] |
| if args.port == 0 { |
| args.port = podPort |
| } |
| |
| // prepare PortForward args |
| labelSelector := ComponentSelectorMap[compName] |
| cfg, err := kube.BuildConfig(args.KubeConfigPath, args.Context) |
| if err != nil { |
| return fmt.Errorf("build kube config failed, err: %s", err) |
| } |
| // todo: unify kube client |
| cli, err := kubernetes.NewForConfig(cfg) |
| if err != nil { |
| return fmt.Errorf("create kube RESTClient failed, err: %s", err) |
| } |
| pods, err := cli.CoreV1().Pods(args.namespace).List(context.Background(), metav1.ListOptions{ |
| LabelSelector: labelSelector, |
| }) |
| if err != nil { |
| return fmt.Errorf("list pod failed, err: %s", err) |
| } |
| if len(pods.Items) < 1 { |
| return fmt.Errorf("no %s pods found", string(compName)) |
| } |
| // use name of the first pod |
| podName := pods.Items[0].Name |
| |
| pf, err := kube.NewPortForward(podName, args.namespace, args.host, args.port, podPort, cfg) |
| if err != nil { |
| return fmt.Errorf("create PortForward failed, err: %s", err) |
| } |
| if err := pf.Run(); err != nil { |
| pf.Stop() |
| return fmt.Errorf("PortForward running failed, err: %s", err) |
| } |
| |
| logger.CmdSugar().Infof("PortForward to %s pod is running", podName) |
| |
| // wait for interrupt |
| go func() { |
| signals := make(chan os.Signal, 1) |
| signal.Notify(signals, os.Interrupt) |
| defer signal.Stop(signals) |
| <-signals |
| logger.CmdSugar().Info("PortForward stops") |
| pf.Stop() |
| }() |
| |
| if args.openBrowser { |
| address := net.JoinHostPort(args.host, strconv.Itoa(args.port)) |
| url := "http://" + address |
| openBrowser(url, writer) |
| } |
| |
| pf.Wait() |
| |
| return nil |
| } |
| |
| // openBrowser uses syscall from different runtime |
| func openBrowser(url string, writer io.Writer) { |
| var err error |
| |
| fmt.Fprintf(writer, "open browser in %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. pls open %s in your browser.\n", runtime.GOOS, url) |
| } |
| |
| if err != nil { |
| fmt.Fprintf(writer, "open browser failed; open %s in your browser.\n", url) |
| } |
| } |