diff --git a/docs/cli/vela.md b/docs/cli/vela.md index b9cdf3c20..4765e1842 100644 --- a/docs/cli/vela.md +++ b/docs/cli/vela.md @@ -30,6 +30,7 @@ vela [flags] * [vela install](vela_install.md) - Initialize vela on both client and server * [vela logs](vela_logs.md) - Tail logs for application * [vela metric](vela_metric.md) - Attach metric trait to an app +* [vela port-forward](vela_port-forward.md) - Forward one or more local ports to a Pod of specified service * [vela route](vela_route.md) - Attach route trait to an app * [vela scaler](vela_scaler.md) - Attach scaler trait to an app * [vela svc](vela_svc.md) - Manage services @@ -40,4 +41,4 @@ vela [flags] * [vela version](vela_version.md) - Prints out build version information * [vela workloads](vela_workloads.md) - List workloads -###### Auto generated by spf13/cobra on 28-Oct-2020 +###### Auto generated by spf13/cobra on 29-Oct-2020 diff --git a/docs/cli/vela_port-forward.md b/docs/cli/vela_port-forward.md new file mode 100644 index 000000000..2ac909bc8 --- /dev/null +++ b/docs/cli/vela_port-forward.md @@ -0,0 +1,31 @@ +## vela port-forward + +Forward one or more local ports to a Pod of a service in an application + +### Synopsis + +Forward one or more local ports to a Pod of a service in an application + +``` +vela port-forward APP_NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N] [flags] +``` + +### Options + +``` + --address strings 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. (default [localhost]) + -h, --help help for port-forward + --pod-running-timeout duration The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running (default 1m0s) +``` + +### Options inherited from parent commands + +``` + -e, --env string specify environment name for application +``` + +### SEE ALSO + +* [vela](vela.md) - + +###### Auto generated by spf13/cobra on 29-Oct-2020 diff --git a/e2e/application/application_test.go b/e2e/application/application_test.go index 53494dd83..a4ec96ee4 100644 --- a/e2e/application/application_test.go +++ b/e2e/application/application_test.go @@ -29,6 +29,7 @@ var _ = ginkgo.Describe("Application", func() { e2e.ApplicationStatusContext("app status", applicationName, workloadType) e2e.ApplicationCompStatusContext("svc status", applicationName, workloadType, envName) e2e.ApplicationExecContext("exec -- COMMAND", applicationName) + e2e.ApplicationPortForwardContext("port-forward", applicationName) e2e.ApplicationInitIntercativeCliContext("init", appNameForInit, workloadType) e2e.WorkloadDeleteContext("delete", applicationName) e2e.WorkloadDeleteContext("delete", appNameForInit) diff --git a/e2e/cli.go b/e2e/cli.go index db4b7a4b6..455074170 100644 --- a/e2e/cli.go +++ b/e2e/cli.go @@ -37,6 +37,16 @@ func Exec(cli string) (string, error) { s := session.Wait(30 * time.Second) return string(s.Out.Contents()) + string(s.Err.Contents()), nil } +func ExecAndTerminate(cli string) (string, error) { + var output []byte + session, err := AsyncExec(cli) + if err != nil { + return string(output), err + } + time.Sleep(3 * time.Second) + s := session.Terminate() + return string(s.Out.Contents()) + string(s.Err.Contents()), nil +} func LongTimeExec(cli string, timeout time.Duration) (string, error) { var output []byte diff --git a/e2e/commonContext.go b/e2e/commonContext.go index 0ebd5f8bf..14f92d0df 100644 --- a/e2e/commonContext.go +++ b/e2e/commonContext.go @@ -241,6 +241,17 @@ var ( }) } + ApplicationPortForwardContext = func(context string, appName string) bool { + return ginkgo.Context(context, func() { + ginkgo.It("should get output of portward successfully", func() { + cli := fmt.Sprintf("vela port-forward %s 8080:8080 ", appName) + output, err := ExecAndTerminate(cli) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(output).To(gomega.ContainSubstring("Forward successfully")) + }) + }) + } + ApplicationInitIntercativeCliContext = func(context string, appName string, workloadType string) bool { return ginkgo.Context(context, func() { ginkgo.It("should init app through interactive questions", func() { diff --git a/pkg/commands/cli.go b/pkg/commands/cli.go index c19e2ede4..728b75aba 100644 --- a/pkg/commands/cli.go +++ b/pkg/commands/cli.go @@ -87,6 +87,7 @@ func NewCommand() *cobra.Command { NewDashboardCommand(commandArgs, ioStream, fake.FrontendSource), NewExecCommand(commandArgs, ioStream), + NewPortForwardCommand(commandArgs, ioStream), NewLogsCommand(commandArgs, ioStream), NewTemplateCommand(commandArgs, ioStream), diff --git a/pkg/commands/portforward.go b/pkg/commands/portforward.go new file mode 100644 index 000000000..b5e322eec --- /dev/null +++ b/pkg/commands/portforward.go @@ -0,0 +1,185 @@ +package commands + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" + "github.com/oam-dev/kubevela/api/types" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + + "github.com/oam-dev/kubevela/pkg/application" + "github.com/oam-dev/kubevela/pkg/commands/util" + velacmdutil "github.com/oam-dev/kubevela/pkg/commands/util" + cmdpf "k8s.io/kubectl/pkg/cmd/portforward" + k8scmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +type VelaPortForwardOptions struct { + Cmd *cobra.Command + Args []string + ioStreams velacmdutil.IOStreams + + context.Context + VelaC types.Args + Env *types.EnvMeta + App *application.Application + + f k8scmdutil.Factory + kcPortForwardOptions *cmdpf.PortForwardOptions + ClientSet kubernetes.Interface +} + +func NewPortForwardCommand(c types.Args, ioStreams velacmdutil.IOStreams) *cobra.Command { + o := &VelaPortForwardOptions{ + VelaC: c, + ioStreams: ioStreams, + kcPortForwardOptions: &cmdpf.PortForwardOptions{ + PortForwarder: &defaultPortForwarder{ioStreams}, + }, + } + cmd := &cobra.Command{ + Use: "port-forward APP_NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]", + Short: "Forward one or more local ports to a Pod of a service in an application", + Long: "Forward one or more local ports to a Pod of a service in an application", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + ioStreams.Error("Please specify application name and list of ports.") + return nil + } + 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.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", + ) + return cmd +} + +func (o *VelaPortForwardOptions) Init(ctx context.Context, cmd *cobra.Command, argsIn []string) error { + o.Context = ctx + o.Cmd = cmd + o.Args = argsIn + + env, err := GetEnv(o.Cmd) + if err != nil { + return err + } + o.Env = env + + app, err := application.Load(env.Name, o.Args[0]) + if err != nil { + return err + } + o.App = app + + cf := genericclioptions.NewConfigFlags(true) + cf.Namespace = &o.Env.Namespace + o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf)) + + if o.ClientSet == nil { + c, err := kubernetes.NewForConfig(o.VelaC.Config) + if err != nil { + return err + } + o.ClientSet = c + } + return nil +} + +func (o *VelaPortForwardOptions) Complete() error { + svcName, err := util.AskToChooseOneService(o.App.GetComponents()) + if err != nil { + return err + } + podName, err := o.getPodName(svcName) + if err != nil { + return err + } + + args := make([]string, len(o.Args)) + copy(args, o.Args) + args[0] = podName + return o.kcPortForwardOptions.Complete(o.f, o.Cmd, args) +} + +func (o *VelaPortForwardOptions) getPodName(svcName string) (string, error) { + podList, err := o.ClientSet.CoreV1().Pods(o.Env.Namespace).List(o.Context, v1.ListOptions{ + LabelSelector: labels.Set(map[string]string{ + oam.LabelAppComponent: svcName, + }).String(), + }) + if err != nil { + return "", err + } + if podList != nil && len(podList.Items) == 0 { + return "", fmt.Errorf("cannot get pods") + } + for _, p := range podList.Items { + if strings.HasPrefix(p.Name, svcName+"-") { + return p.Name, nil + } + } + return podList.Items[0].Name, nil +} + +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 { + velacmdutil.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() +} diff --git a/pkg/commands/portforward_test.go b/pkg/commands/portforward_test.go new file mode 100644 index 000000000..0df25e5dd --- /dev/null +++ b/pkg/commands/portforward_test.go @@ -0,0 +1,50 @@ +package commands + +import ( + "context" + "os" + "testing" + + "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" + "github.com/oam-dev/kubevela/api/types" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sfake "k8s.io/client-go/kubernetes/fake" + "k8s.io/kubectl/pkg/cmd/portforward" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestPortForwardCommand(t *testing.T) { + fakePod := corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "fakePod", + Namespace: "default", + ResourceVersion: "10", + Labels: map[string]string{ + oam.LabelAppComponent: "fakeComp", + }}, + } + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + fakeC := types.Args{ + Config: tf.ClientConfigVal, + } + cmd := NewPortForwardCommand(fakeC, io) + cmd.PersistentFlags().StringP("env", "e", "", "") + fakeClientSet := k8sfake.NewSimpleClientset(&corev1.PodList{ + Items: []corev1.Pod{fakePod}, + }) + + o := &VelaPortForwardOptions{ + ioStreams: io, + kcPortForwardOptions: &portforward.PortForwardOptions{}, + f: tf, + ClientSet: fakeClientSet, + } + err := o.Init(context.Background(), cmd, []string{"fakeApp", "8081:8080"}) + assert.NoError(t, err) +}