From bd17575b87eebe4a7c314b0b336b18cb1615d694 Mon Sep 17 00:00:00 2001 From: fengcaiwen Date: Thu, 25 May 2023 14:24:42 +0800 Subject: [PATCH] feat: ssh support use remote kubeconfig --- cmd/kubevpn/cmds/dev.go | 3 +- cmd/kubevpn/cmds/proxy.go | 3 ++ pkg/dev/main.go | 2 +- pkg/handler/connect.go | 79 +++++++++++++++++++++++++++++++++------ pkg/util/ssh.go | 61 +++++++++++++++++++++++++++--- 5 files changed, 130 insertions(+), 18 deletions(-) diff --git a/cmd/kubevpn/cmds/dev.go b/cmd/kubevpn/cmds/dev.go index debe7f9f..f1ec0102 100644 --- a/cmd/kubevpn/cmds/dev.go +++ b/cmd/kubevpn/cmds/dev.go @@ -65,7 +65,8 @@ Startup your kubernetes workloads in local Docker container with same volume态e kubevpn dev --ssh-alias deployment/productpage `)), - Args: cli.RequiresMinArgs(1), + Args: cli.RequiresMinArgs(1), + DisableFlagsInUseLine: true, PreRunE: func(cmd *cobra.Command, args []string) error { if !util.IsAdmin() { util.RunWithElevated() diff --git a/cmd/kubevpn/cmds/proxy.go b/cmd/kubevpn/cmds/proxy.go index c96a0a50..e11b53aa 100644 --- a/cmd/kubevpn/cmds/proxy.go +++ b/cmd/kubevpn/cmds/proxy.go @@ -115,4 +115,7 @@ func addSshFlags(cmd *cobra.Command, sshConf *util.SshConfig) { 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.Flags().StringVar(&sshConf.RemoteKubeconfig, "remote-kubeconfig", "", "Remote kubeconfig abstract path of ssh server, default is /$ssh-user/.kube/config") + lookup := cmd.Flags().Lookup("remote-kubeconfig") + lookup.NoOptDefVal = "~/.kube/config" } diff --git a/pkg/dev/main.go b/pkg/dev/main.go index d4f8f4c2..332cc278 100644 --- a/pkg/dev/main.go +++ b/pkg/dev/main.go @@ -394,7 +394,7 @@ func DoDev(devOptions *Options, flags *pflag.FlagSet, f cmdutil.Factory) error { } } - if err := connect.InitClient(f); err != nil { + if err = connect.InitClient(f); err != nil { return err } if err = connect.PreCheckResource(); err != nil { diff --git a/pkg/handler/connect.go b/pkg/handler/connect.go index 4ab13ec5..42308aa5 100644 --- a/pkg/handler/connect.go +++ b/pkg/handler/connect.go @@ -10,6 +10,7 @@ import ( "net/netip" "net/url" "os" + "path/filepath" "strconv" "strings" "syscall" @@ -601,11 +602,52 @@ func SshJump(conf *util.SshConfig, flags *pflag.FlagSet) (err error) { err = er.(error) } }() + configFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() - if flags != nil { - lookup := flags.Lookup("kubeconfig") - if lookup != nil && lookup.Value != nil && lookup.Value.String() != "" { - configFlags.KubeConfig = pointer.String(lookup.Value.String()) + + if conf.RemoteKubeconfig != "" || flags.Changed("remote-kubeconfig") { + var stdOut []byte + var errOut []byte + if len(conf.RemoteKubeconfig) != 0 && conf.RemoteKubeconfig[0] == '~' { + conf.RemoteKubeconfig = filepath.Join("/", conf.User, conf.RemoteKubeconfig[1:]) + } + if conf.RemoteKubeconfig == "" { + // if `--remote-kubeconfig` is parsed then Entrypoint is reset + conf.RemoteKubeconfig = filepath.Join("/", conf.User, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName) + } + stdOut, errOut, err = util.Run(conf, + fmt.Sprintf("sh -c 'kubectl config view --flatten --raw --kubeconfig %s || minikube kubectl -- config view --flatten --raw --kubeconfig %s'", + conf.RemoteKubeconfig, + conf.RemoteKubeconfig), + []string{clientcmd.RecommendedConfigPathEnvVar, conf.RemoteKubeconfig}, + ) + if err != nil { + return errors.Wrap(err, string(errOut)) + } + if len(stdOut) == 0 { + return errors.Errorf("can not get kubeconfig %s from remote ssh server: %s", conf.RemoteKubeconfig, string(errOut)) + } + + var temp *os.File + if temp, err = os.CreateTemp("", "kubevpn"); err != nil { + return err + } + if err = temp.Close(); err != nil { + return err + } + if err = os.WriteFile(temp.Name(), stdOut, 0644); err != nil { + return err + } + if err = os.Chmod(temp.Name(), 0644); err != nil { + return err + } + configFlags.KubeConfig = pointer.String(temp.Name()) + } else { + if flags != nil { + lookup := flags.Lookup("kubeconfig") + if lookup != nil && lookup.Value != nil && lookup.Value.String() != "" { + configFlags.KubeConfig = pointer.String(lookup.Value.String()) + } } } matchVersionFlags := cmdutil.NewMatchVersionFlags(configFlags) @@ -613,9 +655,21 @@ func SshJump(conf *util.SshConfig, flags *pflag.FlagSet) (err error) { if err != nil { return err } - err = api.FlattenConfig(&rawConfig) - server := rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster].Server - u, err := url.Parse(server) + if err = api.FlattenConfig(&rawConfig); err != nil { + return err + } + if rawConfig.Contexts == nil { + return errors.New("kubeconfig is invalid") + } + kubeContext := rawConfig.Contexts[rawConfig.CurrentContext] + if kubeContext == nil { + return errors.New("kubeconfig is invalid") + } + cluster := rawConfig.Clusters[kubeContext.Cluster] + if cluster == nil { + return errors.New("kubeconfig is invalid") + } + u, err := url.Parse(cluster.Server) if err != nil { return err } @@ -656,12 +710,15 @@ func SshJump(conf *util.SshConfig, flags *pflag.FlagSet) (err error) { if err != nil { return err } - _ = temp.Close() - err = os.WriteFile(temp.Name(), marshal, 0644) - if err != nil { + if err = temp.Close(); err != nil { + return err + } + if err = os.WriteFile(temp.Name(), marshal, 0644); err != nil { + return err + } + if err = os.Chmod(temp.Name(), 0644); err != nil { return err } - _ = os.Chmod(temp.Name(), 0644) log.Infof("using temp kubeconfig %s", temp.Name()) err = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, temp.Name()) if err != nil { diff --git a/pkg/util/ssh.go b/pkg/util/ssh.go index f8d54a04..24e6bffe 100644 --- a/pkg/util/ssh.go +++ b/pkg/util/ssh.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "errors" "fmt" "io" @@ -19,11 +20,12 @@ import ( ) type SshConfig struct { - Addr string - User string - Password string - Keyfile string - ConfigAlias string + Addr string + User string + Password string + Keyfile string + ConfigAlias string + RemoteKubeconfig string } func Main(remoteEndpoint, localEndpoint *netip.AddrPort, conf *SshConfig, done chan struct{}) error { @@ -92,6 +94,55 @@ func Main(remoteEndpoint, localEndpoint *netip.AddrPort, conf *SshConfig, done c } } +func Run(conf *SshConfig, cmd string, env []string) (output []byte, errOut []byte, err error) { + var remote *ssh.Client + if conf.ConfigAlias != "" { + remote, err = jumpRecursion(conf.ConfigAlias) + } else { + var auth []ssh.AuthMethod + if conf.Keyfile != "" { + auth = append(auth, publicKeyFile(conf.Keyfile)) + } + if conf.Password != "" { + auth = append(auth, ssh.Password(conf.Password)) + } + // refer to https://godoc.org/golang.org/x/crypto/ssh for other authentication types + sshConfig := &ssh.ClientConfig{ + // SSH connection username + User: conf.User, + Auth: auth, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + // Connect to SSH remote server using serverEndpoint + remote, err = ssh.Dial("tcp", conf.Addr, sshConfig) + } + if err != nil { + log.Errorf("Dial INTO remote server error: %s", err) + return + } + defer remote.Close() + var session *ssh.Session + session, err = remote.NewSession() + if err != nil { + return + } + if len(env) == 2 { + // /etc/ssh/sshd_config + // AcceptEnv DEBIAN_FRONTEND + if err = session.Setenv(env[0], env[1]); err != nil { + log.Warn(err) + err = nil + } + } + defer remote.Close() + var out bytes.Buffer + var er bytes.Buffer + session.Stdout = &out + session.Stderr = &er + err = session.Run(cmd) + return out.Bytes(), er.Bytes(), err +} + func publicKeyFile(file string) ssh.AuthMethod { var err error if len(file) != 0 && file[0] == '~' {