mirror of https://github.com/kubevela/kubevela.git
335 lines
9.9 KiB
Go
335 lines
9.9 KiB
Go
/*
|
|
Copyright 2021 The KubeVela Authors.
|
|
|
|
Licensed 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 cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/portforward"
|
|
"k8s.io/client-go/transport/spdy"
|
|
cmdpf "k8s.io/kubectl/pkg/cmd/portforward"
|
|
k8scmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/utils/pointer"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
pkgmulticluster "github.com/kubevela/pkg/multicluster"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
"github.com/oam-dev/kubevela/pkg/multicluster"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
"github.com/oam-dev/kubevela/pkg/utils/util"
|
|
querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
|
|
"github.com/oam-dev/kubevela/references/appfile"
|
|
)
|
|
|
|
// VelaPortForwardOptions for vela port-forward
|
|
type VelaPortForwardOptions struct {
|
|
Cmd *cobra.Command
|
|
Args []string
|
|
ioStreams util.IOStreams
|
|
ClusterName string
|
|
ComponentName string
|
|
ResourceName string
|
|
ResourceType string
|
|
|
|
Ctx context.Context
|
|
VelaC common.Args
|
|
|
|
namespace string
|
|
App *v1beta1.Application
|
|
targetResource struct {
|
|
kind string
|
|
name string
|
|
cluster string
|
|
namespace string
|
|
}
|
|
targetPort int
|
|
|
|
f k8scmdutil.Factory
|
|
kcPortForwardOptions *cmdpf.PortForwardOptions
|
|
ClientSet kubernetes.Interface
|
|
Client client.Client
|
|
}
|
|
|
|
// NewPortForwardCommand is vela port-forward command
|
|
func NewPortForwardCommand(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command {
|
|
o := &VelaPortForwardOptions{
|
|
ioStreams: ioStreams,
|
|
kcPortForwardOptions: &cmdpf.PortForwardOptions{
|
|
PortForwarder: &defaultPortForwarder{ioStreams},
|
|
},
|
|
}
|
|
cmd := &cobra.Command{
|
|
Use: "port-forward APP_NAME",
|
|
Short: "Forward local ports to container/service port of vela application.",
|
|
Long: "Forward local ports to container/service port of vela application.",
|
|
Example: "port-forward APP_NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]",
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
o.VelaC = c
|
|
return nil
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
ioStreams.Error("Please specify application name.")
|
|
return nil
|
|
}
|
|
if o.ResourceType != "pod" && o.ResourceType != "service" {
|
|
o.ResourceType = "service"
|
|
}
|
|
if o.ResourceType == "pod" && len(args) < 2 {
|
|
return errors.New("not port specified for port-forward")
|
|
}
|
|
var err error
|
|
o.namespace, err = GetFlagNamespaceOrEnv(cmd, c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newClient, err := o.VelaC.GetClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Client = newClient
|
|
if err := o.Init(context.Background(), cmd, args); err != nil {
|
|
return err
|
|
}
|
|
if err := o.Complete(); err != nil {
|
|
return err
|
|
}
|
|
if err := o.Run(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
Annotations: map[string]string{
|
|
types.TagCommandOrder: order,
|
|
types.TagCommandType: types.TypeApp,
|
|
},
|
|
}
|
|
|
|
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.")
|
|
cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout,
|
|
"The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running",
|
|
)
|
|
cmd.Flags().StringVarP(&o.ComponentName, "component", "c", "", "filter the pod by the component name")
|
|
cmd.Flags().StringVarP(&o.ClusterName, "cluster", "", "", "filter the pod by the cluster name")
|
|
cmd.Flags().StringVarP(&o.ResourceName, "resource-name", "", "", "specify the resource name")
|
|
cmd.Flags().StringVarP(&o.ResourceType, "resource-type", "t", "", "specify the resource type, support the service, and pod")
|
|
addNamespaceAndEnvArg(cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Init will initialize
|
|
func (o *VelaPortForwardOptions) Init(ctx context.Context, cmd *cobra.Command, argsIn []string) error {
|
|
o.Ctx = ctx
|
|
o.Cmd = cmd
|
|
o.Args = argsIn
|
|
|
|
app, err := appfile.LoadApplication(o.namespace, o.Args[0], o.VelaC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.App = app
|
|
|
|
if o.ResourceType == "service" {
|
|
var selectService *querytypes.ResourceItem
|
|
services, err := GetApplicationServices(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{
|
|
Component: o.ComponentName,
|
|
Cluster: o.ClusterName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load the application services: %w", err)
|
|
}
|
|
|
|
if o.ResourceName != "" {
|
|
for i, service := range services {
|
|
if service.Object.GetName() == o.ResourceName {
|
|
selectService = &services[i]
|
|
break
|
|
}
|
|
}
|
|
if selectService == nil {
|
|
fmt.Println("The Service you specified does not exist, please select it from the list.")
|
|
}
|
|
}
|
|
if len(services) > 0 {
|
|
if selectService == nil {
|
|
selectService, o.targetPort, err = AskToChooseOneService(services, len(o.Args) < 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if selectService != nil {
|
|
o.targetResource.cluster = selectService.Cluster
|
|
o.targetResource.name = selectService.Object.GetName()
|
|
o.targetResource.namespace = selectService.Object.GetNamespace()
|
|
o.targetResource.kind = selectService.Object.GetKind()
|
|
}
|
|
} else if o.ResourceName == "" {
|
|
// If users do not specify the resource name and there is no service, switch to query the pod
|
|
o.ResourceType = "pod"
|
|
}
|
|
}
|
|
|
|
if o.ResourceType == "pod" {
|
|
var selectPod *querytypes.PodBase
|
|
pods, err := GetApplicationPods(o.Ctx, o.App.Name, o.namespace, o.VelaC, Filter{
|
|
Component: o.ComponentName,
|
|
Cluster: o.ClusterName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load the application services: %w", err)
|
|
}
|
|
|
|
if o.ResourceName != "" {
|
|
for i, pod := range pods {
|
|
if pod.Metadata.Name == o.ResourceName {
|
|
selectPod = &pods[i]
|
|
break
|
|
}
|
|
}
|
|
if selectPod == nil {
|
|
fmt.Println("The Service you specified does not exist, please select it from the list.")
|
|
}
|
|
}
|
|
if selectPod == nil {
|
|
selectPod, err = AskToChooseOnePod(pods)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if selectPod != nil {
|
|
o.targetResource.cluster = selectPod.Cluster
|
|
o.targetResource.name = selectPod.Metadata.Name
|
|
o.targetResource.namespace = selectPod.Metadata.Namespace
|
|
o.targetResource.kind = "Pod"
|
|
}
|
|
}
|
|
|
|
cf := genericclioptions.NewConfigFlags(true)
|
|
cf.Namespace = pointer.String(o.targetResource.namespace)
|
|
cf.WrapConfigFn = func(cfg *rest.Config) *rest.Config {
|
|
cfg.Wrap(pkgmulticluster.NewTransportWrapper(pkgmulticluster.ForCluster(o.targetResource.cluster)))
|
|
return cfg
|
|
}
|
|
o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf))
|
|
o.Ctx = multicluster.ContextWithClusterName(ctx, o.targetResource.cluster)
|
|
config, err := o.VelaC.GetConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Wrap(pkgmulticluster.NewTransportWrapper())
|
|
forwardClient, err := client.New(config, client.Options{Scheme: common.Scheme})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.VelaC.SetClient(forwardClient)
|
|
if o.ClientSet == nil {
|
|
c, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.ClientSet = c
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Complete will complete the config of port-forward
|
|
func (o *VelaPortForwardOptions) Complete() error {
|
|
var forwardTypeName string
|
|
switch o.targetResource.kind {
|
|
case "Service":
|
|
forwardTypeName = "svc/" + o.targetResource.name
|
|
case "Pod":
|
|
forwardTypeName = "pod/" + o.targetResource.name
|
|
}
|
|
|
|
if len(o.Args) < 2 {
|
|
formatPort := func(p int) string {
|
|
val := strconv.Itoa(p)
|
|
if val == "80" {
|
|
val = "8080:80"
|
|
} else if val == "443" {
|
|
val = "8443:443"
|
|
}
|
|
return val
|
|
}
|
|
pt := o.targetPort
|
|
if pt == 0 {
|
|
return errors.New("not port specified for port-forward")
|
|
}
|
|
o.Args = append(o.Args, formatPort(pt))
|
|
}
|
|
args := make([]string, len(o.Args))
|
|
copy(args, o.Args)
|
|
args[0] = forwardTypeName
|
|
o.kcPortForwardOptions.Namespace = o.targetResource.namespace
|
|
o.ioStreams.Infof("trying to connect the remote endpoint %s ..", strings.Join(args, " "))
|
|
return o.kcPortForwardOptions.Complete(o.f, o.Cmd, args)
|
|
}
|
|
|
|
// Run will execute port-forward
|
|
func (o *VelaPortForwardOptions) Run() error {
|
|
go func() {
|
|
<-o.kcPortForwardOptions.ReadyChannel
|
|
o.ioStreams.Info("\nForward successfully! Opening browser ...")
|
|
local, _ := splitPort(o.Args[1])
|
|
var url = "http://127.0.0.1:" + local
|
|
if err := OpenBrowser(url); err != nil {
|
|
o.ioStreams.Errorf("\nFailed to open browser: %v", err)
|
|
}
|
|
}()
|
|
|
|
return o.kcPortForwardOptions.RunPortForward()
|
|
}
|
|
|
|
func splitPort(port string) (local, remote string) {
|
|
parts := strings.Split(port, ":")
|
|
if len(parts) == 2 {
|
|
return parts[0], parts[1]
|
|
}
|
|
return parts[0], parts[0]
|
|
}
|
|
|
|
type defaultPortForwarder struct {
|
|
util.IOStreams
|
|
}
|
|
|
|
func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts cmdpf.PortForwardOptions) error {
|
|
transport, upgrader, err := spdy.RoundTripperFor(opts.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
|
fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fw.ForwardPorts()
|
|
}
|