You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by li...@apache.org on 2023/07/14 10:01:32 UTC
[dubbo-admin] branch refactor-with-go updated: Implement dubboctl dashboard subcmd (#1197)
This is an automated email from the ASF dual-hosted git repository.
liujun pushed a commit to branch refactor-with-go
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git
The following commit(s) were added to refs/heads/refactor-with-go by this push:
new f4b37199 Implement dubboctl dashboard subcmd (#1197)
f4b37199 is described below
commit f4b37199b583b0503af000109981567eb786707c
Author: Scout Wang <33...@users.noreply.github.com>
AuthorDate: Fri Jul 14 18:01:23 2023 +0800
Implement dubboctl dashboard subcmd (#1197)
---
pkg/dubboctl/cmd/{profile.go => dashboard.go} | 20 ++-
pkg/dubboctl/cmd/profile.go | 4 +-
pkg/dubboctl/cmd/root.go | 1 +
pkg/dubboctl/cmd/subcmd/dashboard_all_cmds.go | 229 ++++++++++++++++++++++++++
pkg/dubboctl/cmd/subcmd/manifest_install.go | 6 +-
pkg/dubboctl/cmd/subcmd/profile_diff.go | 2 +-
pkg/dubboctl/cmd/subcmd/profile_list.go | 2 +-
pkg/dubboctl/internal/kube/client.go | 2 +-
pkg/dubboctl/internal/kube/common.go | 33 +++-
pkg/dubboctl/internal/kube/port_foward.go | 152 +++++++++++++++++
10 files changed, 432 insertions(+), 19 deletions(-)
diff --git a/pkg/dubboctl/cmd/profile.go b/pkg/dubboctl/cmd/dashboard.go
similarity index 58%
copy from pkg/dubboctl/cmd/profile.go
copy to pkg/dubboctl/cmd/dashboard.go
index e0c3534b..e1553192 100644
--- a/pkg/dubboctl/cmd/profile.go
+++ b/pkg/dubboctl/cmd/dashboard.go
@@ -20,14 +20,18 @@ import (
"github.com/spf13/cobra"
)
-func addProfile(rootCmd *cobra.Command) {
- profileCmd := &cobra.Command{
- Use: "profile",
- Short: "Commands related to profiles",
- Long: "Commands help user to list and describe profiles",
+func addDashboard(rootCmd *cobra.Command) {
+ dashboardCmd := &cobra.Command{
+ Use: "dashboard",
+ Short: "Commands related to control plane components dashboards",
+ Long: "Commands help user to open control plane components dashboards directly. Now support Admin, Grafana, Nacos, Prometheus, Skywalking and Zipkin",
}
- subcmd.ConfigProfileListArgs(profileCmd)
- subcmd.ConfigProfileDiffArgs(profileCmd)
+ subcmd.ConfigDashboardAdminCmd(dashboardCmd)
+ subcmd.ConfigDashboardGrafanaCmd(dashboardCmd)
+ subcmd.ConfigDashboardNacosCmd(dashboardCmd)
+ subcmd.ConfigDashboardPrometheusCmd(dashboardCmd)
+ subcmd.ConfigDashboardSkywalkingCmd(dashboardCmd)
+ subcmd.ConfigDashboardZipkinCmd(dashboardCmd)
- rootCmd.AddCommand(profileCmd)
+ rootCmd.AddCommand(dashboardCmd)
}
diff --git a/pkg/dubboctl/cmd/profile.go b/pkg/dubboctl/cmd/profile.go
index e0c3534b..bafb1f4f 100644
--- a/pkg/dubboctl/cmd/profile.go
+++ b/pkg/dubboctl/cmd/profile.go
@@ -26,8 +26,8 @@ func addProfile(rootCmd *cobra.Command) {
Short: "Commands related to profiles",
Long: "Commands help user to list and describe profiles",
}
- subcmd.ConfigProfileListArgs(profileCmd)
- subcmd.ConfigProfileDiffArgs(profileCmd)
+ subcmd.ConfigProfileListCmd(profileCmd)
+ subcmd.ConfigProfileDiffCmd(profileCmd)
rootCmd.AddCommand(profileCmd)
}
diff --git a/pkg/dubboctl/cmd/root.go b/pkg/dubboctl/cmd/root.go
index c19f8752..f4a3f3ef 100644
--- a/pkg/dubboctl/cmd/root.go
+++ b/pkg/dubboctl/cmd/root.go
@@ -41,4 +41,5 @@ func getRootCmd(args []string) *cobra.Command {
func addSubCommands(rootCmd *cobra.Command) {
addManifest(rootCmd)
addProfile(rootCmd)
+ addDashboard(rootCmd)
}
diff --git a/pkg/dubboctl/cmd/subcmd/dashboard_all_cmds.go b/pkg/dubboctl/cmd/subcmd/dashboard_all_cmds.go
new file mode 100644
index 00000000..704bee89
--- /dev/null
+++ b/pkg/dubboctl/cmd/subcmd/dashboard_all_cmds.go
@@ -0,0 +1,229 @@
+// 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 subcmd
+
+import (
+ "context"
+ "fmt"
+ "github.com/apache/dubbo-admin/pkg/dubboctl/identifier"
+ "github.com/apache/dubbo-admin/pkg/dubboctl/internal/kube"
+ "github.com/apache/dubbo-admin/pkg/dubboctl/internal/operator"
+ "github.com/apache/dubbo-admin/pkg/logger"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap/zapcore"
+ "io"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "net"
+ "os"
+ "os/exec"
+ "os/signal"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+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[operator.ComponentName]int{
+ operator.Admin: 8080,
+ operator.Grafana: 3000,
+ operator.Nacos: 8848,
+ operator.Prometheus: 9090,
+ operator.Skywalking: 8080,
+ operator.Zipkin: 9411,
+ }
+ // selectors are coming from /deploy/charts and /deploy/kubernetes
+ ComponentSelectorMap = map[operator.ComponentName]string{
+ operator.Admin: "app.kubernetes.io/name=dubbo-admin",
+ operator.Grafana: "app.kubernetes.io/name=grafana",
+ operator.Nacos: "app.kubernetes.io/name=nacos",
+ operator.Prometheus: "app=prometheus",
+ operator.Skywalking: "app=skywalking, component=ui",
+ operator.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 operator.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 8888
+ # do not open browser
+ dubboctl dashboard %s --openBrowser false
+ # specify namespace of Admin
+ dubboctl dashboard %s --namespace user_specified
+`, ComponentPortMap[compName], lowerNameStr, lowerNameStr, 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, operator.Admin)
+}
+
+func ConfigDashboardGrafanaCmd(baseCmd *cobra.Command) {
+ commonDashboardCmd(baseCmd, operator.Grafana)
+}
+
+func ConfigDashboardNacosCmd(baseCmd *cobra.Command) {
+ commonDashboardCmd(baseCmd, operator.Nacos)
+}
+
+func ConfigDashboardPrometheusCmd(baseCmd *cobra.Command) {
+ commonDashboardCmd(baseCmd, operator.Prometheus)
+}
+
+func ConfigDashboardSkywalkingCmd(baseCmd *cobra.Command) {
+ commonDashboardCmd(baseCmd, operator.Skywalking)
+}
+
+func ConfigDashboardZipkinCmd(baseCmd *cobra.Command) {
+ commonDashboardCmd(baseCmd, operator.Zipkin)
+}
+
+func portForward(args *DashboardCommonArgs, compName operator.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 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)
+ }
+}
diff --git a/pkg/dubboctl/cmd/subcmd/manifest_install.go b/pkg/dubboctl/cmd/subcmd/manifest_install.go
index 23aad1d4..566f453a 100644
--- a/pkg/dubboctl/cmd/subcmd/manifest_install.go
+++ b/pkg/dubboctl/cmd/subcmd/manifest_install.go
@@ -58,10 +58,10 @@ func ConfigManifestInstallCmd(baseCmd *cobra.Command) {
},
}
addManifestGenerateFlags(miCmd, mgArgs)
- miCmd.PersistentFlags().StringVarP(&miArgs.KubeConfigPath, "kubeconfig", "", "",
- "Path to kubeconfig")
+ miCmd.PersistentFlags().StringVarP(&miArgs.KubeConfigPath, "kubeConfig", "", "",
+ "Path to kubeConfig")
miCmd.PersistentFlags().StringVarP(&miArgs.Context, "context", "", "",
- "Context in kubeconfig to use")
+ "Context in kubeConfig to use")
baseCmd.AddCommand(miCmd)
}
diff --git a/pkg/dubboctl/cmd/subcmd/profile_diff.go b/pkg/dubboctl/cmd/subcmd/profile_diff.go
index 1a864387..e193bd44 100644
--- a/pkg/dubboctl/cmd/subcmd/profile_diff.go
+++ b/pkg/dubboctl/cmd/subcmd/profile_diff.go
@@ -40,7 +40,7 @@ func (pda *ProfileDiffArgs) setDefault() {
}
}
-func ConfigProfileDiffArgs(baseCmd *cobra.Command) {
+func ConfigProfileDiffCmd(baseCmd *cobra.Command) {
pdArgs := &ProfileDiffArgs{}
pdCmd := &cobra.Command{
Use: "diff",
diff --git a/pkg/dubboctl/cmd/subcmd/profile_list.go b/pkg/dubboctl/cmd/subcmd/profile_list.go
index dd170172..858f0f16 100644
--- a/pkg/dubboctl/cmd/subcmd/profile_list.go
+++ b/pkg/dubboctl/cmd/subcmd/profile_list.go
@@ -40,7 +40,7 @@ func (pla *ProfileListArgs) setDefault() {
}
}
-func ConfigProfileListArgs(baseCmd *cobra.Command) {
+func ConfigProfileListCmd(baseCmd *cobra.Command) {
plArgs := &ProfileListArgs{}
plCmd := &cobra.Command{
Use: "list",
diff --git a/pkg/dubboctl/internal/kube/client.go b/pkg/dubboctl/internal/kube/client.go
index 876be155..59dbe4db 100644
--- a/pkg/dubboctl/internal/kube/client.go
+++ b/pkg/dubboctl/internal/kube/client.go
@@ -31,8 +31,8 @@ import (
// CtlClient wraps controller-runtime client and is used by dubboctl
type CtlClient struct {
- client.Client
opts *CtlClientOptions
+ client.Client
}
type CtlClientOptions struct {
diff --git a/pkg/dubboctl/internal/kube/common.go b/pkg/dubboctl/internal/kube/common.go
index 148ce6d7..5ac91598 100644
--- a/pkg/dubboctl/internal/kube/common.go
+++ b/pkg/dubboctl/internal/kube/common.go
@@ -17,14 +17,18 @@ package kube
import (
"fmt"
+ "k8s.io/apimachinery/pkg/runtime/serializer"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"os"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch/v5"
+ corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
+ kubescheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kubectl/pkg/scheme"
@@ -135,10 +139,33 @@ func BuildConfig(kubecfgPath string, ctx string) (*rest.Config, error) {
if err != nil {
return nil, err
}
- // setDefaults(cfg)
+ setDefaults(cfg)
return cfg, nil
}
-func setDefaults(cfg *rest.Config) {
- // todo:// add schema
+func setDefaults(config *rest.Config) *rest.Config {
+ if config.GroupVersion == nil || config.GroupVersion.Empty() {
+ config.GroupVersion = &corev1.SchemeGroupVersion
+ }
+ if len(config.APIPath) == 0 {
+ if len(config.GroupVersion.Group) == 0 {
+ config.APIPath = "/api"
+ } else {
+ config.APIPath = "/apis"
+ }
+ }
+ if len(config.ContentType) == 0 {
+ config.ContentType = runtime.ContentTypeJSON
+ }
+ if config.NegotiatedSerializer == nil {
+ config.NegotiatedSerializer = serializer.NewCodecFactory(dubboScheme()).WithoutConversion()
+ }
+
+ return config
+}
+
+func dubboScheme() *runtime.Scheme {
+ newScheme := runtime.NewScheme()
+ utilruntime.Must(kubescheme.AddToScheme(newScheme))
+ return newScheme
}
diff --git a/pkg/dubboctl/internal/kube/port_foward.go b/pkg/dubboctl/internal/kube/port_foward.go
new file mode 100644
index 00000000..ad9e064d
--- /dev/null
+++ b/pkg/dubboctl/internal/kube/port_foward.go
@@ -0,0 +1,152 @@
+// 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 kube
+
+import (
+ "context"
+ "fmt"
+ "github.com/apache/dubbo-admin/pkg/logger"
+ "io"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/portforward"
+ "k8s.io/client-go/transport/spdy"
+ "net/http"
+ "net/url"
+ "os"
+)
+
+type PortForward struct {
+ podName string
+ namespace string
+ // localAddress and localPort form a local listening address, eg: localhost:8080
+ localAddress string
+ localPort int
+ // port of the target pod
+ podPort int
+ stopCh chan struct{}
+ cfg *rest.Config
+ client *rest.RESTClient
+}
+
+func (pf *PortForward) Run() error {
+ readyCh := make(chan struct{}, 1)
+ errCh := make(chan error, 1)
+ // start a goroutine to process portforward
+ go func() {
+ for {
+ select {
+ case <-pf.stopCh:
+ return
+ default:
+ // fail-fast is target pod is not running
+ if err := pf.inspectPodStatus(); err != nil {
+ errCh <- err
+ return
+ }
+ fwReq := pf.client.Post().Resource("pods").Namespace(pf.namespace).Name(pf.podName).SubResource("portforward")
+ kubeFw, err := pf.createKubePortForwarder(fwReq.URL(), readyCh)
+ if err != nil {
+ errCh <- err
+ return
+ }
+ // for lost connection to target pod scenario, ForwardPorts would return nil.
+ // so we put portforward processing in a loop. it would retry until user interrupts.
+ if err = kubeFw.ForwardPorts(); err != nil {
+ errCh <- err
+ return
+ }
+ logger.CmdSugar().Infof("lost connection to %s pod", pf.podName)
+ }
+ }
+ }()
+
+ select {
+ case <-readyCh:
+ return nil
+ case err := <-errCh:
+ return fmt.Errorf("running portforward failed, err: %s", err)
+ }
+}
+
+// inspectPodStatus check status of the target pod. If this pod is not running, fail fast.
+func (pf *PortForward) inspectPodStatus() error {
+ podReq := pf.client.Get().Resource("pods").Namespace(pf.namespace).Name(pf.podName)
+ logger.CmdSugar().Info(podReq.URL().String())
+ obj, err := podReq.Do(context.Background()).Get()
+ if err != nil {
+ return fmt.Errorf("get information of pod %s in %s namespace failed, err: %s", pf.podName, pf.namespace, err)
+ }
+ pod, ok := obj.(*v1.Pod)
+ if !ok {
+ return fmt.Errorf("wanted pod but got %T", obj)
+ }
+ if pod.Status.Phase != v1.PodRunning {
+ return fmt.Errorf("pod %s is not running. now it is %s", pf.podName, pod.Status.Phase)
+ }
+ return nil
+}
+
+// createKubePortForwarder makes use of kube api to create PortForwarder.
+// It needs readyCh to tell PortForward that kube PortForwarder is ready.
+func (pf *PortForward) createKubePortForwarder(reqUrl *url.URL, readyCh chan struct{}) (*portforward.PortForwarder, error) {
+ trans, upgrader, err := spdy.RoundTripperFor(pf.cfg)
+ if err != nil {
+ return nil, fmt.Errorf("creating spdy RoundTripper failed, err: %s", err)
+ }
+ dialer := spdy.NewDialer(upgrader, &http.Client{Transport: trans}, "POST", reqUrl)
+ fw, err := portforward.NewOnAddresses(
+ dialer,
+ []string{pf.localAddress},
+ []string{fmt.Sprintf("%d:%d", pf.localPort, pf.podPort)},
+ pf.stopCh,
+ readyCh,
+ io.Discard,
+ os.Stderr,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("creating kube portforward failed, err: %s", err)
+ }
+ return fw, nil
+}
+
+// Stop close stopCh and free up resources
+func (pf *PortForward) Stop() {
+ close(pf.stopCh)
+}
+
+// Wait wait for closing stopCh which means that Stop function is the only way to trigger
+func (pf *PortForward) Wait() {
+ <-pf.stopCh
+}
+
+func NewPortForward(podName, namespace, localAddress string, localPort, podPort int, cfg *rest.Config) (*PortForward, error) {
+ pf := &PortForward{
+ podName: podName,
+ namespace: namespace,
+ localAddress: localAddress,
+ localPort: localPort,
+ podPort: podPort,
+ stopCh: make(chan struct{}),
+ cfg: cfg,
+ }
+ cli, err := rest.RESTClientFor(cfg)
+ if err != nil {
+ return nil, err
+ }
+ pf.client = cli
+ return pf, nil
+}