From 5f38656bed8540e46b1d69f8bfdcc3e6f3c1082a Mon Sep 17 00:00:00 2001 From: neogopher Date: Wed, 11 Oct 2023 13:09:29 +0530 Subject: [PATCH 1/3] Refactor DevPod up command to allow SSH agent forwarding --- cmd/up.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index 8868bf796..e942eb91c 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -36,6 +36,7 @@ import ( "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" + gosshagent "golang.org/x/crypto/ssh/agent" ) // UpCmd holds the up cmd flags @@ -350,16 +351,22 @@ func (cmd *UpCmd) devPodUpMachine( return nil, err } + // ssh tunnel + sshCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) + if log.GetLevel() == logrus.DebugLevel { + sshCmd += " --debug" + } + // create container etc. log.Infof("Creating devcontainer...") defer log.Debugf("Done creating devcontainer") - command := fmt.Sprintf( + agentCommand := fmt.Sprintf( "'%s' agent workspace up --workspace-info '%s'", client.AgentPath(), workspaceInfo, ) if log.GetLevel() == logrus.DebugLevel { - command += " --debug" + agentCommand += " --debug" } // create pipes @@ -378,15 +385,15 @@ func (cmd *UpCmd) devPodUpMachine( cancelCtx, cancel := context.WithCancel(ctx) defer cancel() - errChan := make(chan error, 1) + errChan := make(chan error, 2) go func() { - defer log.Debugf("Done executing up command") + defer log.Debugf("Done executing ssh server helper command") defer cancel() writer := log.Writer(logrus.InfoLevel, false) defer writer.Close() - log.Debugf("Inject and run command: %s", command) + log.Debugf("Inject and run command: %s", sshCmd) err := agent.InjectAgentAndExecute( cancelCtx, func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { @@ -401,19 +408,82 @@ func (cmd *UpCmd) devPodUpMachine( client.AgentPath(), client.AgentURL(), true, - command, + sshCmd, stdinReader, stdoutWriter, writer, log.ErrorStreamOnly(), ) - if err != nil { + if err != nil && !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "signal: ") { errChan <- fmt.Errorf("executing agent command: %w", err) } else { errChan <- nil } }() + // create pipes + stdoutReader2, stdoutWriter2, err := os.Pipe() + if err != nil { + return nil, err + } + stdinReader2, stdinWriter2, err := os.Pipe() + if err != nil { + return nil, err + } + defer stdoutWriter2.Close() + defer stdinWriter2.Close() + + // connect to container + go func() { + defer cancel() + + log.Debugf("Attempting to create SSH client") + // start ssh client as root / default user + sshClient, err := devssh.StdioClient(stdoutReader, stdinWriter, false) + if err != nil { + errChan <- errors.Wrap(err, "create ssh client") + return + } + defer log.Debugf("Connection to SSH Server closed") + defer sshClient.Close() + + log.Debugf("SSH client created") + + sess, err := sshClient.NewSession() + if err != nil { + errChan <- errors.Wrap(err, "create ssh session") + } + defer sess.Close() + + log.Debugf("SSH session created") + + var identityAgent string + if identityAgent == "" { + identityAgent = os.Getenv("SSH_AUTH_SOCK") + } + + if identityAgent != "" { + err = gosshagent.ForwardToRemote(sshClient, identityAgent) + if err != nil { + errChan <- errors.Wrap(err, "forward agent") + } + err = gosshagent.RequestAgentForwarding(sess) + if err != nil { + errChan <- errors.Wrap(err, "request agent forwarding failed") + } + } + + writer := log.Writer(logrus.InfoLevel, false) + defer writer.Close() + + err = devssh.Run(ctx, sshClient, agentCommand, stdinReader2, stdoutWriter2, writer) + if err != nil { + errChan <- errors.Wrap(err, "run agent command") + } else { + errChan <- nil + } + }() + // create container etc. var result *config2.Result if cmd.Proxy { @@ -427,8 +497,8 @@ func (cmd *UpCmd) devPodUpMachine( result, err = tunnelserver.RunProxyServer( cancelCtx, tunnelClient, - stdoutReader, - stdinWriter, + stdoutReader2, + stdinWriter2, log, ) if err != nil { @@ -437,8 +507,8 @@ func (cmd *UpCmd) devPodUpMachine( } else { result, err = tunnelserver.RunUpServer( cancelCtx, - stdoutReader, - stdinWriter, + stdoutReader2, + stdinWriter2, client.AgentInjectGitCredentials(), client.AgentInjectDockerCredentials(), client.WorkspaceConfig(), @@ -450,6 +520,10 @@ func (cmd *UpCmd) devPodUpMachine( } // wait until command finished + if err := <-errChan; err != nil { + return result, err + } + return result, <-errChan } From a7f1fc91c54ff08b585c92ba304f706d5e832f2f Mon Sep 17 00:00:00 2001 From: neogopher Date: Wed, 11 Oct 2023 23:28:05 +0530 Subject: [PATCH 2/3] Refactor setupcontainer to allow SSH agent forwarding --- pkg/devcontainer/setup.go | 98 +++++++++++++++++++++++++++++---- pkg/devcontainer/setup/setup.go | 20 +++++++ 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index d960eb44c..7d9a854c5 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -7,6 +7,7 @@ import ( "io" "os" "runtime" + "strings" "github.com/loft-sh/devpod/pkg/agent" "github.com/loft-sh/devpod/pkg/agent/tunnelserver" @@ -14,8 +15,10 @@ import ( "github.com/loft-sh/devpod/pkg/devcontainer/config" "github.com/loft-sh/devpod/pkg/driver" provider2 "github.com/loft-sh/devpod/pkg/provider" + devssh "github.com/loft-sh/devpod/pkg/ssh" "github.com/pkg/errors" "github.com/sirupsen/logrus" + gosshagent "golang.org/x/crypto/ssh/agent" ) func (r *runner) setupContainer( @@ -66,17 +69,23 @@ func (r *runner) setupContainer( // check if docker driver _, isDockerDriver := r.Driver.(driver.DockerDriver) + // ssh tunnel + sshCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) + if r.Log.GetLevel() == logrus.DebugLevel { + sshCmd += " --debug" + } + // setup container r.Log.Infof("Setup container...") - command := fmt.Sprintf("'%s' agent container setup --setup-info '%s' --container-workspace-info '%s'", agent.ContainerDevPodHelperLocation, compressed, workspaceConfigCompressed) + setupCommand := fmt.Sprintf("'%s' agent container setup --setup-info '%s' --container-workspace-info '%s'", agent.ContainerDevPodHelperLocation, compressed, workspaceConfigCompressed) if runtime.GOOS == "linux" || !isDockerDriver { - command += " --chown-workspace" + setupCommand += " --chown-workspace" } if !isDockerDriver { - command += " --stream-mounts" + setupCommand += " --stream-mounts" } if r.Log.GetLevel() == logrus.DebugLevel { - command += " --debug" + setupCommand += " --debug" } // create pipes @@ -95,28 +104,90 @@ func (r *runner) setupContainer( cancelCtx, cancel := context.WithCancel(ctx) defer cancel() - errChan := make(chan error, 1) + errChan := make(chan error, 2) go func() { - defer r.Log.Debugf("Done executing up command") + defer r.Log.Debugf("Done executing ssh server helper command") defer cancel() writer := r.Log.Writer(logrus.InfoLevel, false) defer writer.Close() - r.Log.Debugf("Run command in container: %s", command) - err = r.Driver.CommandDevContainer(cancelCtx, r.ID, "root", command, stdinReader, stdoutWriter, writer) - if err != nil { + r.Log.Debugf("Run command in container: %s", sshCmd) + err = r.Driver.CommandDevContainer(cancelCtx, r.ID, "root", sshCmd, stdinReader, stdoutWriter, writer) + if err != nil && !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "signal: ") { errChan <- fmt.Errorf("executing container command: %w", err) } else { errChan <- nil } }() + // create pipes + stdoutReader2, stdoutWriter2, err := os.Pipe() + if err != nil { + return nil, err + } + stdinReader2, stdinWriter2, err := os.Pipe() + if err != nil { + return nil, err + } + defer stdoutWriter2.Close() + defer stdinWriter2.Close() + + go func() { + defer cancel() + + r.Log.Debugf("Attempting to create SSH client") + // start ssh client as root / default user + sshClient, err := devssh.StdioClient(stdoutReader, stdinWriter, false) + if err != nil { + errChan <- errors.Wrap(err, "create ssh client") + return + } + defer r.Log.Debugf("Connection to SSH Server closed") + defer sshClient.Close() + + r.Log.Debugf("SSH client created") + + sess, err := sshClient.NewSession() + if err != nil { + errChan <- errors.Wrap(err, "create ssh session") + } + defer sess.Close() + + r.Log.Debugf("SSH session created") + + var identityAgent string + if identityAgent == "" { + identityAgent = os.Getenv("SSH_AUTH_SOCK") + } + + if identityAgent != "" { + err = gosshagent.ForwardToRemote(sshClient, identityAgent) + if err != nil { + errChan <- errors.Wrap(err, "forward agent") + } + err = gosshagent.RequestAgentForwarding(sess) + if err != nil { + errChan <- errors.Wrap(err, "request agent forwarding failed") + } + } + + writer := r.Log.Writer(logrus.InfoLevel, false) + defer writer.Close() + + err = devssh.Run(ctx, sshClient, setupCommand, stdinReader2, stdoutWriter2, writer) + if err != nil { + errChan <- errors.Wrap(err, "run agent command") + } else { + errChan <- nil + } + }() + // start server result, err = tunnelserver.RunSetupServer( cancelCtx, - stdoutReader, - stdinWriter, + stdoutReader2, + stdinWriter2, r.WorkspaceConfig.Agent.InjectDockerCredentials != "false", config.GetMounts(result), r.Log, @@ -125,5 +196,10 @@ func (r *runner) setupContainer( return nil, errors.Wrap(err, "run tunnel machine") } + // wait until command finished + if err := <-errChan; err != nil { + return result, err + } + return result, <-errChan } diff --git a/pkg/devcontainer/setup/setup.go b/pkg/devcontainer/setup/setup.go index 5b2c1ad12..0e1d4e120 100644 --- a/pkg/devcontainer/setup/setup.go +++ b/pkg/devcontainer/setup/setup.go @@ -57,6 +57,12 @@ func SetupContainer(setupInfo *config.Result, extraWorkspaceEnv []string, chownW log.Errorf("Error linking /home/root: %v", err) } + // chown agent sock file + err = ChownAgentSock(setupInfo, log) + if err != nil { + return errors.Wrap(err, "chown ssh agent sock file") + } + // run commands log.Debugf("Run post create commands...") err = PostCreateCommands(setupInfo, log) @@ -206,6 +212,20 @@ func PatchEtcEnvironment(mergedConfig *config.MergedDevContainerConfig, log log. return nil } +func ChownAgentSock(setupInfo *config.Result, log log.Logger) error { + user := config.GetRemoteUser(setupInfo) + + agentSockFile := os.Getenv("SSH_AUTH_SOCK") + if agentSockFile != "" { + err := copy2.ChownR(filepath.Dir(agentSockFile), user) + if err != nil { + log.Warn(err) + } + } + + return nil +} + func PostCreateCommands(setupInfo *config.Result, log log.Logger) error { remoteUser := config.GetRemoteUser(setupInfo) mergedConfig := setupInfo.MergedConfig From 72b839403e9b35421a31b7e9ab8c7725fe7b228b Mon Sep 17 00:00:00 2001 From: neogopher Date: Thu, 26 Oct 2023 12:52:47 +0530 Subject: [PATCH 3/3] Extract the sshTunnelling and execution code to a seperate package and reuse --- cmd/up.go | 158 ++-------------------- pkg/devcontainer/setup.go | 124 +---------------- pkg/devcontainer/setup/setup.go | 2 +- pkg/devcontainer/sshtunnel/sshtunnel.go | 170 ++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 263 deletions(-) create mode 100644 pkg/devcontainer/sshtunnel/sshtunnel.go diff --git a/cmd/up.go b/cmd/up.go index e942eb91c..9bdce81b8 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -19,6 +19,7 @@ import ( "github.com/loft-sh/devpod/pkg/command" "github.com/loft-sh/devpod/pkg/config" config2 "github.com/loft-sh/devpod/pkg/devcontainer/config" + "github.com/loft-sh/devpod/pkg/devcontainer/sshtunnel" "github.com/loft-sh/devpod/pkg/ide/fleet" "github.com/loft-sh/devpod/pkg/ide/jetbrains" "github.com/loft-sh/devpod/pkg/ide/jupyter" @@ -36,7 +37,6 @@ import ( "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" - gosshagent "golang.org/x/crypto/ssh/agent" ) // UpCmd holds the up cmd flags @@ -351,15 +351,17 @@ func (cmd *UpCmd) devPodUpMachine( return nil, err } - // ssh tunnel - sshCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) - if log.GetLevel() == logrus.DebugLevel { - sshCmd += " --debug" - } - // create container etc. log.Infof("Creating devcontainer...") defer log.Debugf("Done creating devcontainer") + + // ssh tunnel command + sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) + if log.GetLevel() == logrus.DebugLevel { + sshTunnelCmd += " --debug" + } + + // create agent command agentCommand := fmt.Sprintf( "'%s' agent workspace up --workspace-info '%s'", client.AgentPath(), @@ -369,32 +371,8 @@ func (cmd *UpCmd) devPodUpMachine( agentCommand += " --debug" } - // create pipes - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, err - } - stdinReader, stdinWriter, err := os.Pipe() - if err != nil { - return nil, err - } - defer stdoutWriter.Close() - defer stdinWriter.Close() - - // start machine on stdio - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - errChan := make(chan error, 2) - go func() { - defer log.Debugf("Done executing ssh server helper command") - defer cancel() - - writer := log.Writer(logrus.InfoLevel, false) - defer writer.Close() - - log.Debugf("Inject and run command: %s", sshCmd) - err := agent.InjectAgentAndExecute( + agentInjectFunc := func(cancelCtx context.Context, sshCmd string, sshTunnelStdinReader, sshTunnelStdoutWriter *os.File, writer io.WriteCloser) error { + return agent.InjectAgentAndExecute( cancelCtx, func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return client.Command(ctx, client2.CommandOptions{ @@ -409,122 +387,14 @@ func (cmd *UpCmd) devPodUpMachine( client.AgentURL(), true, sshCmd, - stdinReader, - stdoutWriter, + sshTunnelStdinReader, + sshTunnelStdoutWriter, writer, log.ErrorStreamOnly(), ) - if err != nil && !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "signal: ") { - errChan <- fmt.Errorf("executing agent command: %w", err) - } else { - errChan <- nil - } - }() - - // create pipes - stdoutReader2, stdoutWriter2, err := os.Pipe() - if err != nil { - return nil, err - } - stdinReader2, stdinWriter2, err := os.Pipe() - if err != nil { - return nil, err } - defer stdoutWriter2.Close() - defer stdinWriter2.Close() - // connect to container - go func() { - defer cancel() - - log.Debugf("Attempting to create SSH client") - // start ssh client as root / default user - sshClient, err := devssh.StdioClient(stdoutReader, stdinWriter, false) - if err != nil { - errChan <- errors.Wrap(err, "create ssh client") - return - } - defer log.Debugf("Connection to SSH Server closed") - defer sshClient.Close() - - log.Debugf("SSH client created") - - sess, err := sshClient.NewSession() - if err != nil { - errChan <- errors.Wrap(err, "create ssh session") - } - defer sess.Close() - - log.Debugf("SSH session created") - - var identityAgent string - if identityAgent == "" { - identityAgent = os.Getenv("SSH_AUTH_SOCK") - } - - if identityAgent != "" { - err = gosshagent.ForwardToRemote(sshClient, identityAgent) - if err != nil { - errChan <- errors.Wrap(err, "forward agent") - } - err = gosshagent.RequestAgentForwarding(sess) - if err != nil { - errChan <- errors.Wrap(err, "request agent forwarding failed") - } - } - - writer := log.Writer(logrus.InfoLevel, false) - defer writer.Close() - - err = devssh.Run(ctx, sshClient, agentCommand, stdinReader2, stdoutWriter2, writer) - if err != nil { - errChan <- errors.Wrap(err, "run agent command") - } else { - errChan <- nil - } - }() - - // create container etc. - var result *config2.Result - if cmd.Proxy { - // create client on stdin & stdout - tunnelClient, err := tunnelserver.NewTunnelClient(os.Stdin, os.Stdout, true) - if err != nil { - return nil, errors.Wrap(err, "create tunnel client") - } - - // create proxy server - result, err = tunnelserver.RunProxyServer( - cancelCtx, - tunnelClient, - stdoutReader2, - stdinWriter2, - log, - ) - if err != nil { - return nil, errors.Wrap(err, "run proxy tunnel") - } - } else { - result, err = tunnelserver.RunUpServer( - cancelCtx, - stdoutReader2, - stdinWriter2, - client.AgentInjectGitCredentials(), - client.AgentInjectDockerCredentials(), - client.WorkspaceConfig(), - log, - ) - if err != nil { - return nil, errors.Wrap(err, "run tunnel machine") - } - } - - // wait until command finished - if err := <-errChan; err != nil { - return result, err - } - - return result, <-errChan + return sshtunnel.ExecuteCommand(ctx, client, agentInjectFunc, sshTunnelCmd, agentCommand, cmd.Proxy, false, false, nil, log) } func startJupyterNotebookInBrowser( diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 7d9a854c5..a28cd48ca 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -7,18 +7,15 @@ import ( "io" "os" "runtime" - "strings" "github.com/loft-sh/devpod/pkg/agent" - "github.com/loft-sh/devpod/pkg/agent/tunnelserver" "github.com/loft-sh/devpod/pkg/compress" "github.com/loft-sh/devpod/pkg/devcontainer/config" + "github.com/loft-sh/devpod/pkg/devcontainer/sshtunnel" "github.com/loft-sh/devpod/pkg/driver" provider2 "github.com/loft-sh/devpod/pkg/provider" - devssh "github.com/loft-sh/devpod/pkg/ssh" "github.com/pkg/errors" "github.com/sirupsen/logrus" - gosshagent "golang.org/x/crypto/ssh/agent" ) func (r *runner) setupContainer( @@ -70,9 +67,9 @@ func (r *runner) setupContainer( _, isDockerDriver := r.Driver.(driver.DockerDriver) // ssh tunnel - sshCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) + sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) if r.Log.GetLevel() == logrus.DebugLevel { - sshCmd += " --debug" + sshTunnelCmd += " --debug" } // setup container @@ -88,118 +85,9 @@ func (r *runner) setupContainer( setupCommand += " --debug" } - // create pipes - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, err - } - stdinReader, stdinWriter, err := os.Pipe() - if err != nil { - return nil, err - } - defer stdoutWriter.Close() - defer stdinWriter.Close() - - // start machine on stdio - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - errChan := make(chan error, 2) - go func() { - defer r.Log.Debugf("Done executing ssh server helper command") - defer cancel() - - writer := r.Log.Writer(logrus.InfoLevel, false) - defer writer.Close() - - r.Log.Debugf("Run command in container: %s", sshCmd) - err = r.Driver.CommandDevContainer(cancelCtx, r.ID, "root", sshCmd, stdinReader, stdoutWriter, writer) - if err != nil && !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "signal: ") { - errChan <- fmt.Errorf("executing container command: %w", err) - } else { - errChan <- nil - } - }() - - // create pipes - stdoutReader2, stdoutWriter2, err := os.Pipe() - if err != nil { - return nil, err - } - stdinReader2, stdinWriter2, err := os.Pipe() - if err != nil { - return nil, err - } - defer stdoutWriter2.Close() - defer stdinWriter2.Close() - - go func() { - defer cancel() - - r.Log.Debugf("Attempting to create SSH client") - // start ssh client as root / default user - sshClient, err := devssh.StdioClient(stdoutReader, stdinWriter, false) - if err != nil { - errChan <- errors.Wrap(err, "create ssh client") - return - } - defer r.Log.Debugf("Connection to SSH Server closed") - defer sshClient.Close() - - r.Log.Debugf("SSH client created") - - sess, err := sshClient.NewSession() - if err != nil { - errChan <- errors.Wrap(err, "create ssh session") - } - defer sess.Close() - - r.Log.Debugf("SSH session created") - - var identityAgent string - if identityAgent == "" { - identityAgent = os.Getenv("SSH_AUTH_SOCK") - } - - if identityAgent != "" { - err = gosshagent.ForwardToRemote(sshClient, identityAgent) - if err != nil { - errChan <- errors.Wrap(err, "forward agent") - } - err = gosshagent.RequestAgentForwarding(sess) - if err != nil { - errChan <- errors.Wrap(err, "request agent forwarding failed") - } - } - - writer := r.Log.Writer(logrus.InfoLevel, false) - defer writer.Close() - - err = devssh.Run(ctx, sshClient, setupCommand, stdinReader2, stdoutWriter2, writer) - if err != nil { - errChan <- errors.Wrap(err, "run agent command") - } else { - errChan <- nil - } - }() - - // start server - result, err = tunnelserver.RunSetupServer( - cancelCtx, - stdoutReader2, - stdinWriter2, - r.WorkspaceConfig.Agent.InjectDockerCredentials != "false", - config.GetMounts(result), - r.Log, - ) - if err != nil { - return nil, errors.Wrap(err, "run tunnel machine") - } - - // wait until command finished - if err := <-errChan; err != nil { - return result, err + agentInjectFunc := func(cancelCtx context.Context, sshCmd string, sshTunnelStdinReader, sshTunnelStdoutWriter *os.File, writer io.WriteCloser) error { + return r.Driver.CommandDevContainer(cancelCtx, r.ID, "root", sshCmd, sshTunnelStdinReader, sshTunnelStdoutWriter, writer) } - return result, <-errChan + return sshtunnel.ExecuteCommand(ctx, nil, agentInjectFunc, sshTunnelCmd, setupCommand, false, true, r.WorkspaceConfig.Agent.InjectDockerCredentials != "false", result, r.Log) } diff --git a/pkg/devcontainer/setup/setup.go b/pkg/devcontainer/setup/setup.go index 0e1d4e120..36258d9ea 100644 --- a/pkg/devcontainer/setup/setup.go +++ b/pkg/devcontainer/setup/setup.go @@ -219,7 +219,7 @@ func ChownAgentSock(setupInfo *config.Result, log log.Logger) error { if agentSockFile != "" { err := copy2.ChownR(filepath.Dir(agentSockFile), user) if err != nil { - log.Warn(err) + return err } } diff --git a/pkg/devcontainer/sshtunnel/sshtunnel.go b/pkg/devcontainer/sshtunnel/sshtunnel.go new file mode 100644 index 000000000..5ef7f6b84 --- /dev/null +++ b/pkg/devcontainer/sshtunnel/sshtunnel.go @@ -0,0 +1,170 @@ +package sshtunnel + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/loft-sh/log" + + "github.com/loft-sh/devpod/pkg/agent/tunnelserver" + client2 "github.com/loft-sh/devpod/pkg/client" + config2 "github.com/loft-sh/devpod/pkg/devcontainer/config" + devssh "github.com/loft-sh/devpod/pkg/ssh" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + gosshagent "golang.org/x/crypto/ssh/agent" +) + +type AgentInjectFunc func(context.Context, string, *os.File, *os.File, io.WriteCloser) error + +// ExecuteCommand runs the command in an SSH Tunnel and returns the result. +func ExecuteCommand(ctx context.Context, client client2.WorkspaceClient, agentInject AgentInjectFunc, sshCommand, command string, proxy, setupContainer, allowDockerCredentials bool, result *config2.Result, log log.Logger) (*config2.Result, error) { + // create pipes + sshTunnelStdoutReader, sshTunnelStdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + sshTunnelStdinReader, sshTunnelStdinWriter, err := os.Pipe() + if err != nil { + return nil, err + } + defer sshTunnelStdoutWriter.Close() + defer sshTunnelStdinWriter.Close() + + // start machine on stdio + cancelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + errChan := make(chan error, 2) + go func() { + defer log.Debugf("Done executing ssh server helper command") + defer cancel() + + writer := log.Writer(logrus.InfoLevel, false) + defer writer.Close() + + log.Debugf("Inject and run command: %s", sshCommand) + err := agentInject(cancelCtx, sshCommand, sshTunnelStdinReader, sshTunnelStdoutWriter, writer) + if err != nil && !errors.Is(err, context.Canceled) && !strings.Contains(err.Error(), "signal: ") { + errChan <- fmt.Errorf("executing agent command: %w", err) + } else { + errChan <- nil + } + }() + + // create pipes + gRPCConnStdoutReader, gRPCConnStdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + gRPCConnStdinReader, gRPCConnStdinWriter, err := os.Pipe() + if err != nil { + return nil, err + } + defer gRPCConnStdoutWriter.Close() + defer gRPCConnStdinWriter.Close() + + // connect to container + go func() { + defer cancel() + + log.Debugf("Attempting to create SSH client") + // start ssh client as root / default user + sshClient, err := devssh.StdioClient(sshTunnelStdoutReader, sshTunnelStdinWriter, false) + if err != nil { + errChan <- errors.Wrap(err, "create ssh client") + return + } + defer log.Debugf("Connection to SSH Server closed") + defer sshClient.Close() + + log.Debugf("SSH client created") + + sess, err := sshClient.NewSession() + if err != nil { + errChan <- errors.Wrap(err, "create ssh session") + } + defer sess.Close() + + log.Debugf("SSH session created") + + identityAgent := os.Getenv("SSH_AUTH_SOCK") + if identityAgent != "" { + err = gosshagent.ForwardToRemote(sshClient, identityAgent) + if err != nil { + errChan <- errors.Wrap(err, "forward agent") + } + err = gosshagent.RequestAgentForwarding(sess) + if err != nil { + errChan <- errors.Wrap(err, "request agent forwarding failed") + } + } + + writer := log.Writer(logrus.InfoLevel, false) + defer writer.Close() + + err = devssh.Run(ctx, sshClient, command, gRPCConnStdinReader, gRPCConnStdoutWriter, writer) + if err != nil { + errChan <- errors.Wrap(err, "run agent command") + } else { + errChan <- nil + } + }() + + // create container etc. + if proxy { + // create client on stdin & stdout + tunnelClient, err := tunnelserver.NewTunnelClient(os.Stdin, os.Stdout, true) + if err != nil { + return nil, errors.Wrap(err, "create tunnel client") + } + + // create proxy server + result, err = tunnelserver.RunProxyServer( + cancelCtx, + tunnelClient, + gRPCConnStdoutReader, + gRPCConnStdinWriter, + log, + ) + if err != nil { + return nil, errors.Wrap(err, "run proxy tunnel") + } + } else if setupContainer { + // start server + result, err = tunnelserver.RunSetupServer( + cancelCtx, + gRPCConnStdoutReader, + gRPCConnStdinWriter, + allowDockerCredentials, + config2.GetMounts(result), + log, + ) + if err != nil { + return nil, errors.Wrap(err, "run tunnel machine") + } + } else { + result, err = tunnelserver.RunUpServer( + cancelCtx, + gRPCConnStdoutReader, + gRPCConnStdinWriter, + client.AgentInjectGitCredentials(), + client.AgentInjectDockerCredentials(), + client.WorkspaceConfig(), + log, + ) + if err != nil { + return nil, errors.Wrap(err, "run tunnel machine") + } + } + + // wait until command finished + if err := <-errChan; err != nil { + return result, err + } + + return result, <-errChan +}