diff --git a/README.md b/README.md index 41196c9d..9aacd152 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ reviews ClusterIP 172.27.255.155 9080/TCP 9m6s app= ### Reverse proxy ```shell -➜ ~ kubevpn connect --workloads deployment/productpage +➜ ~ kubevpn proxy deployment/productpage got cidr from cache traffic manager not exist, try to create it... pod [kubevpn-traffic-manager] status is Running @@ -207,7 +207,7 @@ Hello world!% Support HTTP, GRPC and WebSocket etc. with specific header `"a: 1"` will route to your local machine ```shell -➜ ~ kubevpn connect --workloads=deployment/productpage --headers a=1 +➜ ~ kubevpn proxy deployment/productpage --headers a=1 got cidr from cache traffic manager not exist, try to create it... pod [kubevpn-traffic-manager] status is Running diff --git a/README_ZH.md b/README_ZH.md index daa2bcfe..c38c7f0f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -150,7 +150,7 @@ reviews ClusterIP 172.27.255.155 9080/TCP 9m6s app= ### 反向代理 ```shell -➜ ~ kubevpn connect --workloads deployment/productpage +➜ ~ kubevpn proxy deployment/productpage got cidr from cache traffic manager not exist, try to create it... pod [kubevpn-traffic-manager] status is Running @@ -205,7 +205,7 @@ Hello world!% 支持 HTTP, GRPC 和 WebSocket 等, 携带了指定 header `"a: 1"` 的流量,将会路由到本地 ```shell -➜ ~ kubevpn connect --workloads=deployment/productpage --headers a=1 +➜ ~ kubevpn proxy deployment/productpage --headers a=1 got cidr from cache traffic manager not exist, try to create it... pod [kubevpn-traffic-manager] status is Running diff --git a/cmd/kubevpn/cmds/connect.go b/cmd/kubevpn/cmds/connect.go index 136704db..bfa15520 100644 --- a/cmd/kubevpn/cmds/connect.go +++ b/cmd/kubevpn/cmds/connect.go @@ -31,15 +31,6 @@ func CmdConnect(f cmdutil.Factory) *cobra.Command { # Connect to k8s cluster network kubevpn connect - # Reverse proxy - - reverse deployment - kubevpn connect --workloads=deployment/productpage - - reverse service - kubevpn connect --workloads=service/productpage - - # Reverse proxy with mesh, traffic with header a=1, will hit local PC, otherwise no effect - kubevpn connect --workloads=service/productpage --headers a=1 - # Connect to api-server behind of bastion host or ssh jump host kubevpn connect --ssh-addr 192.168.1.100:22 --ssh-username root --ssh-keyfile /Users/naison/.ssh/ssh.pem @@ -67,7 +58,6 @@ func CmdConnect(f cmdutil.Factory) *cobra.Command { if err := connect.InitClient(f); err != nil { return err } - connect.PreCheckResource() if err := connect.DoConnect(); err != nil { log.Errorln(err) handler.Cleanup(syscall.SIGQUIT) @@ -81,8 +71,6 @@ func CmdConnect(f cmdutil.Factory) *cobra.Command { select {} }, } - cmd.Flags().StringArrayVar(&connect.Workloads, "workloads", []string{}, "Kubernetes workloads, special which workloads you want to proxy it to local PC, If not special, just connect to cluster network, like: pods/tomcat, deployment/nginx, replicaset/tomcat etc") - cmd.Flags().StringToStringVarP(&connect.Headers, "headers", "H", map[string]string{}, "Traffic with special headers with reverse it to local PC, you should startup your service after reverse workloads successfully, If not special, redirect all traffic to local PC, format is k=v, like: k1=v1,k2=v2") cmd.Flags().BoolVar(&config.Debug, "debug", false, "enable debug mode or not, true or false") cmd.Flags().StringVar(&config.Image, "image", config.Image, "use this image to startup container") diff --git a/cmd/kubevpn/cmds/dev.go b/cmd/kubevpn/cmds/dev.go index 2667d180..5b33c2af 100644 --- a/cmd/kubevpn/cmds/dev.go +++ b/cmd/kubevpn/cmds/dev.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "os" - "path/filepath" "syscall" "github.com/docker/cli/cli" @@ -13,7 +12,6 @@ import ( "github.com/docker/docker/api/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "k8s.io/client-go/util/retry" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" @@ -35,6 +33,7 @@ func CmdDev(f cmdutil.Factory) *cobra.Command { Env: opts.NewListOpts(nil), Volumes: opts.NewListOpts(nil), ExtraHosts: opts.NewListOpts(nil), + NoProxy: false, } var sshConf = util.SshConfig{} cmd := &cobra.Command{ @@ -45,6 +44,7 @@ func CmdDev(f cmdutil.Factory) *cobra.Command { # Dev reverse proxy - reverse deployment kubevpn dev deployment/productpage + - reverse service kubevpn dev service/productpage @@ -75,11 +75,9 @@ func CmdDev(f cmdutil.Factory) *cobra.Command { return handler.SshJump(sshConf, cmd.Flags()) }, RunE: func(cmd *cobra.Command, args []string) error { - devOptions.Workload = args[0] - connect := handler.ConnectOptions{ Headers: devOptions.Headers, - Workloads: []string{devOptions.Workload}, + Workloads: args, } if devOptions.ParentContainer != "" { @@ -103,7 +101,26 @@ func CmdDev(f cmdutil.Factory) *cobra.Command { if err := connect.InitClient(f); err != nil { return err } - connect.PreCheckResource() + err2 := connect.PreCheckResource() + if err2 != nil { + return err2 + } + + if len(connect.Workloads) > 1 { + return fmt.Errorf("can only dev one workloads at same time, workloads: %v", connect.Workloads) + } + if len(connect.Workloads) < 1 { + return fmt.Errorf("you must provide resource to dev, workloads : %v is invaild", connect.Workloads) + } + + devOptions.Workload = connect.Workloads[0] + // if no-proxy is true, not needs to intercept traffic + if devOptions.NoProxy { + if len(connect.Headers) != 0 { + return fmt.Errorf("not needs to provide headers if is no-proxy mode") + } + connect.Workloads = []string{} + } defer func() { handler.Cleanup(syscall.SIGQUIT) select {} @@ -119,37 +136,11 @@ func CmdDev(f cmdutil.Factory) *cobra.Command { } return err }, - PostRun: func(*cobra.Command, []string) { - if util.IsWindows() { - err := retry.OnError(retry.DefaultRetry, func(err error) bool { - return err != nil - }, func() error { - return driver.UninstallWireGuardTunDriver() - }) - if err != nil { - var wd string - wd, err = os.Getwd() - if err != nil { - return - } - filename := filepath.Join(wd, "wintun.dll") - var temp *os.File - if temp, err = os.CreateTemp("", ""); err != nil { - return - } - if err = temp.Close(); err != nil { - return - } - if err = os.Rename(filename, temp.Name()); err != nil { - log.Debugln(err) - } - } - } - }, } cmd.Flags().StringToStringVarP(&devOptions.Headers, "headers", "H", map[string]string{}, "Traffic with special headers with reverse it to local PC, you should startup your service after reverse workloads successfully, If not special, redirect all traffic to local PC, format is k=v, like: k1=v1,k2=v2") cmd.Flags().BoolVar(&config.Debug, "debug", false, "enable debug mode or not, true or false") cmd.Flags().StringVar(&config.Image, "image", config.Image, "use this image to startup container") + cmd.Flags().BoolVar(&devOptions.NoProxy, "no-proxy", false, "Whether proxy remote workloads traffic into local or not, true: just startup container on local without inject containers to intercept traffic, false: intercept traffic and forward to local") cmdutil.AddContainerVarFlags(cmd, &devOptions.ContainerName, devOptions.ContainerName) cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("container", completion.ContainerCompletionFunc(f))) diff --git a/cmd/kubevpn/cmds/proxy.go b/cmd/kubevpn/cmds/proxy.go new file mode 100644 index 00000000..91ebddf6 --- /dev/null +++ b/cmd/kubevpn/cmds/proxy.go @@ -0,0 +1,114 @@ +package cmds + +import ( + "fmt" + "io" + defaultlog "log" + "net/http" + "os" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + utilcomp "k8s.io/kubectl/pkg/util/completion" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/wencaiwulue/kubevpn/pkg/config" + "github.com/wencaiwulue/kubevpn/pkg/driver" + "github.com/wencaiwulue/kubevpn/pkg/handler" + "github.com/wencaiwulue/kubevpn/pkg/util" +) + +func CmdProxy(f cmdutil.Factory) *cobra.Command { + var connect = handler.ConnectOptions{} + var sshConf = util.SshConfig{} + cmd := &cobra.Command{ + Use: "proxy", + Short: i18n.T("Connect to kubernetes cluster network, or proxy kubernetes workloads inbound traffic into local PC"), + Long: templates.LongDesc(i18n.T(`Connect to kubernetes cluster network, or proxy kubernetes workloads inbound traffic into local PC`)), + Example: templates.Examples(i18n.T(` + # Reverse proxy + - proxy deployment + kubevpn proxy deployment/productpage + + - proxy service + kubevpn proxy service/productpage + + - proxy multiple workloads + kubevpn proxy deployment/authors deployment/productpage + or + kubevpn proxy deployment authors productpage + + # Reverse proxy with mesh, traffic with header a=1, will hit local PC, otherwise no effect + kubevpn proxy service/productpage --headers a=1 + + # Connect to api-server behind of bastion host or ssh jump host and proxy kubernetes resource traffic into local PC + kubevpn proxy --ssh-addr 192.168.1.100:22 --ssh-username root --ssh-keyfile /Users/naison/.ssh/ssh.pem service/productpage --headers a=1 + + # it also support ProxyJump, like + ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────────┐ + │ pc ├────►│ ssh1 ├────►│ ssh2 ├────►│ ssh3 ├─────►... ─────► │ api-server │ + └──────┘ └──────┘ └──────┘ └──────┘ └────────────┘ + kubevpn proxy service/productpage --ssh-alias --headers a=1 + +`)), + PreRunE: func(cmd *cobra.Command, args []string) (err error) { + if !util.IsAdmin() { + util.RunWithElevated() + os.Exit(0) + } + go http.ListenAndServe("localhost:6060", nil) + util.InitLogger(config.Debug) + defaultlog.Default().SetOutput(io.Discard) + if util.IsWindows() { + driver.InstallWireGuardTunDriver() + } + return handler.SshJump(sshConf, cmd.Flags()) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := connect.InitClient(f); err != nil { + return err + } + if len(args) == 0 { + fmt.Fprintf(os.Stdout, "You must specify the type of resource to proxy. %s\n\n", cmdutil.SuggestAPIResources("kubevpn")) + fullCmdName := cmd.Parent().CommandPath() + usageString := "Required resource not specified." + if len(fullCmdName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "explain") { + usageString = fmt.Sprintf("%s\nUse \"%s explain \" for a detailed description of that resource (e.g. %[2]s explain pods).", usageString, fullCmdName) + } + return cmdutil.UsageErrorf(cmd, usageString) + } + connect.Workloads = args + err := connect.PreCheckResource() + if err != nil { + return err + } + if err = connect.DoConnect(); err != nil { + log.Errorln(err) + handler.Cleanup(syscall.SIGQUIT) + } else { + fmt.Println() + fmt.Println(`---------------------------------------------------------------------------`) + fmt.Println(` Now you can access resources in the kubernetes cluster, enjoy it :) `) + fmt.Println(`---------------------------------------------------------------------------`) + fmt.Println() + } + select {} + }, + } + cmd.Flags().StringToStringVarP(&connect.Headers, "headers", "H", map[string]string{}, "Traffic with special headers with reverse it to local PC, you should startup your service after reverse workloads successfully, If not special, redirect all traffic to local PC, format is k=v, like: k1=v1,k2=v2") + cmd.Flags().BoolVar(&config.Debug, "debug", false, "Enable debug mode or not, true or false") + cmd.Flags().StringVar(&config.Image, "image", config.Image, "Use this image to startup container") + + // for ssh jumper host + cmd.Flags().StringVar(&sshConf.Addr, "ssh-addr", "", "Optional ssh jump server address to dial as :, eg: 127.0.0.1:22") + cmd.Flags().StringVar(&sshConf.User, "ssh-username", "", "Optional username for ssh jump server") + cmd.Flags().StringVar(&sshConf.Password, "ssh-password", "", "Optional password for ssh jump server") + cmd.Flags().StringVar(&sshConf.Keyfile, "ssh-keyfile", "", "Optional file with private key for SSH authentication") + cmd.Flags().StringVar(&sshConf.ConfigAlias, "ssh-alias", "", "Optional config alias with ~/.ssh/config for SSH authentication") + + cmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f) + return cmd +} diff --git a/cmd/kubevpn/cmds/root.go b/cmd/kubevpn/cmds/root.go index 17719e30..04fe9793 100644 --- a/cmd/kubevpn/cmds/root.go +++ b/cmd/kubevpn/cmds/root.go @@ -29,6 +29,7 @@ func NewKubeVPNCommand() *cobra.Command { Message: "Client Commands:", Commands: []*cobra.Command{ CmdConnect(factory), + CmdProxy(factory), CmdDev(factory), CmdReset(factory), CmdUpgrade(factory), diff --git a/pkg/dev/main.go b/pkg/dev/main.go index bb98dcca..87f83d4d 100644 --- a/pkg/dev/main.go +++ b/pkg/dev/main.go @@ -37,6 +37,7 @@ type Options struct { Workload string Factory cmdutil.Factory ContainerName string + NoProxy bool // docker options Platform string diff --git a/pkg/handler/connect.go b/pkg/handler/connect.go index bb793682..b4e725dd 100644 --- a/pkg/handler/connect.go +++ b/pkg/handler/connect.go @@ -564,7 +564,17 @@ func SshJump(conf util.SshConfig, flags *pflag.FlagSet) (err error) { // pods without controller // pod/productpage-without-controller --> pod/productpage-without-controller // service/productpage-without-pod --> controller/controllerName -func (c *ConnectOptions) PreCheckResource() { +func (c *ConnectOptions) PreCheckResource() error { + list, err := util.GetUnstructuredObjectList(c.factory, c.Namespace, c.Workloads) + if err != nil { + return err + } + var resources []string + for _, info := range list { + resources = append(resources, fmt.Sprintf("%s/%s", info.Mapping.GroupVersionKind.GroupKind().String(), info.Name)) + } + c.Workloads = resources + // normal workloads, like pod with controller, deployments, statefulset, replicaset etc... for i, workload := range c.Workloads { ownerReference, err := util.GetTopOwnerReference(c.factory, c.Namespace, workload) @@ -576,7 +586,7 @@ func (c *ConnectOptions) PreCheckResource() { for i, workload := range c.Workloads { object, err := util.GetUnstructuredObject(c.factory, c.Namespace, workload) if err != nil { - continue + return err } if object.Mapping.Resource.Resource != "services" { continue @@ -606,11 +616,12 @@ func (c *ConnectOptions) PreCheckResource() { } // only a single service, not support it yet if controller.Len() == 0 { - log.Fatalf("Not support resources: %s", workload) + return fmt.Errorf("not support resources: %s", workload) } } } } + return nil } func (c *ConnectOptions) GetRunningPodList() ([]v1.Pod, error) { diff --git a/pkg/handler/function_test.go b/pkg/handler/function_test.go index c2486fd2..fd5793d7 100644 --- a/pkg/handler/function_test.go +++ b/pkg/handler/function_test.go @@ -264,7 +264,7 @@ func server(port int) { func kubevpnConnect(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) - cmd := exec.CommandContext(context.Background(), "kubevpn", "connect", "--debug", "--workloads", "deployments/reviews") + cmd := exec.CommandContext(context.Background(), "kubevpn", "proxy", "deployments/reviews", "--debug") go func() { var checker = func(log string) { if strings.Contains(log, "dns service ok") { diff --git a/pkg/util/util.go b/pkg/util/util.go index 008c318d..4d5238df 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -251,11 +251,35 @@ func GetUnstructuredObject(f cmdutil.Factory, namespace string, workloads string return nil, err } if len(infos) == 0 { - return nil, errors.New("Not found") + return nil, fmt.Errorf("not found workloads %s", workloads) } return infos[0], err } +func GetUnstructuredObjectList(f cmdutil.Factory, namespace string, workloads []string) ([]*runtimeresource.Info, error) { + do := f.NewBuilder(). + Unstructured(). + NamespaceParam(namespace).DefaultNamespace().AllNamespaces(false). + ResourceTypeOrNameArgs(true, workloads...). + ContinueOnError(). + Latest(). + Flatten(). + TransformRequests(func(req *rest.Request) { req.Param("includeObject", "Object") }). + Do() + if err := do.Err(); err != nil { + log.Warn(err) + return nil, err + } + infos, err := do.Infos() + if err != nil { + return nil, err + } + if len(infos) == 0 { + return nil, fmt.Errorf("not found resource %v", workloads) + } + return infos, err +} + func GetUnstructuredObjectBySelector(f cmdutil.Factory, namespace string, selector string) ([]*runtimeresource.Info, error) { do := f.NewBuilder(). Unstructured().