github.com/oam-dev/kubevela@v1.9.11/references/cli/portforward.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/cobra"
    29  	"k8s.io/cli-runtime/pkg/genericclioptions"
    30  	"k8s.io/client-go/kubernetes"
    31  	"k8s.io/client-go/rest"
    32  	"k8s.io/client-go/tools/portforward"
    33  	"k8s.io/client-go/transport/spdy"
    34  	cmdpf "k8s.io/kubectl/pkg/cmd/portforward"
    35  	k8scmdutil "k8s.io/kubectl/pkg/cmd/util"
    36  	"k8s.io/utils/pointer"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  
    39  	pkgmulticluster "github.com/kubevela/pkg/multicluster"
    40  
    41  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    42  	"github.com/oam-dev/kubevela/apis/types"
    43  	"github.com/oam-dev/kubevela/pkg/multicluster"
    44  	"github.com/oam-dev/kubevela/pkg/utils/common"
    45  	"github.com/oam-dev/kubevela/pkg/utils/util"
    46  	querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
    47  	"github.com/oam-dev/kubevela/references/appfile"
    48  )
    49  
    50  // VelaPortForwardOptions for vela port-forward
    51  type VelaPortForwardOptions struct {
    52  	Cmd           *cobra.Command
    53  	Args          []string
    54  	ioStreams     util.IOStreams
    55  	ClusterName   string
    56  	ComponentName string
    57  	ResourceName  string
    58  	ResourceType  string
    59  
    60  	Ctx   context.Context
    61  	VelaC common.Args
    62  
    63  	namespace      string
    64  	App            *v1beta1.Application
    65  	targetResource struct {
    66  		kind      string
    67  		name      string
    68  		cluster   string
    69  		namespace string
    70  	}
    71  	targetPort int
    72  
    73  	f                    k8scmdutil.Factory
    74  	kcPortForwardOptions *cmdpf.PortForwardOptions
    75  	ClientSet            kubernetes.Interface
    76  	Client               client.Client
    77  }
    78  
    79  // NewPortForwardCommand is vela port-forward command
    80  func NewPortForwardCommand(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command {
    81  	o := &VelaPortForwardOptions{
    82  		ioStreams: ioStreams,
    83  		kcPortForwardOptions: &cmdpf.PortForwardOptions{
    84  			PortForwarder: &defaultPortForwarder{ioStreams},
    85  		},
    86  	}
    87  	cmd := &cobra.Command{
    88  		Use:     "port-forward",
    89  		Short:   "Forward local ports to container/service port of vela application.",
    90  		Long:    "Forward local ports to container/service port of vela application.",
    91  		Example: "port-forward APP_NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]",
    92  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    93  			o.VelaC = c
    94  			return nil
    95  		},
    96  		RunE: func(cmd *cobra.Command, args []string) error {
    97  			if len(args) < 1 {
    98  				ioStreams.Error("Please specify application name.")
    99  				return nil
   100  			}
   101  			if o.ResourceType != "pod" && o.ResourceType != "service" {
   102  				o.ResourceType = "service"
   103  			}
   104  			if o.ResourceType == "pod" && len(args) < 2 {
   105  				return errors.New("not port specified for port-forward")
   106  			}
   107  			var err error
   108  			o.namespace, err = GetFlagNamespaceOrEnv(cmd, c)
   109  			if err != nil {
   110  				return err
   111  			}
   112  
   113  			newClient, err := o.VelaC.GetClient()
   114  			if err != nil {
   115  				return err
   116  			}
   117  			o.Client = newClient
   118  			if err := o.Init(context.Background(), cmd, args); err != nil {
   119  				return err
   120  			}
   121  			if err := o.Complete(); err != nil {
   122  				return err
   123  			}
   124  			if err := o.Run(); err != nil {
   125  				return err
   126  			}
   127  			return nil
   128  		},
   129  		Annotations: map[string]string{
   130  			types.TagCommandOrder: order,
   131  			types.TagCommandType:  types.TypeApp,
   132  		},
   133  	}
   134  
   135  	cmd.Flags().StringSliceVar(&o.kcPortForwardOptions.Address, "address", []string{"localhost"}, "Addresses to listen on (comma separated). Only accepts IP addresses or localhost as a value. When localhost is supplied, vela will try to bind on both 127.0.0.1 and ::1 and will fail if neither of these addresses are available to bind.")
   136  	cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout,
   137  		"The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running",
   138  	)
   139  	cmd.Flags().StringVarP(&o.ComponentName, "component", "c", "", "filter the pod by the component name")
   140  	cmd.Flags().StringVarP(&o.ClusterName, "cluster", "", "", "filter the pod by the cluster name")
   141  	cmd.Flags().StringVarP(&o.ResourceName, "resource-name", "", "", "specify the resource name")
   142  	cmd.Flags().StringVarP(&o.ResourceType, "resource-type", "t", "", "specify the resource type, support the service, and pod")
   143  	addNamespaceAndEnvArg(cmd)
   144  	return cmd
   145  }
   146  
   147  // Init will initialize
   148  func (o *VelaPortForwardOptions) Init(ctx context.Context, cmd *cobra.Command, argsIn []string) error {
   149  	o.Ctx = ctx
   150  	o.Cmd = cmd
   151  	o.Args = argsIn
   152  
   153  	app, err := appfile.LoadApplication(o.namespace, o.Args[0], o.VelaC)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	o.App = app
   158  
   159  	if o.ResourceType == "service" {
   160  		var selectService *querytypes.ResourceItem
   161  		services, err := GetApplicationServices(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{
   162  			Component: o.ComponentName,
   163  			Cluster:   o.ClusterName,
   164  		})
   165  		if err != nil {
   166  			return fmt.Errorf("failed to load the application services: %w", err)
   167  		}
   168  
   169  		if o.ResourceName != "" {
   170  			for i, service := range services {
   171  				if service.Object.GetName() == o.ResourceName {
   172  					selectService = &services[i]
   173  					break
   174  				}
   175  			}
   176  			if selectService == nil {
   177  				fmt.Println("The Service you specified does not exist, please select it from the list.")
   178  			}
   179  		}
   180  		if len(services) > 0 {
   181  			if selectService == nil {
   182  				selectService, o.targetPort, err = AskToChooseOneService(services, len(o.Args) < 2)
   183  				if err != nil {
   184  					return err
   185  				}
   186  			}
   187  			if selectService != nil {
   188  				o.targetResource.cluster = selectService.Cluster
   189  				o.targetResource.name = selectService.Object.GetName()
   190  				o.targetResource.namespace = selectService.Object.GetNamespace()
   191  				o.targetResource.kind = selectService.Object.GetKind()
   192  			}
   193  		} else if o.ResourceName == "" {
   194  			// If users do not specify the resource name and there is no service, switch to query the pod
   195  			o.ResourceType = "pod"
   196  		}
   197  	}
   198  
   199  	if o.ResourceType == "pod" {
   200  		var selectPod *querytypes.PodBase
   201  		pods, err := GetApplicationPods(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{
   202  			Component: o.ComponentName,
   203  			Cluster:   o.ClusterName,
   204  		})
   205  		if err != nil {
   206  			return fmt.Errorf("failed to load the application services: %w", err)
   207  		}
   208  
   209  		if o.ResourceName != "" {
   210  			for i, pod := range pods {
   211  				if pod.Metadata.Name == o.ResourceName {
   212  					selectPod = &pods[i]
   213  					break
   214  				}
   215  			}
   216  			if selectPod == nil {
   217  				fmt.Println("The Service you specified does not exist, please select it from the list.")
   218  			}
   219  		}
   220  		if selectPod == nil {
   221  			selectPod, err = AskToChooseOnePod(pods)
   222  			if err != nil {
   223  				return err
   224  			}
   225  		}
   226  		if selectPod != nil {
   227  			o.targetResource.cluster = selectPod.Cluster
   228  			o.targetResource.name = selectPod.Metadata.Name
   229  			o.targetResource.namespace = selectPod.Metadata.Namespace
   230  			o.targetResource.kind = "Pod"
   231  		}
   232  	}
   233  
   234  	cf := genericclioptions.NewConfigFlags(true)
   235  	cf.Namespace = pointer.String(o.targetResource.namespace)
   236  	cf.WrapConfigFn = func(cfg *rest.Config) *rest.Config {
   237  		cfg.Wrap(pkgmulticluster.NewTransportWrapper(pkgmulticluster.ForCluster(o.targetResource.cluster)))
   238  		return cfg
   239  	}
   240  	o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf))
   241  	o.Ctx = multicluster.ContextWithClusterName(ctx, o.targetResource.cluster)
   242  	config, err := o.VelaC.GetConfig()
   243  	if err != nil {
   244  		return err
   245  	}
   246  	config.Wrap(pkgmulticluster.NewTransportWrapper())
   247  	forwardClient, err := client.New(config, client.Options{Scheme: common.Scheme})
   248  	if err != nil {
   249  		return err
   250  	}
   251  	o.VelaC.SetClient(forwardClient)
   252  	if o.ClientSet == nil {
   253  		c, err := kubernetes.NewForConfig(config)
   254  		if err != nil {
   255  			return err
   256  		}
   257  		o.ClientSet = c
   258  	}
   259  	return nil
   260  }
   261  
   262  // Complete will complete the config of port-forward
   263  func (o *VelaPortForwardOptions) Complete() error {
   264  	var forwardTypeName string
   265  	switch o.targetResource.kind {
   266  	case "Service":
   267  		forwardTypeName = "svc/" + o.targetResource.name
   268  	case "Pod":
   269  		forwardTypeName = "pod/" + o.targetResource.name
   270  	}
   271  
   272  	if len(o.Args) < 2 {
   273  		formatPort := func(p int) string {
   274  			val := strconv.Itoa(p)
   275  			if val == "80" {
   276  				val = "8080:80"
   277  			} else if val == "443" {
   278  				val = "8443:443"
   279  			}
   280  			return val
   281  		}
   282  		pt := o.targetPort
   283  		if pt == 0 {
   284  			return errors.New("not port specified for port-forward")
   285  		}
   286  		o.Args = append(o.Args, formatPort(pt))
   287  	}
   288  	args := make([]string, len(o.Args))
   289  	copy(args, o.Args)
   290  	args[0] = forwardTypeName
   291  	o.kcPortForwardOptions.Namespace = o.targetResource.namespace
   292  	o.ioStreams.Infof("trying to connect the remote endpoint %s ..", strings.Join(args, " "))
   293  	return o.kcPortForwardOptions.Complete(o.f, o.Cmd, args)
   294  }
   295  
   296  // Run will execute port-forward
   297  func (o *VelaPortForwardOptions) Run() error {
   298  	go func() {
   299  		<-o.kcPortForwardOptions.ReadyChannel
   300  		o.ioStreams.Info("\nForward successfully! Opening browser ...")
   301  		local, _ := splitPort(o.Args[1])
   302  		var url = "http://127.0.0.1:" + local
   303  		if err := OpenBrowser(url); err != nil {
   304  			o.ioStreams.Errorf("\nFailed to open browser: %v", err)
   305  		}
   306  	}()
   307  
   308  	return o.kcPortForwardOptions.RunPortForward()
   309  }
   310  
   311  func splitPort(port string) (local, remote string) {
   312  	parts := strings.Split(port, ":")
   313  	if len(parts) == 2 {
   314  		return parts[0], parts[1]
   315  	}
   316  	return parts[0], parts[0]
   317  }
   318  
   319  type defaultPortForwarder struct {
   320  	util.IOStreams
   321  }
   322  
   323  func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts cmdpf.PortForwardOptions) error {
   324  	transport, upgrader, err := spdy.RoundTripperFor(opts.Config)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
   329  	fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	return fw.ForwardPorts()
   334  }