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
+}